From 68f99798ceba6d03a15dd1f0c9ab6850b7b17cfc Mon Sep 17 00:00:00 2001 From: Xavier Oliveira Date: Wed, 27 May 2026 09:24:10 +0100 Subject: [PATCH] feat: paginate workshops and videos pages --- frontend-plataforma-tutoriais/index.html | 4 +- .../package-lock.json | 482 +++++++++++++++++- frontend-plataforma-tutoriais/package.json | 3 + .../src/components/chatbot/index.tsx | 39 ++ .../src/components/chatbot/n8n-chat-theme.css | 51 ++ .../src/components/header/index.tsx | 126 +++-- .../src/components/header/styles.module.css | 31 +- .../src/components/sidebar/styles.module.css | 2 +- .../src/hooks/useGetVideos.ts | 42 +- .../src/hooks/useGetVideosLength.ts | 32 ++ .../src/hooks/useGetVideosSearch.ts | 30 ++ .../src/hooks/useGetWorkshops.ts | 23 +- .../src/hooks/useGetWorkshopsLength.ts | 32 ++ .../src/hooks/useGetWorkshopsSearch.ts | 30 ++ .../src/hooks/useNextVideos.ts | 23 + .../src/hooks/useNextWorkshops.ts | 23 + .../src/hooks/usePreloadImages.ts | 17 - .../src/hooks/useVideoWatch.ts | 29 ++ frontend-plataforma-tutoriais/src/index.css | 246 ++++----- frontend-plataforma-tutoriais/src/main.tsx | 1 + .../src/pages/private/_layout.tsx | 7 +- .../admin/createUser/styles.module.css | 1 + .../pages/private/admin/createVideo/index.tsx | 7 + .../admin/createVideo/styles.module.css | 1 + .../private/admin/createWorkshop/index.tsx | 12 +- .../admin/createWorkshop/styles.module.css | 7 + .../pages/private/admin/editVideo/[id].tsx | 159 +++--- .../private/admin/editVideo/styles.module.css | 13 +- .../pages/private/admin/editWorkshop/[id].tsx | 226 ++++---- .../admin/editWorkshop/styles.module.css | 45 +- .../private/admin/user/styles.module.css | 1 + .../src/pages/private/dashboard/index.tsx | 208 ++++---- .../pages/private/dashboard/styles.module.css | 81 ++- .../pages/private/profile/styles.module.css | 2 +- .../src/pages/private/search/index.tsx | 126 ++--- .../pages/private/search/styles.module.css | 27 +- .../pages/private/utilizador/videos/index.tsx | 9 +- .../utilizador/videos/styles.module.css | 11 + .../private/utilizador/workshops/index.tsx | 11 +- .../utilizador/workshops/styles.module.css | 10 + .../src/pages/private/video/[id].tsx | 143 ++++-- .../src/pages/private/video/styles.module.css | 125 ++++- .../src/pages/private/videos/index.tsx | 263 +++++----- .../pages/private/videos/styles.module.css | 21 +- .../src/pages/private/workshop/[id].tsx | 88 ++-- .../pages/private/workshop/styles.module.css | 20 +- .../src/pages/private/workshops/index.tsx | 359 +++++++------ .../pages/private/workshops/styles.module.css | 12 +- frontend-plataforma-tutoriais/src/types.tsx | 31 +- .../src/utils/imageSkeleton.ts | 10 + plataforma-tutorias/.gitattributes | 26 +- .../app/Http/Controllers/AuthController.php | 2 - .../Http/Controllers/CategoryController.php | 4 +- .../Http/Controllers/VideoViewController.php | 24 + .../app/Http/Controllers/VideosController.php | 219 +++++++- .../Http/Controllers/WorkshopsController.php | 171 +++++-- .../app/Http/Requests/CreateVideoRequest.php | 13 + .../Http/Requests/CreateWorkshopRequest.php | 4 +- .../app/Http/Requests/UpdateVideoRequest.php | 13 + .../Http/Requests/UpdateWorkshopRequest.php | 4 +- plataforma-tutorias/app/Models/Video.php | 56 ++ plataforma-tutorias/app/Models/VideoView.php | 11 + .../Providers/TelescopeServiceProvider.php | 65 +++ plataforma-tutorias/composer.json | 1 + plataforma-tutorias/composer.lock | 127 ++++- plataforma-tutorias/config/app.php | 1 + plataforma-tutorias/config/telescope.php | 212 ++++++++ ..._100000_create_telescope_entries_table.php | 70 +++ ..._05_15_170751_create_video_views_table.php | 31 ++ ...05_26_093322_add_order_to_videos_table.php | 28 + plataforma-tutorias/routes/api.php | 10 +- .../storage/debugbar/.gitignore | 2 + 72 files changed, 3352 insertions(+), 1044 deletions(-) create mode 100644 frontend-plataforma-tutoriais/src/components/chatbot/index.tsx create mode 100644 frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css create mode 100644 frontend-plataforma-tutoriais/src/hooks/useGetVideosLength.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useGetVideosSearch.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsLength.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsSearch.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useNextVideos.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useNextWorkshops.ts delete mode 100644 frontend-plataforma-tutoriais/src/hooks/usePreloadImages.ts create mode 100644 frontend-plataforma-tutoriais/src/hooks/useVideoWatch.ts create mode 100644 frontend-plataforma-tutoriais/src/utils/imageSkeleton.ts create mode 100644 plataforma-tutorias/app/Http/Controllers/VideoViewController.php create mode 100644 plataforma-tutorias/app/Models/VideoView.php create mode 100644 plataforma-tutorias/app/Providers/TelescopeServiceProvider.php create mode 100644 plataforma-tutorias/config/telescope.php create mode 100644 plataforma-tutorias/database/migrations/2018_08_08_100000_create_telescope_entries_table.php create mode 100644 plataforma-tutorias/database/migrations/2026_05_15_170751_create_video_views_table.php create mode 100644 plataforma-tutorias/database/migrations/2026_05_26_093322_add_order_to_videos_table.php create mode 100644 plataforma-tutorias/storage/debugbar/.gitignore diff --git a/frontend-plataforma-tutoriais/index.html b/frontend-plataforma-tutoriais/index.html index 6c2a395..928b727 100644 --- a/frontend-plataforma-tutoriais/index.html +++ b/frontend-plataforma-tutoriais/index.html @@ -15,7 +15,9 @@ -
+
+ +
diff --git a/frontend-plataforma-tutoriais/package-lock.json b/frontend-plataforma-tutoriais/package-lock.json index 951c65a..f34cd3d 100644 --- a/frontend-plataforma-tutoriais/package-lock.json +++ b/frontend-plataforma-tutoriais/package-lock.json @@ -8,7 +8,10 @@ "name": "frontend-plataforma-tutoriais", "version": "0.0.0", "dependencies": { + "@n8n/chat": "^1.21.0", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", + "motion": "^12.40.0", "plyr": "^3.8.4", "plyr-react": "^6.0.0", "react": "^19.2.4", @@ -170,7 +173,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -180,7 +182,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -211,10 +212,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -273,7 +273,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -642,7 +641,6 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -656,6 +654,27 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@n8n/chat": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@n8n/chat/-/chat-1.21.0.tgz", + "integrity": "sha512-7gHwci11kqCjNoPPaw4yopyJY0WdFZrGPsRedlZOdyDcVRsZP7euui1cHmeJQZyp4IcDN9RR8a+AhXFax3952A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@n8n/design-system": "2.21.0", + "@vueuse/core": "^10.11.0", + "highlight.js": "11.8.0", + "markdown-it-link-attributes": "^4.0.1", + "uuid": "10.0.0", + "vue": "^3.5.13", + "vue-markdown-render": "^2.2.1" + } + }, + "node_modules/@n8n/design-system": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@n8n/design-system/-/design-system-2.21.0.tgz", + "integrity": "sha512-pTXjnoH9eS8kTw2s7xGB1TofckVvIPkdyVV5bgSh7FvbnSclKQoNP512IiR5XrmNEPcufZ2tGs6XK1k2ny1jwA==", + "license": "SEE LICENSE IN LICENSE.md" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", @@ -1123,6 +1142,12 @@ "integrity": "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==", "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", @@ -1444,6 +1469,194 @@ } } }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1504,7 +1717,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -1558,6 +1770,22 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -1822,6 +2050,18 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2019,6 +2259,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2119,6 +2365,33 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2197,6 +2470,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", + "integrity": "sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2624,6 +2906,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loadjs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loadjs/-/loadjs-4.3.0.tgz", @@ -2675,6 +2966,56 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-link-attributes": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz", + "integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==", + "license": "MIT" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2688,6 +3029,47 @@ "node": "*" } }, + "node_modules/motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz", + "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.40.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2696,10 +3078,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -2824,7 +3205,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2870,10 +3250,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", - "dev": true, + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -2890,7 +3269,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2942,6 +3321,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rangetouch": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/rangetouch/-/rangetouch-2.0.1.tgz", @@ -3235,7 +3623,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3336,7 +3723,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3370,6 +3757,12 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/uncontrollable": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", @@ -3448,6 +3841,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "8.0.8", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", @@ -3526,6 +3933,39 @@ } } }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-markdown-render": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.3.0.tgz", + "integrity": "sha512-ZWVVKba8t0tKBlaUGaWmNynIk38gE7Bt3psC/iN2NsqpdGY15VGfBeBvF0A8cEmwHnjNVJo2IzUUqkhhfldhtg==", + "license": "MIT", + "dependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/frontend-plataforma-tutoriais/package.json b/frontend-plataforma-tutoriais/package.json index d1ead52..3c14f56 100644 --- a/frontend-plataforma-tutoriais/package.json +++ b/frontend-plataforma-tutoriais/package.json @@ -10,7 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@n8n/chat": "^1.21.0", "bootstrap": "^5.3.8", + "bootstrap-icons": "^1.13.1", + "motion": "^12.40.0", "plyr": "^3.8.4", "plyr-react": "^6.0.0", "react": "^19.2.4", diff --git a/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx b/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx new file mode 100644 index 0000000..e186711 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/components/chatbot/index.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import '@n8n/chat/style.css'; +import { createChat } from '@n8n/chat'; +import './n8n-chat-theme.css'; + +export default function N8nChat() { + useEffect(() => { + const style = document.createElement('style'); + document.head.appendChild(style); + + const app = createChat({ + webhookUrl: 'https://n8n.dev.livetech.pt/webhook/b29d21a8-9fbd-4cdd-810f-30470bffe32e', + target: '#n8n-chat', + mode: 'window', + initialMessages: ['Olá! 👋 Como posso ajudar?'], + i18n: { + en: { + title: 'Assistente AI', + subtitle: '', + footer: '', + getStarted: 'Nova conversa', + inputPlaceholder: 'Escreva a sua pergunta...', + closeButtonTooltip: 'Fechar', + }, + }, + }); + + return () => { + app.unmount(); + }; + }, []); + + return ( +
+ ); +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css b/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css new file mode 100644 index 0000000..0fe3c28 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/components/chatbot/n8n-chat-theme.css @@ -0,0 +1,51 @@ +:root { + --chat--header--background: var(--primary-color); + --chat--header--color: var(--text-white); + --chat--header--title-size: 22px; + --chat--body--background: var(--bg-white); + --chat--color-primary: var(--primary-color); + --chat--color-primary-shade-50: var(--primary-color-dark); + --chat--message--user--background: var(--bg-primary-color-opacity); + --chat--message--user--color: var(--text-primary-color); + --chat--message--bot--background: var(--bg-grey); + --chat--message--bot--color: var(--text-black); + --chat--input--border-color-active: var(--shadow-primary); + --chat--color-disabled: var(--primary-color); + --chat--input--text-color: var(--text-primary-color); + --chat--heading--font-size: 22px; + --chat--border-radius: var(--border-radius); + --chat--message--border-radius: var(--border-radius); + --chat--message--border-color: var(--shadow-primary); + --chat--input--send--button--color: var(--primary-color); + --chat--toggle--background: var(--primary-color); + --chat--toggle--border: 2px solid var(--primary-color); +} + +.chat-window-wrapper .chat-window-toggle { + border: var(--chat--toggle--border) !important; + margin-right: 10px; + margin-bottom: 10px; + transition: all 0.3s ease; +} + +.chat-window-wrapper .chat-window-toggle:hover { + background-color: var(--bg-grey) !important; + color: var(--text-primary-color) !important; +} + +.chat-layout .chat-header h1 { + margin: 0 !important; + letter-spacing: 1px !important; +} + +.chat-inputs textarea { + color: var(--text-black) !important; + align-self: center; +} + +.chat-message a { + color: var(--primary-color) !important; + text-decoration: underline !important; + pointer-events: auto !important; + cursor: pointer !important; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/components/header/index.tsx b/frontend-plataforma-tutoriais/src/components/header/index.tsx index 8d338a6..9526048 100644 --- a/frontend-plataforma-tutoriais/src/components/header/index.tsx +++ b/frontend-plataforma-tutoriais/src/components/header/index.tsx @@ -8,14 +8,17 @@ import type { Video } from "../../types"; import { CgSpinner } from "react-icons/cg"; import { useDebounce } from "../../hooks/useDebounce"; import { useGetVideos } from "../../hooks/useGetVideos"; -import { useGetWorkshops } from "../../hooks/useGetWorkshops"; -import { usePreloadImages } from "../../hooks/usePreloadImages"; +import { useGetVideosLength } from "../../hooks/useGetVideosLength"; +import { useGetVideosSearch } from "../../hooks/useGetVideosSearch"; +import { PiCheckCircleFill } from "react-icons/pi"; +import { useGetWorkshopsSearch } from "../../hooks/useGetWorkshopsSearch"; +import { imageSkeletonFadeStyle, onImageSkeletonLoad } from "../../utils/imageSkeleton"; +import { motion, AnimatePresence } from "framer-motion"; export default function Header() { const [showMenu, setShowMenu] = useState(false); const [showSearch, setShowSearch] = useState(false); const [search, setSearch] = useState(""); - const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(false); const [searchCompleted, setSearchCompleted] = useState(false); const [videosSearched, setVideosSearched] = useState([]); @@ -23,18 +26,37 @@ export default function Header() { const user = JSON.parse(localStorage.getItem("user") || "{}") as User; const isAdmin = user.role_id === 1; const debouncedSearch = useDebounce(search, 500); + const [showDropdown, setShowDropdown] = useState(false); const { getVideos } = useGetVideos(); - const { getWorkshops } = useGetWorkshops(); - const { preloadImages } = usePreloadImages(); + const { getVideosLength } = useGetVideosLength(); + const { getVideosSearch } = useGetVideosSearch(); + const { getWorkshopsSearch } = useGetWorkshopsSearch(); + const [videosStats, setVideosStats] = useState({ + videos: 0, + videosWatched: 0 + }); const navigate = useNavigate(); + useEffect(() => { + if (isAdmin) return; + + const fetchProgressVideos = async () => { + const videosLengthData = await getVideosLength(); + if ("videos" in videosLengthData) { + setVideosStats(videosLengthData as { videos: number, videosWatched: number }); + } + }; + fetchProgressVideos(); + }, []); + const handleCloseMenu = () => setShowMenu(false); const handleShowMenu = () => setShowMenu(true); const handleCloseSearch = () => setShowSearch(false); const handleShowSearch = () => setShowSearch(true); useEffect(() => { + if (debouncedSearch.trim() === "") { setVideosSearched([]); setWorkshopsSearched([]); @@ -47,40 +69,42 @@ export default function Header() { setSearchCompleted(false); const fetchAll = async () => { + try { - const [videosData, workshopsData] = await Promise.all([ - getVideos(debouncedSearch), - getWorkshops(debouncedSearch), + + const [videosData, videosSearchedData, workshopsData] = await Promise.all([ + getVideos({ page: 1 }), + getVideosSearch(debouncedSearch), + getWorkshopsSearch(debouncedSearch), ]); - if (Array.isArray(videosData)) { - setVideosSearched(videosData); + if ("videos" in videosSearchedData) { + setVideosSearched(videosSearchedData.videos); } else { setVideosSearched([]); } - if (Array.isArray(workshopsData)) { - setWorkshopsSearched(workshopsData); + if ("workshops" in workshopsData) { + setWorkshopsSearched(workshopsData.workshops); } else { setWorkshopsSearched([]); } - await preloadImages([ - ...(videosSearched as Video[]).map((v: Video) => `http://127.0.0.1:8000/storage/${v.thumbnail}`), - ...(workshopsSearched as Workshop[]).map((w: Workshop) => `http://127.0.0.1:8000/storage/${w.image}`), - ]); - setSearchCompleted(true); + } catch (e) { + setVideosSearched([]); setWorkshopsSearched([]); setSearchCompleted(true); + } finally { setLoading(false); } }; fetchAll(); + }, [debouncedSearch]); function handleSearch(event: React.FormEvent) { @@ -126,19 +150,21 @@ export default function Header() {
  • Workshops
  • -
  • - Utilizadores -
  • + {isAdmin && ( +
  • + Utilizadores +
  • + )}
  • Contactos
  • - {!isAdmin && ( + {!isAdmin && videosStats.videos > 0 && (
    Vídeos assistidos - 0/{videos.filter((video) => video.is_active).length} + {videosStats.videosWatched}/{videosStats.videos}
    )} @@ -151,14 +177,12 @@ export default function Header() { diff --git a/frontend-plataforma-tutoriais/src/components/header/styles.module.css b/frontend-plataforma-tutoriais/src/components/header/styles.module.css index 0a6cc11..ceb45e3 100644 --- a/frontend-plataforma-tutoriais/src/components/header/styles.module.css +++ b/frontend-plataforma-tutoriais/src/components/header/styles.module.css @@ -114,12 +114,12 @@ width: 200px; text-decoration: none; color: var(--text-white); - background-color: var(--primary-color); - border-radius: 8px; + background: var(--bg-gradient); + box-shadow: var(--shadow-primary); + border-radius: var(--border-radius-button); padding: 10px 12px; font-weight: 600; transition: all 0.3s ease; - &:hover { background-color: var(--secondary-color); } @@ -174,6 +174,9 @@ } .boxVideo { + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: skeleton 1.5s infinite; height: 200px; position: relative; overflow: hidden; @@ -181,7 +184,12 @@ } .boxVideo::after { - content: ''; + content: '\F4F4'; + color: var(--bg-grey); + font-size: 4rem; + align-content: center; + text-align: center; + font-family: 'bootstrap-icons'; position: absolute; top: 0; left: 0; @@ -189,6 +197,11 @@ height: 100%; background: linear-gradient(180deg, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.8) 100%); z-index: 1; + transition: all 0.3s ease; +} + +.boxVideo:hover::after { + color: var(--bg-primary-color); } .boxVideoInfo{ @@ -268,12 +281,17 @@ } .thumbnailWorkshop{ + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: skeleton 1.5s infinite; width: 100%; object-fit: cover; border-top-right-radius: var(--border-radius); border-top-left-radius: var(--border-radius); transition: all 0.3s ease; height: 150px; + position: relative; + overflow: hidden; } .icon{ @@ -284,6 +302,11 @@ animation: spin 1s linear infinite; } +@keyframes skeleton { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + @keyframes spin { from { transform: rotate(0deg); diff --git a/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css b/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css index 6b19f61..15dc290 100644 --- a/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css +++ b/frontend-plataforma-tutoriais/src/components/sidebar/styles.module.css @@ -91,7 +91,7 @@ text-decoration: none; color: var(--text-white); background-color: var(--primary-color); - border-radius: 8px; + border-radius: var(--border-radius-button); padding: 10px 12px; font-weight: 600; transition: all 0.3s ease; diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetVideos.ts b/frontend-plataforma-tutoriais/src/hooks/useGetVideos.ts index 55aad6a..8bd101e 100644 --- a/frontend-plataforma-tutoriais/src/hooks/useGetVideos.ts +++ b/frontend-plataforma-tutoriais/src/hooks/useGetVideos.ts @@ -1,22 +1,42 @@ import type { ApiErrorResponse, Video } from "../types"; +type GetVideosParams = { + page?: number; + search?: string; + category?: string; + status?: "active" | "inactive"; + watched?: 0 | 1; +}; + export function useGetVideos() { - async function getVideos(searchQuery?: string) { - const response = await fetch(`http://127.0.0.1:8000/api/videos?search=${encodeURIComponent(searchQuery ?? "")}`, { - method: "GET", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("token")}` - }, - }); + async function getVideos(params: GetVideosParams) { + const query = new URLSearchParams(); + + if (params.page !== undefined) query.append("page", params.page.toString()); + if (params.search) query.append("search", params.search); + if (params.category) query.append("category", params.category); + if (params.status) query.append("status", params.status); + if (params.watched !== undefined) query.append("watched", params.watched.toString()); + + const response = await fetch(`http://127.0.0.1:8000/api/videos?${query.toString()}`, { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + } + ); const data = await response.json(); if (response.ok) { - return data.data as Video[]; + return { + videos: data.data as Video[], + meta: data.meta + }; } else { - return (data as ApiErrorResponse); + return data as ApiErrorResponse; } } diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetVideosLength.ts b/frontend-plataforma-tutoriais/src/hooks/useGetVideosLength.ts new file mode 100644 index 0000000..db1f9fd --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useGetVideosLength.ts @@ -0,0 +1,32 @@ +import type { ApiErrorResponse } from "../types"; + +export function useGetVideosLength() { + async function getVideosLength() { + try { + const response = await fetch("http://127.0.0.1:8000/api/videos-length", { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + }); + + const data = await response.json(); + + if (response.ok) { + return { + videos: data.data.videos, + videosWatched: data.data.videosWatched, + }; + } + + return data as ApiErrorResponse; + + } catch (error) { + return error as ApiErrorResponse; + } + } + + return { getVideosLength }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetVideosSearch.ts b/frontend-plataforma-tutoriais/src/hooks/useGetVideosSearch.ts new file mode 100644 index 0000000..53b115d --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useGetVideosSearch.ts @@ -0,0 +1,30 @@ +import type { ApiErrorResponse, Video } from "../types"; + +export function useGetVideosSearch() { + async function getVideosSearch(searchQuery: string = "") { + const response = await fetch( + `http://127.0.0.1:8000/api/videos-search?search=${encodeURIComponent(searchQuery)}`, + { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + } + ); + + const data = await response.json(); + + if (response.ok) { + return { + videos: data.data as Video[], + meta: data.meta + }; + } else { + return data as ApiErrorResponse; + } + } + + return { getVideosSearch }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetWorkshops.ts b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshops.ts index 8fa9d2b..0090bc4 100644 --- a/frontend-plataforma-tutoriais/src/hooks/useGetWorkshops.ts +++ b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshops.ts @@ -1,8 +1,22 @@ import type { ApiErrorResponse, Workshop } from "../types"; +type GetWorkshopsParams = { + search?: string; + status?: string; + page?: number; + per_page?: number; +}; + export function useGetWorkshops() { - async function getWorkshops(searchQuery?: string) { - const response = await fetch(`http://127.0.0.1:8000/api/workshops?search=${encodeURIComponent(searchQuery ?? "")}`, { + async function getWorkshops(params: GetWorkshopsParams) { + const query = new URLSearchParams(); + + if (params.search) query.append("search", params.search); + if (params.status) query.append("status", params.status); + if (params.page) query.append("page", params.page.toString()); + if (params.per_page) query.append("per_page", params.per_page.toString()); + + const response = await fetch(`http://127.0.0.1:8000/api/workshops?${query.toString()}`, { method: "GET", headers: { Accept: "application/json", @@ -14,7 +28,10 @@ export function useGetWorkshops() { const data = await response.json(); if (response.ok) { - return data.data as Workshop[]; + return { + workshops: data.data as Workshop[], + meta: data.meta + }; } else { return (data as ApiErrorResponse); } diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsLength.ts b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsLength.ts new file mode 100644 index 0000000..ff911e9 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsLength.ts @@ -0,0 +1,32 @@ +import type { ApiErrorResponse } from "../types"; + +export function useGetWorkshopsLength() { + async function getWorkshopsLength() { + try { + const response = await fetch("http://127.0.0.1:8000/api/workshops-length", { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + }); + + const data = await response.json(); + + if (response.ok) { + return { + workshops: data.data.workshops, + workshopsInscribed: data.data.workshopsInscribed, + }; + } + + return data as ApiErrorResponse; + + } catch (error) { + return error as ApiErrorResponse; + } + } + + return { getWorkshopsLength }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsSearch.ts b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsSearch.ts new file mode 100644 index 0000000..1cfeee2 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useGetWorkshopsSearch.ts @@ -0,0 +1,30 @@ +import type { ApiErrorResponse, Workshop } from "../types"; + +export function useGetWorkshopsSearch() { + async function getWorkshopsSearch(searchQuery: string = "") { + const response = await fetch( + `http://127.0.0.1:8000/api/workshops-search?search=${encodeURIComponent(searchQuery)}`, + { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + } + ); + + const data = await response.json(); + + if (response.ok) { + return { + workshops: data.data as Workshop[], + meta: data.meta + }; + } else { + return data as ApiErrorResponse; + } + } + + return { getWorkshopsSearch }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/useNextVideos.ts b/frontend-plataforma-tutoriais/src/hooks/useNextVideos.ts new file mode 100644 index 0000000..8c91421 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useNextVideos.ts @@ -0,0 +1,23 @@ +import type { ApiErrorResponse, Video } from "../types"; + +export function useNextVideos() { + async function getNextVideos() { + const response = await fetch("http://127.0.0.1:8000/api/next-videos", { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + }); + const data = await response.json(); + if (response.ok) { + return { + videos: data.data as Video[], + }; + } else { + return data as ApiErrorResponse; + } + } + return { getNextVideos }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/useNextWorkshops.ts b/frontend-plataforma-tutoriais/src/hooks/useNextWorkshops.ts new file mode 100644 index 0000000..e681fad --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useNextWorkshops.ts @@ -0,0 +1,23 @@ +import type { ApiErrorResponse, NextWorkshopsResponse } from "../types"; + +export function useNextWorkshops() { + async function getNextWorkshops() { + const response = await fetch("http://127.0.0.1:8000/api/next-workshops", { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + }); + const data = await response.json(); + if (response.ok) { + return { + workshops: data.data as NextWorkshopsResponse[], + }; + } else { + return data as ApiErrorResponse; + } + } + return { getNextWorkshops }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/hooks/usePreloadImages.ts b/frontend-plataforma-tutoriais/src/hooks/usePreloadImages.ts deleted file mode 100644 index 35ff3fa..0000000 --- a/frontend-plataforma-tutoriais/src/hooks/usePreloadImages.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback } from "react"; - -export function usePreloadImages() { - const preloadImages = useCallback ((urls: string[]): Promise => { - return Promise.all( - urls.map(url => new Promise((resolve) => { - const img = new Image(); - img.onload = () => resolve(); - img.onerror = () => resolve(); - img.src = url; - })) - ); - }, []); - - return { preloadImages }; -} - diff --git a/frontend-plataforma-tutoriais/src/hooks/useVideoWatch.ts b/frontend-plataforma-tutoriais/src/hooks/useVideoWatch.ts new file mode 100644 index 0000000..4cab5c4 --- /dev/null +++ b/frontend-plataforma-tutoriais/src/hooks/useVideoWatch.ts @@ -0,0 +1,29 @@ +import { useState, useRef } from "react"; + +export function useVideoWatch(videoId: number, initialWatched: boolean) { + const [watched, setWatched] = useState(initialWatched); + const alreadySent = useRef(false); + + const markAsWatched = async () => { + if (alreadySent.current || watched ) return; + + alreadySent.current = true; + + try { + await fetch(`http://127.0.0.1:8000/api/video/${videoId}/watch`, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}` + }, + }); + + setWatched(true); + } catch (error) { + console.error("Erro ao marcar o vídeo como assistido: ", error); + alreadySent.current = false; + } + } + + return { watched, markAsWatched }; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/index.css b/frontend-plataforma-tutoriais/src/index.css index 986ce25..0bd9dc0 100644 --- a/frontend-plataforma-tutoriais/src/index.css +++ b/frontend-plataforma-tutoriais/src/index.css @@ -1,157 +1,164 @@ :root { - --primary-color: #B20112; - --secondary-color: #C4534A; - --tertiary-color: #0054B0; - --success-color: #08a35d; - --neutral-color: #8A716E; - --primary-contrast-color: #410002; + --primary-color: #B20112; + --secondary-color: #C4534A; + --tertiary-color: #0054B0; + --success-color: #08a35d; + --neutral-color: #8A716E; + --primary-contrast-color: #410002; - --text-primary-color: var(--primary-color); - --text-secondary-color: var(--secondary-color); - --text-tertiary-color: var(--tertiary-color); - --text-neutral-color: var(--neutral-color); - --text-primary-contrast-color: var(--primary-contrast-color); - --text-white: #ffffff; - --text-black: #000000; + --text-primary-color: var(--primary-color); + --text-secondary-color: var(--secondary-color); + --text-tertiary-color: var(--tertiary-color); + --text-neutral-color: var(--neutral-color); + --text-primary-contrast-color: var(--primary-contrast-color); + --text-white: #ffffff; + --text-black: #000000; - --size-font-title: 2.2rem; - --size-font-subtitle: 1.7rem; - --size-font-text: 1.2rem; - --size-font-small: 1rem; + --size-font-title: clamp(1.8rem, calc(1.607vw + 0.629rem), 2.2rem); + --size-font-subtitle: clamp(1.55rem, calc(1.116vw + 0.614rem), 1.7rem); + --size-font-text: clamp(1.2rem, calc(0.394vw + 0.811rem), 1.2rem); + --size-font-small: clamp(1rem, calc(0.295vw + 0.708rem), 1rem); + + --border-radius: 15px; + --border-radius-input: 5px; + --border-radius-button: 10px; + --border-primary-color: var(--primary-color); + --border-secondary-color: var(--secondary-color); + --border-tertiary-color: var(--tertiary-color); + --border-neutral-color: var(--neutral-color); - - --border-radius: 15px; - --border-radius-input: 5px; - --border-radius-button: 10px; - --border-primary-color: var(--primary-color); - --border-secondary-color: var(--secondary-color); - --border-tertiary-color: var(--tertiary-color); - --border-neutral-color: var(--neutral-color); + --bg-white: #ffffff; + --bg-grey: #F3F3F3; + --bg-primary-color: var(--primary-color); + --bg-primary-color-opacity: #FFEDEA; + --bg-secondary-color: var(--secondary-color); + --bg-tertiary-color: var(--tertiary-color); + --bg-tertiary-color-opacity: #CFE2FF; + --bg-success-color-opacity: #D1E7DD; + --bg-neutral-color: var(--neutral-color); + --bg-gradient: linear-gradient(135deg, #b20112 0%, #8e010e 100%); - --bg-white: #ffffff; - --bg-grey: #F3F3F3; - --bg-primary-color: var(--primary-color); - --bg-primary-color-opacity: #FFEDEA; - --bg-secondary-color: var(--secondary-color); - --bg-tertiary-color: var(--tertiary-color); - --bg-tertiary-color-opacity: #CFE2FF; - --bg-success-color-opacity: #D1E7DD; - --bg-neutral-color: var(--neutral-color); - --bg-gradient: linear-gradient(135deg, #b20112 0%, #8e010e 100%); + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow:rgba(0, 0, 0, 0.1) 0 10px 15px -3px, + rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + --shadow-primary:0 4px 10px rgba(178, 1, 18, 0.4), + 0 2px 6px rgba(142, 1, 14, 0.3); + ; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow:rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - --shadow-primary:0 4px 10px rgba(178, 1, 18, 0.4), 0 2px 6px rgba(142, 1, 14, 0.3);; + --font-family: 'Manrope', + sans-serif; - --font-family: 'Manrope', sans-serif; + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } + @media (max-width: 1024px) { + font-size: 16px; + } } @media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); - } + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); + } - #social .button-icon { - filter: invert(1) brightness(2); - } + #social .button-icon { + filter: invert(1) brightness(2); + } } #root { - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; - background: var(--bg-white); + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; + background: var(--bg-white); } -*{ +* { box-sizing: border-box; margin: 0; padding: 0; } body { - margin: 0; - font-family: var(--font-family); - box-sizing: border-box; - margin: 0; - padding: 0; + margin: 0; + font-family: var(--font-family); + box-sizing: border-box; + margin: 0; + padding: 0; } h1, h2 { - font-weight: 500; - color: var(--text-h); + font-weight: 500; + color: var(--text-h); } h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } } + h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + + @media (max-width: 1024px) { + font-size: 20px; + } } + p { - margin: 0; + margin: 0; } code, .counter { - display: inline-flex; - border-radius: 4px; - color: var(--text-h); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); } code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); } /* Pagination */ @@ -161,7 +168,7 @@ html body .pagination .page-item.active .page-link { color: var(--text-primary-color) !important; } -.page-link{ +.page-link { color: var(--text-neutral-color) !important; } @@ -169,10 +176,7 @@ input:focus, textarea:focus, select:focus, button:focus { - outline: none !important; - box-shadow: none !important; - border: none; -} - - - + outline: none !important; + box-shadow: none !important; + border: none; +} \ No newline at end of file diff --git a/frontend-plataforma-tutoriais/src/main.tsx b/frontend-plataforma-tutoriais/src/main.tsx index b797766..18ca7dd 100644 --- a/frontend-plataforma-tutoriais/src/main.tsx +++ b/frontend-plataforma-tutoriais/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { RouterProvider } from 'react-router' import {router} from './routes' import './index.css' +import 'bootstrap-icons/font/bootstrap-icons.css'; createRoot(document.getElementById('root')!).render( diff --git a/frontend-plataforma-tutoriais/src/pages/private/_layout.tsx b/frontend-plataforma-tutoriais/src/pages/private/_layout.tsx index e4c8823..8c3610e 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/_layout.tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/_layout.tsx @@ -1,9 +1,9 @@ -import { Link, Outlet, useNavigate } from "react-router"; +import { Outlet, useNavigate } from "react-router"; import styles from "./styles.module.css"; import Header from "../../components/header"; import Sidebar from "../../components/sidebar"; -import Footer from "../../components/footer"; import { useEffect } from "react"; +import Chatbot from "../../components/chatbot"; export default function ProtectedLayout() { @@ -29,8 +29,11 @@ export default function ProtectedLayout() {
    + + {/*
    */} +
    ); diff --git a/frontend-plataforma-tutoriais/src/pages/private/admin/createUser/styles.module.css b/frontend-plataforma-tutoriais/src/pages/private/admin/createUser/styles.module.css index 95f234e..f280ba8 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/admin/createUser/styles.module.css +++ b/frontend-plataforma-tutoriais/src/pages/private/admin/createUser/styles.module.css @@ -24,6 +24,7 @@ .LinkIcon, .linkText{ color: var(--text-black); + font-size: var(--size-font-text); } .button:hover .LinkIcon, .button:hover .linkText{ diff --git a/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/index.tsx b/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/index.tsx index 9005230..bd1d1c4 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/index.tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/index.tsx @@ -19,6 +19,7 @@ export default function CreateVideo() { const [checkingRole, setCheckingRole] = useState(true); const [creating, setCreating] = useState(false); const [categories, setCategories] = useState([]); + const [order, setOrder] = useState(0); setTimeout(() => { setCheckingRole(false); @@ -68,6 +69,7 @@ export default function CreateVideo() { formData.append("url", videoFile); formData.append("thumbnail", thumbnailFile); formData.append("tags", tags); + formData.append("order", order.toString()); category_ids.forEach(id => { formData.append("category_ids[]", id); @@ -112,6 +114,7 @@ export default function CreateVideo() { setVideoFile(null); setThumbnailFile(null); setTags(""); + setOrder(0); setCategoryIds([]); setCreating(false); Swal.fire({ @@ -248,6 +251,10 @@ export default function CreateVideo() { setThumbnailFile(e.target.files?.[0] ?? null)} /> +
    + + setOrder(parseInt(e.target.value, 10))} /> +
    diff --git a/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/styles.module.css b/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/styles.module.css index 832cf9f..67dd5cf 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/styles.module.css +++ b/frontend-plataforma-tutoriais/src/pages/private/admin/createVideo/styles.module.css @@ -6,6 +6,7 @@ .LinkIcon, .linkText{ color: var(--text-black); + font-size: var(--size-font-text); } .button:hover .LinkIcon, .button:hover .linkText{ diff --git a/frontend-plataforma-tutoriais/src/pages/private/admin/createWorkshop/index.tsx b/frontend-plataforma-tutoriais/src/pages/private/admin/createWorkshop/index.tsx index 2c6784a..b454694 100644 --- a/frontend-plataforma-tutoriais/src/pages/private/admin/createWorkshop/index.tsx +++ b/frontend-plataforma-tutoriais/src/pages/private/admin/createWorkshop/index.tsx @@ -110,22 +110,22 @@ export default function CreateWorkshop() {

    Adicionar Workshop

    - + setTitle(e.target.value)} required />
    - +