diff --git a/frontend/package.json b/frontend/package.json index 4768fe1..a76bd1d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,8 @@ }, "dependencies": { "pinia": "^3.0.4", - "vue": "^3.5.32" + "vue": "^3.5.32", + "vue-router": "^5.0.6" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fd8113a..d623233 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: vue: specifier: ^3.5.32 version: 3.5.34(typescript@6.0.3) + vue-router: + specifier: ^5.0.6 + version: 5.0.6(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)) devDependencies: '@tsconfig/node24': specifier: ^24.0.4 @@ -23,7 +26,7 @@ importers: version: 24.12.2 '@vitejs/plugin-vue': specifier: ^6.0.6 - version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3)) '@vue/eslint-config-typescript': specifier: ^14.7.0 version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0))))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) @@ -59,10 +62,10 @@ importers: version: 6.0.3 vite: specifier: ^8.0.8 - version: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + version: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) vite-plugin-vue-devtools: specifier: ^8.1.1 - version: 8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)) + version: 8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.8(typescript@6.0.3) @@ -624,6 +627,15 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + '@vue/babel-helper-vue-transform-on@1.5.0': resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} @@ -655,6 +667,9 @@ packages: '@vue/devtools-api@7.7.9': resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + '@vue/devtools-api@8.1.1': + resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} + '@vue/devtools-core@8.1.1': resolution: {integrity: sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A==} peerDependencies: @@ -738,6 +753,14 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -773,6 +796,16 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -908,6 +941,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1138,6 +1174,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1145,6 +1185,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1167,6 +1211,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1274,6 +1321,12 @@ packages: typescript: optional: true + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -1295,6 +1348,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1302,6 +1358,10 @@ packages: resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} engines: {node: ^18.17.0 || >=20.5.0} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1321,6 +1381,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1395,6 +1458,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -1402,6 +1468,10 @@ packages: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1497,6 +1567,21 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-router@5.0.6: + resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vue-tsc@3.2.8: resolution: {integrity: sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==} hasBin: true @@ -1511,6 +1596,9 @@ packages: typescript: optional: true + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1536,6 +1624,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2055,10 +2148,10 @@ snapshots: '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.6(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) vue: 3.5.34(typescript@6.0.3) '@volar/language-core@2.4.28': @@ -2073,6 +2166,16 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 + '@vue-macros/common@3.1.2(vue@3.5.34(typescript@6.0.3))': + dependencies: + '@vue/compiler-sfc': 3.5.34 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.34(typescript@6.0.3) + '@vue/babel-helper-vue-transform-on@1.5.0': {} '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': @@ -2136,6 +2239,10 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.9 + '@vue/devtools-api@8.1.1': + dependencies: + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-core@8.1.1(vue@3.5.34(typescript@6.0.3))': dependencies: '@vue/devtools-kit': 8.1.1 @@ -2236,6 +2343,16 @@ snapshots: ansis@4.2.0: {} + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.3 + pathe: 2.0.3 + + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.3 + ast-kit: 2.2.0 + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.27: {} @@ -2266,6 +2383,14 @@ snapshots: caniuse-lite@1.0.30001792: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + convert-source-map@2.0.0: {} copy-anything@4.0.5: @@ -2399,6 +2524,8 @@ snapshots: esutils@2.0.3: {} + exsolve@1.0.8: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -2564,6 +2691,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2572,6 +2705,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2591,6 +2728,13 @@ snapshots: mitt@3.0.1: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + mrmime@2.0.1: {} ms@2.1.3: {} @@ -2695,6 +2839,18 @@ snapshots: optionalDependencies: typescript: 6.0.3 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -2712,6 +2868,8 @@ snapshots: punycode@2.3.1: {} + quansync@0.2.11: {} + queue-microtask@1.2.3: {} read-package-json-fast@4.0.0: @@ -2719,6 +2877,8 @@ snapshots: json-parse-even-better-errors: 4.0.0 npm-normalize-package-bin: 4.0.0 + readdirp@5.0.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -2750,6 +2910,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + scule@1.3.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -2811,6 +2973,8 @@ snapshots: typescript@6.0.3: {} + ufo@1.6.4: {} + undici-types@7.16.0: {} unplugin-utils@0.3.1: @@ -2818,6 +2982,12 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.4 + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2830,17 +3000,17 @@ snapshots: util-deprecate@1.0.2: {} - vite-dev-rpc@1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + vite-dev-rpc@1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)): dependencies: birpc: 2.9.0 - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) - vite-hot-client: 2.2.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) + vite-hot-client: 2.2.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)) - vite-hot-client@2.2.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + vite-hot-client@2.2.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)): dependencies: - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) - vite-plugin-inspect@11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + vite-plugin-inspect@11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)): dependencies: ansis: 4.2.0 debug: 4.4.3 @@ -2850,26 +3020,26 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) - vite-dev-rpc: 1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) + vite-dev-rpc: 1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)) transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)): + vite-plugin-vue-devtools@8.1.1(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4))(vue@3.5.34(typescript@6.0.3)): dependencies: '@vue/devtools-core': 8.1.1(vue@3.5.34(typescript@6.0.3)) '@vue/devtools-kit': 8.1.1 '@vue/devtools-shared': 8.1.1 sirv: 3.0.2 - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) - vite-plugin-inspect: 11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)) - vite-plugin-vue-inspector: 5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) + vite-plugin-inspect: 11.3.3(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)) + vite-plugin-vue-inspector: 5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)) transitivePeerDependencies: - '@nuxt/kit' - supports-color - vue - vite-plugin-vue-inspector@5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + vite-plugin-vue-inspector@5.4.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -2880,11 +3050,11 @@ snapshots: '@vue/compiler-dom': 3.5.34 kolorist: 1.8.0 magic-string: 0.30.21 - vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4) transitivePeerDependencies: - supports-color - vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0): + vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)(yaml@2.8.4): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2895,6 +3065,7 @@ snapshots: '@types/node': 24.12.2 fsevents: 2.3.3 jiti: 2.7.0 + yaml: 2.8.4 vscode-uri@3.1.0: {} @@ -2910,6 +3081,30 @@ snapshots: transitivePeerDependencies: - supports-color + vue-router@5.0.6(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)): + dependencies: + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.34(typescript@6.0.3)) + '@vue/devtools-api': 8.1.1 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.16 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.34(typescript@6.0.3) + yaml: 2.8.4 + optionalDependencies: + '@vue/compiler-sfc': 3.5.34 + pinia: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)) + vue-tsc@3.2.8(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 @@ -2926,6 +3121,8 @@ snapshots: optionalDependencies: typescript: 6.0.3 + webpack-virtual-modules@0.6.2: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2944,4 +3141,6 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.4: {} + yocto-queue@0.1.0: {} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index abfd315..349edd7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,96 @@ - + - + diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000..7ca78da --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1,323 @@ +/* UniVerse – Aero Green Design System */ + +:root { + --color-primary: #22C55E; + --color-primary-dark: #16A34A; + --color-primary-light: #86EFAC; + --color-aqua: #06B6D4; + --color-sky: #7DD3FC; + --color-white-glass: rgba(255, 255, 255, 0.75); + --color-surface: rgba(255, 255, 255, 0.85); + --color-border-glass: rgba(255, 255, 255, 0.8); + --color-text: #1E293B; + --color-text-secondary: #64748B; + --color-bg-start: #E0F2FE; + --color-bg-mid: #DCFCE7; + --color-error: #EF4444; + --color-success: #22C55E; + --color-warning: #F59E0B; + --gradient-bg: linear-gradient(135deg, #E0F2FE 0%, #DCFCE7 50%, #E0F2FE 100%); + --gradient-brand: linear-gradient(135deg, #22C55E 0%, #06B6D4 60%, #7DD3FC 100%); + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.9); + --shadow-card: 0 4px 16px rgba(0, 0, 0, 0.06); + --sidebar-width: 240px; + --topbar-height: 60px; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + color: var(--color-text); + -webkit-font-smoothing: antialiased; +} + +body { + background: var(--gradient-bg); + background-attachment: fixed; + min-height: 100vh; +} + + +a { + color: var(--color-primary-dark); + text-decoration: none; +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + outline: none; +} + +input, textarea, select { + font-family: inherit; + outline: none; +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: rgba(0,0,0,0.04); border-radius: 3px; } +::-webkit-scrollbar-thumb { background: rgba(34,197,94,0.3); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(34,197,94,0.5); } + +/* Glass panel utility */ +.glass-panel { + background: var(--color-white-glass); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--color-border-glass); + border-radius: var(--radius-md); + box-shadow: var(--shadow-glass); +} + +/* Primary glossy button */ +.btn-primary { + background: linear-gradient(180deg, #4ADE80 0%, #22C55E 50%, #16A34A 100%); + border: 1px solid #15803D; + border-radius: var(--radius-sm); + color: white; + padding: 10px 20px; + font-weight: 600; + font-size: 14px; + position: relative; + overflow: hidden; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; +} +.btn-primary::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 50%; + background: linear-gradient(180deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.1) 100%); + border-radius: 8px 8px 0 0; + pointer-events: none; +} +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(34,197,94,0.4); +} +.btn-primary:active { + transform: translateY(0); +} +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.btn-sm { + padding: 6px 12px; + font-size: 12px; +} + +/* Secondary button */ +.btn-secondary { + background: rgba(255,255,255,0.7); + border: 1px solid rgba(34,197,94,0.4); + border-radius: var(--radius-sm); + color: var(--color-primary-dark); + padding: 10px 20px; + font-weight: 600; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; + backdrop-filter: blur(8px); +} +.btn-secondary:hover { + background: rgba(34,197,94,0.1); + border-color: var(--color-primary); +} + +/* Danger button */ +.btn-danger { + background: linear-gradient(180deg, #FCA5A5 0%, #EF4444 50%, #DC2626 100%); + border: 1px solid #B91C1C; + border-radius: var(--radius-sm); + color: white; + padding: 10px 20px; + font-weight: 600; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; +} +.btn-danger:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(239,68,68,0.4); +} + +/* Ghost button */ +.btn-ghost { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + padding: 8px 14px; + font-weight: 500; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; +} +.btn-ghost:hover { + background: rgba(0,0,0,0.05); + color: var(--color-text); +} + +/* Glass input */ +.glass-input { + background: rgba(255,255,255,0.6); + border: 1px solid rgba(255,255,255,0.8); + border-radius: var(--radius-sm); + padding: 10px 14px; + font-size: 14px; + color: var(--color-text); + width: 100%; + transition: all 0.2s; + backdrop-filter: blur(8px); +} +.glass-input:focus { + background: rgba(255,255,255,0.85); + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(34,197,94,0.15); +} +.glass-input::placeholder { + color: var(--color-text-secondary); +} + +/* Badge */ +.badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + white-space: nowrap; +} +.badge-green { background: rgba(34,197,94,0.15); color: #15803D; border: 1px solid rgba(34,197,94,0.3); } +.badge-blue { background: rgba(6,182,212,0.15); color: #0E7490; border: 1px solid rgba(6,182,212,0.3); } +.badge-orange { background: rgba(251,146,60,0.15); color: #C2410C; border: 1px solid rgba(251,146,60,0.3); } +.badge-gray { background: rgba(100,116,139,0.1); color: #64748B; border: 1px solid rgba(100,116,139,0.2); } +.badge-red { background: rgba(239,68,68,0.12); color: #B91C1C; border: 1px solid rgba(239,68,68,0.2); } +.badge-purple { background: rgba(139,92,246,0.12); color: #6D28D9; border: 1px solid rgba(139,92,246,0.2); } + +/* Tag chip */ +.tag-chip { + display: inline-flex; + align-items: center; + padding: 3px 10px; + background: rgba(34,197,94,0.1); + border: 1px solid rgba(34,197,94,0.25); + border-radius: 20px; + font-size: 12px; + color: var(--color-primary-dark); + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} +.tag-chip:hover, +.tag-chip.active { + background: rgba(34,197,94,0.2); + border-color: var(--color-primary); +} + +/* Page layout helpers */ +.page-content { + padding: 24px; + max-width: 1200px; + margin: 0 auto; +} + +.page-title { + font-size: 22px; + font-weight: 700; + color: var(--color-text); + margin-bottom: 20px; +} + +.section-title { + font-size: 16px; + font-weight: 700; + color: var(--color-text); + margin-bottom: 14px; +} + +/* Grid helpers */ +.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } +.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } + +@media (max-width: 768px) { + .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } + .page-content { padding: 16px; } +} + +/* Flex helpers */ +.flex { display: flex; } +.flex-col { display: flex; flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.gap-4 { gap: 16px; } + +/* Text helpers */ +.text-sm { font-size: 12px; } +.text-secondary { color: var(--color-text-secondary); } +.font-bold { font-weight: 700; } +.font-semibold { font-weight: 600; } + +/* Stars rating */ +.stars { color: #FBBF24; font-size: 14px; letter-spacing: 1px; } + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +.fade-in { animation: fadeIn 0.3s ease; } + +@keyframes spin { + to { transform: rotate(360deg); } +} +.spinner { + width: 20px; height: 20px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + display: inline-block; +} +.spinner-green { + border-color: rgba(34,197,94,0.2); + border-top-color: var(--color-primary); +} + + +#app { + min-height: 100vh; +} diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..d174cf8 --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/TheWelcome.vue b/frontend/src/components/TheWelcome.vue new file mode 100644 index 0000000..8b731d9 --- /dev/null +++ b/frontend/src/components/TheWelcome.vue @@ -0,0 +1,95 @@ + + + diff --git a/frontend/src/components/WelcomeItem.vue b/frontend/src/components/WelcomeItem.vue new file mode 100644 index 0000000..6d7086a --- /dev/null +++ b/frontend/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/icons/IconCommunity.vue b/frontend/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000..2dc8b05 --- /dev/null +++ b/frontend/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/src/components/icons/IconDocumentation.vue b/frontend/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000..6d4791c --- /dev/null +++ b/frontend/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/src/components/icons/IconEcosystem.vue b/frontend/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000..c3a4f07 --- /dev/null +++ b/frontend/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/src/components/icons/IconSupport.vue b/frontend/src/components/icons/IconSupport.vue new file mode 100644 index 0000000..7452834 --- /dev/null +++ b/frontend/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/frontend/src/components/icons/IconTooling.vue b/frontend/src/components/icons/IconTooling.vue new file mode 100644 index 0000000..660598d --- /dev/null +++ b/frontend/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/frontend/src/components/layout/AppBottomNav.vue b/frontend/src/components/layout/AppBottomNav.vue new file mode 100644 index 0000000..83fa441 --- /dev/null +++ b/frontend/src/components/layout/AppBottomNav.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue new file mode 100644 index 0000000..3f850af --- /dev/null +++ b/frontend/src/components/layout/AppSidebar.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/frontend/src/components/layout/AppTopbar.vue b/frontend/src/components/layout/AppTopbar.vue new file mode 100644 index 0000000..22a4fa2 --- /dev/null +++ b/frontend/src/components/layout/AppTopbar.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/frontend/src/components/ui/AchievementBadge.vue b/frontend/src/components/ui/AchievementBadge.vue new file mode 100644 index 0000000..089bd07 --- /dev/null +++ b/frontend/src/components/ui/AchievementBadge.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/frontend/src/components/ui/CoinChip.vue b/frontend/src/components/ui/CoinChip.vue new file mode 100644 index 0000000..c22ab54 --- /dev/null +++ b/frontend/src/components/ui/CoinChip.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/components/ui/DataTable.vue b/frontend/src/components/ui/DataTable.vue new file mode 100644 index 0000000..7317a96 --- /dev/null +++ b/frontend/src/components/ui/DataTable.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/components/ui/EmptyState.vue b/frontend/src/components/ui/EmptyState.vue new file mode 100644 index 0000000..8a8eeb9 --- /dev/null +++ b/frontend/src/components/ui/EmptyState.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/components/ui/FilterChips.vue b/frontend/src/components/ui/FilterChips.vue new file mode 100644 index 0000000..ecf52b1 --- /dev/null +++ b/frontend/src/components/ui/FilterChips.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/frontend/src/components/ui/GlassCard.vue b/frontend/src/components/ui/GlassCard.vue new file mode 100644 index 0000000..8db1116 --- /dev/null +++ b/frontend/src/components/ui/GlassCard.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/frontend/src/components/ui/LectureCard.vue b/frontend/src/components/ui/LectureCard.vue new file mode 100644 index 0000000..b7b5621 --- /dev/null +++ b/frontend/src/components/ui/LectureCard.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/frontend/src/components/ui/LoadingSpinner.vue b/frontend/src/components/ui/LoadingSpinner.vue new file mode 100644 index 0000000..947f283 --- /dev/null +++ b/frontend/src/components/ui/LoadingSpinner.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/components/ui/ModalDialog.vue b/frontend/src/components/ui/ModalDialog.vue new file mode 100644 index 0000000..8a4a3b2 --- /dev/null +++ b/frontend/src/components/ui/ModalDialog.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/frontend/src/components/ui/ProgressBar.vue b/frontend/src/components/ui/ProgressBar.vue new file mode 100644 index 0000000..3b2a807 --- /dev/null +++ b/frontend/src/components/ui/ProgressBar.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/components/ui/SearchInput.vue b/frontend/src/components/ui/SearchInput.vue new file mode 100644 index 0000000..08f75a9 --- /dev/null +++ b/frontend/src/components/ui/SearchInput.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/components/ui/StatsWidget.vue b/frontend/src/components/ui/StatsWidget.vue new file mode 100644 index 0000000..2ea9e3c --- /dev/null +++ b/frontend/src/components/ui/StatsWidget.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/src/components/ui/StatusBadge.vue b/frontend/src/components/ui/StatusBadge.vue new file mode 100644 index 0000000..84a84df --- /dev/null +++ b/frontend/src/components/ui/StatusBadge.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend/src/components/ui/ToastNotification.vue b/frontend/src/components/ui/ToastNotification.vue new file mode 100644 index 0000000..d1f6b78 --- /dev/null +++ b/frontend/src/components/ui/ToastNotification.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 5f77a89..5dcad83 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,9 +1,14 @@ +import './assets/main.css' + import { createApp } from 'vue' import { createPinia } from 'pinia' + import App from './App.vue' +import router from './router' const app = createApp(App) app.use(createPinia()) +app.use(router) app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..3201561 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,41 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { path: '/login', name: 'login', component: () => import('@/views/auth/LoginView.vue'), meta: { public: true } }, + + // Student + { path: '/', name: 'dashboard', component: () => import('@/views/student/DashboardView.vue'), meta: { role: 'student' } }, + { path: '/catalog', name: 'catalog', component: () => import('@/views/student/CatalogView.vue'), meta: { role: 'student' } }, + { path: '/lecture/:id', name: 'lecture-detail', component: () => import('@/views/student/LectureDetailView.vue'), meta: { role: 'student' } }, + { path: '/my-lectures', name: 'my-lectures', component: () => import('@/views/student/MyLecturesView.vue'), meta: { role: 'student' } }, + { path: '/review/:id', name: 'review-form', component: () => import('@/views/student/ReviewFormView.vue'), meta: { role: 'student' } }, + { path: '/profile', name: 'profile', component: () => import('@/views/student/ProfileView.vue') }, + { path: '/achievements', name: 'achievements', component: () => import('@/views/student/AchievementsView.vue'), meta: { role: 'student' } }, + { path: '/notifications', name: 'notifications', component: () => import('@/views/student/NotificationsView.vue') }, + + // Teacher + { path: '/teacher', name: 'teacher-dashboard', component: () => import('@/views/teacher/TeacherDashboardView.vue'), meta: { role: 'teacher' } }, + { path: '/teacher/lectures', name: 'teacher-lectures', component: () => import('@/views/teacher/TeacherLecturesView.vue'), meta: { role: 'teacher' } }, + { path: '/teacher/analytics', name: 'teacher-analytics', component: () => import('@/views/teacher/TeacherAnalyticsView.vue'), meta: { role: 'teacher' } }, + + // Admin + { path: '/admin', name: 'admin-dashboard', component: () => import('@/views/admin/AdminDashboardView.vue'), meta: { role: 'admin' } }, + { path: '/admin/users', name: 'admin-users', component: () => import('@/views/admin/AdminUsersView.vue'), meta: { role: 'admin' } }, + { path: '/admin/lectures', name: 'admin-lectures', component: () => import('@/views/admin/AdminLecturesView.vue'), meta: { role: 'admin' } }, + { path: '/admin/llm-queue', name: 'admin-llm', component: () => import('@/views/admin/AdminLLMQueueView.vue'), meta: { role: 'admin' } }, + + { path: '/:pathMatch(.*)*', redirect: '/' }, + ], +}) + +router.beforeEach((to) => { + const auth = useAuthStore() + if (!to.meta.public && !auth.isAuthenticated) { + return '/login' + } +}) + +export default router diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..df2125d --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,90 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { User, UserRole } from '@/types' + +const defaultUsers: Record = { + student: { + id: 'stu-1', + name: 'Алексей Морозов', + email: 'a.morozov@sfedu.ru', + role: 'student', + institute: 'ИКТИБ', + department: 'Программная инженерия', + year: 3, + direction: 'Программная инженерия', + coins: 340, + level: 3, + xp: 120, + lecturesAttended: 12, + hoursLearned: 18.5, + achievements: ['1', '2', '3'], + }, + teacher: { + id: 't-1', + name: 'Михаил Сергеевич Волков', + email: 'm.volkov@sfedu.ru', + role: 'teacher', + institute: 'ИКТИБ', + department: 'каф. Информатики', + year: 0, + direction: 'Информатика и вычислительная техника', + coins: 90, + level: 4, + xp: 240, + lecturesAttended: 24, + hoursLearned: 56, + achievements: ['1', '2', '3', '4'], + }, + admin: { + id: 'adm-1', + name: 'Виктор Алексеев', + email: 'admin@sfedu.ru', + role: 'admin', + institute: 'ЮФУ', + department: 'Администрация', + year: 0, + direction: 'Цифровое развитие', + coins: 0, + level: 5, + xp: 500, + lecturesAttended: 0, + hoursLearned: 0, + achievements: [], + }, +} + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const isAuthenticated = ref(false) + const loading = ref(false) + const error = ref(null) + + async function login(role: UserRole = 'student', shouldFail = false) { + loading.value = true + error.value = null + await new Promise(r => setTimeout(r, 800)) + if (shouldFail) { + loading.value = false + error.value = 'Не удалось подтвердить доступ через ЮФУ. Попробуйте еще раз.' + return false + } + user.value = { ...defaultUsers[role] } + isAuthenticated.value = true + loading.value = false + return true + } + + function logout() { + user.value = null + isAuthenticated.value = false + } + + function switchRole(role?: UserRole) { + if (!user.value) return + const roles: UserRole[] = ['student', 'teacher', 'admin'] + const nextRole = (role ?? roles[(roles.indexOf(user.value.role) + 1) % roles.length]) as UserRole + user.value = { ...defaultUsers[nextRole] } + } + + return { user, isAuthenticated, loading, error, login, logout, switchRole } +}) diff --git a/frontend/src/stores/lectures.ts b/frontend/src/stores/lectures.ts new file mode 100644 index 0000000..97fa342 --- /dev/null +++ b/frontend/src/stores/lectures.ts @@ -0,0 +1,163 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import type { Lecture } from '@/types' + +export const LECTURES: Lecture[] = [ + { + id: '1', + title: 'Введение в нейронные сети и глубокое обучение', + description: 'Лекция охватывает базовые концепции нейронных сетей: перцептрон, многослойные сети, метод обратного распространения ошибки, а также современные архитектуры — CNN, RNN, Transformer. Рассматриваются практические примеры на Python с использованием PyTorch.', + teacher: 'Волков М.С.', + teacherTitle: 'Профессор', + department: 'каф. Информатики', + institute: 'ИКТИБ', + date: '2025-05-07', + time: '14:00', + duration: 90, + building: 'ИКТИБ', + room: '305', + format: 'offline', + totalSeats: 30, + freeSeats: 12, + tags: ['#ML', '#ИИ', '#Python', '#нейросети'], + rating: 4.8, + reviewCount: 24, + status: 'upcoming', + }, + { + id: '2', + title: 'Квантовые вычисления: от теории к практике', + description: 'Обзор квантовых алгоритмов и их применения. Рассматриваются кубиты, суперпозиция, запутанность, алгоритмы Шора и Гровера, а также введение в программирование на Qiskit.', + teacher: 'Петров А.И.', + teacherTitle: 'Доцент', + department: 'каф. Теоретической физики', + institute: 'ИФиМКН', + date: '2025-05-08', + time: '16:00', + duration: 120, + building: 'ИФиМКН', + room: '201', + format: 'offline', + totalSeats: 25, + freeSeats: 5, + tags: ['#квантовые-вычисления', '#физика', '#алгоритмы'], + rating: 4.6, + reviewCount: 18, + status: 'upcoming', + }, + { + id: '3', + title: 'Современные методы биоинформатики', + description: 'Введение в биоинформатику: анализ последовательностей ДНК/РНК, геномная сборка, аннотация генов, инструменты BLAST, Biopython. Актуальные задачи вычислительной биологии.', + teacher: 'Смирнова Е.В.', + teacherTitle: 'Доктор биологических наук', + department: 'каф. Биологии', + institute: 'АГиС', + date: '2025-05-09', + time: '10:00', + duration: 90, + building: 'АГиС', + room: '118', + format: 'offline', + totalSeats: 20, + freeSeats: 2, + tags: ['#биоинформатика', '#генетика', '#Python'], + rating: 4.7, + reviewCount: 31, + status: 'upcoming', + }, + { + id: '4', + title: 'Философия цифровой эпохи', + description: 'Как цифровые технологии меняют мышление, идентичность и общество. Тема охватывает этику ИИ, постгуманизм, цифровой дуализм и проблему сознания в эпоху автоматизации.', + teacher: 'Дмитриев К.О.', + teacherTitle: 'Кандидат философских наук', + department: 'каф. Философии', + institute: 'ИФиСН', + date: '2025-05-10', + time: '18:00', + duration: 90, + building: 'Онлайн', + format: 'online', + totalSeats: 40, + freeSeats: 16, + tags: ['#философия', '#этика', '#ИИ'], + rating: 4.5, + reviewCount: 42, + status: 'upcoming', + }, + { + id: '5', + title: 'Право в информационном обществе', + description: 'Правовые аспекты работы с данными: GDPR, ФЗ-152, авторское право в сети, кибербезопасность с точки зрения права, ответственность разработчиков и операторов персональных данных.', + teacher: 'Захарова Н.А.', + teacherTitle: 'Доцент', + department: 'каф. Гражданского права', + institute: 'ЮФ', + date: '2025-05-12', + time: '15:30', + duration: 90, + building: 'ЮФ', + room: '412', + format: 'offline', + totalSeats: 30, + freeSeats: 0, + registrationClosed: true, + tags: ['#право', '#данные', '#GDPR'], + rating: 4.4, + reviewCount: 15, + status: 'upcoming', + }, + { + id: '6', + title: 'Нейромаркетинг и поведение потребителей', + description: 'Как нейронауки применяются в маркетинге: eye-tracking, EEG-анализ реакций, влияние UX на покупки, нейропсихология принятия решений и кейсы ведущих брендов.', + teacher: 'Орлов П.Р.', + teacherTitle: 'Кандидат экономических наук', + department: 'каф. Маркетинга', + institute: 'ИУЭиП', + date: '2025-05-14', + time: '11:00', + duration: 120, + building: 'Онлайн', + format: 'online', + totalSeats: 35, + freeSeats: 27, + tags: ['#маркетинг', '#нейронауки', '#поведение'], + rating: 4.3, + reviewCount: 9, + status: 'upcoming', + }, +] + +export const useLecturesStore = defineStore('lectures', () => { + const lectures = ref(LECTURES) + const registered = ref(['1', '3']) + + const all = computed(() => lectures.value) + const registeredIds = computed(() => registered.value) + const registeredLectures = computed(() => + lectures.value.filter(l => registered.value.includes(l.id)) + ) + + function register(lectureId: string) { + if (!registered.value.includes(lectureId)) { + const l = lectures.value.find(x => x.id === lectureId) + if (!l || l.freeSeats === 0 || l.registrationClosed) return + registered.value.push(lectureId) + l.freeSeats-- + } + } + + function unregister(lectureId: string) { + registered.value = registered.value.filter(id => id !== lectureId) + const l = lectures.value.find(x => x.id === lectureId) + if (l) l.freeSeats++ + } + + function isRegistered(lectureId: string) { + return registered.value.includes(lectureId) + } + + return { lectures, registered, all, registeredIds, registeredLectures, register, unregister, isRegistered } +}) diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..efb8e2d --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { Achievement, Notification, CoinTransaction } from '@/types' + +export const useUserStore = defineStore('user', () => { + const achievements = ref([ + { id: '1', title: 'Первый отзыв', description: 'Оставьте первый отзыв о лекции', icon: '⭐', unlocked: true, unlockedAt: '2025-04-10', coins: 20 }, + { id: '2', title: 'Межфакультетский исследователь', description: 'Посетите лекции 3 разных институтов', icon: '🔭', unlocked: true, unlockedAt: '2025-04-18', coins: 50 }, + { id: '3', title: '10 часов лекций', description: 'Наберите 10 часов посещённых лекций', icon: '⏱', unlocked: true, unlockedAt: '2025-04-25', coins: 30 }, + { id: '4', title: 'Полезный критик', description: 'Получите 5 монет за качественный отзыв', icon: '💡', unlocked: false }, + { id: '5', title: 'Знаток науки', description: 'Посетите лекции по 5 разным тематикам', icon: '🎓', unlocked: false }, + { id: '6', title: 'Ранний пташка', description: 'Запишитесь на лекцию за 7 дней до начала', icon: '🌅', unlocked: false }, + { id: '7', title: 'Социальная бабочка', description: 'Приведите друга на межфакультетскую лекцию', icon: '🦋', unlocked: false }, + ]) + + const notifications = ref([ + { id: '1', type: 'reminder', title: 'Напоминание о лекции', body: 'Завтра в 14:00 — «Введение в нейронные сети». Ауд. 305, ИКТИБ', read: false, createdAt: '2025-05-06T09:00:00' }, + { id: '2', type: 'coins', title: 'Начислено 20 монет', body: 'Ваш отзыв о лекции «Квантовые вычисления» признан полезным', read: false, createdAt: '2025-05-05T18:30:00' }, + { id: '3', type: 'achievement', title: 'Новое достижение!', body: 'Вы получили значок «Межфакультетский исследователь» 🔭', read: false, createdAt: '2025-05-04T12:00:00' }, + { id: '4', type: 'recommendation', title: 'Рекомендация для вас', body: 'Новая лекция «Нейромаркетинг» — может быть интересна вам', read: true, createdAt: '2025-05-03T10:00:00' }, + { id: '5', type: 'schedule-change', title: 'Изменение расписания', body: 'Лекция «Философия цифровой эпохи» перенесена с 18:00 на 19:00', read: true, createdAt: '2025-05-02T16:00:00' }, + { id: '6', type: 'coins', title: 'Начислено 30 монет', body: 'Поздравляем с достижением «10 часов лекций»!', read: true, createdAt: '2025-04-25T11:00:00' }, + ]) + + const coinHistory = ref([ + { id: '1', date: '2025-05-05', description: 'Полезный отзыв о лекции', amount: 20, type: 'earned' }, + { id: '2', date: '2025-04-25', description: 'Достижение «10 часов лекций»', amount: 30, type: 'earned' }, + { id: '3', date: '2025-04-18', description: 'Достижение «Исследователь»', amount: 50, type: 'earned' }, + { id: '4', date: '2025-04-10', description: 'Первый отзыв', amount: 20, type: 'earned' }, + { id: '5', date: '2025-04-05', description: 'Покупка стикерпака ЮФУ', amount: -80, type: 'spent' }, + { id: '6', date: '2025-03-20', description: 'Посещение серии лекций', amount: 60, type: 'earned' }, + { id: '7', date: '2025-03-10', description: 'Покупка термокружки ЮФУ', amount: -120, type: 'spent' }, + { id: '8', date: '2025-02-28', description: 'Первое посещение лекции вне факультета', amount: 40, type: 'earned' }, + ]) + + function markAllRead() { + notifications.value.forEach(n => (n.read = true)) + } + + const unreadCount = () => notifications.value.filter(n => !n.read).length + + return { achievements, notifications, coinHistory, markAllRead, unreadCount } +}) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..7e28474 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,83 @@ +export type UserRole = 'student' | 'teacher' | 'admin' + +export interface User { + id: string + name: string + email: string + role: UserRole + avatar?: string + institute?: string + department?: string + year?: number + direction?: string + coins: number + level: number + xp?: number + lecturesAttended?: number + hoursLearned?: number + achievements?: string[] +} + +export interface Lecture { + id: string + title: string + description: string + teacher: string + teacherTitle?: string + department?: string + institute: string + date: string + time: string + duration: number + building: string + room?: string + format: 'online' | 'offline' + totalSeats: number + freeSeats: number + registrationClosed?: boolean + tags: string[] + rating: number + reviewCount: number + status?: 'upcoming' | 'ongoing' | 'completed' + registered?: boolean +} + +export interface Review { + id: string + lectureId: string + userId: string + userName: string + text: string + sentiment: 'positive' | 'neutral' | 'negative' + coins?: number + createdAt: string + status: 'pending' | 'analyzing' | 'done' | 'rejected' + quality?: number +} + +export interface Achievement { + id: string + title: string + description: string + icon: string + unlocked: boolean + unlockedAt?: string + coins?: number +} + +export interface Notification { + id: string + type: 'reminder' | 'schedule-change' | 'achievement' | 'coins' | 'recommendation' + title: string + body: string + read: boolean + createdAt: string +} + +export interface CoinTransaction { + id: string + date: string + description: string + amount: number + type: 'earned' | 'spent' +} diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue new file mode 100644 index 0000000..756ad2a --- /dev/null +++ b/frontend/src/views/AboutView.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue new file mode 100644 index 0000000..d5c0217 --- /dev/null +++ b/frontend/src/views/HomeView.vue @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/views/admin/AdminDashboardView.vue b/frontend/src/views/admin/AdminDashboardView.vue new file mode 100644 index 0000000..66db735 --- /dev/null +++ b/frontend/src/views/admin/AdminDashboardView.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/src/views/admin/AdminLLMQueueView.vue b/frontend/src/views/admin/AdminLLMQueueView.vue new file mode 100644 index 0000000..90278fe --- /dev/null +++ b/frontend/src/views/admin/AdminLLMQueueView.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/views/admin/AdminLecturesView.vue b/frontend/src/views/admin/AdminLecturesView.vue new file mode 100644 index 0000000..6e28f52 --- /dev/null +++ b/frontend/src/views/admin/AdminLecturesView.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/src/views/admin/AdminUsersView.vue b/frontend/src/views/admin/AdminUsersView.vue new file mode 100644 index 0000000..6133d68 --- /dev/null +++ b/frontend/src/views/admin/AdminUsersView.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue new file mode 100644 index 0000000..4983ad5 --- /dev/null +++ b/frontend/src/views/auth/LoginView.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/frontend/src/views/student/AchievementsView.vue b/frontend/src/views/student/AchievementsView.vue new file mode 100644 index 0000000..b74536c --- /dev/null +++ b/frontend/src/views/student/AchievementsView.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/frontend/src/views/student/CatalogView.vue b/frontend/src/views/student/CatalogView.vue new file mode 100644 index 0000000..4ddb4f3 --- /dev/null +++ b/frontend/src/views/student/CatalogView.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/frontend/src/views/student/DashboardView.vue b/frontend/src/views/student/DashboardView.vue new file mode 100644 index 0000000..33233b9 --- /dev/null +++ b/frontend/src/views/student/DashboardView.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/frontend/src/views/student/LectureDetailView.vue b/frontend/src/views/student/LectureDetailView.vue new file mode 100644 index 0000000..7535250 --- /dev/null +++ b/frontend/src/views/student/LectureDetailView.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/src/views/student/MyLecturesView.vue b/frontend/src/views/student/MyLecturesView.vue new file mode 100644 index 0000000..065f904 --- /dev/null +++ b/frontend/src/views/student/MyLecturesView.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/frontend/src/views/student/NotificationsView.vue b/frontend/src/views/student/NotificationsView.vue new file mode 100644 index 0000000..8492e4b --- /dev/null +++ b/frontend/src/views/student/NotificationsView.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/views/student/ProfileView.vue b/frontend/src/views/student/ProfileView.vue new file mode 100644 index 0000000..833730f --- /dev/null +++ b/frontend/src/views/student/ProfileView.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/frontend/src/views/student/ReviewFormView.vue b/frontend/src/views/student/ReviewFormView.vue new file mode 100644 index 0000000..e9c920f --- /dev/null +++ b/frontend/src/views/student/ReviewFormView.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/views/teacher/TeacherAnalyticsView.vue b/frontend/src/views/teacher/TeacherAnalyticsView.vue new file mode 100644 index 0000000..dfa11eb --- /dev/null +++ b/frontend/src/views/teacher/TeacherAnalyticsView.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/views/teacher/TeacherDashboardView.vue b/frontend/src/views/teacher/TeacherDashboardView.vue new file mode 100644 index 0000000..bf603de --- /dev/null +++ b/frontend/src/views/teacher/TeacherDashboardView.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/views/teacher/TeacherLecturesView.vue b/frontend/src/views/teacher/TeacherLecturesView.vue new file mode 100644 index 0000000..1f01fe5 --- /dev/null +++ b/frontend/src/views/teacher/TeacherLecturesView.vue @@ -0,0 +1,50 @@ + + + + +