From 937b3fc8117741dc9914343f1fbb2a03902752a4 Mon Sep 17 00:00:00 2001 From: WaveringAna Date: Sun, 26 Jan 2025 01:30:51 -0500 Subject: [PATCH] almost production ready --- .gitignore | 2 + .preludeignore | 3 +- ...7a00bf5dad445ef7d484936ce6e0cbe21c965.json | 28 ++ ...0bf379e5728f378626198a0a9ed024a413273.json | 28 ++ Cargo.lock | 52 ++++ Cargo.toml | 1 + DEPLOYMENT.md | 258 ++++++++++++++++++ Dockerfile | 51 +++- README.md | 174 ++++++++++++ build.sh | 50 ++++ docker-compose.yml | 38 ++- frontend/bun.lock | 90 +++++- frontend/package.json | 1 + frontend/src/api/client.ts | 16 +- frontend/src/components/LinkList.tsx | 22 +- frontend/src/components/StatisticsModal.tsx | 115 ++++++++ frontend/src/types/api.ts | 12 +- frontend/vite.config.ts | 45 +-- src/handlers.rs | 85 +++++- src/main.rs | 32 ++- src/models.rs | 23 +- 21 files changed, 1071 insertions(+), 55 deletions(-) create mode 100644 .sqlx/query-8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965.json create mode 100644 .sqlx/query-c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273.json create mode 100644 DEPLOYMENT.md create mode 100644 README.md create mode 100755 build.sh create mode 100644 frontend/src/components/StatisticsModal.tsx diff --git a/.gitignore b/.gitignore index d5621cf..f23e992 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ **/node_modules node_modules .env +.env.* +/static \ No newline at end of file diff --git a/.preludeignore b/.preludeignore index 08db334..ec5b9d9 100644 --- a/.preludeignore +++ b/.preludeignore @@ -1,2 +1,3 @@ .sqlx -.env \ No newline at end of file +.env +.env.* \ No newline at end of file diff --git a/.sqlx/query-8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965.json b/.sqlx/query-8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965.json new file mode 100644 index 0000000..b1389f8 --- /dev/null +++ b/.sqlx/query-8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n query_source as \"source!\",\n COUNT(*)::bigint as \"count!\"\n FROM clicks\n WHERE link_id = $1\n AND query_source IS NOT NULL\n AND query_source != ''\n GROUP BY query_source\n ORDER BY COUNT(*) DESC\n LIMIT 10\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "source!", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + true, + null + ] + }, + "hash": "8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965" +} diff --git a/.sqlx/query-c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273.json b/.sqlx/query-c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273.json new file mode 100644 index 0000000..48d5dda --- /dev/null +++ b/.sqlx/query-c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n DATE(created_at)::date as \"date!\",\n COUNT(*)::bigint as \"clicks!\"\n FROM clicks\n WHERE link_id = $1\n GROUP BY DATE(created_at)\n ORDER BY DATE(created_at) ASC -- Changed from DESC to ASC\n LIMIT 30\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "date!", + "type_info": "Date" + }, + { + "ordinal": 1, + "name": "clicks!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273" +} diff --git a/Cargo.lock b/Cargo.lock index eb85298..de84c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,29 @@ dependencies = [ "smallvec", ] +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + [[package]] name = "actix-http" version = "3.9.0" @@ -1033,6 +1056,12 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.9.5" @@ -1374,6 +1403,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2019,6 +2058,7 @@ name = "simplelink" version = "0.1.0" dependencies = [ "actix-cors", + "actix-files", "actix-web", "anyhow", "argon2", @@ -2587,6 +2627,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -2665,6 +2711,12 @@ dependencies = [ "serde", ] +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index a8c9acc..e2c2be6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] jsonwebtoken = "9" actix-web = "4.4" +actix-files = "0.6" actix-cors = "0.6" tokio = { version = "1.36", features = ["full"] } sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] } diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..70e81bb --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,258 @@ +# SimpleLink Deployment Guide + +## Environment Configuration + +### Environment Files + +#### Development (.env.development) +```env +VITE_API_URL=http://localhost:3000 +NODE_ENV=development +RUST_ENV=debug +JWT_SECRET=dev-secret-key +POSTGRES_DB=shortener_dev +POSTGRES_USER=shortener +POSTGRES_PASSWORD=shortener123 +``` + +#### Production (.env.production) +```env +VITE_API_URL=https://your-production-domain.com +NODE_ENV=production +RUST_ENV=release +JWT_SECRET=your-secure-production-key +POSTGRES_DB=shortener_prod +POSTGRES_USER=shortener_prod +POSTGRES_PASSWORD=secure-password +``` + +#### Staging (.env.staging) +```env +VITE_API_URL=https://staging.your-domain.com +NODE_ENV=production +RUST_ENV=release +JWT_SECRET=your-staging-key +POSTGRES_DB=shortener_staging +POSTGRES_USER=shortener_staging +POSTGRES_PASSWORD=staging-password +``` + +## Docker Deployment + +### Basic Commands + +```bash +# Build and run with specific environment +docker-compose --env-file .env.development up --build # Development +docker-compose --env-file .env.staging up --build # Staging +docker-compose --env-file .env.production up --build # Production + +# Run in detached mode +docker-compose --env-file .env.production up -d --build + +# Stop containers +docker-compose down + +# View logs +docker-compose logs -f +``` + +### Override Single Variables +```bash +VITE_API_URL=https://custom-domain.com docker-compose up --build +``` + +### Using Docker Compose Override Files + +#### Development (docker-compose.dev.yml) +```yaml +services: + app: + build: + args: + VITE_API_URL: http://localhost:3000 + NODE_ENV: development + RUST_ENV: debug + volumes: + - ./src:/usr/src/app/src + environment: + RUST_LOG: debug +``` + +#### Production (docker-compose.prod.yml) +```yaml +services: + app: + build: + args: + VITE_API_URL: https://your-production-domain.com + NODE_ENV: production + RUST_ENV: release + deploy: + replicas: 2 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +### Using Override Files +```bash +# Development +docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build + +# Production +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build +``` + +## Environment Variables Reference + +### Build-time Variables +- `VITE_API_URL`: Frontend API endpoint +- `NODE_ENV`: Node.js environment (development/production) +- `RUST_ENV`: Rust build type (debug/release) + +### Runtime Variables +- `SERVER_HOST`: Backend host address +- `SERVER_PORT`: Backend port +- `JWT_SECRET`: JWT signing key +- `RUST_LOG`: Logging level +- `DATABASE_URL`: PostgreSQL connection string + +### Database Variables +- `POSTGRES_DB`: Database name +- `POSTGRES_USER`: Database user +- `POSTGRES_PASSWORD`: Database password + +## Container Structure + +### Frontend Container +- Build tool: Bun +- Source: `/app/frontend` +- Output: `/app/frontend/dist` +- Static files location: `/app/static` + +### Backend Container +- Build tool: Cargo +- Source: `/usr/src/app` +- Binary: `/app/simplelink` +- Migrations: `/app/migrations` + +## Production Deployment Checklist + +1. Environment Setup + - [ ] Set secure database passwords + - [ ] Generate strong JWT secret + - [ ] Configure proper API URL + - [ ] Set appropriate logging levels + +2. Database + - [ ] Configure backup strategy + - [ ] Set up proper indexes + - [ ] Configure connection pooling + +3. Security + - [ ] Enable SSL/TLS + - [ ] Set up proper firewalls + - [ ] Configure CORS properly + - [ ] Use secrets management + +4. Monitoring + - [ ] Set up logging aggregation + - [ ] Configure health checks + - [ ] Set up metrics collection + +5. Performance + - [ ] Configure proper cache headers + - [ ] Set up CDN if needed + - [ ] Configure database connection pool size + +## Common Operations + +### View Container Logs +```bash +# All containers +docker-compose logs -f + +# Specific container +docker-compose logs -f app +docker-compose logs -f db +``` + +### Access Database +```bash +# Connect to database container +docker-compose exec db psql -U shortener -d shortener + +# Backup database +docker-compose exec db pg_dump -U shortener shortener > backup.sql + +# Restore database +cat backup.sql | docker-compose exec -T db psql -U shortener -d shortener +``` + +### Container Management +```bash +# Restart single service +docker-compose restart app + +# View container status +docker-compose ps + +# View resource usage +docker-compose top +``` + +## Troubleshooting + +### Database Connection Issues +1. Check if database container is running: + ```bash + docker-compose ps db + ``` +2. Verify database credentials in environment files +3. Check database logs: + ```bash + docker-compose logs db + ``` + +### Frontend Build Issues +1. Clear node_modules and rebuild: + ```bash + docker-compose down + rm -rf frontend/node_modules + docker-compose up --build + ``` + +### Backend Issues +1. Check backend logs: + ```bash + docker-compose logs app + ``` +2. Verify environment variables are set correctly +3. Check database connectivity + +## Build Script Usage + +The `build.sh` script handles environment-specific builds and static file generation. + +### Basic Usage +```bash +# Default production build +./build.sh + +# Development build +ENV=development ./build.sh + +# Staging build +ENV=staging ./build.sh + +# Production build with custom API URL +VITE_API_URL=https://api.example.com ./build.sh + +# Development build with custom API URL +ENV=development VITE_API_URL=http://localhost:8080 ./build.sh + +# Show help +./build.sh --help \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 75f422a..5fdc9e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,29 @@ -# Build stage -FROM rust:latest as builder +# Frontend build stage +FROM oven/bun:latest as frontend-builder +WORKDIR /app/frontend + +# Install bun +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/root/.bun/bin:${PATH}" + +# Copy frontend files +COPY frontend/package.json frontend/bun.lock ./ +RUN bun install + +COPY frontend/ ./ + +# Build frontend with environment variables +# These can be overridden at build time +ARG VITE_API_URL=http://localhost:3000 +ARG NODE_ENV=production +ENV VITE_API_URL=$VITE_API_URL +ENV NODE_ENV=$NODE_ENV + +RUN echo "VITE_API_URL=${VITE_API_URL}" > .env.production +RUN bun run build + +# Rust build stage +FROM rust:latest as backend-builder # Install PostgreSQL client libraries and SSL dependencies RUN apt-get update && \ @@ -16,8 +40,9 @@ COPY src/ src/ COPY migrations/ migrations/ COPY .sqlx/ .sqlx/ -# Build your application -RUN cargo build --release +# Build application +ARG RUST_ENV=release +RUN cargo build --${RUST_ENV} # Runtime stage FROM debian:bookworm-slim @@ -29,17 +54,19 @@ RUN apt-get update && \ WORKDIR /app -# Copy the binary from builder -COPY --from=builder /usr/src/app/target/release/simplelink /app/simplelink -# Copy migrations folder for SQLx -COPY --from=builder /usr/src/app/migrations /app/migrations +# Copy the binary and migrations from backend builder +COPY --from=backend-builder /usr/src/app/target/release/simplelink /app/simplelink +COPY --from=backend-builder /usr/src/app/migrations /app/migrations -# Expose the port (this is just documentation) -EXPOSE 8080 +# Copy static files from frontend builder +COPY --from=frontend-builder /app/frontend/dist /app/static + +# Expose the port +EXPOSE 3000 # Set default network configuration ENV SERVER_HOST=0.0.0.0 -ENV SERVER_PORT=8080 +ENV SERVER_PORT=3000 # Run the binary -CMD ["./simplelink"] \ No newline at end of file +CMD ["./simplelink"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a099c0 --- /dev/null +++ b/README.md @@ -0,0 +1,174 @@ +# SimpleLink + +A modern link shortening and tracking service built with Rust and React. + +## Features + +- 🔗 Link shortening with custom codes +- 📊 Click tracking and analytics +- 🔒 User authentication +- 📱 Responsive design +- 🌓 Dark/light mode +- 📈 Source attribution tracking + +## Tech Stack + +### Backend +- Rust +- Actix-web +- SQLx +- PostgreSQL +- JWT Authentication + +### Frontend +- React +- TypeScript +- Tailwind CSS +- Shadcn/ui +- Vite +- Recharts + +## Development Setup + +### Prerequisites +- Rust (latest stable) +- Bun (or Node.js) +- PostgreSQL +- Docker (optional) + +### Environment Variables + +#### Backend (.env) +```env +DATABASE_URL=postgres://user:password@localhost:5432/simplelink +SERVER_HOST=127.0.0.1 +SERVER_PORT=3000 +JWT_SECRET=your-secret-key +``` + +#### Frontend Environment Files + +Development (.env.development): +```env +VITE_API_URL=http://localhost:3000 +``` + +Production (.env.production): +```env +VITE_API_URL=https://your-production-domain.com +``` + +### Local Development + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/simplelink.git +cd simplelink +``` + +2. Set up the database: +```bash +psql -U postgres +CREATE DATABASE simplelink; +``` + +3. Run database migrations: +```bash +cargo run --bin migrate +``` + +4. Start the backend server: +```bash +cargo run +``` + +5. In a new terminal, start the frontend development server: +```bash +cd frontend +bun install +bun run dev +``` + +The app will be available at: +- Frontend: http://localhost:5173 +- Backend API: http://localhost:3000 + +### Building for Production + +Use the build script to create a production build: +```bash +./build.sh +``` + +This will: +1. Build the frontend with production settings +2. Copy static files to the correct location +3. Prepare everything for deployment + +You can override the API URL during build: +```bash +VITE_API_URL=https://api.yoursite.com ./build.sh +``` + +### Docker Deployment + +Build and run using Docker Compose: +```bash +docker-compose up --build +``` + +Or build and run the containers separately: +```bash +# Build the image +docker build -t simplelink . + +# Run the container +docker run -p 3000:3000 \ + -e DATABASE_URL=postgres://user:password@db:5432/simplelink \ + -e JWT_SECRET=your-secret-key \ + simplelink +``` + +## Project Structure + +``` +simplelink/ +├── src/ # Rust backend code +│ ├── handlers/ # Request handlers +│ ├── models/ # Database models +│ └── main.rs # Application entry point +├── migrations/ # Database migrations +├── frontend/ # React frontend +│ ├── src/ +│ │ ├── components/ # React components +│ │ ├── api/ # API client +│ │ └── types/ # TypeScript types +│ └── vite.config.ts # Vite configuration +├── static/ # Built frontend files (generated) +├── Cargo.toml # Rust dependencies +├── docker-compose.yml # Docker composition +└── build.sh # Build script +``` + +## API Endpoints + +- `POST /api/auth/register` - Register new user +- `POST /api/auth/login` - Login user +- `POST /api/shorten` - Create short link +- `GET /api/links` - Get all user links +- `DELETE /api/links/{id}` - Delete link +- `GET /api/links/{id}/clicks` - Get click statistics +- `GET /api/links/{id}/sources` - Get source statistics +- `GET /{short_code}` - Redirect to original URL + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..6b496ce --- /dev/null +++ b/build.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +# Default environment is production +ENV=${ENV:-production} +echo "Building for environment: $ENV" + +# Load environment variables from the appropriate .env file +if [ -f "frontend/.env.$ENV" ]; then + echo "Loading environment variables from frontend/.env.$ENV" + export $(cat frontend/.env.$ENV | grep -v '^#' | xargs) +else + echo "Warning: No .env.$ENV file found in frontend directory" +fi + +# Allow override of VITE_API_URL through command line +VITE_API_URL=${VITE_API_URL:-${VITE_API_URL:-http://localhost:3000}} +echo "Using API URL: $VITE_API_URL" + +echo "Building frontend..." +cd frontend +bun install + +# Export variables for Vite to pick up +export VITE_API_URL +export NODE_ENV=$ENV + +echo "Running build..." +bun run build + +echo "Copying static files..." +cd .. +rm -rf static +mkdir -p static +cp -r frontend/dist/* static/ + +echo "Build complete!" + +# Usage information if no arguments provided +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + echo "Usage:" + echo " ./build.sh # Builds with production environment" + echo " ENV=development ./build.sh # Builds with development environment" + echo " VITE_API_URL=https://api.example.com ./build.sh # Overrides API URL" + echo "" + echo "Environment Variables:" + echo " ENV - Build environment (development/staging/production)" + echo " VITE_API_URL - Override the API URL" + exit 0 +fi diff --git a/docker-compose.yml b/docker-compose.yml index de2fac9..2afdf52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,50 @@ version: '3.8' + +# Define reusable environment variables +x-environment: &common-env + SERVER_HOST: 0.0.0.0 + SERVER_PORT: 3000 + RUST_LOG: info + services: + app: + build: + context: . + dockerfile: Dockerfile + args: + # Build-time variables + VITE_API_URL: ${VITE_API_URL:-http://localhost:3000} + NODE_ENV: ${NODE_ENV:-production} + RUST_ENV: ${RUST_ENV:-release} + container_name: shortener-app + ports: + - "3000:3000" + environment: + <<: *common-env # Include common environment variables + DATABASE_URL: postgres://shortener:shortener123@db:5432/shortener + JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-me-in-production} + depends_on: + db: + condition: service_healthy + restart: unless-stopped + db: image: postgres:15-alpine container_name: shortener-db environment: - POSTGRES_DB: shortener - POSTGRES_USER: shortener - POSTGRES_PASSWORD: shortener123 + POSTGRES_DB: ${POSTGRES_DB:-shortener} + POSTGRES_USER: ${POSTGRES_USER:-shortener} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-shortener123} ports: - "5432:5432" volumes: - shortener-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U shortener"] + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-shortener}" ] interval: 5s timeout: 5s retries: 5 + restart: unless-stopped volumes: shortener-data: - diff --git a/frontend/bun.lock b/frontend/bun.lock index fa4c8a7..4da5da1 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -10,6 +10,7 @@ "@mantine/form": "^7.16.1", "@mantine/hooks": "^7.16.1", "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tabs": "^1.1.2", @@ -22,6 +23,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", + "recharts": "^2.15.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.1", @@ -220,6 +222,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -232,6 +236,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.5", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], @@ -240,6 +246,10 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], @@ -262,8 +272,14 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.0", "", { "os": "android", "cpu": "arm" }, "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A=="], @@ -340,6 +356,24 @@ "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], + "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -424,8 +458,32 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -434,6 +492,8 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.88", "", {}, "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw=="], "enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="], @@ -466,8 +526,12 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], + "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -520,6 +584,8 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -582,6 +648,8 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -608,6 +676,8 @@ "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -634,6 +704,8 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -646,7 +718,7 @@ "react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], - "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "react-number-format": ["react-number-format@5.4.3", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="], @@ -656,10 +728,18 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-textarea-autosize": ["react-textarea-autosize@8.5.6", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw=="], + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "recharts": ["recharts@2.15.0", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw=="], + + "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -702,6 +782,8 @@ "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="], @@ -732,6 +814,8 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "victory-vendor": ["victory-vendor@36.9.2", "", { "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" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + "vite": ["vite@6.0.11", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.4.49", "rollup": "^4.23.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -762,6 +846,10 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], } } diff --git a/frontend/package.json b/frontend/package.json index 723053d..484a1df 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", + "recharts": "^2.15.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.1" diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 6b96b09..32a8ca9 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { CreateLinkRequest, Link, AuthResponse } from '../types/api'; +import { CreateLinkRequest, Link, AuthResponse, ClickStats, SourceStats } from '../types/api'; // Create axios instance with default config const api = axios.create({ @@ -45,4 +45,16 @@ export const getAllLinks = async () => { export const deleteLink = async (id: number) => { await api.delete(`/links/${id}`); -}; \ No newline at end of file +}; + +export const getLinkClickStats = async (id: number) => { + const response = await api.get(`/links/${id}/clicks`); + return response.data; +}; + +export const getLinkSourceStats = async (id: number) => { + const response = await api.get(`/links/${id}/sources`); + return response.data; +}; + +export { api }; \ No newline at end of file diff --git a/frontend/src/components/LinkList.tsx b/frontend/src/components/LinkList.tsx index b23d200..d953a1c 100644 --- a/frontend/src/components/LinkList.tsx +++ b/frontend/src/components/LinkList.tsx @@ -12,7 +12,7 @@ import { } from "@/components/ui/table" import { Button } from "@/components/ui/button" import { useToast } from "@/hooks/use-toast" -import { Copy, Trash2 } from "lucide-react" +import { Copy, Trash2, BarChart2 } from "lucide-react" import { Dialog, DialogContent, @@ -22,6 +22,8 @@ import { DialogFooter, } from "@/components/ui/dialog" +import { StatisticsModal } from "./StatisticsModal" + interface LinkListProps { refresh?: number; } @@ -33,6 +35,10 @@ export function LinkList({ refresh = 0 }: LinkListProps) { isOpen: false, linkId: null, }) + const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({ + isOpen: false, + linkId: null, + }); const { toast } = useToast() const fetchLinks = async () => { @@ -145,6 +151,15 @@ export function LinkList({ refresh = 0 }: LinkListProps) { Copy link +