From 31883c579b94b7c66399679045497a0bd66f3ec4 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 15:25:43 +0300 Subject: [PATCH 01/87] =?UTF-8?q?ci:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=BE=D0=B9=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/gitea-push-docker.yml | 109 ++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/gitea-push-docker.yml b/.gitea/workflows/gitea-push-docker.yml index 65417ff..7da12c0 100644 --- a/.gitea/workflows/gitea-push-docker.yml +++ b/.gitea/workflows/gitea-push-docker.yml @@ -1,17 +1,48 @@ -name: Create and publish a Docker image +name: 🚀 Create and publish a Docker image on: push: - branches: ['main', 'staging'] + branches: ['main', 'dev'] env: - CONTEXT: ./backend + BACKEND_PATH: backend + # FRONTEND_PATH: frontend + SERVER_DOMAIN: ${{ gitea.server_url.replace('https://', '') }} jobs: - build-and-push-image: + detect-changes: runs-on: ubuntu-latest - name: Publish image + name: Detect changes in backend and frontend container: catthehacker/ubuntu:act-latest + outputs: + backend_changed: ${{ steps.backend-changed.outputs.backend }} + # frontend_changed: ${{ steps.frontend-changed.outputs.frontend }} + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Check for backend changes + id: backend-changed + uses: dorny/paths-filter@v2 + with: + filters: | + backend: + - '${{ env.BACKEND_PATH }}/**' + + # - name: Check for frontend changes + # id: frontend-changed + # uses: dorny/paths-filter@v2 + # with: + # filters: | + # frontend: + # - '${{ env.FRONTEND_PATH }}/**' + + backend: + runs-on: ubuntu-latest + name: Build & publish backend image + container: catthehacker/ubuntu:act-latest + needs: [detect-changes] + if: ${{ needs.detect-changes.outputs.backend_changed == 'true' }} permissions: contents: read packages: write @@ -24,17 +55,69 @@ jobs: id: meta uses: https://github.com/docker/metadata-action@v4 with: - images: ${{ vars.SERVER_DOMAIN }}/${{ gitea.repository }} - - name: Build an image from Dockerfile - run: | - cd ${{ env.CONTEXT }} && - docker build -f UniVerse.Api/Dockerfile -t ${{ steps.meta.outputs.tags }} . + images: ${{ vars.SERVER_DOMAIN }}/${{ gitea.repository }}/backend + - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 with: registry: ${{ vars.SERVER_DOMAIN }} username: ${{ gitea.actor }} password: ${{ secrets.TOKEN }} - - name: Push - run: | - docker push '${{ steps.meta.outputs.tags }}' + + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: ./${{ env.BACKEND_PATH }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # frontend: + # runs-on: ubuntu-latest + # name: Build & publish frontend image + # container: catthehacker/ubuntu:act-latest + # needs: [detect-changes] + # if: ${{ needs.detect-changes.outputs.frontend_changed == 'true' }} + # permissions: + # contents: read + # packages: write + + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + + # - name: Log in to the Container registry + # uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + # with: + # registry: ${{ vars.SERVER_DOMAIN }} + # username: ${{ gitea.actor }} + # password: ${{ secrets.TOKEN }} + + # - name: Extract metadata (tags, labels) for Docker + # id: meta + # uses: https://github.com/docker/metadata-action@v4 + # with: + # images: ${{ vars.SERVER_DOMAIN }}/${{ gitea.repository }}/frontend + + # - name: Build and push Docker image + # uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + # with: + # context: ./${{ env.FRONTEND_PATH }} + # push: true + # tags: ${{ steps.meta.outputs.tags }} + # labels: ${{ steps.meta.outputs.labels }} + + deploy: + runs-on: ubuntu-latest + # needs: [frontend, backend] + needs: [backend] + # always() - костыль для того, чтобы деплой выполнялся даже если один из билдов пропущен + if: github.ref == 'refs/heads/dev' && always() && (needs.backend.result == 'success' || needs.frontend.result == 'success') + name: Update stack on Portainer + steps: + - name: Deploy Stage + uses: fjogeleit/http-request-action@v1 + with: + url: ${{ secrets.PORTAINER_WEBHOOK_URL }} + method: 'POST' + ignoreSsl: true + timeout: 60000 From e64f287ca37348d96f51f5837ba3f178083ca1b7 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 15:37:14 +0300 Subject: [PATCH 02/87] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20com?= =?UTF-8?q?pose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/docker-compose-prod.yml | 41 +++++++++++++++++++++++++++++++++ backend/docker-compose-test.yml | 35 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 backend/docker-compose-prod.yml create mode 100644 backend/docker-compose-test.yml diff --git a/backend/docker-compose-prod.yml b/backend/docker-compose-prod.yml new file mode 100644 index 0000000..1e3f650 --- /dev/null +++ b/backend/docker-compose-prod.yml @@ -0,0 +1,41 @@ +services: + app: + container_name: UniVerse + image: git.zetcraft.ru/serega404/universe/backend:main + restart: always + ports: + - "8088:8080" + environment: + - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true + - AzureAd:Instance=https://login.microsoftonline.com/ + - AzureAd:TenantId=sfedu.ru + - AzureAd:ClientId= + - AzureAd:ClientSecret= + - AzureAd:Domain=sfedu.onmicrosoft.com + - AzureAd:CallbackPath=/signin-oidc + networks: + - backend + + db: + image: postgres:18-alpine + restart: always + ports: + - "5432" + volumes: + - database_data:/var/lib/postgresql + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DATABASE} + networks: + - backend + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}" ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + +networks: + frontend: + backend: diff --git a/backend/docker-compose-test.yml b/backend/docker-compose-test.yml new file mode 100644 index 0000000..b924bfb --- /dev/null +++ b/backend/docker-compose-test.yml @@ -0,0 +1,35 @@ +services: + app: + container_name: UniVerse + build: + context: ./backend + dockerfile: Dockerfile + restart: always + ports: + - "8088:8080" + environment: + - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true + - AzureAd:Instance=https://login.microsoftonline.com/ + - AzureAd:TenantId=sfedu.ru + - AzureAd:ClientId= + - AzureAd:ClientSecret= + - AzureAd:Domain=sfedu.onmicrosoft.com + - AzureAd:CallbackPath=/signin-oidc + + db: + image: postgres:18-alpine + restart: always + ports: + - "5432" + volumes: + - database_data:/var/lib/postgresql + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DATABASE} + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}" ] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s From 8f53fcfe13257f24d14f05687593ba9e49c8934a Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 15:55:50 +0300 Subject: [PATCH 03/87] fix: docker context Co-authored-by: Copilot --- .gitea/workflows/gitea-push-docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/gitea-push-docker.yml b/.gitea/workflows/gitea-push-docker.yml index 7da12c0..3363e1d 100644 --- a/.gitea/workflows/gitea-push-docker.yml +++ b/.gitea/workflows/gitea-push-docker.yml @@ -68,6 +68,7 @@ jobs: uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: context: ./${{ env.BACKEND_PATH }} + file: ./${{ env.BACKEND_PATH }}/UniVerse.Api/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 89c81c8e27345d9974b92b12fa2d68fcb17e2ad9 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 16:17:58 +0300 Subject: [PATCH 04/87] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20env=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot --- .env.example | 34 +++++++++++++++++++++++++++++++++ backend/docker-compose-prod.yml | 34 ++++++++++++++++++++++++++------- backend/docker-compose-test.yml | 32 +++++++++++++++++++++++++------ 3 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c4354b --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Postgre +POSTGRES_USER=universe +POSTGRES_PASSWORD= +POSTGRES_DATABASE=universe + +# Azure AD +AzureAd_Instance=https://login.microsoftonline.com/ +AzureAd_TenantId=sfedu.ru +AzureAd_ClientId= +AzureAd_ClientSecret= +AzureAd_Domain=sfedu.onmicrosoft.com +AzureAd_CallbackPath=/signin-oidc + +# JWT +JWT_SECRET= +JWT_ISSUER=UniVerse +JWT_AUDIENCE=UniVerse +JWT_ACCESS_TOKEN_EXPIRATION_MINUTES=30 +JWT_REFRESH_TOKEN_EXPIRATION_DAYS=30 + +# CORS +CORS_ALLOWED_ORIGINS=http://localhost:3000 + +# LLM +LLM_BASE_URL= +LLM_API_KEY= +LLM_MODEL= + +# Modeus API +MODEUS_API_BASE_URL= +MODEUS_API_KEY= + +# Gamification +GAMIFICATION_XP_THRESHOLDS=[0, 100, 300, 600, 1000, 1500, 2500, 4000] \ No newline at end of file diff --git a/backend/docker-compose-prod.yml b/backend/docker-compose-prod.yml index 1e3f650..e9f40dd 100644 --- a/backend/docker-compose-prod.yml +++ b/backend/docker-compose-prod.yml @@ -7,12 +7,32 @@ services: - "8088:8080" environment: - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true - - AzureAd:Instance=https://login.microsoftonline.com/ - - AzureAd:TenantId=sfedu.ru - - AzureAd:ClientId= - - AzureAd:ClientSecret= - - AzureAd:Domain=sfedu.onmicrosoft.com - - AzureAd:CallbackPath=/signin-oidc + + - AzureAd:Instance=${AzureAd_Instance:-https://login.microsoftonline.com/} + - AzureAd:TenantId=${AzureAd_TenantId:-sfedu.ru} + - AzureAd:ClientId=${AzureAd_ClientId} + - AzureAd:ClientSecret=${AzureAd_ClientSecret} + - AzureAd:Domain=${AzureAd_Domain:-sfedu.onmicrosoft.com} + - AzureAd:CallbackPath=${AzureAd_CallbackPath:-/signin-oidc} + + - Jwt:Secret=${JWT_SECRET} + - Jwt:Issuer=${JWT_ISSUER:-UniVerse} + - Jwt:Audience=${JWT_AUDIENCE:-UniVerse} + - Jwt:AccessTokenExpirationMinutes=${JWT_ACCESS_TOKEN_EXPIRATION_MINUTES:-30} + - Jwt:RefreshTokenExpirationDays=${JWT_REFRESH_TOKEN_EXPIRATION_DAYS:-30} + + - Cors:Origins=${CORS_ALLOWED_ORIGINS:-http://localhost:3000} + + - Llm:BaseUrl=${LLM_BASE_URL} + - Llm:ApiKey=${LLM_API_KEY} + - Llm:Model=${LLM_MODEL} + + - ModeusApi:BaseUrl=${MODEUS_API_BASE_URL} + - ModeusApi:ApiKey=${MODEUS_API_KEY} + + - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:[0, 100, 300, 600, 1000, 1500, 2500, 4000]} + + - ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE:-universe};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} networks: - backend @@ -26,7 +46,7 @@ services: environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_DB=${POSTGRES_DATABASE} + - POSTGRES_DB=${POSTGRES_DATABASE:-universe} networks: - backend healthcheck: diff --git a/backend/docker-compose-test.yml b/backend/docker-compose-test.yml index b924bfb..86ec590 100644 --- a/backend/docker-compose-test.yml +++ b/backend/docker-compose-test.yml @@ -9,12 +9,32 @@ services: - "8088:8080" environment: - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true - - AzureAd:Instance=https://login.microsoftonline.com/ - - AzureAd:TenantId=sfedu.ru - - AzureAd:ClientId= - - AzureAd:ClientSecret= - - AzureAd:Domain=sfedu.onmicrosoft.com - - AzureAd:CallbackPath=/signin-oidc + + - AzureAd:Instance=${AzureAd_Instance:-https://login.microsoftonline.com/} + - AzureAd:TenantId=${AzureAd_TenantId:-sfedu.ru} + - AzureAd:ClientId=${AzureAd_ClientId} + - AzureAd:ClientSecret=${AzureAd_ClientSecret} + - AzureAd:Domain=${AzureAd_Domain:-sfedu.onmicrosoft.com} + - AzureAd:CallbackPath=${AzureAd_CallbackPath:-/signin-oidc} + + - Jwt:Secret=${JWT_SECRET} + - Jwt:Issuer=${JWT_ISSUER:-UniVerse} + - Jwt:Audience=${JWT_AUDIENCE:-UniVerse} + - Jwt:AccessTokenExpirationMinutes=${JWT_EXPIRE_DAYS:-30} + - Jwt:RefreshTokenExpirationDays=${JWT_EXPIRE_DAYS:-30} + + - Cors:Origins=${CORS_ALLOWED_ORIGINS:-http://localhost:3000} + + - Llm:BaseUrl=${LLM_BASE_URL} + - Llm:ApiKey=${LLM_API_KEY} + - Llm:Model=${LLM_MODEL} + + - ModeusApi:BaseUrl=${MODEUS_API_BASE_URL} + - ModeusApi:ApiKey=${MODEUS_API_KEY} + + - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:[0, 100, 300, 600, 1000, 1500, 2500, 4000]} + + - ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} db: image: postgres:18-alpine From 7c18dbc0147a14b53b667f63e67ee70e566278c8 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 16:53:15 +0300 Subject: [PATCH 05/87] =?UTF-8?q?chore:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260506134139_Initial.Designer.cs} | 4 ++-- .../20260506134139_Initial.cs} | 2 +- .../{Data => }/Migrations/AppDbContextModelSnapshot.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename backend/UniVerse.Infrastructure/{Data/Migrations/20260428124938_Initial.Designer.cs => Migrations/20260506134139_Initial.Designer.cs} (99%) rename backend/UniVerse.Infrastructure/{Data/Migrations/20260428124938_Initial.cs => Migrations/20260506134139_Initial.cs} (99%) rename backend/UniVerse.Infrastructure/{Data => }/Migrations/AppDbContextModelSnapshot.cs (99%) diff --git a/backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.Designer.cs b/backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.Designer.cs similarity index 99% rename from backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.Designer.cs rename to backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.Designer.cs index 358add9..b807140 100644 --- a/backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.Designer.cs +++ b/backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.Designer.cs @@ -9,10 +9,10 @@ using UniVerse.Infrastructure.Data; #nullable disable -namespace UniVerse.Infrastructure.Data.Migrations +namespace UniVerse.Infrastructure.Migrations { [DbContext(typeof(AppDbContext))] - [Migration("20260428124938_Initial")] + [Migration("20260506134139_Initial")] partial class Initial { /// diff --git a/backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.cs b/backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.cs similarity index 99% rename from backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.cs rename to backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.cs index d6c7761..a2f128a 100644 --- a/backend/UniVerse.Infrastructure/Data/Migrations/20260428124938_Initial.cs +++ b/backend/UniVerse.Infrastructure/Migrations/20260506134139_Initial.cs @@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace UniVerse.Infrastructure.Data.Migrations +namespace UniVerse.Infrastructure.Migrations { /// public partial class Initial : Migration diff --git a/backend/UniVerse.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/backend/UniVerse.Infrastructure/Migrations/AppDbContextModelSnapshot.cs similarity index 99% rename from backend/UniVerse.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs rename to backend/UniVerse.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 45112aa..91dae44 100644 --- a/backend/UniVerse.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/backend/UniVerse.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -8,7 +8,7 @@ using UniVerse.Infrastructure.Data; #nullable disable -namespace UniVerse.Infrastructure.Data.Migrations +namespace UniVerse.Infrastructure.Migrations { [DbContext(typeof(AppDbContext))] partial class AppDbContextModelSnapshot : ModelSnapshot From e4d47d4dff5c3f367bf4c21388492200d3a84cca Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Wed, 6 May 2026 16:53:39 +0300 Subject: [PATCH 06/87] =?UTF-8?q?fix:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/appsettings.Development.json | 10 ++++++++++ backend/UniVerse.Api/appsettings.json | 14 ++------------ backend/docker-compose-prod.yml | 2 +- backend/docker-compose-test.yml | 3 ++- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/UniVerse.Api/appsettings.Development.json b/backend/UniVerse.Api/appsettings.Development.json index 3e1a225..511304d 100644 --- a/backend/UniVerse.Api/appsettings.Development.json +++ b/backend/UniVerse.Api/appsettings.Development.json @@ -4,5 +4,15 @@ "Default": "Information", "Microsoft.AspNetCore": "Information" } + }, + "ConnectionStrings": { + "DefaultConnection": "Host=db;Port=5444;Database=universe;Username=universe;Password=pass" + }, + "Jwt": { + "Secret": "default-dev-secret-key-change-in-production-32chars!!", + "Issuer": "UniVerse", + "Audience": "UniVerse", + "AccessTokenExpirationMinutes": "30", + "RefreshTokenExpirationDays": "30" } } diff --git a/backend/UniVerse.Api/appsettings.json b/backend/UniVerse.Api/appsettings.json index 461626d..0844925 100644 --- a/backend/UniVerse.Api/appsettings.json +++ b/backend/UniVerse.Api/appsettings.json @@ -7,16 +7,6 @@ } }, "AllowedHosts": "*", - "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Port=5432;Database=universe;Username=postgres;Password=postgres" - }, - "Jwt": { - "Secret": "default-dev-secret-key-change-in-production-32chars!!", - "Issuer": "UniVerse", - "Audience": "UniVerse", - "AccessTokenExpirationMinutes": "30", - "RefreshTokenExpirationDays": "30" - }, "Cors": { "Origins": [ "http://localhost:5173", @@ -39,8 +29,8 @@ "MinimumLevel": { "Default": "Information", "Override": { - "Microsoft": "Warning", - "System": "Warning" + "Microsoft": "Information", + "System": "Information" } } } diff --git a/backend/docker-compose-prod.yml b/backend/docker-compose-prod.yml index e9f40dd..4f21a13 100644 --- a/backend/docker-compose-prod.yml +++ b/backend/docker-compose-prod.yml @@ -30,7 +30,7 @@ services: - ModeusApi:BaseUrl=${MODEUS_API_BASE_URL} - ModeusApi:ApiKey=${MODEUS_API_KEY} - - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:[0, 100, 300, 600, 1000, 1500, 2500, 4000]} + - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:-[0, 100, 300, 600, 1000, 1500, 2500, 4000]} - ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE:-universe};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} networks: diff --git a/backend/docker-compose-test.yml b/backend/docker-compose-test.yml index 86ec590..6513329 100644 --- a/backend/docker-compose-test.yml +++ b/backend/docker-compose-test.yml @@ -8,6 +8,7 @@ services: ports: - "8088:8080" environment: + - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true - AzureAd:Instance=${AzureAd_Instance:-https://login.microsoftonline.com/} @@ -32,7 +33,7 @@ services: - ModeusApi:BaseUrl=${MODEUS_API_BASE_URL} - ModeusApi:ApiKey=${MODEUS_API_KEY} - - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:[0, 100, 300, 600, 1000, 1500, 2500, 4000]} + - Gamification:XpThresholds=${GAMIFICATION_XP_THRESHOLDS:-[0, 100, 300, 600, 1000, 1500, 2500, 4000]} - ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} From 655ab1b5c567093858bc456066427c4b7e98e7da Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 00:56:44 +0300 Subject: [PATCH 07/87] =?UTF-8?q?=D0=98=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20Vue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.editorconfig | 8 + frontend/.gitattributes | 1 + frontend/.gitignore | 39 + frontend/.oxlintrc.json | 10 + frontend/.prettierrc.json | 6 + frontend/.vscode/extensions.json | 9 + frontend/README.md | 48 + frontend/env.d.ts | 1 + frontend/eslint.config.ts | 26 + frontend/index.html | 13 + frontend/package.json | 43 + frontend/pnpm-lock.yaml | 2947 ++++++++++++++++++++++++++++++ frontend/src/App.vue | 11 + frontend/src/main.ts | 9 + frontend/src/stores/counter.ts | 12 + frontend/tsconfig.app.json | 18 + frontend/tsconfig.json | 11 + frontend/tsconfig.node.json | 27 + frontend/vite.config.ts | 18 + 19 files changed, 3257 insertions(+) create mode 100644 frontend/.editorconfig create mode 100644 frontend/.gitattributes create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/.vscode/extensions.json create mode 100644 frontend/README.md create mode 100644 frontend/env.d.ts create mode 100644 frontend/eslint.config.ts create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/stores/counter.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..3b510aa --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/frontend/.gitattributes b/frontend/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/frontend/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..cd68f14 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..d5648b9 --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"], + "env": { + "browser": true + }, + "categories": { + "correctness": "error" + } +} diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..29a2402 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..3f84126 --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "oxc.oxc-vscode", + "esbenp.prettier-vscode" + ] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c7deeeb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,48 @@ +# universe + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +pnpm install +``` + +### Compile and Hot-Reload for Development + +```sh +pnpm dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +pnpm build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +pnpm lint +``` diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts new file mode 100644 index 0000000..89fdc89 --- /dev/null +++ b/frontend/eslint.config.ts @@ -0,0 +1,26 @@ +import { globalIgnores } from 'eslint/config' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import pluginVue from 'eslint-plugin-vue' +import pluginOxlint from 'eslint-plugin-oxlint' +import skipFormatting from 'eslint-config-prettier/flat' + +// To allow more languages other than `ts` in `.vue` files, uncomment the following lines: +// import { configureVueProject } from '@vue/eslint-config-typescript' +// configureVueProject({ scriptLangs: ['ts', 'tsx'] }) +// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup + +export default defineConfigWithVueTs( + { + name: 'app/files-to-lint', + files: ['**/*.{vue,ts,mts,tsx}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + ...pluginVue.configs['flat/essential'], + vueTsConfigs.recommended, + + ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'), + + skipFormatting, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9e5fc8f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4768fe1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "universe", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "run-s lint:*", + "lint:oxlint": "oxlint . --fix", + "lint:eslint": "eslint . --fix --cache", + "format": "prettier --write --experimental-cli src/" + }, + "dependencies": { + "pinia": "^3.0.4", + "vue": "^3.5.32" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.12.2", + "@vitejs/plugin-vue": "^6.0.6", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.9.1", + "eslint": "^10.2.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-oxlint": "~1.60.0", + "eslint-plugin-vue": "~10.8.0", + "jiti": "^2.6.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.60.0", + "prettier": "3.8.3", + "typescript": "~6.0.0", + "vite": "^8.0.8", + "vite-plugin-vue-devtools": "^8.1.1", + "vue-tsc": "^3.2.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..fd8113a --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2947 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)) + vue: + specifier: ^3.5.32 + version: 3.5.34(typescript@6.0.3) + devDependencies: + '@tsconfig/node24': + specifier: ^24.0.4 + version: 24.0.4 + '@types/node': + specifier: ^24.12.2 + 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)) + '@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) + '@vue/tsconfig': + specifier: ^0.9.1 + version: 0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)) + eslint: + specifier: ^10.2.1 + version: 10.3.0(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.3.0(jiti@2.7.0)) + eslint-plugin-oxlint: + specifier: ~1.60.0 + version: 1.60.0(oxlint@1.60.0) + eslint-plugin-vue: + specifier: ~10.8.0 + version: 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))) + jiti: + specifier: ^2.6.1 + version: 2.7.0 + npm-run-all2: + specifier: ^8.0.4 + version: 8.0.4 + oxlint: + specifier: ~1.60.0 + version: 1.60.0 + prettier: + specifier: 3.8.3 + version: 3.8.3 + typescript: + specifier: ~6.0.0 + version: 6.0.3 + vite: + specifier: ^8.0.8 + version: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + 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)) + vue-tsc: + specifier: ^3.2.6 + version: 3.2.8(typescript@6.0.3) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxlint/binding-android-arm-eabi@1.60.0': + resolution: {integrity: sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.60.0': + resolution: {integrity: sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.60.0': + resolution: {integrity: sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.60.0': + resolution: {integrity: sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.60.0': + resolution: {integrity: sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': + resolution: {integrity: sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.60.0': + resolution: {integrity: sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.60.0': + resolution: {integrity: sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.60.0': + resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.60.0': + resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.60.0': + resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.60.0': + resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.60.0': + resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.60.0': + resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.60.0': + resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.60.0': + resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.60.0': + resolution: {integrity: sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.60.0': + resolution: {integrity: sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.60.0': + resolution: {integrity: sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.13': + resolution: {integrity: sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==} + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + + '@tsconfig/node24@24.0.4': + resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.6': + resolution: {integrity: sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-core@8.1.1': + resolution: {integrity: sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-kit@8.1.1': + resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/devtools-shared@8.1.1': + resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} + + '@vue/eslint-config-typescript@14.7.0': + resolution: {integrity: sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 || ^10.0.0 + eslint-plugin-vue: ^9.28.0 || ^10.0.0 + typescript: '>=4.8.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@3.2.8': + resolution: {integrity: sha512-9OiSPQFiAAWNVnXb0d2dcTmcKnFQamhuNES6ayyISrb/mwPWVgoGdAqSfCWqKhQpa3D5gDTcYD+w7ObiheZ81g==} + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vue/tsconfig@0.9.1': + resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==} + peerDependencies: + typescript: '>= 5.8' + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.27: + resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + engines: {node: '>=6.0.0'} + hasBin: true + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.351: + resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-oxlint@1.60.0: + resolution: {integrity: sha512-9RUD23k7ablez1qg7JWnyPYPOlbucDDqaDr+qNUi0TbIQCPqIPCLzfllgqKF9lOxlg+l17H8hISErmarvm2J1w==} + peerDependencies: + oxlint: ~1.60.0 + + eslint-plugin-vue@10.8.0: + resolution: {integrity: sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.3.0: + resolution: {integrity: sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-all2@8.0.4: + resolution: {integrity: sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==} + engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'} + hasBin: true + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxlint@1.60.0: + resolution: {integrity: sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.18.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-package-json-fast@4.0.0: + resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} + engines: {node: ^18.17.0 || >=20.5.0} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.2: + resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.2.0: + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 + + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.1.1: + resolution: {integrity: sha512-9qTpOmZ2vHpvlI9hdVXAQ1Ry4I8GcBArU7aPi0qfIaV7fQIXy0L1nb6X4mFY2Gw0dYshHuLbIl0Ulb572SCjsQ==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-inspector@5.4.0: + resolution: {integrity: sha512-Iq/024CydcE46FZqWPU4t4lw4uYOdLnFSO1RNxJVt2qY9zxIjmnkBqhHnYaReWM82kmNnaXs7OkfgRrV2GEjyw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-tsc@3.2.8: + resolution: {integrity: sha512-27vTLJ6Q2370obOd0PFYoYoKnmXJ521uUIedrs3Zhhhg/8YG10VOCMmwt+JQslatpAMTDbnWiitLnoD5VlIvog==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.3.0(jiti@2.7.0))': + dependencies: + eslint: 10.3.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.127.0': {} + + '@oxlint/binding-android-arm-eabi@1.60.0': + optional: true + + '@oxlint/binding-android-arm64@1.60.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.60.0': + optional: true + + '@oxlint/binding-darwin-x64@1.60.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.60.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.60.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.60.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.60.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.60.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.60.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.60.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.60.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.60.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.60.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.13': {} + + '@rolldown/pluginutils@1.0.0-rc.17': {} + + '@tsconfig/node24@24.0.4': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@typescript-eslint/eslint-plugin@8.59.2(@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))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 10.3.0(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.3.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@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))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.13 + vite: 8.0.10(@types/node@24.12.2)(jiti@2.7.0) + vue: 3.5.34(typescript@6.0.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) + '@vue/shared': 3.5.34 + optionalDependencies: + '@babel/core': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.3 + '@vue/compiler-sfc': 3.5.34 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.34': + 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 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + 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 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-core@8.1.1(vue@3.5.34(typescript@6.0.3))': + dependencies: + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-shared': 8.1.1 + vue: 3.5.34(typescript@6.0.3) + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-kit@8.1.1': + dependencies: + '@vue/devtools-shared': 8.1.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.1.1': {} + + '@vue/eslint-config-typescript@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)': + dependencies: + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.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))) + fast-glob: 3.3.3 + typescript-eslint: 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.7.0)) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@3.2.8': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.4 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34(typescript@6.0.3) + + '@vue/shared@3.5.34': {} + + '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))': + optionalDependencies: + typescript: 6.0.3 + vue: 3.5.34(typescript@6.0.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.1.2: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.27: {} + + birpc@2.9.0: {} + + boolbase@1.0.0: {} + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.27 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.351 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + caniuse-lite@1.0.30001792: {} + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.351: {} + + entities@7.0.1: {} + + error-stack-parser-es@1.0.5: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.7.0)): + dependencies: + eslint: 10.3.0(jiti@2.7.0) + + eslint-plugin-oxlint@1.60.0(oxlint@1.60.0): + dependencies: + jsonc-parser: 3.3.1 + oxlint: 1.60.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))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + eslint: 10.3.0(jiti@2.7.0) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + vue-eslint-parser: 10.4.0(eslint@10.3.0(jiti@2.7.0)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.3.0(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.3.0(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hookable@5.5.3: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-what@5.5.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + memorystream@0.3.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + mitt@3.0.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + npm-normalize-package-bin@4.0.0: {} + + npm-run-all2@8.0.4: + dependencies: + ansi-styles: 6.2.3 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + picomatch: 4.0.4 + pidtree: 0.6.0 + read-package-json-fast: 4.0.0 + shell-quote: 1.8.3 + which: 5.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + oxlint@1.60.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.60.0 + '@oxlint/binding-android-arm64': 1.60.0 + '@oxlint/binding-darwin-arm64': 1.60.0 + '@oxlint/binding-darwin-x64': 1.60.0 + '@oxlint/binding-freebsd-x64': 1.60.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.60.0 + '@oxlint/binding-linux-arm-musleabihf': 1.60.0 + '@oxlint/binding-linux-arm64-gnu': 1.60.0 + '@oxlint/binding-linux-arm64-musl': 1.60.0 + '@oxlint/binding-linux-ppc64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-musl': 1.60.0 + '@oxlint/binding-linux-s390x-gnu': 1.60.0 + '@oxlint/binding-linux-x64-gnu': 1.60.0 + '@oxlint/binding-linux-x64-musl': 1.60.0 + '@oxlint/binding-openharmony-arm64': 1.60.0 + '@oxlint/binding-win32-arm64-msvc': 1.60.0 + '@oxlint/binding-win32-ia32-msvc': 1.60.0 + '@oxlint/binding-win32-x64-msvc': 1.60.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pidtree@0.6.0: {} + + pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.34(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + read-package-json-fast@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + npm-normalize-package-bin: 4.0.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@6.3.1: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.2(@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))(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.3.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite-dev-rpc@1.1.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + 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-hot-client@2.2.0(vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0)): + dependencies: + 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)): + dependencies: + ansis: 4.2.0 + debug: 4.4.3 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + 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)) + 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)): + 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)) + 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)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + '@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) + transitivePeerDependencies: + - supports-color + + vite@8.0.10(@types/node@24.12.2)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + jiti: 2.7.0 + + vscode-uri@3.1.0: {} + + vue-eslint-parser@10.4.0(eslint@10.3.0(jiti@2.7.0)): + dependencies: + debug: 4.4.3 + eslint: 10.3.0(jiti@2.7.0) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + vue-tsc@3.2.8(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.2.8 + typescript: 6.0.3 + + vue@3.5.34(typescript@6.0.3): + 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@3.5.34(typescript@6.0.3)) + '@vue/shared': 3.5.34 + optionalDependencies: + typescript: 6.0.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + word-wrap@1.2.5: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xml-name-validator@4.0.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..abfd315 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..5f77a89 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' + +const app = createApp(App) + +app.use(createPinia()) + +app.mount('#app') diff --git a/frontend/src/stores/counter.ts b/frontend/src/stores/counter.ts new file mode 100644 index 0000000..b6757ba --- /dev/null +++ b/frontend/src/stores/counter.ts @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..c0f2d86 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, + + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..c9b2bad --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,27 @@ +// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping. +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + // Most tools use transpilation instead of Node.js's native type-stripping. + // Bundler mode provides a smoother developer experience. + "module": "preserve", + "moduleResolution": "bundler", + + // Include Node.js types and avoid accidentally including other `@types/*` packages. + "types": ["node"], + + // Disable emitting output during `vue-tsc --build`, which is used for type-checking only. + "noEmit": true, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..4217010 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +}) From 047611fd24ce58d3901c80685762b66c725c1b60 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 01:06:22 +0300 Subject: [PATCH 08/87] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=B8=D0=BB=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD=20(=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B7=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 239 +++++++++++- frontend/src/App.vue | 99 ++++- frontend/src/assets/base.css | 86 +++++ frontend/src/assets/main.css | 323 ++++++++++++++++ frontend/src/components/HelloWorld.vue | 41 +++ frontend/src/components/TheWelcome.vue | 95 +++++ frontend/src/components/WelcomeItem.vue | 87 +++++ .../src/components/icons/IconCommunity.vue | 7 + .../components/icons/IconDocumentation.vue | 7 + .../src/components/icons/IconEcosystem.vue | 7 + frontend/src/components/icons/IconSupport.vue | 7 + frontend/src/components/icons/IconTooling.vue | 19 + .../src/components/layout/AppBottomNav.vue | 85 +++++ frontend/src/components/layout/AppSidebar.vue | 130 +++++++ frontend/src/components/layout/AppTopbar.vue | 176 +++++++++ .../src/components/ui/AchievementBadge.vue | 58 +++ frontend/src/components/ui/CoinChip.vue | 32 ++ frontend/src/components/ui/DataTable.vue | 67 ++++ frontend/src/components/ui/EmptyState.vue | 32 ++ frontend/src/components/ui/FilterChips.vue | 24 ++ frontend/src/components/ui/GlassCard.vue | 38 ++ frontend/src/components/ui/LectureCard.vue | 196 ++++++++++ frontend/src/components/ui/LoadingSpinner.vue | 23 ++ frontend/src/components/ui/ModalDialog.vue | 80 ++++ frontend/src/components/ui/ProgressBar.vue | 50 +++ frontend/src/components/ui/SearchInput.vue | 51 +++ frontend/src/components/ui/StatsWidget.vue | 72 ++++ frontend/src/components/ui/StatusBadge.vue | 43 +++ .../src/components/ui/ToastNotification.vue | 51 +++ frontend/src/main.ts | 5 + frontend/src/router/index.ts | 41 +++ frontend/src/stores/auth.ts | 90 +++++ frontend/src/stores/lectures.ts | 163 +++++++++ frontend/src/stores/user.ts | 43 +++ frontend/src/types/index.ts | 83 +++++ frontend/src/views/AboutView.vue | 15 + frontend/src/views/HomeView.vue | 9 + .../src/views/admin/AdminDashboardView.vue | 88 +++++ .../src/views/admin/AdminLLMQueueView.vue | 54 +++ .../src/views/admin/AdminLecturesView.vue | 124 +++++++ frontend/src/views/admin/AdminUsersView.vue | 76 ++++ frontend/src/views/auth/LoginView.vue | 176 +++++++++ .../src/views/student/AchievementsView.vue | 75 ++++ frontend/src/views/student/CatalogView.vue | 345 ++++++++++++++++++ frontend/src/views/student/DashboardView.vue | 172 +++++++++ .../src/views/student/LectureDetailView.vue | 126 +++++++ frontend/src/views/student/MyLecturesView.vue | 111 ++++++ .../src/views/student/NotificationsView.vue | 62 ++++ frontend/src/views/student/ProfileView.vue | 132 +++++++ frontend/src/views/student/ReviewFormView.vue | 94 +++++ .../views/teacher/TeacherAnalyticsView.vue | 94 +++++ .../views/teacher/TeacherDashboardView.vue | 69 ++++ .../src/views/teacher/TeacherLecturesView.vue | 50 +++ 54 files changed, 4497 insertions(+), 28 deletions(-) create mode 100644 frontend/src/assets/base.css create mode 100644 frontend/src/assets/main.css create mode 100644 frontend/src/components/HelloWorld.vue create mode 100644 frontend/src/components/TheWelcome.vue create mode 100644 frontend/src/components/WelcomeItem.vue create mode 100644 frontend/src/components/icons/IconCommunity.vue create mode 100644 frontend/src/components/icons/IconDocumentation.vue create mode 100644 frontend/src/components/icons/IconEcosystem.vue create mode 100644 frontend/src/components/icons/IconSupport.vue create mode 100644 frontend/src/components/icons/IconTooling.vue create mode 100644 frontend/src/components/layout/AppBottomNav.vue create mode 100644 frontend/src/components/layout/AppSidebar.vue create mode 100644 frontend/src/components/layout/AppTopbar.vue create mode 100644 frontend/src/components/ui/AchievementBadge.vue create mode 100644 frontend/src/components/ui/CoinChip.vue create mode 100644 frontend/src/components/ui/DataTable.vue create mode 100644 frontend/src/components/ui/EmptyState.vue create mode 100644 frontend/src/components/ui/FilterChips.vue create mode 100644 frontend/src/components/ui/GlassCard.vue create mode 100644 frontend/src/components/ui/LectureCard.vue create mode 100644 frontend/src/components/ui/LoadingSpinner.vue create mode 100644 frontend/src/components/ui/ModalDialog.vue create mode 100644 frontend/src/components/ui/ProgressBar.vue create mode 100644 frontend/src/components/ui/SearchInput.vue create mode 100644 frontend/src/components/ui/StatsWidget.vue create mode 100644 frontend/src/components/ui/StatusBadge.vue create mode 100644 frontend/src/components/ui/ToastNotification.vue create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/stores/lectures.ts create mode 100644 frontend/src/stores/user.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/views/AboutView.vue create mode 100644 frontend/src/views/HomeView.vue create mode 100644 frontend/src/views/admin/AdminDashboardView.vue create mode 100644 frontend/src/views/admin/AdminLLMQueueView.vue create mode 100644 frontend/src/views/admin/AdminLecturesView.vue create mode 100644 frontend/src/views/admin/AdminUsersView.vue create mode 100644 frontend/src/views/auth/LoginView.vue create mode 100644 frontend/src/views/student/AchievementsView.vue create mode 100644 frontend/src/views/student/CatalogView.vue create mode 100644 frontend/src/views/student/DashboardView.vue create mode 100644 frontend/src/views/student/LectureDetailView.vue create mode 100644 frontend/src/views/student/MyLecturesView.vue create mode 100644 frontend/src/views/student/NotificationsView.vue create mode 100644 frontend/src/views/student/ProfileView.vue create mode 100644 frontend/src/views/student/ReviewFormView.vue create mode 100644 frontend/src/views/teacher/TeacherAnalyticsView.vue create mode 100644 frontend/src/views/teacher/TeacherDashboardView.vue create mode 100644 frontend/src/views/teacher/TeacherLecturesView.vue 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 @@ + + + + + From 99a9c3bf4df2cc965f9003240045857848c7e2b6 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 01:24:17 +0300 Subject: [PATCH 09/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B4=D0=BE=D0=BA=D0=B5=D1=80=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=20=D0=B4=D0=BB=D1=8F=20=D1=84=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/Dockerfile | 18 ++++++++++++++++++ frontend/nginx.conf | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 frontend/Dockerfile create mode 100755 frontend/nginx.conf diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..431e793 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:24-slim AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +FROM base AS prod + +COPY pnpm-lock.yaml /app +WORKDIR /app +RUN pnpm fetch --prod + +COPY . /app +RUN pnpm run build + +FROM nginx:1.30-alpine +COPY --from=prod /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100755 index 0000000..cd5feb0 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,32 @@ +resolver 127.0.0.11; + +upstream backend_app { + zone backend_app 64k; + server app:8080 resolve; +} + +server { + listen 80 default_server; + gzip on; + gzip_types text/plain application/xml text/css application/javascript; + gzip_min_length 1000; + client_max_body_size 0; + + location / { + root /usr/share/nginx/html; + include /etc/nginx/mime.types; + try_files $uri /index.html; + } + + location /api { + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 1m; + proxy_connect_timeout 1m; + proxy_pass http://backend_app/api; + } + +} \ No newline at end of file From 6926565d705edf32f4875eb2612c965ec74542c8 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 01:25:28 +0300 Subject: [PATCH 10/87] =?UTF-8?q?ci:=20=D0=92=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B8=D0=BB=20ci/cd=20=D0=B4=D0=BB=D1=8F=20=D1=84=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/gitea-push-docker.yml | 81 +++++++++++++------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/.gitea/workflows/gitea-push-docker.yml b/.gitea/workflows/gitea-push-docker.yml index 3363e1d..06e4422 100644 --- a/.gitea/workflows/gitea-push-docker.yml +++ b/.gitea/workflows/gitea-push-docker.yml @@ -6,7 +6,7 @@ on: env: BACKEND_PATH: backend - # FRONTEND_PATH: frontend + FRONTEND_PATH: frontend SERVER_DOMAIN: ${{ gitea.server_url.replace('https://', '') }} jobs: @@ -16,7 +16,7 @@ jobs: container: catthehacker/ubuntu:act-latest outputs: backend_changed: ${{ steps.backend-changed.outputs.backend }} - # frontend_changed: ${{ steps.frontend-changed.outputs.frontend }} + frontend_changed: ${{ steps.frontend-changed.outputs.frontend }} steps: - name: Checkout repository uses: actions/checkout@v3 @@ -29,13 +29,13 @@ jobs: backend: - '${{ env.BACKEND_PATH }}/**' - # - name: Check for frontend changes - # id: frontend-changed - # uses: dorny/paths-filter@v2 - # with: - # filters: | - # frontend: - # - '${{ env.FRONTEND_PATH }}/**' + - name: Check for frontend changes + id: frontend-changed + uses: dorny/paths-filter@v2 + with: + filters: | + frontend: + - '${{ env.FRONTEND_PATH }}/**' backend: runs-on: ubuntu-latest @@ -72,45 +72,44 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # frontend: - # runs-on: ubuntu-latest - # name: Build & publish frontend image - # container: catthehacker/ubuntu:act-latest - # needs: [detect-changes] - # if: ${{ needs.detect-changes.outputs.frontend_changed == 'true' }} - # permissions: - # contents: read - # packages: write + frontend: + runs-on: ubuntu-latest + name: Build & publish frontend image + container: catthehacker/ubuntu:act-latest + needs: [detect-changes] + if: ${{ needs.detect-changes.outputs.frontend_changed == 'true' }} + permissions: + contents: read + packages: write - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 + steps: + - name: Checkout repository + uses: actions/checkout@v3 - # - name: Log in to the Container registry - # uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - # with: - # registry: ${{ vars.SERVER_DOMAIN }} - # username: ${{ gitea.actor }} - # password: ${{ secrets.TOKEN }} + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ vars.SERVER_DOMAIN }} + username: ${{ gitea.actor }} + password: ${{ secrets.TOKEN }} - # - name: Extract metadata (tags, labels) for Docker - # id: meta - # uses: https://github.com/docker/metadata-action@v4 - # with: - # images: ${{ vars.SERVER_DOMAIN }}/${{ gitea.repository }}/frontend + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: https://github.com/docker/metadata-action@v4 + with: + images: ${{ vars.SERVER_DOMAIN }}/${{ gitea.repository }}/frontend - # - name: Build and push Docker image - # uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - # with: - # context: ./${{ env.FRONTEND_PATH }} - # push: true - # tags: ${{ steps.meta.outputs.tags }} - # labels: ${{ steps.meta.outputs.labels }} + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: ./${{ env.FRONTEND_PATH }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} deploy: runs-on: ubuntu-latest - # needs: [frontend, backend] - needs: [backend] + needs: [frontend, backend] # always() - костыль для того, чтобы деплой выполнялся даже если один из билдов пропущен if: github.ref == 'refs/heads/dev' && always() && (needs.backend.result == 'success' || needs.frontend.result == 'success') name: Update stack on Portainer From 444415c84ba111ff799679dc5e7e1d64226d7a50 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 01:29:54 +0300 Subject: [PATCH 11/87] ci: fix dockerfile --- frontend/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 431e793..6a0aba9 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -6,11 +6,11 @@ RUN corepack enable FROM base AS prod -COPY pnpm-lock.yaml /app WORKDIR /app +COPY pnpm-lock.yaml ./ RUN pnpm fetch --prod -COPY . /app +COPY . ./ RUN pnpm run build FROM nginx:1.30-alpine From 3af1932480ace540ad1c4162c42e4b0d9e12f498 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 8 May 2026 03:02:09 +0300 Subject: [PATCH 12/87] =?UTF-8?q?docs:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/Program.cs | 6 +++--- ...docker-compose-prod.yml => docker-compose-prod.yml | 11 ++++++++++- ...docker-compose-test.yml => docker-compose-test.yml | 8 ++++++++ frontend/index.html | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) rename backend/docker-compose-prod.yml => docker-compose-prod.yml (90%) rename backend/docker-compose-test.yml => docker-compose-test.yml (93%) diff --git a/backend/UniVerse.Api/Program.cs b/backend/UniVerse.Api/Program.cs index 4aeda3a..076268e 100644 --- a/backend/UniVerse.Api/Program.cs +++ b/backend/UniVerse.Api/Program.cs @@ -29,7 +29,7 @@ builder.Services.AddDbContext(options => npgsql => { npgsql.EnableRetryOnFailure(3); - npgsql.MigrationsAssembly("UniVerse.Infrastructure"); + npgsql.MigrationsAssembly("UniVerse.Infrastructure"); // Указывает EF Core, в какой сборке искать/хранить миграции. }); }); @@ -50,7 +50,7 @@ builder.Services.AddAuthentication(options => ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"] ?? "default-dev-secret-key-change-in-production-32chars!!")) + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)) }; }); builder.Services.AddAuthorization(); @@ -114,7 +114,7 @@ builder.Services.AddSwaggerGen(options => { Title = "UniVerse API", Version = "v1", - Description = "University schedule, reviews, and gamification platform" + Description = "Universe" }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme diff --git a/backend/docker-compose-prod.yml b/docker-compose-prod.yml similarity index 90% rename from backend/docker-compose-prod.yml rename to docker-compose-prod.yml index 4f21a13..b5a96db 100644 --- a/backend/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -4,7 +4,7 @@ services: image: git.zetcraft.ru/serega404/universe/backend:main restart: always ports: - - "8088:8080" + - "8080:8080" environment: - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true @@ -35,6 +35,7 @@ services: - ConnectionStrings:DefaultConnection=Host=db;Port=5432;Database=${POSTGRES_DATABASE:-universe};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD} networks: - backend + - frontend db: image: postgres:18-alpine @@ -56,6 +57,14 @@ services: retries: 3 start_period: 30s + frontend: + image: git.zetcraft.ru/serega404/universe/frontend:main + restart: unless-stopped + ports: + - "80" + networks: + - frontend + networks: frontend: backend: diff --git a/backend/docker-compose-test.yml b/docker-compose-test.yml similarity index 93% rename from backend/docker-compose-test.yml rename to docker-compose-test.yml index 6513329..8915fd7 100644 --- a/backend/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -54,3 +54,11 @@ services: timeout: 5s retries: 3 start_period: 30s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80" + restart: unless-stopped diff --git a/frontend/index.html b/frontend/index.html index 9e5fc8f..ed3f053 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite App + UniVerse
From 32ca5963c84934fb5584dce2c2cea02c67c9754a Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 10 May 2026 21:57:19 +0300 Subject: [PATCH 13/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BB=20MS=20Auth=20=D0=B4=D0=BB=D1=8F=20=D1=84=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- .../Controllers/AuthController.cs | 140 +++++++++++++++++- .../DTOs/Auth/AuthDtos.cs | 2 +- .../Interfaces/IAuthService.cs | 2 +- .../Services/AuthService.cs | 16 +- docker-compose-prod.yml | 3 + docker-compose-test.yml | 2 + 7 files changed, 163 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index beed8e9..4cb1b4f 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,13 @@ docker run --rm -p 8080:8080 \ ## Аутентификация - `POST /api/v1/auth/login/dev` — дев-логин (только в `Development`). Удобно для локальной разработки. -- `POST /api/v1/auth/login/microsoft` — заготовка под Microsoft Entra ID (сейчас не реализовано). +- `GET /api/v1/auth/login/microsoft` — старт входа через Microsoft Entra ID (бэкенд сам делает редирект на Microsoft). +- `GET /api/v1/auth/callback/microsoft` — callback, куда Microsoft возвращает `code`. +- `POST /api/v1/auth/login/microsoft` — обмен `authorizationCode` на токены (полезно для интеграций/ручных тестов). Тело: `{ "authorizationCode": "...", "redirectUri"?: "..." }`. - `POST /api/v1/auth/refresh`, `POST /api/v1/auth/logout`, `GET /api/v1/auth/me` +Для Microsoft Entra ID нужны настройки (через env или appsettings): `AzureAd:TenantId`, `AzureAd:ClientId`, `AzureAd:ClientSecret` (и при необходимости `AzureAd:Instance`, `AzureAd:RedirectUri`, `AzureAd:PostLoginRedirectUri`). + Большинство методов API защищены `[Authorize]`. ## Фоновый LLM-анализ отзывов diff --git a/backend/UniVerse.Api/Controllers/AuthController.cs b/backend/UniVerse.Api/Controllers/AuthController.cs index d0cf3cb..38b5303 100644 --- a/backend/UniVerse.Api/Controllers/AuthController.cs +++ b/backend/UniVerse.Api/Controllers/AuthController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; using UniVerse.Application.DTOs.Auth; using UniVerse.Application.Interfaces; +using System.Security.Cryptography; using System.Security.Claims; namespace UniVerse.Api.Controllers; @@ -11,16 +13,122 @@ namespace UniVerse.Api.Controllers; public class AuthController : ControllerBase { private readonly IAuthService _auth; - public AuthController(IAuthService auth) => _auth = auth; + private readonly IConfiguration _config; + + private const string MicrosoftStateCookieName = "msAuthState"; + private const string MicrosoftReturnUrlCookieName = "msAuthReturnUrl"; + + public AuthController(IAuthService auth, IConfiguration config) + { + _auth = auth; + _config = config; + } [HttpPost("login/microsoft")] public async Task> LoginMicrosoft([FromBody] LoginMicrosoftRequest request) { - var result = await _auth.LoginWithMicrosoftAsync(request.AuthorizationCode); + var result = await _auth.LoginWithMicrosoftAsync(request.AuthorizationCode, request.RedirectUri); SetRefreshTokenCookie(result.RefreshToken); return Ok(result.Response); } + // Server-driven auth flow: frontend just navigates here; backend builds Microsoft authorize URL. + // Optional returnUrl is stored in a short-lived cookie and used by callback. + [HttpGet("login/microsoft")] + [AllowAnonymous] + public IActionResult LoginMicrosoftRedirect([FromQuery] string? returnUrl = null) + { + var tenantId = _config["AzureAd:TenantId"]; + var clientId = _config["AzureAd:ClientId"]; + var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/"; + + if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId)) + return Problem("Microsoft authentication is not configured (AzureAd:TenantId/ClientId).", statusCode: StatusCodes.Status500InternalServerError); + + var redirectUri = _config["AzureAd:RedirectUri"] ?? BuildAbsoluteUrl("/api/v1/auth/callback/microsoft"); + + var state = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); + Response.Cookies.Append(MicrosoftStateCookieName, state, new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps, + SameSite = SameSiteMode.Lax, + Expires = DateTimeOffset.UtcNow.AddMinutes(10) + }); + + if (!string.IsNullOrWhiteSpace(returnUrl) && IsAllowedReturnUrl(returnUrl)) + { + Response.Cookies.Append(MicrosoftReturnUrlCookieName, returnUrl, new CookieOptions + { + HttpOnly = true, + Secure = Request.IsHttps, + SameSite = SameSiteMode.Lax, + Expires = DateTimeOffset.UtcNow.AddMinutes(10) + }); + } + + var authorizeEndpoint = $"{instance.TrimEnd('/')}/{tenantId}/oauth2/v2.0/authorize"; + var scope = _config["AzureAd:Scopes"] ?? "openid profile email offline_access User.Read"; + + var authorizeUrl = QueryHelpers.AddQueryString(authorizeEndpoint, new Dictionary + { + ["client_id"] = clientId, + ["response_type"] = "code", + ["redirect_uri"] = redirectUri, + ["response_mode"] = "query", + ["scope"] = scope, + ["state"] = state + }); + + return Redirect(authorizeUrl); + } + + [HttpGet("callback/microsoft")] + [AllowAnonymous] + public async Task CallbackMicrosoft( + [FromQuery] string? code = null, + [FromQuery] string? state = null, + [FromQuery] string? error = null, + [FromQuery(Name = "error_description")] string? errorDescription = null) + { + if (!string.IsNullOrEmpty(error)) + { + return Unauthorized(new + { + error, + errorDescription + }); + } + + if (string.IsNullOrWhiteSpace(code)) + return BadRequest(new { error = "missing_code" }); + + var expectedState = Request.Cookies[MicrosoftStateCookieName]; + if (string.IsNullOrWhiteSpace(expectedState) || string.IsNullOrWhiteSpace(state) || !string.Equals(expectedState, state, StringComparison.Ordinal)) + return Unauthorized(new { error = "invalid_state" }); + + Response.Cookies.Delete(MicrosoftStateCookieName); + + var redirectUri = _config["AzureAd:RedirectUri"] ?? BuildAbsoluteUrl("/api/v1/auth/callback/microsoft"); + + var result = await _auth.LoginWithMicrosoftAsync(code, redirectUri); + SetRefreshTokenCookie(result.RefreshToken); + + var returnUrl = Request.Cookies[MicrosoftReturnUrlCookieName] ?? _config["AzureAd:PostLoginRedirectUri"]; + Response.Cookies.Delete(MicrosoftReturnUrlCookieName); + + if (!string.IsNullOrWhiteSpace(returnUrl) && IsAllowedReturnUrl(returnUrl)) + { + // Put access token in URL fragment so it is not sent as Referer to the backend. + // Frontend can read it from location.hash on the landing page. + var fragment = $"access_token={Uri.EscapeDataString(result.Response.AccessToken)}&expires_at={Uri.EscapeDataString(result.Response.ExpiresAt.ToString("O"))}"; + return Redirect($"{returnUrl}#{fragment}"); + } + + // Useful for manual testing without frontend: you'll see JSON in the browser. + return Ok(result.Response); + } + [HttpPost("login/dev")] public async Task> DevLogin([FromBody] DevLoginRequest request) { @@ -70,4 +178,32 @@ public class AuthController : ControllerBase Expires = DateTime.UtcNow.AddDays(30) }); } + + private string BuildAbsoluteUrl(string path) + { + if (!path.StartsWith('/')) path = "/" + path; + return $"{Request.Scheme}://{Request.Host}{path}"; + } + + private bool IsAllowedReturnUrl(string returnUrl) + { + if (Uri.TryCreate(returnUrl, UriKind.Relative, out _)) + return true; + + if (!Uri.TryCreate(returnUrl, UriKind.Absolute, out var absolute)) + return false; + + var allowedOrigins = _config.GetSection("Cors:Origins").Get() ?? Array.Empty(); + foreach (var origin in allowedOrigins) + { + if (!Uri.TryCreate(origin, UriKind.Absolute, out var allowed)) + continue; + if (string.Equals(allowed.Scheme, absolute.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(allowed.Host, absolute.Host, StringComparison.OrdinalIgnoreCase) + && allowed.Port == absolute.Port) + return true; + } + + return false; + } } diff --git a/backend/UniVerse.Application/DTOs/Auth/AuthDtos.cs b/backend/UniVerse.Application/DTOs/Auth/AuthDtos.cs index a9a6770..efd97ed 100644 --- a/backend/UniVerse.Application/DTOs/Auth/AuthDtos.cs +++ b/backend/UniVerse.Application/DTOs/Auth/AuthDtos.cs @@ -7,6 +7,6 @@ public record AuthResult(AuthResponse Response, string RefreshToken); public record UserAuthDto(int Id, string Email, string? DisplayName, UserRole Role); -public record LoginMicrosoftRequest(string AuthorizationCode); +public record LoginMicrosoftRequest(string AuthorizationCode, string? RedirectUri = null); public record DevLoginRequest(string Email, string? DisplayName = null, UserRole Role = UserRole.Student); diff --git a/backend/UniVerse.Application/Interfaces/IAuthService.cs b/backend/UniVerse.Application/Interfaces/IAuthService.cs index 9e33059..c0d615b 100644 --- a/backend/UniVerse.Application/Interfaces/IAuthService.cs +++ b/backend/UniVerse.Application/Interfaces/IAuthService.cs @@ -5,7 +5,7 @@ namespace UniVerse.Application.Interfaces; public interface IAuthService { - Task LoginWithMicrosoftAsync(string authorizationCode); + Task LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null); Task DevLoginAsync(string email, string? displayName, Domain.Enums.UserRole role); Task RefreshTokenAsync(string refreshToken); Task RevokeRefreshTokenAsync(string refreshToken); diff --git a/backend/UniVerse.Infrastructure/Services/AuthService.cs b/backend/UniVerse.Infrastructure/Services/AuthService.cs index 5b684bc..94ff77d 100644 --- a/backend/UniVerse.Infrastructure/Services/AuthService.cs +++ b/backend/UniVerse.Infrastructure/Services/AuthService.cs @@ -30,16 +30,26 @@ public class AuthService : IAuthService _gamification = gamification; } - public async Task LoginWithMicrosoftAsync(string authorizationCode) + public async Task LoginWithMicrosoftAsync(string authorizationCode, string? redirectUri = null) { var tenantId = _config["AzureAd:TenantId"]; var clientId = _config["AzureAd:ClientId"]; var clientSecret = _config["AzureAd:ClientSecret"]; + var instance = _config["AzureAd:Instance"] ?? "https://login.microsoftonline.com/"; + + if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret)) + throw new UnauthorizedException("Microsoft authentication is not configured (AzureAd:TenantId/ClientId/ClientSecret)."); + + var effectiveRedirectUri = redirectUri + ?? _config["AzureAd:RedirectUri"] + ?? "http://localhost:5173/auth/callback"; + + var authority = $"{instance.TrimEnd('/')}/{tenantId}"; var app = ConfidentialClientApplicationBuilder.Create(clientId) .WithClientSecret(clientSecret) - .WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}")) - .WithRedirectUri(_config["AzureAd:RedirectUri"] ?? "http://localhost:5173/auth/callback") + .WithAuthority(new Uri(authority)) + .WithRedirectUri(effectiveRedirectUri) .Build(); AuthenticationResult result; diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index b5a96db..a2c3dc8 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -14,6 +14,9 @@ services: - AzureAd:ClientSecret=${AzureAd_ClientSecret} - AzureAd:Domain=${AzureAd_Domain:-sfedu.onmicrosoft.com} - AzureAd:CallbackPath=${AzureAd_CallbackPath:-/signin-oidc} + # https:///api/v1/auth/callback/microsoft + - AzureAd:RedirectUri=${AzureAd_RedirectUri} + - AzureAd:PostLoginRedirectUri=${AzureAd_PostLoginRedirectUri:-} - Jwt:Secret=${JWT_SECRET} - Jwt:Issuer=${JWT_ISSUER:-UniVerse} diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 8915fd7..7000317 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -17,6 +17,8 @@ services: - AzureAd:ClientSecret=${AzureAd_ClientSecret} - AzureAd:Domain=${AzureAd_Domain:-sfedu.onmicrosoft.com} - AzureAd:CallbackPath=${AzureAd_CallbackPath:-/signin-oidc} + - AzureAd:RedirectUri=${AzureAd_RedirectUri:-http://localhost:8088/api/v1/auth/callback/microsoft} + - AzureAd:PostLoginRedirectUri=${AzureAd_PostLoginRedirectUri:-} - Jwt:Secret=${JWT_SECRET} - Jwt:Issuer=${JWT_ISSUER:-UniVerse} From 6e473e23d069dca20703b0290c0f52855e65fb37 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 10 May 2026 22:13:46 +0300 Subject: [PATCH 14/87] =?UTF-8?q?docs:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D1=91=D1=81=20swagger=20=D0=BD=D0=B0=20api/docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/Program.cs | 12 ++++++++++-- backend/UniVerse.Api/Properties/launchSettings.json | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/UniVerse.Api/Program.cs b/backend/UniVerse.Api/Program.cs index 076268e..4da37e8 100644 --- a/backend/UniVerse.Api/Program.cs +++ b/backend/UniVerse.Api/Program.cs @@ -145,8 +145,16 @@ app.UseMiddleware(); if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "UniVerse API v1")); + app.UseSwagger(c => + { + c.RouteTemplate = "api/docs/{documentName}/swagger.json"; + }); + + app.UseSwaggerUI(c => + { + c.RoutePrefix = "api/docs"; + c.SwaggerEndpoint("v1/swagger.json", "UniVerse API v1"); + }); } app.UseCors(); diff --git a/backend/UniVerse.Api/Properties/launchSettings.json b/backend/UniVerse.Api/Properties/launchSettings.json index 385142c..5648baf 100644 --- a/backend/UniVerse.Api/Properties/launchSettings.json +++ b/backend/UniVerse.Api/Properties/launchSettings.json @@ -4,7 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "api/docs", "applicationUrl": "http://localhost:5019", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" From 2450361a1d843d19b30db39cafb75707706dbf78 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 10 May 2026 22:38:00 +0300 Subject: [PATCH 15/87] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D1=8B=20Azure=20AD=20=D0=B2=20dev=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/appsettings.Development.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/UniVerse.Api/appsettings.Development.json b/backend/UniVerse.Api/appsettings.Development.json index 511304d..d1699dd 100644 --- a/backend/UniVerse.Api/appsettings.Development.json +++ b/backend/UniVerse.Api/appsettings.Development.json @@ -14,5 +14,13 @@ "Audience": "UniVerse", "AccessTokenExpirationMinutes": "30", "RefreshTokenExpirationDays": "30" + }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "sfedu.ru", + "ClientId": "", + "ClientSecret": "", + "Domain": "sfedu.onmicrosoft.com", + "CallbackPath": "/signin-oidc" } -} +} \ No newline at end of file From aaba62b7392563c72232d5f81d5d900932d82ff7 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sun, 10 May 2026 22:38:31 +0300 Subject: [PATCH 16/87] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F=20appsettings.Development.jso?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 928da2c..4d98306 100644 --- a/.gitignore +++ b/.gitignore @@ -139,7 +139,8 @@ $RECYCLE.BIN/ .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* @@ -160,3 +161,4 @@ Network Trash Folder Temporary Items .apdisk +backend/UniVerse.Api/appsettings.Development.json From a04c20c85780bf339c09821c429d76ffac8b6ae4 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 00:20:39 +0300 Subject: [PATCH 17/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20.net=20Aspire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/Program.cs | 14 +- backend/UniVerse.Api/UniVerse.Api.csproj | 1 + backend/UniVerse.AppHost/AppHost.cs | 15 +++ .../Properties/launchSettings.json | 32 +++++ .../UniVerse.AppHost/UniVerse.AppHost.csproj | 19 +++ .../appsettings.Development.json | 8 ++ backend/UniVerse.AppHost/appsettings.json | 9 ++ backend/UniVerse.AppHost/aspire.config.json | 5 + .../UniVerse.ServiceDefaults/Extensions.cs | 126 ++++++++++++++++++ .../UniVerse.ServiceDefaults.csproj | 23 ++++ backend/UniVerse.sln | 12 ++ frontend/README.md | 18 +++ frontend/package.json | 1 + 13 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 backend/UniVerse.AppHost/AppHost.cs create mode 100644 backend/UniVerse.AppHost/Properties/launchSettings.json create mode 100644 backend/UniVerse.AppHost/UniVerse.AppHost.csproj create mode 100644 backend/UniVerse.AppHost/appsettings.Development.json create mode 100644 backend/UniVerse.AppHost/appsettings.json create mode 100644 backend/UniVerse.AppHost/aspire.config.json create mode 100644 backend/UniVerse.ServiceDefaults/Extensions.cs create mode 100644 backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj diff --git a/backend/UniVerse.Api/Program.cs b/backend/UniVerse.Api/Program.cs index 4da37e8..6204f5d 100644 --- a/backend/UniVerse.Api/Program.cs +++ b/backend/UniVerse.Api/Program.cs @@ -14,6 +14,13 @@ using UniVerse.Infrastructure.ExternalServices; var builder = WebApplication.CreateBuilder(args); +var useAspire = builder.Configuration.GetValue("Aspire:Enabled"); + +if (useAspire) +{ + builder.AddServiceDefaults(); +} + // --- Serilog --- Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) @@ -139,6 +146,11 @@ builder.Services.AddSwaggerGen(options => var app = builder.Build(); +if (useAspire) +{ + app.MapDefaultEndpoints(); +} + // --- Middleware Pipeline --- app.UseMiddleware(); app.UseMiddleware(); @@ -162,4 +174,4 @@ app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/backend/UniVerse.Api/UniVerse.Api.csproj b/backend/UniVerse.Api/UniVerse.Api.csproj index 86f5e3f..69aaade 100644 --- a/backend/UniVerse.Api/UniVerse.Api.csproj +++ b/backend/UniVerse.Api/UniVerse.Api.csproj @@ -27,6 +27,7 @@ + diff --git a/backend/UniVerse.AppHost/AppHost.cs b/backend/UniVerse.AppHost/AppHost.cs new file mode 100644 index 0000000..36f7eb4 --- /dev/null +++ b/backend/UniVerse.AppHost/AppHost.cs @@ -0,0 +1,15 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var api = builder + .AddProject("universe-api") + .WithEnvironment("Aspire__Enabled", "true"); + +// Запуск фронтенда (Vue + Vite) в dev-режиме вместе. +// Требования: установлен pnpm (или включён corepack), зависимости фронта установлены. +builder + .AddExecutable("universe-frontend", "pnpm", workingDirectory: "../../frontend") + .WithArgs("run", "dev:aspire") + // Используется в vite.config.ts для server.proxy['/api'].target + .WithEnvironment("VITE_API_PROXY_TARGET", api.GetEndpoint("http")); + +builder.Build().Run(); diff --git a/backend/UniVerse.AppHost/Properties/launchSettings.json b/backend/UniVerse.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..2a6dfce --- /dev/null +++ b/backend/UniVerse.AppHost/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17156;http://localhost:15060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21010", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23046", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22274" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19138", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18238", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20274" + } + } + } +} diff --git a/backend/UniVerse.AppHost/UniVerse.AppHost.csproj b/backend/UniVerse.AppHost/UniVerse.AppHost.csproj new file mode 100644 index 0000000..2ccc522 --- /dev/null +++ b/backend/UniVerse.AppHost/UniVerse.AppHost.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + fb90d29a-6c48-471b-b19f-d2f431a5ef38 + + + + + + + + + + + diff --git a/backend/UniVerse.AppHost/appsettings.Development.json b/backend/UniVerse.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/UniVerse.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/UniVerse.AppHost/appsettings.json b/backend/UniVerse.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/backend/UniVerse.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/backend/UniVerse.AppHost/aspire.config.json b/backend/UniVerse.AppHost/aspire.config.json new file mode 100644 index 0000000..a62b559 --- /dev/null +++ b/backend/UniVerse.AppHost/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "UniVerse.AppHost.csproj" + } +} diff --git a/backend/UniVerse.ServiceDefaults/Extensions.cs b/backend/UniVerse.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..224d3d9 --- /dev/null +++ b/backend/UniVerse.ServiceDefaults/Extensions.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj b/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj new file mode 100644 index 0000000..a056dd4 --- /dev/null +++ b/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + true + true + + + + + + + + + + + + + + + diff --git a/backend/UniVerse.sln b/backend/UniVerse.sln index 5b76058..ad0e781 100644 --- a/backend/UniVerse.sln +++ b/backend/UniVerse.sln @@ -8,6 +8,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniVerse.Application", "Uni EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniVerse.Infrastructure", "UniVerse.Infrastructure\UniVerse.Infrastructure.csproj", "{A1B2C3D4-1111-2222-3333-444455558888}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniVerse.AppHost", "UniVerse.AppHost\UniVerse.AppHost.csproj", "{CC38B044-852A-4E9C-AB35-EF7E35088490}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniVerse.ServiceDefaults", "UniVerse.ServiceDefaults\UniVerse.ServiceDefaults.csproj", "{28475301-CBE1-4AE9-937F-8FD89E3EA6F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +34,13 @@ Global {A1B2C3D4-1111-2222-3333-444455558888}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-1111-2222-3333-444455558888}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-1111-2222-3333-444455558888}.Release|Any CPU.Build.0 = Release|Any CPU + {CC38B044-852A-4E9C-AB35-EF7E35088490}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC38B044-852A-4E9C-AB35-EF7E35088490}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC38B044-852A-4E9C-AB35-EF7E35088490}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC38B044-852A-4E9C-AB35-EF7E35088490}.Release|Any CPU.Build.0 = Release|Any CPU + {28475301-CBE1-4AE9-937F-8FD89E3EA6F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28475301-CBE1-4AE9-937F-8FD89E3EA6F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28475301-CBE1-4AE9-937F-8FD89E3EA6F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28475301-CBE1-4AE9-937F-8FD89E3EA6F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/frontend/README.md b/frontend/README.md index c7deeeb..f2916fd 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -29,6 +29,24 @@ See [Vite Configuration Reference](https://vite.dev/config/). pnpm install ``` +## Запуск вместе с backend (Aspire) + +Если запускать приложение через `backend/UniVerse.AppHost`, то фронтенд (Vite dev server) поднимается автоматически. + +1) Установить зависимости фронтенда: + +```sh +pnpm -C frontend install +``` + +2) Запустить Aspire AppHost: + +```sh +dotnet run --project backend/UniVerse.AppHost/UniVerse.AppHost.csproj +``` + +Обычно фронтенд слушает `http://localhost:5173` (если порт занят — Vite выберет следующий свободный). + ### Compile and Hot-Reload for Development ```sh diff --git a/frontend/package.json b/frontend/package.json index a76bd1d..38aad31 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:aspire": "vite --host 0.0.0.0 --port 5173", "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "build-only": "vite build", From 331ad86c515a9154c837f4c4ec8c8c94182eabb6 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 00:21:07 +0300 Subject: [PATCH 18/87] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20Microsoft.OpenApi=20?= =?UTF-8?q?=D0=B8=20FluentValidation.DependencyInjectionExtensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/UniVerse.Api.csproj | 4 ++-- backend/UniVerse.Application/UniVerse.Application.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/UniVerse.Api/UniVerse.Api.csproj b/backend/UniVerse.Api/UniVerse.Api.csproj index 69aaade..0b7b9cd 100644 --- a/backend/UniVerse.Api/UniVerse.Api.csproj +++ b/backend/UniVerse.Api/UniVerse.Api.csproj @@ -17,11 +17,11 @@ - + - + diff --git a/backend/UniVerse.Application/UniVerse.Application.csproj b/backend/UniVerse.Application/UniVerse.Application.csproj index d83e800..93a624c 100644 --- a/backend/UniVerse.Application/UniVerse.Application.csproj +++ b/backend/UniVerse.Application/UniVerse.Application.csproj @@ -8,7 +8,7 @@ - + From 8e376de9f0f99a24ccc3a2076ec7983215f0da1e Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 00:21:24 +0300 Subject: [PATCH 19/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=20jetbrains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.idea/.idea.UniVerse/.idea/.name | 1 + backend/.idea/.idea.UniVerse/.idea/indexLayout.xml | 4 +++- backend/.idea/.idea.UniVerse/.idea/vcs.xml | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 backend/.idea/.idea.UniVerse/.idea/.name create mode 100644 backend/.idea/.idea.UniVerse/.idea/vcs.xml diff --git a/backend/.idea/.idea.UniVerse/.idea/.name b/backend/.idea/.idea.UniVerse/.idea/.name new file mode 100644 index 0000000..adb8b9a --- /dev/null +++ b/backend/.idea/.idea.UniVerse/.idea/.name @@ -0,0 +1 @@ +UniVerse \ No newline at end of file diff --git a/backend/.idea/.idea.UniVerse/.idea/indexLayout.xml b/backend/.idea/.idea.UniVerse/.idea/indexLayout.xml index 7b08163..16d8e1d 100644 --- a/backend/.idea/.idea.UniVerse/.idea/indexLayout.xml +++ b/backend/.idea/.idea.UniVerse/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + ../frontend + diff --git a/backend/.idea/.idea.UniVerse/.idea/vcs.xml b/backend/.idea/.idea.UniVerse/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/backend/.idea/.idea.UniVerse/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From b42e4e5157408ee300d164f8b68e8d329b540b06 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 01:16:09 +0300 Subject: [PATCH 20/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20Direct?= =?UTF-8?q?ory.Build.props=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= =?UTF-8?q?=20launchSettings.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Directory.Build.props | 6 ++++ .../Properties/launchSettings.json | 32 ++++++++++++++----- .../UniVerse.AppHost/UniVerse.AppHost.csproj | 2 +- 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 backend/Directory.Build.props diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props new file mode 100644 index 0000000..1aefd4f --- /dev/null +++ b/backend/Directory.Build.props @@ -0,0 +1,6 @@ + + + false + true + + diff --git a/backend/UniVerse.AppHost/Properties/launchSettings.json b/backend/UniVerse.AppHost/Properties/launchSettings.json index 2a6dfce..6b6e316 100644 --- a/backend/UniVerse.AppHost/Properties/launchSettings.json +++ b/backend/UniVerse.AppHost/Properties/launchSettings.json @@ -5,27 +5,43 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:17156;http://localhost:15060", + "applicationUrl": "https://127.0.0.1:17156;http://127.0.0.1:15060", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21010", - "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23046", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22274" + "HTTP_PROXY": "", + "HTTPS_PROXY": "", + "ALL_PROXY": "", + "http_proxy": "", + "https_proxy": "", + "all_proxy": "", + "NO_PROXY": "localhost,127.0.0.1,::1", + "no_proxy": "localhost,127.0.0.1,::1", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://127.0.0.1:21010", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://127.0.0.1:23046", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://127.0.0.1:22274" } }, "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:15060", + "applicationUrl": "http://127.0.0.1:15060", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true", - "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19138", - "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18238", - "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20274" + "HTTP_PROXY": "", + "HTTPS_PROXY": "", + "ALL_PROXY": "", + "http_proxy": "", + "https_proxy": "", + "all_proxy": "", + "NO_PROXY": "localhost,127.0.0.1,::1", + "no_proxy": "localhost,127.0.0.1,::1", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://127.0.0.1:19138", + "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://127.0.0.1:18238", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://127.0.0.1:20274" } } } diff --git a/backend/UniVerse.AppHost/UniVerse.AppHost.csproj b/backend/UniVerse.AppHost/UniVerse.AppHost.csproj index 2ccc522..8aa441a 100644 --- a/backend/UniVerse.AppHost/UniVerse.AppHost.csproj +++ b/backend/UniVerse.AppHost/UniVerse.AppHost.csproj @@ -13,7 +13,7 @@ - + From 75282fe8dd7bff01eddb144115bf695d76f2de07 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 01:16:30 +0300 Subject: [PATCH 21/87] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA=D1=82=D0=B0=D1=85=20=D0=B1=D1=8D=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.Api/UniVerse.Api.csproj | 6 +++--- .../UniVerse.Application.csproj | 8 ++++---- .../UniVerse.Infrastructure.csproj | 4 ++-- .../UniVerse.ServiceDefaults.csproj | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/UniVerse.Api/UniVerse.Api.csproj b/backend/UniVerse.Api/UniVerse.Api.csproj index 0b7b9cd..e6b73df 100644 --- a/backend/UniVerse.Api/UniVerse.Api.csproj +++ b/backend/UniVerse.Api/UniVerse.Api.csproj @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -19,8 +19,8 @@ - - + + diff --git a/backend/UniVerse.Application/UniVerse.Application.csproj b/backend/UniVerse.Application/UniVerse.Application.csproj index 93a624c..d068bc7 100644 --- a/backend/UniVerse.Application/UniVerse.Application.csproj +++ b/backend/UniVerse.Application/UniVerse.Application.csproj @@ -9,11 +9,11 @@ - - + + - - + + diff --git a/backend/UniVerse.Infrastructure/UniVerse.Infrastructure.csproj b/backend/UniVerse.Infrastructure/UniVerse.Infrastructure.csproj index a0ba73b..9f3f72c 100644 --- a/backend/UniVerse.Infrastructure/UniVerse.Infrastructure.csproj +++ b/backend/UniVerse.Infrastructure/UniVerse.Infrastructure.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj b/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj index a056dd4..64c017e 100644 --- a/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj +++ b/backend/UniVerse.ServiceDefaults/UniVerse.ServiceDefaults.csproj @@ -11,13 +11,13 @@ - - - - - - - + + + + + + + From 71e7d84e0f4be6b60d6e9024219d27cd65265c1d Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 01:32:51 +0300 Subject: [PATCH 22/87] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20HTTP-=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=20=D0=B4=D0=BB=D1=8F=20=D1=84=D1=80=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20Aspire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/UniVerse.AppHost/AppHost.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/UniVerse.AppHost/AppHost.cs b/backend/UniVerse.AppHost/AppHost.cs index 36f7eb4..b6b7120 100644 --- a/backend/UniVerse.AppHost/AppHost.cs +++ b/backend/UniVerse.AppHost/AppHost.cs @@ -9,6 +9,7 @@ var api = builder builder .AddExecutable("universe-frontend", "pnpm", workingDirectory: "../../frontend") .WithArgs("run", "dev:aspire") + .WithHttpEndpoint(targetPort: 5173, port: 5173, name: "http", isProxied: false) // Используется в vite.config.ts для server.proxy['/api'].target .WithEnvironment("VITE_API_PROXY_TARGET", api.GetEndpoint("http")); From 779b6aba77522349c321ffa36ded140a41b3a28c Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Mon, 11 May 2026 01:33:38 +0300 Subject: [PATCH 23/87] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.env.example | 2 + frontend/src/App.vue | 2 +- frontend/src/api/client.ts | 81 ++++++ frontend/src/api/index.ts | 70 ++++++ frontend/src/api/mappers.ts | 115 +++++++++ frontend/src/api/types.ts | 132 ++++++++++ frontend/src/components/layout/AppSidebar.vue | 2 +- frontend/src/router/index.ts | 11 +- frontend/src/stores/auth.ts | 205 ++++++++++----- frontend/src/stores/lectures.ts | 236 +++++++----------- frontend/src/stores/user.ts | 80 +++--- frontend/src/views/auth/AuthCallbackView.vue | 67 +++++ frontend/src/views/auth/LoginView.vue | 73 +----- .../src/views/student/AchievementsView.vue | 23 +- frontend/src/views/student/CatalogView.vue | 34 ++- frontend/src/views/student/DashboardView.vue | 21 +- .../src/views/student/LectureDetailView.vue | 45 ++-- frontend/src/views/student/MyLecturesView.vue | 26 +- frontend/src/views/student/ProfileView.vue | 19 +- frontend/src/views/student/ReviewFormView.vue | 28 ++- frontend/vite.config.ts | 35 ++- 21 files changed, 942 insertions(+), 365 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/mappers.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/views/auth/AuthCallbackView.vue diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..c649528 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=/api +VITE_AUTH_RETURN_URL=/auth/callback diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 349edd7..85ce08e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,7 +10,7 @@ import ToastNotification from '@/components/ui/ToastNotification.vue' const auth = useAuthStore() const route = useRoute() -const isAuthPage = computed(() => route.path === '/login') +const isAuthPage = computed(() => Boolean(route.meta.public)) interface Toast { id: number; message: string; type: 'success' | 'error' | 'info' } const toasts = ref([]) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..1e06d95 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,81 @@ +const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '/api').replace(/\/$/, '') +const API_PREFIX = '/v1' + +let accessToken: string | null = null + +export class ApiError extends Error { + constructor( + message: string, + public status: number, + public details?: unknown, + ) { + super(message) + this.name = 'ApiError' + } +} + +export function setApiAccessToken(token: string | null) { + accessToken = token +} + +export function getApiAccessToken() { + return accessToken +} + +function makeUrl(path: string, query?: Record) { + const normalizedPath = path.startsWith('/') ? path : `/${path}` + const url = new URL(`${API_BASE_URL}${API_PREFIX}${normalizedPath}`, window.location.origin) + + Object.entries(query ?? {}).forEach(([key, value]) => { + if (value === undefined || value === null || value === '') return + url.searchParams.set(key, String(value)) + }) + + return url.toString() +} + +export function buildApiUrl(path: string, query?: Record) { + return makeUrl(path, query) +} + +async function parseResponse(response: Response) { + if (response.status === 204) return undefined + + const contentType = response.headers.get('content-type') ?? '' + if (contentType.includes('application/json')) return response.json() + + const text = await response.text() + return text || undefined +} + +export async function apiRequest( + path: string, + options: RequestInit & { query?: Record } = {}, +): Promise { + const headers = new Headers(options.headers) + if (!headers.has('Accept')) headers.set('Accept', 'application/json') + if (options.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json') + if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`) + + const response = await fetch(makeUrl(path, options.query), { + ...options, + headers, + credentials: 'include', + }) + const body = await parseResponse(response) + + if (!response.ok) { + const message = + typeof body === 'object' && body && 'message' in body + ? String((body as { message: unknown }).message) + : `API request failed with status ${response.status}` + throw new ApiError(message, response.status, body) + } + + return body as T +} + +export function extractItems(payload: T[] | { items?: T[] } | undefined): T[] { + if (Array.isArray(payload)) return payload + return payload?.items ?? [] +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..16e7bf9 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,70 @@ +import { apiRequest, extractItems } from './client' +import type { + AchievementDto, + AuthResponse, + CoinTransactionDto, + LectureDto, + LectureQuery, + PagedResult, + ReviewDto, + UserAchievementDto, + UserDto, + UserStatsDto, +} from './types' + +export const authApi = { + loginMicrosoft: (authorizationCode: string, redirectUri?: string) => + apiRequest('/auth/login/microsoft', { + method: 'POST', + body: JSON.stringify({ authorizationCode, redirectUri }), + }), + refresh: () => apiRequest('/auth/refresh', { method: 'POST' }), + logout: () => apiRequest('/auth/logout', { method: 'POST' }), + me: () => apiRequest('/auth/me'), +} + +export const lecturesApi = { + async list(query: LectureQuery = {}) { + const payload = await apiRequest | LectureDto[]>('/lectures', { + query: query as Record, + }) + return extractItems(payload) + }, + get: (id: string | number) => apiRequest(`/lectures/${id}`), + enroll: (id: string | number) => apiRequest(`/lectures/${id}/enroll`, { method: 'POST' }), + unenroll: (id: string | number) => apiRequest(`/lectures/${id}/enroll`, { method: 'DELETE' }), + async reviews(id: string | number) { + const payload = await apiRequest | ReviewDto[]>(`/lectures/${id}/reviews`) + return extractItems(payload) + }, +} + +export const usersApi = { + get: (id: string | number) => apiRequest(`/users/${id}`), + stats: (id: string | number) => apiRequest(`/users/${id}/stats`), + async enrollments(id: string | number) { + const payload = await apiRequest | LectureDto[] | undefined>(`/users/${id}/enrollments`) + return extractItems(payload) + }, + async achievements(id: string | number) { + const payload = await apiRequest | UserAchievementDto[] | AchievementDto[]>( + `/users/${id}/achievements`, + ) + if (Array.isArray(payload)) return payload + return payload.items ?? [] + }, + async transactions(id: string | number) { + const payload = await apiRequest | CoinTransactionDto[]>( + `/users/${id}/transactions`, + ) + return extractItems(payload) + }, +} + +export const reviewsApi = { + create: (lectureId: string | number, rating: 'Like' | 'Neutral' | 'Dislike', text: string) => + apiRequest('/reviews', { + method: 'POST', + body: JSON.stringify({ lectureId: Number(lectureId), rating, text }), + }), +} diff --git a/frontend/src/api/mappers.ts b/frontend/src/api/mappers.ts new file mode 100644 index 0000000..119ef3c --- /dev/null +++ b/frontend/src/api/mappers.ts @@ -0,0 +1,115 @@ +import type { Achievement, CoinTransaction, Lecture, Review, User, UserRole } from '@/types' +import type { + AchievementDto, + CoinTransactionDto, + LectureDto, + ReviewDto, + UserAuthDto, + UserDto, + UserStatsDto, + UserAchievementDto, +} from './types' + +export function mapApiRole(role: string | undefined): UserRole { + if (role === 'Teacher') return 'teacher' + if (role === 'Admin') return 'admin' + return 'student' +} + +export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): User { + return { + id: String(user.id), + name: user.displayName || user.email || 'Пользователь UniVerse', + email: user.email || '', + role: mapApiRole(user.role), + avatar: 'avatarUrl' in user ? user.avatarUrl ?? undefined : undefined, + institute: 'ЮФУ', + department: '', + year: 0, + direction: '', + coins: stats?.coins ?? ('coins' in user ? user.coins : 0), + level: stats?.level ?? ('level' in user ? user.level : 1), + xp: stats?.xp ?? ('xp' in user ? user.xp : 0), + lecturesAttended: stats?.attendedLectures ?? 0, + hoursLearned: stats ? Math.round(stats.attendedLectures * 1.5 * 10) / 10 : 0, + achievements: stats ? Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)) : [], + } +} + +export function mapApiLecture(lecture: LectureDto): Lecture { + const startsAt = new Date(lecture.startsAt) + const endsAt = new Date(lecture.endsAt) + const durationMs = endsAt.getTime() - startsAt.getTime() + const duration = Number.isFinite(durationMs) && durationMs > 0 ? Math.round(durationMs / 60000) : 90 + const totalSeats = lecture.maxEnrollments || 0 + const enrolled = lecture.enrollmentsCount || 0 + const freeSeats = Math.max(totalSeats - enrolled, 0) + const locationName = lecture.locationName || (lecture.format === 'Online' ? 'Онлайн' : 'Аудитория уточняется') + + return { + id: String(lecture.id), + title: lecture.title || lecture.courseName || 'Лекция без названия', + description: lecture.description || 'Описание появится позже.', + teacher: lecture.teacherName || 'Преподаватель уточняется', + teacherTitle: '', + department: '', + institute: lecture.courseName || 'ЮФУ', + date: startsAt.toISOString().slice(0, 10), + time: startsAt.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }), + duration, + building: lecture.format === 'Online' ? 'Онлайн' : locationName, + room: lecture.format === 'Online' ? undefined : locationName, + format: lecture.format === 'Online' ? 'online' : 'offline', + totalSeats, + freeSeats, + registrationClosed: !lecture.isOpen, + tags: lecture.courseName ? [`#${lecture.courseName}`] : [], + rating: 0, + reviewCount: 0, + status: startsAt.getTime() > Date.now() ? 'upcoming' : 'completed', + registered: lecture.isEnrolled, + } +} + +export function mapApiReview(review: ReviewDto): Review { + const sentiment = review.sentiment === 'Negative' ? 'negative' : review.sentiment === 'Neutral' ? 'neutral' : 'positive' + const status = + review.llmStatus === 'Rejected' ? 'rejected' : review.llmStatus === 'Analyzed' ? 'done' : 'pending' + + return { + id: String(review.id), + lectureId: String(review.lectureId), + userId: String(review.userId), + userName: review.userName || 'Анонимный отзыв', + text: review.text || '', + sentiment, + createdAt: review.createdAt, + status, + quality: review.qualityScore ?? undefined, + } +} + +export function mapApiAchievement(input: AchievementDto | UserAchievementDto): Achievement { + const dto = 'achievement' in input ? input.achievement : input + const awardedAt = 'achievement' in input ? input.awardedAt : undefined + + return { + id: String(dto.id), + title: dto.name || 'Достижение', + description: dto.description || dto.condition || '', + icon: dto.iconUrl || '⭐', + unlocked: Boolean(awardedAt), + unlockedAt: awardedAt, + coins: dto.coinReward, + } +} + +export function mapApiCoinTransaction(transaction: CoinTransactionDto): CoinTransaction { + return { + id: String(transaction.id), + date: transaction.createdAt.slice(0, 10), + description: transaction.description || transaction.type, + amount: transaction.amount, + type: transaction.amount >= 0 ? 'earned' : 'spent', + } +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..2f9ede9 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,132 @@ +export type ApiUserRole = 'Student' | 'Teacher' | 'Admin' +export type ApiLectureFormat = 'Online' | 'Offline' +export type ApiReviewRating = 'Like' | 'Neutral' | 'Dislike' +export type ApiReviewLlmStatus = 'Pending' | 'Analyzed' | 'Rejected' +export type ApiReviewSentiment = 'Positive' | 'Neutral' | 'Negative' +export type ApiCoinTransactionType = + | 'ReviewReward' + | 'AchievementReward' + | 'AttendanceReward' + | 'AdminAdjustment' + +export interface PagedResult { + items: T[] + totalCount: number + page: number + pageSize: number + totalPages: number +} + +export interface AuthResponse { + accessToken: string + expiresAt: string + user: UserAuthDto +} + +export interface LoginMicrosoftRequest { + authorizationCode: string + redirectUri?: string +} + +export interface UserAuthDto { + id: number + email: string + displayName?: string | null + role: ApiUserRole +} + +export interface UserDto extends UserAuthDto { + avatarUrl?: string | null + isActive: boolean + xp: number + coins: number + level: number + createdAt: string +} + +export interface UserStatsDto { + totalLectures: number + attendedLectures: number + totalReviews: number + xp: number + coins: number + level: number + achievementsCount: number +} + +export interface LectureDto { + id: number + courseId: number + courseName?: string | null + teacherId?: number | null + teacherName?: string | null + locationId?: number | null + locationName?: string | null + title?: string | null + description?: string | null + format: ApiLectureFormat + startsAt: string + endsAt: string + isOpen: boolean + maxEnrollments: number + enrollmentsCount: number + onlineUrl?: string | null + createdAt: string + isEnrolled?: boolean +} + +export interface ReviewDto { + id: number + lectureId: number + lectureTitle?: string | null + userId: number + userName?: string | null + rating: ApiReviewRating + text?: string | null + llmStatus: ApiReviewLlmStatus + sentiment: ApiReviewSentiment + qualityScore?: number | null + isInformative?: boolean | null + llmTags?: string[] | null + createdAt: string +} + +export interface AchievementDto { + id: number + name?: string | null + description?: string | null + iconUrl?: string | null + xpReward: number + coinReward: number + condition?: string | null + createdAt: string +} + +export interface UserAchievementDto { + id: number + achievement: AchievementDto + awardedAt: string +} + +export interface CoinTransactionDto { + id: number + amount: number + type: ApiCoinTransactionType + reviewId?: number | null + achievementId?: number | null + description?: string | null + createdAt: string +} + +export interface LectureQuery { + DateFrom?: string + DateTo?: string + CourseId?: number + TeacherId?: number + Format?: ApiLectureFormat + IsOpen?: boolean + TagId?: number + Search?: string + Page?: number + PageSize?: number +} diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 3f850af..d2ae79c 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -54,7 +54,7 @@ function isActive(to: string) { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3201561..54cb61b 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -5,6 +5,12 @@ const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', name: 'login', component: () => import('@/views/auth/LoginView.vue'), meta: { public: true } }, + { + path: '/auth/callback', + name: 'auth-callback', + component: () => import('@/views/auth/AuthCallbackView.vue'), + meta: { public: true }, + }, // Student { path: '/', name: 'dashboard', component: () => import('@/views/student/DashboardView.vue'), meta: { role: 'student' } }, @@ -31,8 +37,11 @@ const router = createRouter({ ], }) -router.beforeEach((to) => { +router.beforeEach(async (to) => { const auth = useAuthStore() + if (!auth.initialized && !to.meta.public) { + await auth.initialize() + } if (!to.meta.public && !auth.isAuthenticated) { return '/login' } diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index df2125d..a280c6c 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -1,90 +1,159 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' -import type { User, UserRole } from '@/types' +import { computed, ref } from 'vue' +import { authApi } from '@/api' +import { mapApiUser } from '@/api/mappers' +import { buildApiUrl, setApiAccessToken } from '@/api/client' +import type { AuthResponse } from '@/api/types' +import type { User } 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: [], - }, +const TOKEN_STORAGE_KEY = 'universe.accessToken' + +function applyAuthResponse(response: AuthResponse) { + localStorage.setItem(TOKEN_STORAGE_KEY, response.accessToken) + setApiAccessToken(response.accessToken) + return mapApiUser(response.user) +} + +function getAuthReturnUrl() { + return import.meta.env.VITE_AUTH_RETURN_URL || '/auth/callback' +} + +function getAbsoluteAuthReturnUrl() { + return new URL(getAuthReturnUrl(), window.location.origin).toString() } export const useAuthStore = defineStore('auth', () => { const user = ref(null) - const isAuthenticated = ref(false) const loading = ref(false) + const initialized = ref(false) const error = ref(null) + const accessToken = ref(localStorage.getItem(TOKEN_STORAGE_KEY)) - async function login(role: UserRole = 'student', shouldFail = false) { + if (accessToken.value) setApiAccessToken(accessToken.value) + + const isAuthenticated = computed(() => Boolean(user.value && accessToken.value)) + + async function hydrateFromResponse(response: AuthResponse) { + accessToken.value = response.accessToken + user.value = applyAuthResponse(response) + error.value = null + } + + async function initialize() { + if (initialized.value) return isAuthenticated.value loading.value = true error.value = null - await new Promise(r => setTimeout(r, 800)) - if (shouldFail) { - loading.value = false - error.value = 'Не удалось подтвердить доступ через ЮФУ. Попробуйте еще раз.' + + try { + const refreshed = await authApi.refresh() + await hydrateFromResponse(refreshed) + const me = await authApi.me() + user.value = mapApiUser(me) + return true + } catch (refreshError) { + if (accessToken.value) { + try { + const me = await authApi.me() + user.value = mapApiUser(me) + return true + } catch { + // Fall through to local cleanup below. + } + } + clearSession() + error.value = refreshError instanceof Error ? refreshError.message : null return false + } finally { + initialized.value = true + loading.value = false } - user.value = { ...defaultUsers[role] } - isAuthenticated.value = true - loading.value = false + } + + function startMicrosoftLogin() { + window.location.assign(buildApiUrl('/auth/login/microsoft', { returnUrl: getAuthReturnUrl() })) return true } - function logout() { + async function completeMicrosoftLogin(code: string, state: string | null) { + loading.value = true + error.value = null + try { + const redirectUri = getAbsoluteAuthReturnUrl() + const response = await authApi.loginMicrosoft(code, redirectUri) + await hydrateFromResponse(response) + initialized.value = true + return true + } catch (err) { + clearSession() + error.value = err instanceof Error ? err.message : 'Ошибка авторизации через Microsoft.' + throw err + } finally { + loading.value = false + } + } + + async function completeTokenLogin(token: string) { + loading.value = true + error.value = null + try { + accessToken.value = token + localStorage.setItem(TOKEN_STORAGE_KEY, token) + setApiAccessToken(token) + const me = await authApi.me() + user.value = mapApiUser(me) + initialized.value = true + return true + } catch (err) { + clearSession() + error.value = err instanceof Error ? err.message : 'Не удалось получить пользователя после входа.' + throw err + } finally { + loading.value = false + } + } + + async function logout() { + loading.value = true + try { + await authApi.logout() + } catch { + // Local cleanup is still correct if the server session is already gone. + } finally { + clearSession() + initialized.value = true + loading.value = false + } + } + + function clearSession() { user.value = null - isAuthenticated.value = false + accessToken.value = null + localStorage.removeItem(TOKEN_STORAGE_KEY) + setApiAccessToken(null) } - 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] } + function setUser(nextUser: User) { + user.value = nextUser } - return { user, isAuthenticated, loading, error, login, logout, switchRole } + function switchRole() { + error.value = 'Смена роли доступна только через backend.' + } + + return { + user, + accessToken, + isAuthenticated, + loading, + initialized, + error, + initialize, + startMicrosoftLogin, + completeMicrosoftLogin, + completeTokenLogin, + logout, + clearSession, + setUser, + switchRole, + } }) diff --git a/frontend/src/stores/lectures.ts b/frontend/src/stores/lectures.ts index 97fa342..cb55457 100644 --- a/frontend/src/stores/lectures.ts +++ b/frontend/src/stores/lectures.ts @@ -1,163 +1,115 @@ 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', - }, -] +import { lecturesApi, usersApi } from '@/api' +import { mapApiLecture, mapApiReview } from '@/api/mappers' +import type { Lecture, Review } from '@/types' export const useLecturesStore = defineStore('lectures', () => { - const lectures = ref(LECTURES) - const registered = ref(['1', '3']) + const lectures = ref([]) + const registered = ref([]) + const reviewsByLecture = ref>({}) + const loading = ref(false) + const error = ref(null) const all = computed(() => lectures.value) const registeredIds = computed(() => registered.value) const registeredLectures = computed(() => - lectures.value.filter(l => registered.value.includes(l.id)) + lectures.value.filter(l => registered.value.includes(l.id) || l.registered), ) - 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-- + async function fetchLectures() { + loading.value = true + error.value = null + try { + const payload = await lecturesApi.list({ PageSize: 100 }) + lectures.value = payload.map(mapApiLecture) + registered.value = lectures.value.filter(l => l.registered).map(l => l.id) + } catch (err) { + error.value = err instanceof Error ? err.message : 'Не удалось загрузить лекции.' + } finally { + loading.value = false } } - function unregister(lectureId: string) { + async function fetchLecture(id: string) { + error.value = null + try { + const lecture = mapApiLecture(await lecturesApi.get(id)) + const index = lectures.value.findIndex(item => item.id === lecture.id) + if (index >= 0) lectures.value[index] = lecture + else lectures.value.push(lecture) + if (lecture.registered && !registered.value.includes(lecture.id)) registered.value.push(lecture.id) + return lecture + } catch (err) { + error.value = err instanceof Error ? err.message : 'Не удалось загрузить лекцию.' + return lectures.value.find(item => item.id === id) + } + } + + async function fetchRegisteredForUser(userId: string) { + try { + const enrollments = await usersApi.enrollments(userId) + const mapped = enrollments.map(mapApiLecture) + if (mapped.length) { + mapped.forEach(lecture => { + const index = lectures.value.findIndex(item => item.id === lecture.id) + if (index >= 0) lectures.value[index] = { ...lectures.value[index], ...lecture, registered: true } + else lectures.value.push({ ...lecture, registered: true }) + }) + registered.value = mapped.map(lecture => lecture.id) + } + } catch { + // Some backend builds return an empty 200 for this endpoint; catalog detail still carries isEnrolled. + } + } + + async function fetchReviews(lectureId: string) { + try { + reviewsByLecture.value[lectureId] = (await lecturesApi.reviews(lectureId)).map(mapApiReview) + } catch { + reviewsByLecture.value[lectureId] = [] + } + } + + async function register(lectureId: string) { + const lecture = lectures.value.find(item => item.id === lectureId) + if (!lecture || lecture.freeSeats === 0 || lecture.registrationClosed || registered.value.includes(lectureId)) return + + await lecturesApi.enroll(lectureId) + registered.value.push(lectureId) + lecture.freeSeats = Math.max(lecture.freeSeats - 1, 0) + lecture.registered = true + } + + async function unregister(lectureId: string) { + await lecturesApi.unenroll(lectureId) registered.value = registered.value.filter(id => id !== lectureId) - const l = lectures.value.find(x => x.id === lectureId) - if (l) l.freeSeats++ + const lecture = lectures.value.find(item => item.id === lectureId) + if (lecture) { + lecture.freeSeats = Math.min(lecture.freeSeats + 1, lecture.totalSeats) + lecture.registered = false + } } function isRegistered(lectureId: string) { return registered.value.includes(lectureId) } - return { lectures, registered, all, registeredIds, registeredLectures, register, unregister, isRegistered } + return { + lectures, + registered, + reviewsByLecture, + loading, + error, + all, + registeredIds, + registeredLectures, + fetchLectures, + fetchLecture, + fetchRegisteredForUser, + fetchReviews, + register, + unregister, + isRegistered, + } }) diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts index efb8e2d..06d9108 100644 --- a/frontend/src/stores/user.ts +++ b/frontend/src/stores/user.ts @@ -1,37 +1,50 @@ import { defineStore } from 'pinia' import { ref } from 'vue' -import type { Achievement, Notification, CoinTransaction } from '@/types' +import { usersApi } from '@/api' +import { mapApiAchievement, mapApiCoinTransaction } from '@/api/mappers' +import type { Achievement, CoinTransaction, Notification } from '@/types' +import { useAuthStore } from './auth' 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 achievements = ref([]) + const notifications = ref([]) + const coinHistory = ref([]) + const loading = ref(false) + const error = ref(null) - 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' }, - ]) + async function fetchStudentData(userId?: string) { + const auth = useAuthStore() + const id = userId ?? auth.user?.id + if (!id) return - 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' }, - ]) + loading.value = true + error.value = null + try { + const [stats, achievementPayload, transactions] = await Promise.all([ + usersApi.stats(id), + usersApi.achievements(id), + usersApi.transactions(id), + ]) + + if (auth.user) { + auth.setUser({ + ...auth.user, + coins: stats.coins, + level: stats.level, + xp: stats.xp, + lecturesAttended: stats.attendedLectures, + hoursLearned: Math.round(stats.attendedLectures * 1.5 * 10) / 10, + achievements: Array.from({ length: stats.achievementsCount }, (_, index) => String(index + 1)), + }) + } + achievements.value = achievementPayload.map(mapApiAchievement) + coinHistory.value = transactions.map(mapApiCoinTransaction) + } catch (err) { + error.value = err instanceof Error ? err.message : 'Не удалось загрузить данные профиля.' + } finally { + loading.value = false + } + } function markAllRead() { notifications.value.forEach(n => (n.read = true)) @@ -39,5 +52,14 @@ export const useUserStore = defineStore('user', () => { const unreadCount = () => notifications.value.filter(n => !n.read).length - return { achievements, notifications, coinHistory, markAllRead, unreadCount } + return { + achievements, + notifications, + coinHistory, + loading, + error, + fetchStudentData, + markAllRead, + unreadCount, + } }) diff --git a/frontend/src/views/auth/AuthCallbackView.vue b/frontend/src/views/auth/AuthCallbackView.vue new file mode 100644 index 0000000..72579cc --- /dev/null +++ b/frontend/src/views/auth/AuthCallbackView.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue index 4983ad5..b0d8f2d 100644 --- a/frontend/src/views/auth/LoginView.vue +++ b/frontend/src/views/auth/LoginView.vue @@ -1,34 +1,21 @@ @@ -46,21 +33,6 @@ async function login() { Получайте рекомендации, оставляйте отзывы и зарабатывайте монеты за полезную обратную связь. -
-

Роль для демонстрации:

-
- -
-
- @@ -125,39 +93,8 @@ async function login() { padding: 14px 16px; border: 1px solid var(--color-border-glass); } -.demo-label { - font-size: 12px; - color: var(--color-text-secondary); - font-weight: 600; - text-transform: uppercase; - margin-bottom: 8px; -} -.role-options { display: flex; flex-wrap: wrap; gap: 8px; } -.role-option { - background: rgba(255,255,255,0.6); - border: 1px solid var(--color-border-glass); - border-radius: var(--radius-sm); - padding: 8px 12px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - color: var(--color-text); -} -.role-option.active { - border-color: var(--color-primary); - background: rgba(34,197,94,0.12); - color: var(--color-primary-dark); -} .login-actions { display: flex; flex-direction: column; gap: 10px; } .btn-full { width: 100%; justify-content: center; } -.toggle { - font-size: 12px; - color: var(--color-text-secondary); - display: flex; - align-items: center; - gap: 8px; -} .error { font-size: 13px; color: var(--color-error); diff --git a/frontend/src/views/student/AchievementsView.vue b/frontend/src/views/student/AchievementsView.vue index b74536c..d495032 100644 --- a/frontend/src/views/student/AchievementsView.vue +++ b/frontend/src/views/student/AchievementsView.vue @@ -1,10 +1,13 @@ diff --git a/frontend/src/views/student/DashboardView.vue b/frontend/src/views/student/DashboardView.vue index 33233b9..d890b4a 100644 --- a/frontend/src/views/student/DashboardView.vue +++ b/frontend/src/views/student/DashboardView.vue @@ -1,5 +1,5 @@