瀏覽代碼

Partially added web upload
#70 #50

SergiX44 5 年之前
父節點
當前提交
5a0b5adcad

+ 6 - 0
Gruntfile.js

@@ -95,6 +95,12 @@ module.exports = function (grunt) {
                         src: ['styles/**/*', 'highlight.pack.min.js'],
                         src: ['styles/**/*', 'highlight.pack.min.js'],
                         dest: 'static/highlightjs'
                         dest: 'static/highlightjs'
                     },
                     },
+                    {
+                        expand: true,
+                        cwd: 'node_modules/dropzone/dist/min',
+                        src: ['dropzone.min.css', 'dropzone.min.js'],
+                        dest: 'static/dropzone'
+                    },
                     {expand: true, cwd: 'node_modules/jquery/dist', src: ['jquery.min.js'], dest: 'static/jquery'}
                     {expand: true, cwd: 'node_modules/jquery/dist', src: ['jquery.min.js'], dest: 'static/jquery'}
                 ],
                 ],
             },
             },

+ 1 - 1
app/Controllers/DashboardController.php

@@ -59,7 +59,7 @@ class DashboardController extends Controller
 
 
         return view()->render(
         return view()->render(
             $response,
             $response,
-            ($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/admin.twig' : 'dashboard/home.twig',
+            ($this->session->get('admin', false) && $this->session->get('gallery_view', true)) ? 'dashboard/list.twig' : 'dashboard/grid.twig',
             [
             [
                 'medias' => $query->getMedia(),
                 'medias' => $query->getMedia(),
                 'next' => $page < floor($query->getPages()),
                 'next' => $page < floor($query->getPages()),

+ 35 - 8
app/Controllers/UploadController.php

@@ -16,6 +16,28 @@ use Slim\Exception\HttpUnauthorizedException;
 class UploadController extends Controller
 class UploadController extends Controller
 {
 {
 
 
+    /**
+     * @param  Request  $request
+     * @param  Response  $response
+     * @return Response
+     * @throws \Twig\Error\LoaderError
+     * @throws \Twig\Error\RuntimeError
+     * @throws \Twig\Error\SyntaxError
+     */
+    public function webUpload(Request $request, Response $response): Response
+    {
+        $user = $this->database->query('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', $this->session->get('user_id'))->fetch();
+
+        if ($user->token === null || $user->token === '') {
+            $this->session->alert(lang('no_upload_token'), 'danger');
+            return redirect($response, $request->getHeaderLine('Referer'));
+        }
+
+        return view()->render($response, 'upload/web.twig', [
+            'user' => $user,
+        ]);
+    }
+
     /**
     /**
      * @param  Request  $request
      * @param  Request  $request
      * @param  Response  $response
      * @param  Response  $response
@@ -24,7 +46,6 @@ class UploadController extends Controller
      */
      */
     public function upload(Request $request, Response $response): Response
     public function upload(Request $request, Response $response): Response
     {
     {
-
         $json = [
         $json = [
             'message' => null,
             'message' => null,
             'version' => PLATFORM_VERSION,
             'version' => PLATFORM_VERSION,
@@ -40,7 +61,16 @@ class UploadController extends Controller
             return json($response, $json, 400);
             return json($response, $json, 400);
         }
         }
 
 
-        if (isset($request->getUploadedFiles()['upload']) && $request->getUploadedFiles()['upload']->getError() === UPLOAD_ERR_INI_SIZE) {
+        $file = array_values($request->getUploadedFiles());
+        /** @var \Psr\Http\Message\UploadedFileInterface|null $file */
+        $file = isset($file[0]) ? $file[0] : null;
+
+        if ($file === null) {
+            $json['message'] = 'Request without file attached.';
+            return json($response, $json, 400);
+        }
+
+        if ($file->getError() === UPLOAD_ERR_INI_SIZE) {
             $json['message'] = 'File too large (upload_max_filesize too low?).';
             $json['message'] = 'File too large (upload_max_filesize too low?).';
             return json($response, $json, 400);
             return json($response, $json, 400);
         }
         }
@@ -66,9 +96,6 @@ class UploadController extends Controller
             $code = humanRandomString();
             $code = humanRandomString();
         } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
         } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
 
 
-        /** @var \Psr\Http\Message\UploadedFileInterface $file */
-        $file = $request->getUploadedFiles()['upload'];
-
         $fileInfo = pathinfo($file->getClientFilename());
         $fileInfo = pathinfo($file->getClientFilename());
         $storagePath = "$user->user_code/$code.$fileInfo[extension]";
         $storagePath = "$user->user_code/$code.$fileInfo[extension]";
 
 
@@ -169,12 +196,12 @@ class UploadController extends Controller
 
 
         if (!$user) {
         if (!$user) {
             $this->session->alert(lang('token_not_found'), 'danger');
             $this->session->alert(lang('token_not_found'), 'danger');
-            return redirect($response, $request->getHeaderLine('HTTP_REFERER'));
+            return redirect($response, $request->getHeaderLine('Referer'));
         }
         }
 
 
         if (!$user->active) {
         if (!$user->active) {
             $this->session->alert(lang('account_disabled'), 'danger');
             $this->session->alert(lang('account_disabled'), 'danger');
-            return redirect($response, $request->getHeaderLine('HTTP_REFERER'));
+            return redirect($response, $request->getHeaderLine('Referer'));
         }
         }
 
 
         if ($this->session->get('admin', false) || $user->id === $media->user_id) {
         if ($this->session->get('admin', false) || $user->id === $media->user_id) {
@@ -232,7 +259,7 @@ class UploadController extends Controller
             throw new HttpNotFoundException($request);
             throw new HttpNotFoundException($request);
         }
         }
 
 
-        if($ext !== null && pathinfo($media->filename, PATHINFO_EXTENSION) !== $ext){
+        if ($ext !== null && pathinfo($media->filename, PATHINFO_EXTENSION) !== $ext) {
             throw new HttpBadRequestException($request);
             throw new HttpBadRequestException($request);
         }
         }
 
 

+ 4 - 4
app/Controllers/UserController.php

@@ -358,8 +358,8 @@ class UserController extends Controller
         }
         }
 
 
         if ($user->token === null || $user->token === '') {
         if ($user->token === null || $user->token === '') {
-            $this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger');
-            return redirect($response, $request->getHeaderLine('HTTP_REFERER'));
+            $this->session->alert(lang('no_upload_token'), 'danger');
+            return redirect($response, $request->getHeaderLine('Referer'));
         }
         }
 
 
         $json = [
         $json = [
@@ -404,8 +404,8 @@ class UserController extends Controller
         }
         }
 
 
         if ($user->token === null || $user->token === '') {
         if ($user->token === null || $user->token === '') {
-            $this->session->alert('You don\'t have a personal upload token. (Click the update token button and try again)', 'danger');
-            return redirect($response, $request->getHeaderLine('HTTP_REFERER'));
+            $this->session->alert(lang('no_upload_token'), 'danger');
+            return redirect($response, $request->getHeaderLine('Referer'));
         }
         }
 
 
         return view()->render($response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_'.$user->username.'.sh"'),
         return view()->render($response->withHeader('Content-Disposition', 'attachment;filename="xbackbone_uploader_'.$user->username.'.sh"'),

+ 1 - 0
app/routes.php

@@ -15,6 +15,7 @@ use Slim\Routing\RouteCollectorProxy;
 global $app;
 global $app;
 $app->group('', function (RouteCollectorProxy $group) {
 $app->group('', function (RouteCollectorProxy $group) {
     $group->get('/home[/page/{page}]', [DashboardController::class, 'home'])->setName('home');
     $group->get('/home[/page/{page}]', [DashboardController::class, 'home'])->setName('home');
+    $group->get('/upload', [UploadController::class, 'webUpload'])->setName('upload.web');
 
 
     $group->group('', function (RouteCollectorProxy $group) {
     $group->group('', function (RouteCollectorProxy $group) {
         $group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView');
         $group->get('/home/switchView', [DashboardController::class, 'switchView'])->setName('switchView');

+ 5 - 5
composer.lock

@@ -1,7 +1,7 @@
 {
 {
     "_readme": [
     "_readme": [
         "This file locks the dependencies of your project to a known state",
         "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
     "content-hash": "a5d4341b89b81518c0e488cd3bc47127",
     "content-hash": "a5d4341b89b81518c0e488cd3bc47127",
@@ -1862,15 +1862,15 @@
             "authors": [
             "authors": [
                 {
                 {
                     "name": "Freek Van der Herten",
                     "name": "Freek Van der Herten",
+                    "role": "Developer",
                     "email": "freek@spatie.be",
                     "email": "freek@spatie.be",
-                    "homepage": "https://spatie.be",
-                    "role": "Developer"
+                    "homepage": "https://spatie.be"
                 },
                 },
                 {
                 {
                     "name": "Alex Vanderbist",
                     "name": "Alex Vanderbist",
+                    "role": "Developer",
                     "email": "alex.vanderbist@gmail.com",
                     "email": "alex.vanderbist@gmail.com",
-                    "homepage": "https://spatie.be",
-                    "role": "Developer"
+                    "homepage": "https://spatie.be"
                 }
                 }
             ],
             ],
             "description": "A minimal implementation of Dropbox API v2",
             "description": "A minimal implementation of Dropbox API v2",

+ 1 - 1
install/index.php

@@ -257,7 +257,7 @@ $app->post('/', function (Request $request, Response $response, Filesystem $stor
     cleanDirectory(__DIR__.'/../resources/cache');
     cleanDirectory(__DIR__.'/../resources/cache');
     cleanDirectory(__DIR__.'/../resources/sessions');
     cleanDirectory(__DIR__.'/../resources/sessions');
 
 
-    removeDirectory(__DIR__.'/../install');
+    //removeDirectory(__DIR__.'/../install');
 
 
     // if is upgrading and existing installation, put it out maintenance
     // if is upgrading and existing installation, put it out maintenance
     if ($installed) {
     if ($installed) {

+ 23 - 18
package-lock.json

@@ -3,9 +3,9 @@
   "lockfileVersion": 1,
   "lockfileVersion": 1,
   "dependencies": {
   "dependencies": {
     "@fortawesome/fontawesome-free": {
     "@fortawesome/fontawesome-free": {
-      "version": "5.10.2",
-      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.10.2.tgz",
-      "integrity": "sha512-9pw+Nsnunl9unstGEHQ+u41wBEQue6XPBsILXtJF/4fNN1L3avJcMF/gGF86rIjeTAgfLjTY9ndm68/X4f4idQ=="
+      "version": "5.11.2",
+      "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.11.2.tgz",
+      "integrity": "sha512-XiUPoS79r1G7PcpnNtq85TJ7inJWe0v+b5oZJZKb0pGHNIV6+UiNeQWiFGmuQ0aj7GEhnD/v9iqxIsjuRKtEnQ=="
     },
     },
     "abbrev": {
     "abbrev": {
       "version": "1.1.1",
       "version": "1.1.1",
@@ -359,6 +359,11 @@
         "domelementtype": "1"
         "domelementtype": "1"
       }
       }
     },
     },
+    "dropzone": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.5.1.tgz",
+      "integrity": "sha512-3VduRWLxx9hbVr42QieQN25mx/I61/mRdUSuxAmDGdDqZIN8qtP7tcKMa3KfpJjuGjOJGYYUzzeq6eGDnkzesA=="
+    },
     "duplexer": {
     "duplexer": {
       "version": "0.1.1",
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
       "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -744,9 +749,9 @@
           }
           }
         },
         },
         "lodash": {
         "lodash": {
-          "version": "4.17.11",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-          "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
           "dev": true
           "dev": true
         }
         }
       }
       }
@@ -863,9 +868,9 @@
       "dev": true
       "dev": true
     },
     },
     "highlightjs": {
     "highlightjs": {
-      "version": "9.12.0",
-      "resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.12.0.tgz",
-      "integrity": "sha512-eAhWMtDZaOZIQdxIP4UEB1vNp/CVXQPdMSihTSuaExhFIRC0BVpXbtP3mTP1hDoGOyh7nbB3cuC3sOPhG5wGDA=="
+      "version": "9.16.2",
+      "resolved": "https://registry.npmjs.org/highlightjs/-/highlightjs-9.16.2.tgz",
+      "integrity": "sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg=="
     },
     },
     "hooker": {
     "hooker": {
       "version": "0.2.3",
       "version": "0.2.3",
@@ -1043,9 +1048,9 @@
       }
       }
     },
     },
     "lodash": {
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
       "dev": true
     },
     },
     "loud-rejection": {
     "loud-rejection": {
@@ -1347,9 +1352,9 @@
       }
       }
     },
     },
     "popper.js": {
     "popper.js": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.15.0.tgz",
-      "integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA=="
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz",
+      "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw=="
     },
     },
     "pretty-bytes": {
     "pretty-bytes": {
       "version": "3.0.1",
       "version": "3.0.1",
@@ -1616,9 +1621,9 @@
       }
       }
     },
     },
     "tooltip.js": {
     "tooltip.js": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/tooltip.js/-/tooltip.js-1.3.2.tgz",
-      "integrity": "sha512-DeDr9JxYx/lSvQ53ZCRFLxXrmrSyU3fLz6k+ITUTw69AIYtpWij/NmOJQscJ7BwY5lcEwWJWSfqqQWVvTMYZiw==",
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tooltip.js/-/tooltip.js-1.3.3.tgz",
+      "integrity": "sha512-XWWuy/dBdF/F/YpRE955yqBZ4VdLfiTAUdOqoU+wJm6phJlMpEzl/iYHZ+qJswbeT9VG822bNfsETF9wzmoy5A==",
       "requires": {
       "requires": {
         "popper.js": "^1.0.2"
         "popper.js": "^1.0.2"
       }
       }

+ 5 - 4
package.json

@@ -1,13 +1,14 @@
 {
 {
   "dependencies": {
   "dependencies": {
-    "@fortawesome/fontawesome-free": "^5.10.2",
+    "@fortawesome/fontawesome-free": "^5.11.2",
     "bootstrap": "^4.3.1",
     "bootstrap": "^4.3.1",
     "clipboard": "^2.0.4",
     "clipboard": "^2.0.4",
-    "highlightjs": "^9.12.0",
+    "dropzone": "^5.5.1",
+    "highlightjs": "^9.16.2",
     "jquery": "^3.4.1",
     "jquery": "^3.4.1",
     "plyr": "^3.5.6",
     "plyr": "^3.5.6",
-    "popper.js": "^1.15.0",
-    "tooltip.js": "^1.3.2"
+    "popper.js": "^1.16.0",
+    "tooltip.js": "^1.3.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "grunt": "^1.0.4",
     "grunt": "^1.0.4",

+ 2 - 0
resources/lang/en.lang.php

@@ -28,6 +28,7 @@ return [
     'date' => 'Date',
     'date' => 'Date',
     'raw' => 'Show raw',
     'raw' => 'Show raw',
     'download' => 'Download',
     'download' => 'Download',
+    'upload' => 'Upload',
     'delete' => 'Delete',
     'delete' => 'Delete',
     'publish' => 'Publish',
     'publish' => 'Publish',
     'hide' => 'Hide',
     'hide' => 'Hide',
@@ -99,4 +100,5 @@ return [
     'default_lang_behavior' => 'XBackBone will try to match the browser language by default (the fallback is English).',
     'default_lang_behavior' => 'XBackBone will try to match the browser language by default (the fallback is English).',
     'lang_set' => 'System language enforced to "%s"',
     'lang_set' => 'System language enforced to "%s"',
     'prerelease_channel' => 'Prerelease Channel',
     'prerelease_channel' => 'Prerelease Channel',
+    'no_upload_token' => 'You don\'t have a personal upload token. (Generate one and try again)',
 ];
 ];

+ 2 - 0
resources/lang/it.lang.php

@@ -27,6 +27,7 @@ return [
     'date' => 'Data',
     'date' => 'Data',
     'raw' => 'Vedi raw',
     'raw' => 'Vedi raw',
     'download' => 'Scarica',
     'download' => 'Scarica',
+    'upload' => 'Carica',
     'delete' => 'Elimina',
     'delete' => 'Elimina',
     'publish' => 'Pubblica',
     'publish' => 'Pubblica',
     'hide' => 'Nascondi',
     'hide' => 'Nascondi',
@@ -99,4 +100,5 @@ return [
     'default_lang_behavior' => 'Per impostazione predefinita, XBackbone cercherà di abbinare la lingua del browser (il fallback è l\'Inglese).',
     'default_lang_behavior' => 'Per impostazione predefinita, XBackbone cercherà di abbinare la lingua del browser (il fallback è l\'Inglese).',
     'lang_set' => 'Lingua di sistema applicata a "%s"',
     'lang_set' => 'Lingua di sistema applicata a "%s"',
     'prerelease_channel' => 'Canale prerelease',
     'prerelease_channel' => 'Canale prerelease',
+    'no_upload_token' => 'Non hai un token personale per l\'upload associato. (Generane uno e riprova)',
 ];
 ];

+ 2 - 0
resources/templates/base.twig

@@ -17,6 +17,7 @@
     <link href="{{ asset('/static/fontawesome/css/all.min.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/fontawesome/css/all.min.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/highlightjs/styles/monokai.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/highlightjs/styles/monokai.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/plyr/plyr.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/plyr/plyr.css') }}" rel="stylesheet">
+    <link href="{{ asset('/static/dropzone/dropzone.min.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/app/app.css') }}" rel="stylesheet">
     <link href="{{ asset('/static/app/app.css') }}" rel="stylesheet">
     <script>window.AppConfig = {'base_url': '{{ config.base_url }}', 'lang': {'publish': '{{ lang('publish') }}', 'hide': '{{ lang('hide') }}'}}</script>
     <script>window.AppConfig = {'base_url': '{{ config.base_url }}', 'lang': {'publish': '{{ lang('publish') }}', 'hide': '{{ lang('hide') }}'}}</script>
     {% block head %}{% endblock %}
     {% block head %}{% endblock %}
@@ -35,6 +36,7 @@
 <script src="{{ asset('/static/highlightjs/highlight.pack.min.js') }}"></script>
 <script src="{{ asset('/static/highlightjs/highlight.pack.min.js') }}"></script>
 <script src="{{ asset('/static/clipboardjs/clipboard.min.js') }}"></script>
 <script src="{{ asset('/static/clipboardjs/clipboard.min.js') }}"></script>
 <script src="{{ asset('/static/plyr/plyr.min.js') }}"></script>
 <script src="{{ asset('/static/plyr/plyr.min.js') }}"></script>
+<script src="{{ asset('/static/dropzone/dropzone.min.js') }}"></script>
 <script src="{{ asset('/static/app/app.js') }}"></script>
 <script src="{{ asset('/static/app/app.js') }}"></script>
 <script>hljs.initHighlightingOnLoad();</script>
 <script>hljs.initHighlightingOnLoad();</script>
 </body>
 </body>

+ 5 - 0
resources/templates/comp/navbar.twig

@@ -11,6 +11,11 @@
                         {{ lang('home') }}
                         {{ lang('home') }}
                     </a>
                     </a>
                 </li>
                 </li>
+                <li class="nav-item">
+                    <a href="{{ route('upload') }}" class="nav-link {{ request.uri.path starts with '/upload' ? 'active' }}"><i class="fas fa-fw fa-upload"></i>
+                        {{ lang('upload') }}
+                    </a>
+                </li>
                 {% if session.admin %}
                 {% if session.admin %}
                     <li class="nav-item">
                     <li class="nav-item">
                         <a href="{{ route('user.index') }}" class="nav-link {{ request.uri.path starts with '/user' ? 'active' }}"><i class="fas fa-fw fa-users"></i>
                         <a href="{{ route('user.index') }}" class="nav-link {{ request.uri.path starts with '/user' ? 'active' }}"><i class="fas fa-fw fa-users"></i>

+ 0 - 0
resources/templates/dashboard/home.twig → resources/templates/dashboard/grid.twig


+ 1 - 1
resources/templates/dashboard/admin.twig → resources/templates/dashboard/list.twig

@@ -1,6 +1,6 @@
 {% extends 'base.twig' %}
 {% extends 'base.twig' %}
 
 
-{% block title %}Admin Home{% endblock %}
+{% block title %}{{ lang('home') }}{% endblock %}
 
 
 {% block content %}
 {% block content %}
     {% include 'comp/navbar.twig' %}
     {% include 'comp/navbar.twig' %}

+ 20 - 0
resources/templates/upload/web.twig

@@ -0,0 +1,20 @@
+{% extends 'base.twig' %}
+
+{% block title %}{{ lang('upload') }}{% endblock %}
+
+{% block content %}
+    {% include 'comp/navbar.twig' %}
+    <div class="container">
+        {% include 'comp/alert.twig' %}
+        <div class="card shadow-sm">
+            <div class="card-body">
+                <form action="{{ route('upload') }}" class="dropzone" id="upload-dropzone">
+                    <div class="fallback">
+                        <input name="file" type="file" multiple>
+                    </div>
+                    <input type="hidden" name="token" value="{{ user.token }}">
+                </form>
+            </div>
+        </div>
+    </div>
+{% endblock %}