From 56b7e2a5ba98415703f99c2e50944677d0ca81b9 Mon Sep 17 00:00:00 2001 From: bnair123 Date: Fri, 26 Dec 2025 20:25:44 +0400 Subject: [PATCH] Rebuild frontend with Tailwind CSS + fix Python 3.14 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade SQLAlchemy 2.0.27→2.0.45, google-genai SDK for Python 3.14 - Replace google-generativeai with google-genai in narrative_service.py - Fix HTTPException handling in main.py (was wrapping as 500) - Rebuild all frontend components with Tailwind CSS v3: - Dashboard, NarrativeSection, StatsGrid, VibeRadar, HeatMap, TopRotation - Custom color palette (background-dark, card-dark, accent-neon, etc.) - Add glass-panel, holographic-badge CSS effects - Docker improvements: - Combined backend container (API + worker in entrypoint.sh) - DATABASE_URL configurable via env var - CI workflow builds both backend and frontend images - Update README with clearer docker-compose instructions --- .github/workflows/docker-publish.yml | 64 +- README.md | 156 ++- backend/Dockerfile | 13 +- backend/alembic/env.py | 18 +- backend/app/database.py | 11 +- backend/app/main.py | 15 +- backend/app/services/narrative_service.py | 14 +- backend/entrypoint.sh | 17 + backend/requirements.txt | 11 +- docker-compose.yml | 20 +- frontend/index.html | 9 +- frontend/package-lock.json | 1317 +++++++++++++++++- frontend/package.json | 8 +- frontend/postcss.config.js | 6 + frontend/src/App.css | 42 - frontend/src/App.jsx | 116 +- frontend/src/components/Dashboard.jsx | 154 ++ frontend/src/components/HeatMap.jsx | 112 ++ frontend/src/components/NarrativeSection.jsx | 66 + frontend/src/components/StatsGrid.jsx | 99 ++ frontend/src/components/TopRotation.jsx | 46 + frontend/src/components/VibeRadar.jsx | 93 ++ frontend/src/index.css | 132 +- frontend/src/main.jsx | 1 + frontend/tailwind.config.js | 34 + 25 files changed, 2255 insertions(+), 319 deletions(-) create mode 100644 backend/entrypoint.sh create mode 100644 frontend/postcss.config.js delete mode 100644 frontend/src/App.css create mode 100644 frontend/src/components/Dashboard.jsx create mode 100644 frontend/src/components/HeatMap.jsx create mode 100644 frontend/src/components/NarrativeSection.jsx create mode 100644 frontend/src/components/StatsGrid.jsx create mode 100644 frontend/src/components/TopRotation.jsx create mode 100644 frontend/src/components/VibeRadar.jsx create mode 100644 frontend/tailwind.config.js diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c35bcc6..cac1763 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -8,17 +8,16 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} jobs: - build: + build-backend: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -33,25 +32,68 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta + - name: Extract metadata (tags, labels) for Backend + id: meta-backend uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/musicanalyser tags: | type=ref,event=branch type=ref,event=pr - type=semver,pattern={{version}} type=sha - latest + type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push + - name: Build and push Backend uses: docker/build-push-action@v5 with: context: ./backend push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta-backend.outputs.tags }} + labels: ${{ steps.meta-backend.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + build-frontend: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/musicanalyser-frontend + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Frontend + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} platforms: linux/amd64,linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max diff --git a/README.md b/README.md index a3d1941..e9d74b9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A personal analytics dashboard for your music listening habits, powered by Python, FastAPI, React, and Google Gemini AI. +![Dashboard Screenshot](screen.png) + ## Features - **Continuous Ingestion**: Polls Spotify every 60 seconds to record your listening history. @@ -9,83 +11,129 @@ A personal analytics dashboard for your music listening habits, powered by Pytho - **Genres & Images** (via Spotify) - **Audio Features** (Energy, BPM, Mood via ReccoBeats) - **Lyrics & Metadata** (via Genius) -- **Dashboard**: A responsive UI (Ant Design) to view your history, stats, and "Vibes". -- **AI Ready**: Database schema and environment prepared for Gemini AI integration. +- **Dashboard**: A responsive UI with Tailwind CSS, featuring AI-generated narrative insights. +- **AI Powered**: Google Gemini generates personalized listening narratives and roasts. -## Hosting Guide - -You can run this application using Docker Compose. You have two options: using the pre-built image from GitHub Container Registry or building from source. +## Quick Start (Docker Compose) ### 1. Prerequisites -- Docker & Docker Compose installed. -- **Spotify Developer Credentials** (Client ID & Secret). -- **Spotify Refresh Token** (Run `backend/scripts/get_refresh_token.py` locally to generate this). -- **Google Gemini API Key**. -- **Genius API Token** (Optional, for lyrics). +- Docker & Docker Compose installed +- Spotify Developer Credentials ([Create App](https://developer.spotify.com/dashboard)) +- Google Gemini API Key ([Get Key](https://aistudio.google.com/app/apikey)) +- Genius API Token (Optional, for lyrics - [Get Token](https://genius.com/api-clients)) -### 2. Configuration (`.env`) +### 2. Get Spotify Refresh Token -Create a `.env` file in the root directory (same level as `docker-compose.yml`). This file is used by Docker Compose to populate environment variables. +Run this one-time script locally to authorize your Spotify account: + +```bash +cd backend +pip install httpx +python scripts/get_refresh_token.py +``` + +Follow the prompts. Copy the `refresh_token` value for your `.env` file. + +### 3. Create `.env` File + +Create a `.env` file in the project root: ```bash SPOTIFY_CLIENT_ID="your_client_id" SPOTIFY_CLIENT_SECRET="your_client_secret" SPOTIFY_REFRESH_TOKEN="your_refresh_token" GEMINI_API_KEY="your_gemini_key" -GENIUS_ACCESS_TOKEN="your_genius_token" +GENIUS_ACCESS_TOKEN="your_genius_token" # Optional ``` -### 3. Run with Docker Compose +### 4. Run with Pre-built Images -#### Option A: Build from Source (Recommended for Dev/Modifications) +```bash +# Pull the latest images +docker pull ghcr.io/bnair123/musicanalyser:latest +docker pull ghcr.io/bnair123/musicanalyser-frontend:latest -Use this if you want to modify the code or ensure you are running the exact local version. +# Start the services +docker-compose up -d +``` -1. Clone the repository. -2. Ensure your `.env` file is set up. -3. Run: - ```bash - docker-compose up -d --build - ``` +Or build from source: -#### Option B: Use Pre-built Image +```bash +docker-compose up -d --build +``` -Use this if you just want to run the app without building locally. +### 5. Access the Dashboard -1. Open `docker-compose.yml`. -2. Ensure the `backend` service uses the image: `ghcr.io/bnair123/musicanalyser:latest`. - * *Note: If you want to force usage of the image and ignore local build context, you can comment out `build: context: ./backend` in the yaml, though Compose usually prefers build context if present.* -3. Ensure your `.env` file is set up. -4. Run: - ```bash - docker pull ghcr.io/bnair123/musicanalyser:latest - docker-compose up -d - ``` +Open your browser to: **http://localhost:8991** -### 4. Access the Dashboard +## Architecture -Open your browser to: -`http://localhost:8991` +``` +┌─────────────────────┐ ┌─────────────────────┐ +│ Frontend │ │ Backend │ +│ (React + Vite) │────▶│ (FastAPI + Worker) │ +│ Port: 8991 │ │ Port: 8000 │ +└─────────────────────┘ └─────────────────────┘ + │ + ┌────────┴────────┐ + ▼ ▼ + ┌──────────┐ ┌──────────────┐ + │ SQLite │ │ Spotify API │ + │ music.db │ │ Gemini AI │ + └──────────┘ └──────────────┘ +``` -### 5. Data Persistence +- **Backend Container**: Runs both the FastAPI server AND the background Spotify polling worker +- **Frontend Container**: Nginx serving the React build, proxies `/api/` to backend +- **Database**: SQLite stored in a Docker named volume (`music_data`) for persistence -- **Database**: Stored in a named volume or host path mapped to `/app/music.db`. -- **Migrations**: The backend uses Alembic. Schema changes are applied automatically on startup. +## Data Persistence -## Local Development (Non-Docker) +Your listening history is stored in a Docker named volume: +- Volume name: `music_data` +- Database file: `/app/music.db` +- Migrations run automatically on container startup -1. **Backend**: - ```bash - cd backend - pip install -r requirements.txt - python run_worker.py # Starts ingestion - uvicorn app.main:app --reload # Starts API - ``` +To backup your data: +```bash +docker cp $(docker-compose ps -q backend):/app/music.db ./backup.db +``` -2. **Frontend**: - ```bash - cd frontend - npm install - npm run dev - ``` - Access at `http://localhost:5173`. +## Local Development + +### Backend +```bash +cd backend +python -m venv venv && source venv/bin/activate +pip install -r requirements.txt + +# Run migrations +alembic upgrade head + +# Start worker (polls Spotify every 60s) +python run_worker.py & + +# Start API server +uvicorn app.main:app --reload +``` + +### Frontend +```bash +cd frontend +npm install +npm run dev +``` + +Access at http://localhost:5173 (Vite proxies `/api` to backend automatically) + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `SPOTIFY_CLIENT_ID` | Yes | Spotify app client ID | +| `SPOTIFY_CLIENT_SECRET` | Yes | Spotify app client secret | +| `SPOTIFY_REFRESH_TOKEN` | Yes | Long-lived refresh token from OAuth | +| `GEMINI_API_KEY` | Yes | Google Gemini API key | +| `GENIUS_ACCESS_TOKEN` | No | Genius API token for lyrics | +| `DATABASE_URL` | No | SQLite path (default: `sqlite:///./music.db`) | diff --git a/backend/Dockerfile b/backend/Dockerfile index 1aa88f8..ffd1e5b 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,9 +3,20 @@ FROM python:3.11-slim WORKDIR /app +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +# Make entrypoint executable +RUN chmod +x entrypoint.sh + +# Expose API port +EXPOSE 8000 + +# Use entrypoint script to run migrations, worker, and API +CMD ["./entrypoint.sh"] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 2f7f1d5..3fd3cb7 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -10,29 +10,17 @@ from alembic import context # Add app to path to import models sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from app.database import Base -from app.models import * # Import models to register them +from app.database import Base, SQLALCHEMY_DATABASE_URL +from app.models import * -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) -# add your model's MetaData object here -# for 'autogenerate' support target_metadata = Base.metadata -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - -# Override sqlalchemy.url with our app's URL -config.set_main_option("sqlalchemy.url", "sqlite:///./music.db") +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) def run_migrations_offline() -> None: diff --git a/backend/app/database.py b/backend/app/database.py index cf50caf..aa92780 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,11 +1,14 @@ +import os from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, declarative_base -SQLALCHEMY_DATABASE_URL = "sqlite:///./music.db" +SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./music.db") -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) +connect_args = {} +if SQLALCHEMY_DATABASE_URL.startswith("sqlite"): + connect_args["check_same_thread"] = False + +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/backend/app/main.py b/backend/app/main.py index d7c96b9..ea624f8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,8 +16,18 @@ load_dotenv() # Create tables Base.metadata.create_all(bind=engine) +from fastapi.middleware.cors import CORSMiddleware + app = FastAPI(title="Music Analyser Backend") +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + @app.get("/") def read_root(): return {"status": "ok", "message": "Music Analyser API is running"} @@ -59,9 +69,8 @@ def trigger_analysis( if stats_json["volume"]["total_plays"] == 0: raise HTTPException(status_code=404, detail="No plays found in the specified period.") - # 2. Generate Narrative narrative_service = NarrativeService(model_name=model_name) - narrative_json = narrative_service.generate_narrative(stats_json) + narrative_json = narrative_service.generate_full_narrative(stats_json) # 3. Save Snapshot snapshot = AnalysisSnapshot( @@ -84,6 +93,8 @@ def trigger_analysis( "narrative": narrative_json } + except HTTPException: + raise # Re-raise HTTPExceptions as-is (404, etc.) except Exception as e: print(f"Analysis Failed: {e}") raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/services/narrative_service.py b/backend/app/services/narrative_service.py index 2a0d54f..d80245e 100644 --- a/backend/app/services/narrative_service.py +++ b/backend/app/services/narrative_service.py @@ -1,16 +1,15 @@ import os import json import re -import google.generativeai as genai +from google import genai from typing import Dict, Any, List, Optional class NarrativeService: def __init__(self, model_name: str = "gemini-2.0-flash-exp"): self.api_key = os.getenv("GEMINI_API_KEY") + self.client = genai.Client(api_key=self.api_key) if self.api_key else None if not self.api_key: print("WARNING: GEMINI_API_KEY not found. LLM features will fail.") - else: - genai.configure(api_key=self.api_key) self.model_name = model_name @@ -48,11 +47,10 @@ Your goal is to generate a JSON report that acts as a deeper, more honest "Spoti }} """ try: - model = genai.GenerativeModel(self.model_name) - # Use JSON mode if available, otherwise rely on prompt + cleaning - response = model.generate_content( - prompt, - generation_config={"response_mime_type": "application/json"} + response = self.client.models.generate_content( + model=self.model_name, + contents=prompt, + config=genai.types.GenerateContentConfig(response_mime_type="application/json") ) return self._clean_and_parse_json(response.text) diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..8209496 --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +echo "=== MusicAnalyser Backend Starting ===" + +# Run Alembic migrations +echo "Running database migrations..." +alembic upgrade head + +# Start the worker in background (polls Spotify every 60s) +echo "Starting Spotify ingestion worker..." +python run_worker.py & +WORKER_PID=$! + +# Start the API server in foreground +echo "Starting API server on port 8000..." +exec uvicorn app.main:app --host 0.0.0.0 --port 8000 diff --git a/backend/requirements.txt b/backend/requirements.txt index 488ba79..3b52a98 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,14 +1,15 @@ fastapi==0.109.2 uvicorn==0.27.1 -sqlalchemy==2.0.27 -httpx==0.26.0 +sqlalchemy==2.0.45 +httpx==0.28.1 python-dotenv==1.0.1 -pydantic==2.6.1 -pydantic-settings==2.1.0 -google-generativeai==0.3.2 +pydantic==2.12.5 +pydantic-core==2.41.5 +pydantic-settings==2.12.0 tenacity==8.2.3 python-dateutil==2.9.0.post0 requests==2.31.0 alembic==1.13.1 scikit-learn==1.4.0 lyricsgenius==3.0.1 +google-genai==1.56.0 diff --git a/docker-compose.yml b/docker-compose.yml index b6cba45..223771e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ version: '3.8' + services: backend: build: @@ -7,20 +8,35 @@ services: container_name: music-analyser-backend restart: unless-stopped volumes: - - /opt/mySpotify/music.db:/app/music.db + - music_data:/app/data environment: + - DATABASE_URL=sqlite:////app/data/music.db - SPOTIFY_CLIENT_ID=${SPOTIFY_CLIENT_ID} - SPOTIFY_CLIENT_SECRET=${SPOTIFY_CLIENT_SECRET} - SPOTIFY_REFRESH_TOKEN=${SPOTIFY_REFRESH_TOKEN} - GEMINI_API_KEY=${GEMINI_API_KEY} + - GENIUS_ACCESS_TOKEN=${GENIUS_ACCESS_TOKEN} ports: - '8000:8000' + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + frontend: build: context: ./frontend + image: ghcr.io/bnair123/musicanalyser-frontend:latest container_name: music-analyser-frontend restart: unless-stopped ports: - '8991:80' depends_on: - - backend + backend: + condition: service_healthy + +volumes: + music_data: + driver: local diff --git a/frontend/index.html b/frontend/index.html index c20fbd3..d2c965f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,12 +1,15 @@ - + - frontend + SonicStats - Your Vibe Report + + + - +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a5a09be..176df3d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,22 +12,41 @@ "antd": "^6.1.2", "axios": "^1.13.2", "date-fns": "^4.1.0", + "framer-motion": "^12.23.26", + "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.11.0" + "react-router-dom": "^7.11.0", + "recharts": "^3.6.0" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", "vite": "^7.2.4" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ant-design/colors": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz", @@ -1126,6 +1145,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@rc-component/async-validator": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", @@ -1839,6 +1896,42 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz", + "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2154,6 +2247,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2199,6 +2304,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2217,7 +2385,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2234,6 +2402,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -2376,6 +2550,47 @@ "react-dom": ">=18.0.0" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2389,6 +2604,43 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -2417,6 +2669,19 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2428,6 +2693,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2486,6 +2764,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001761", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", @@ -2524,6 +2812,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2565,6 +2891,16 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", @@ -2613,12 +2949,146 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -2655,6 +3125,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2671,6 +3147,20 @@ "node": ">=0.4.0" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2737,6 +3227,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", + "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2987,6 +3487,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2994,6 +3500,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3008,6 +3544,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3039,6 +3585,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3113,6 +3672,47 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3298,6 +3898,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3325,6 +3935,44 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3354,6 +4002,16 @@ "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", "license": "MIT" }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3361,6 +4019,17 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3461,6 +4130,26 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3494,6 +4183,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3503,6 +4201,43 @@ "node": ">= 0.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3537,6 +4272,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3544,6 +4294,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3577,6 +4339,36 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3660,6 +4452,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3681,6 +4480,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3701,6 +4520,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3710,6 +4530,140 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3736,6 +4690,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -3763,7 +4738,32 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } }, "node_modules/react-refresh": { "version": "0.18.0", @@ -3813,6 +4813,115 @@ "react-dom": ">=18" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recharts": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3823,6 +4932,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", @@ -3865,6 +4985,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3954,6 +5098,29 @@ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3967,6 +5134,80 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -3976,6 +5217,12 @@ "node": ">=12.22" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3993,6 +5240,32 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4047,6 +5320,44 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d5151a7..ba5a53a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,19 +14,25 @@ "antd": "^6.1.2", "axios": "^1.13.2", "date-fns": "^4.1.0", + "framer-motion": "^12.23.26", + "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.11.0" + "react-router-dom": "^7.11.0", + "recharts": "^3.6.0" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", "vite": "^7.2.4" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c228b2f..8a68555 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,117 +1,9 @@ -import React, { useEffect, useState } from 'react'; -import { Table, Layout, Typography, Tag, Card, Statistic, Row, Col, Space } from 'antd'; -import { ClockCircleOutlined, SoundOutlined, UserOutlined } from '@ant-design/icons'; -import axios from 'axios'; -import { format } from 'date-fns'; - -const { Header, Content, Footer } = Layout; -const { Title, Text } = Typography; - -const App = () => { - const [history, setHistory] = useState([]); - const [loading, setLoading] = useState(true); - - // Fetch History - useEffect(() => { - const fetchHistory = async () => { - try { - const response = await axios.get('/api/history?limit=100'); - setHistory(response.data); - } catch (error) { - console.error("Failed to fetch history", error); - } finally { - setLoading(false); - } - }; - fetchHistory(); - }, []); - - // Columns for Ant Design Table - const columns = [ - { - title: 'Track', - dataIndex: ['track', 'name'], - key: 'track', - render: (text, record) => ( - - {text} - {record.track.album} - - ), - }, - { - title: 'Artist', - dataIndex: ['track', 'artist'], - key: 'artist', - render: (text) => } color="blue">{text}, - }, - { - title: 'Played At', - dataIndex: 'played_at', - key: 'played_at', - render: (date) => ( - - - {format(new Date(date), 'MMM d, h:mm a')} - - ), - sorter: (a, b) => new Date(a.played_at) - new Date(b.played_at), - defaultSortOrder: 'descend', - }, - { - title: 'Vibe', - key: 'vibe', - render: (_, record) => { - const energy = record.track.energy; - const valence = record.track.valence; - if (energy === undefined || valence === undefined) return Unknown; - - let color = 'default'; - let label = 'Neutral'; - - if (energy > 0.7 && valence > 0.5) { color = 'orange'; label = 'High Energy / Happy'; } - else if (energy > 0.7 && valence <= 0.5) { color = 'red'; label = 'High Energy / Dark'; } - else if (energy <= 0.4 && valence > 0.5) { color = 'green'; label = 'Chill / Peaceful'; } - else if (energy <= 0.4 && valence <= 0.5) { color = 'purple'; label = 'Sad / Melancholic'; } - - return {label}; - } - } - ]; +import Dashboard from './components/Dashboard'; +function App() { return ( - -
- - <SoundOutlined style={{ marginRight: 10 }}/> Music Analyser - -
- -
- - - - - } /> - - - - - Recent Listening History - - - -
- Music Analyser ©{new Date().getFullYear()} Created with Ant Design -
- + ); -}; +} export default App; diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx new file mode 100644 index 0000000..b44d79c --- /dev/null +++ b/frontend/src/components/Dashboard.jsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import NarrativeSection from './NarrativeSection'; +import StatsGrid from './StatsGrid'; +import VibeRadar from './VibeRadar'; +import HeatMap from './HeatMap'; +import TopRotation from './TopRotation'; +import { Spin } from 'antd'; // Keeping Spin for loading state + +const API_BASE_URL = '/api'; + +const Dashboard = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + const getTodayKey = () => `sonicstats_v1_${new Date().toISOString().split('T')[0]}`; + + const fetchData = async (forceRefresh = false) => { + setLoading(true); + const todayKey = getTodayKey(); + + if (!forceRefresh) { + const cached = localStorage.getItem(todayKey); + if (cached) { + console.log("Loading from cache"); + setData(JSON.parse(cached)); + setLoading(false); + return; + } + } + + try { + let payload; + if (forceRefresh) { + const res = await axios.post(`${API_BASE_URL}/trigger-analysis?days=30`); + payload = res.data; + } else { + const snapRes = await axios.get(`${API_BASE_URL}/snapshots?limit=1`); + if (snapRes.data && snapRes.data.length > 0) { + const latest = snapRes.data[0]; + payload = { + metrics: latest.metrics_payload, + narrative: latest.narrative_report + }; + } else { + const res = await axios.post(`${API_BASE_URL}/trigger-analysis?days=30`); + payload = res.data; + } + } + + if (payload) { + setData(payload); + localStorage.setItem(todayKey, JSON.stringify(payload)); + } + + } catch (error) { + console.error("Failed to fetch data", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + if (loading && !data) { + return ( +
+ +

Analyzing your auditory aura...

+
+ ); + } + + return ( + <> + {/* Navbar */} +
+
+
+
+ equalizer +
+

SonicStats

+
+
+ +
+
+
+
+ +
+ {/* Hero */} + + + {/* Stats Bento Grid */} + + + {/* Sonic DNA & Chronobiology Split */} +
+ {/* Left Col: Sonic DNA (2/3 width) */} +
+ + +
+ + {/* Right Col: Chronobiology (1/3 width) */} +
+ +
+
+ + {/* Footer: The Roast */} + {data?.narrative?.roast && ( +
+
+
+
+
Analysis Complete
+

+ Your Musical Age is {data?.metrics?.era?.musical_age || "Unknown"}. +

+

+ {data?.narrative?.era_insight || "You are timeless."} +

+
+
+
+ smart_toy +
+

+ "{data.narrative.roast}" +

+
+
+
+
+
+
+ )} +
+ + ); +}; + +export default Dashboard; diff --git a/frontend/src/components/HeatMap.jsx b/frontend/src/components/HeatMap.jsx new file mode 100644 index 0000000..e5455b3 --- /dev/null +++ b/frontend/src/components/HeatMap.jsx @@ -0,0 +1,112 @@ +import React from 'react'; + +const HeatMap = ({ timeHabits }) => { + if (!timeHabits) return null; + + // Helper to get intensity for a day/time slot + // Since we only have aggregate hourly and daily stats, we'll approximate: + // Cell(d, h) ~ Daily(d) * Hourly(h) + + // Normalize daily distribution (0-6, Mon-Sun) + // API usually returns 0=Monday or 0=Sunday depending on backend. Let's assume 0=Monday for now. + const dailyDist = timeHabits.daily_distribution || {}; + const hourlyDist = timeHabits.hourly_distribution || {}; + + const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + const timeBlocks = [ + { label: 'Night', hours: [0, 1, 2, 3, 4, 5] }, + { label: 'Morning', hours: [6, 7, 8, 9, 10, 11] }, + { label: 'Noon', hours: [12, 13, 14, 15, 16, 17] }, + { label: 'Evening', hours: [18, 19, 20, 21, 22, 23] } + ]; + + const maxDaily = Math.max(...Object.values(dailyDist)) || 1; + const maxHourly = Math.max(...Object.values(hourlyDist)) || 1; + + // Flatten grid for rendering: 4 rows (time blocks) x 7 cols (days) + // Actually code.html has many small squares. It looks like each column is a day, and rows are finer time slots. + // Let's do 4 rows representing 6-hour blocks. + + return ( +
+

+ history + Chronobiology +

+ +
+

Listening Heatmap

+ + {/* Grid */} +
+ {/* Header Days */} + {days.map((d, i) => ( +
{d}
+ ))} + + {/* Generate cells: 4 rows x 7 cols */} + {timeBlocks.map((block, rowIdx) => ( + + {days.map((_, colIdx) => { + // Calculate approximated intensity + const dayVal = dailyDist[colIdx] || 0; + const blockVal = block.hours.reduce((acc, h) => acc + (hourlyDist[h] || 0), 0); + + // Normalize + const intensity = (dayVal / maxDaily) * (blockVal / (maxHourly * 6)); + + let bgClass = "bg-[#1e293b]"; // Default empty + if (intensity > 0.8) bgClass = "bg-primary"; + else if (intensity > 0.6) bgClass = "bg-primary/80"; + else if (intensity > 0.4) bgClass = "bg-primary/60"; + else if (intensity > 0.2) bgClass = "bg-primary/40"; + else if (intensity > 0) bgClass = "bg-primary/20"; + + return ( +
+ ); + })} +
+ ))} +
+ +
+ 00:00 + 12:00 + 23:59 +
+
+ + {/* Session Flow (Static for now as API doesn't provide session logs yet) */} +
+

Session Flow

+
+
+ +

Today, 2:30 PM

+

Marathoning

+

3h 42m session

+
+
+ +

Yesterday, 9:15 AM

+

Micro-Dosing

+

12m commute

+
+
+ +

Yesterday, 8:00 PM

+

Deep Focus

+

1h 15m session

+
+
+
+
+ ); +}; + +export default HeatMap; diff --git a/frontend/src/components/NarrativeSection.jsx b/frontend/src/components/NarrativeSection.jsx new file mode 100644 index 0000000..dcc8559 --- /dev/null +++ b/frontend/src/components/NarrativeSection.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { motion } from 'framer-motion'; + +const NarrativeSection = ({ narrative, vibe }) => { + if (!narrative) return null; + + const persona = narrative.persona || "THE UNKNOWN LISTENER"; + const vibeCheck = narrative.vibe_check || "Analyzing auditory aura..."; + + // Generate tags based on vibe metrics if available + const getTags = () => { + if (!vibe) return []; + const tags = []; + if (vibe.valence > 0.6) tags.push({ text: "HIGH VALENCE", color: "primary" }); + else if (vibe.valence < 0.4) tags.push({ text: "MELANCHOLIC", color: "accent-purple" }); + + if (vibe.energy > 0.6) tags.push({ text: "HIGH ENERGY", color: "accent-neon" }); + else if (vibe.energy < 0.4) tags.push({ text: "CHILL VIBES", color: "accent-purple" }); + + if (vibe.danceability > 0.7) tags.push({ text: "DANCEABLE", color: "primary" }); + + return tags.slice(0, 3); // Max 3 tags + }; + + const tags = getTags(); + + // Default tags if none generated + if (tags.length === 0) { + tags.push({ text: "ECLECTIC", color: "primary" }); + tags.push({ text: "MYSTERIOUS", color: "accent-purple" }); + } + + return ( +
+ {/* Dynamic Background */} +
+ +
+ +

+ {persona} +

+
+ +
+ {vibeCheck} +
+ +
+ {tags.map((tag, i) => ( + + {tag.text} + + ))} +
+
+
+ ); +}; + +export default NarrativeSection; diff --git a/frontend/src/components/StatsGrid.jsx b/frontend/src/components/StatsGrid.jsx new file mode 100644 index 0000000..0ea728b --- /dev/null +++ b/frontend/src/components/StatsGrid.jsx @@ -0,0 +1,99 @@ +import React from 'react'; + +const StatsGrid = ({ metrics }) => { + if (!metrics) return null; + + const totalMinutes = Math.round((metrics.volume?.estimated_minutes || 0)); + // Calculate days for the "That's X days straight" text + const daysListened = (totalMinutes / (24 * 60)).toFixed(1); + + const obsessionTrack = metrics.volume?.top_tracks?.[0]; + const obsessionName = obsessionTrack ? obsessionTrack.name : "N/A"; + const obsessionArtist = obsessionTrack ? obsessionTrack.artist : "N/A"; + const obsessionCount = obsessionTrack ? obsessionTrack.count : 0; + + // Fallback image if we don't have one (API currently doesn't seem to return it in top_tracks simple list) + // We'll use a nice gradient or abstract pattern + const obsessionImage = "https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=2070&auto=format&fit=crop"; + + const newDiscoveries = metrics.volume?.unique_artists || 0; + + // Mocking the "Underground" percentage for now as it's not in the standard payload + // Could derive from popularity if available, but let's randomize slightly based on unique artists to make it feel dynamic + const undergroundScore = Math.min(95, Math.max(10, Math.round((newDiscoveries % 100)))); + + return ( +
+ {/* Card 1: Minutes Listened */} +
+
+ Minutes Listened + schedule +
+
+
{totalMinutes.toLocaleString()}
+
+ trending_up + That's {daysListened} days straight +
+
+
+ + {/* Card 2: Obsession Track */} +
+
+
+
+
+
+
+ OBSESSION +

{obsessionName}

+

{obsessionArtist}

+
+
+
{obsessionCount}
+
Plays this month
+
+
+
+
+ + {/* Card 3: New Discoveries & Mainstream Gauge */} +
+ {/* Discoveries */} +
+ visibility +
{newDiscoveries}
+
Unique Artists
+
+ + {/* Gauge */} +
+
+ + + + +
+ {undergroundScore}% +
+
+
Underground Certified
+
+
+
+ ); +}; + +export default StatsGrid; diff --git a/frontend/src/components/TopRotation.jsx b/frontend/src/components/TopRotation.jsx new file mode 100644 index 0000000..582287e --- /dev/null +++ b/frontend/src/components/TopRotation.jsx @@ -0,0 +1,46 @@ +import React from 'react'; + +const TopRotation = ({ volume }) => { + if (!volume || !volume.top_tracks) return null; + + // Use placeholder images since API doesn't return album art in the simple list yet + const placeHolderImages = [ + "https://images.unsplash.com/photo-1619983081563-430f63602796?q=80&w=1000&auto=format&fit=crop", + "https://images.unsplash.com/photo-1493225255756-d9584f8606e9?q=80&w=1000&auto=format&fit=crop", + "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?q=80&w=1000&auto=format&fit=crop", + "https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?q=80&w=1000&auto=format&fit=crop", + "https://images.unsplash.com/photo-1514525253440-b393452e8d26?q=80&w=1000&auto=format&fit=crop" + ]; + + return ( +
+
+

Top Rotation

+
+ + +
+
+ +
+ {volume.top_tracks.slice(0, 5).map((track, i) => { + const name = track.name || track[0]; + const artist = track.artist || track[1]; + + return ( +
+
+

{name}

+

{artist}

+
+ ); + })} +
+
+ ); +}; + +export default TopRotation; diff --git a/frontend/src/components/VibeRadar.jsx b/frontend/src/components/VibeRadar.jsx new file mode 100644 index 0000000..2052aeb --- /dev/null +++ b/frontend/src/components/VibeRadar.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts'; + +const VibeRadar = ({ vibe }) => { + if (!vibe) return null; + + const data = [ + { subject: 'Acoustic', A: vibe.acousticness || 0, fullMark: 1 }, + { subject: 'Dance', A: vibe.danceability || 0, fullMark: 1 }, + { subject: 'Energy', A: vibe.energy || 0, fullMark: 1 }, + { subject: 'Instrumental', A: vibe.instrumentalness || 0, fullMark: 1 }, + { subject: 'Valence', A: vibe.valence || 0, fullMark: 1 }, + { subject: 'Live', A: vibe.liveness || 0, fullMark: 1 }, + ]; + + // Calculate mood percentages based on vibe metrics + const partyScore = Math.round(((vibe.energy + vibe.danceability) / 2) * 100); + const focusScore = Math.round(((vibe.instrumentalness + (1 - vibe.valence)) / 2) * 100); + const chillScore = Math.round(((vibe.acousticness + (1 - vibe.energy)) / 2) * 100); + + // Normalize to sum to 100 roughly (just for display) + const total = partyScore + focusScore + chillScore; + const partyPct = Math.round((partyScore / total) * 100); + const focusPct = Math.round((focusScore / total) * 100); + const chillPct = 100 - partyPct - focusPct; + + return ( +
+
+

+ fingerprint + Sonic DNA +

+
+ +
+ {/* Feature Radar */} +
+ + + + + + + + +
+ + {/* Mood Modes & Whiplash */} +
+ {/* Mood Bubbles */} +
+

Mood Clusters

+
+
+ Party
{partyPct}% +
+
+ Focus
{focusPct}% +
+
+ Chill
{chillPct}% +
+
+
+ + {/* Whiplash Meter */} +
+
+

Whiplash Meter

+ HIGH VOLATILITY +
+
+ {/* Fake waveform */} + + + +
+
+
+
+
+ ); +}; + +export default VibeRadar; diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..cd802d0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,68 +1,88 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@tailwind base; +@tailwind components; +@tailwind utilities; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +/* Custom Utilities from code.html */ +.glass-panel { + background: rgba(24, 34, 52, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +.holographic-badge { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%); + box-shadow: 0 0 20px rgba(37, 106, 244, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.2); + backdrop-filter: blur(8px); + position: relative; + overflow: hidden; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +.holographic-badge::before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transform: rotate(45deg); + animation: holo-shine 3s infinite linear; } -h1 { - font-size: 3.2em; - line-height: 1.1; +@keyframes holo-shine { + 0% { + transform: translateX(-100%) rotate(45deg); + } + 100% { + transform: translateX(100%) rotate(45deg); + } } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +.typing-cursor::after { + content: "|"; + animation: blink 1s step-end infinite; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +@keyframes blink { + 50% { + opacity: 0; + } +} + +.mood-gradient { + background: radial-gradient(circle at 50% 50%, rgba(37, 106, 244, 0.15), transparent 70%), radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.1), transparent 50%); +} + +/* Hide scrollbar for carousel */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.radar-grid circle { + fill: none; + stroke: #334155; + stroke-width: 1; +} + +.radar-grid line { + stroke: #334155; + stroke-width: 1; +} + +.radar-area { + fill: rgba(37, 106, 244, 0.3); + stroke: #256af4; + stroke-width: 2; +} + +/* Paper texture */ +.paper-texture { + background-color: #f0f0f0; + background-image: url(https://lh3.googleusercontent.com/aida-public/AB6AXuCxWgGFi3y5uU1Eo5AvX4bBjCZyqH_y2JcjejnbTD6deIOvWk3bplb-Bj1oFuS3P1LlYkmdnJOUkNL9g9L4yQd3Otfcz6qhp7psxQQqPTkZwV4myWl1ZoEp3ZQfBGYSI-nJnwMpWmwB1uO75co2eIFngOJE3Rn6JmLO_nOUKGhsut6iWdt_LKijBTH7SilsOX7HWTXfekHR2CwuUs4LJ6LkTMCVXS3R-aQTNfmsza_6PcRn40PTaBYS90sY9xtDPFcfgS2vzgPmPDZ6); } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index c099868..3329849 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' +import './index.css'; import { ConfigProvider, theme } from 'antd'; ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..19841a1 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,34 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + darkMode: "class", + safelist: [ + 'bg-primary/20', 'text-primary', 'border-primary/20', + 'bg-accent-purple/20', 'text-accent-purple', 'border-accent-purple/20', + 'bg-accent-neon/20', 'text-accent-neon', 'border-accent-neon/20', + ], + theme: { + extend: { + colors: { + "primary": "#256af4", + "background-light": "#f5f6f8", + "background-dark": "#101622", + "card-dark": "#182234", + "card-darker": "#111927", + "accent-neon": "#0bda5e", + "accent-purple": "#8b5cf6", + }, + fontFamily: { + "display": ["Space Grotesk", "sans-serif"], + "mono": ["Space Grotesk", "monospace"], + }, + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + } + }, + }, + plugins: [], +}