A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.

Revert "almost production ready"

This reverts commit 937b3fc8117741dc9914343f1fbb2a03902752a4.

-2
.gitignore
··· 2 2 **/node_modules 3 3 node_modules 4 4 .env 5 - .env.* 6 - /static
+1 -2
.preludeignore
··· 1 1 .sqlx 2 - .env 3 - .env.* 2 + .env
-28
.sqlx/query-8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "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 ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "source!", 9 - "type_info": "Text" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "count!", 14 - "type_info": "Int8" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Int4" 20 - ] 21 - }, 22 - "nullable": [ 23 - true, 24 - null 25 - ] 26 - }, 27 - "hash": "8452fbf45386d160bc99ac6c0917a00bf5dad445ef7d484936ce6e0cbe21c965" 28 - }
-28
.sqlx/query-c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273.json
··· 1 - { 2 - "db_name": "PostgreSQL", 3 - "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 ", 4 - "describe": { 5 - "columns": [ 6 - { 7 - "ordinal": 0, 8 - "name": "date!", 9 - "type_info": "Date" 10 - }, 11 - { 12 - "ordinal": 1, 13 - "name": "clicks!", 14 - "type_info": "Int8" 15 - } 16 - ], 17 - "parameters": { 18 - "Left": [ 19 - "Int4" 20 - ] 21 - }, 22 - "nullable": [ 23 - null, 24 - null 25 - ] 26 - }, 27 - "hash": "c723ec75f9ca9482e1bc86108c20bf379e5728f378626198a0a9ed024a413273" 28 - }
-52
Cargo.lock
··· 35 35 ] 36 36 37 37 [[package]] 38 - name = "actix-files" 39 - version = "0.6.6" 40 - source = "registry+https://github.com/rust-lang/crates.io-index" 41 - checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" 42 - dependencies = [ 43 - "actix-http", 44 - "actix-service", 45 - "actix-utils", 46 - "actix-web", 47 - "bitflags", 48 - "bytes", 49 - "derive_more", 50 - "futures-core", 51 - "http-range", 52 - "log", 53 - "mime", 54 - "mime_guess", 55 - "percent-encoding", 56 - "pin-project-lite", 57 - "v_htmlescape", 58 - ] 59 - 60 - [[package]] 61 38 name = "actix-http" 62 39 version = "3.9.0" 63 40 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1057 1034 ] 1058 1035 1059 1036 [[package]] 1060 - name = "http-range" 1061 - version = "0.1.5" 1062 - source = "registry+https://github.com/rust-lang/crates.io-index" 1063 - checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" 1064 - 1065 - [[package]] 1066 1037 name = "httparse" 1067 1038 version = "1.9.5" 1068 1039 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1402 1373 version = "0.3.17" 1403 1374 source = "registry+https://github.com/rust-lang/crates.io-index" 1404 1375 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1405 - 1406 - [[package]] 1407 - name = "mime_guess" 1408 - version = "2.0.5" 1409 - source = "registry+https://github.com/rust-lang/crates.io-index" 1410 - checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1411 - dependencies = [ 1412 - "mime", 1413 - "unicase", 1414 - ] 1415 1376 1416 1377 [[package]] 1417 1378 name = "minimal-lexical" ··· 2058 2019 version = "0.1.0" 2059 2020 dependencies = [ 2060 2021 "actix-cors", 2061 - "actix-files", 2062 2022 "actix-web", 2063 2023 "anyhow", 2064 2024 "argon2", ··· 2628 2588 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2629 2589 2630 2590 [[package]] 2631 - name = "unicase" 2632 - version = "2.8.1" 2633 - source = "registry+https://github.com/rust-lang/crates.io-index" 2634 - checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2635 - 2636 - [[package]] 2637 2591 name = "unicode-bidi" 2638 2592 version = "0.3.18" 2639 2593 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2710 2664 "getrandom", 2711 2665 "serde", 2712 2666 ] 2713 - 2714 - [[package]] 2715 - name = "v_htmlescape" 2716 - version = "0.15.8" 2717 - source = "registry+https://github.com/rust-lang/crates.io-index" 2718 - checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" 2719 2667 2720 2668 [[package]] 2721 2669 name = "valuable"
-1
Cargo.toml
··· 10 10 [dependencies] 11 11 jsonwebtoken = "9" 12 12 actix-web = "4.4" 13 - actix-files = "0.6" 14 13 actix-cors = "0.6" 15 14 tokio = { version = "1.36", features = ["full"] } 16 15 sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] }
-258
DEPLOYMENT.md
··· 1 - # SimpleLink Deployment Guide 2 - 3 - ## Environment Configuration 4 - 5 - ### Environment Files 6 - 7 - #### Development (.env.development) 8 - ```env 9 - VITE_API_URL=http://localhost:3000 10 - NODE_ENV=development 11 - RUST_ENV=debug 12 - JWT_SECRET=dev-secret-key 13 - POSTGRES_DB=shortener_dev 14 - POSTGRES_USER=shortener 15 - POSTGRES_PASSWORD=shortener123 16 - ``` 17 - 18 - #### Production (.env.production) 19 - ```env 20 - VITE_API_URL=https://your-production-domain.com 21 - NODE_ENV=production 22 - RUST_ENV=release 23 - JWT_SECRET=your-secure-production-key 24 - POSTGRES_DB=shortener_prod 25 - POSTGRES_USER=shortener_prod 26 - POSTGRES_PASSWORD=secure-password 27 - ``` 28 - 29 - #### Staging (.env.staging) 30 - ```env 31 - VITE_API_URL=https://staging.your-domain.com 32 - NODE_ENV=production 33 - RUST_ENV=release 34 - JWT_SECRET=your-staging-key 35 - POSTGRES_DB=shortener_staging 36 - POSTGRES_USER=shortener_staging 37 - POSTGRES_PASSWORD=staging-password 38 - ``` 39 - 40 - ## Docker Deployment 41 - 42 - ### Basic Commands 43 - 44 - ```bash 45 - # Build and run with specific environment 46 - docker-compose --env-file .env.development up --build # Development 47 - docker-compose --env-file .env.staging up --build # Staging 48 - docker-compose --env-file .env.production up --build # Production 49 - 50 - # Run in detached mode 51 - docker-compose --env-file .env.production up -d --build 52 - 53 - # Stop containers 54 - docker-compose down 55 - 56 - # View logs 57 - docker-compose logs -f 58 - ``` 59 - 60 - ### Override Single Variables 61 - ```bash 62 - VITE_API_URL=https://custom-domain.com docker-compose up --build 63 - ``` 64 - 65 - ### Using Docker Compose Override Files 66 - 67 - #### Development (docker-compose.dev.yml) 68 - ```yaml 69 - services: 70 - app: 71 - build: 72 - args: 73 - VITE_API_URL: http://localhost:3000 74 - NODE_ENV: development 75 - RUST_ENV: debug 76 - volumes: 77 - - ./src:/usr/src/app/src 78 - environment: 79 - RUST_LOG: debug 80 - ``` 81 - 82 - #### Production (docker-compose.prod.yml) 83 - ```yaml 84 - services: 85 - app: 86 - build: 87 - args: 88 - VITE_API_URL: https://your-production-domain.com 89 - NODE_ENV: production 90 - RUST_ENV: release 91 - deploy: 92 - replicas: 2 93 - logging: 94 - driver: "json-file" 95 - options: 96 - max-size: "10m" 97 - max-file: "3" 98 - ``` 99 - 100 - ### Using Override Files 101 - ```bash 102 - # Development 103 - docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build 104 - 105 - # Production 106 - docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build 107 - ``` 108 - 109 - ## Environment Variables Reference 110 - 111 - ### Build-time Variables 112 - - `VITE_API_URL`: Frontend API endpoint 113 - - `NODE_ENV`: Node.js environment (development/production) 114 - - `RUST_ENV`: Rust build type (debug/release) 115 - 116 - ### Runtime Variables 117 - - `SERVER_HOST`: Backend host address 118 - - `SERVER_PORT`: Backend port 119 - - `JWT_SECRET`: JWT signing key 120 - - `RUST_LOG`: Logging level 121 - - `DATABASE_URL`: PostgreSQL connection string 122 - 123 - ### Database Variables 124 - - `POSTGRES_DB`: Database name 125 - - `POSTGRES_USER`: Database user 126 - - `POSTGRES_PASSWORD`: Database password 127 - 128 - ## Container Structure 129 - 130 - ### Frontend Container 131 - - Build tool: Bun 132 - - Source: `/app/frontend` 133 - - Output: `/app/frontend/dist` 134 - - Static files location: `/app/static` 135 - 136 - ### Backend Container 137 - - Build tool: Cargo 138 - - Source: `/usr/src/app` 139 - - Binary: `/app/simplelink` 140 - - Migrations: `/app/migrations` 141 - 142 - ## Production Deployment Checklist 143 - 144 - 1. Environment Setup 145 - - [ ] Set secure database passwords 146 - - [ ] Generate strong JWT secret 147 - - [ ] Configure proper API URL 148 - - [ ] Set appropriate logging levels 149 - 150 - 2. Database 151 - - [ ] Configure backup strategy 152 - - [ ] Set up proper indexes 153 - - [ ] Configure connection pooling 154 - 155 - 3. Security 156 - - [ ] Enable SSL/TLS 157 - - [ ] Set up proper firewalls 158 - - [ ] Configure CORS properly 159 - - [ ] Use secrets management 160 - 161 - 4. Monitoring 162 - - [ ] Set up logging aggregation 163 - - [ ] Configure health checks 164 - - [ ] Set up metrics collection 165 - 166 - 5. Performance 167 - - [ ] Configure proper cache headers 168 - - [ ] Set up CDN if needed 169 - - [ ] Configure database connection pool size 170 - 171 - ## Common Operations 172 - 173 - ### View Container Logs 174 - ```bash 175 - # All containers 176 - docker-compose logs -f 177 - 178 - # Specific container 179 - docker-compose logs -f app 180 - docker-compose logs -f db 181 - ``` 182 - 183 - ### Access Database 184 - ```bash 185 - # Connect to database container 186 - docker-compose exec db psql -U shortener -d shortener 187 - 188 - # Backup database 189 - docker-compose exec db pg_dump -U shortener shortener > backup.sql 190 - 191 - # Restore database 192 - cat backup.sql | docker-compose exec -T db psql -U shortener -d shortener 193 - ``` 194 - 195 - ### Container Management 196 - ```bash 197 - # Restart single service 198 - docker-compose restart app 199 - 200 - # View container status 201 - docker-compose ps 202 - 203 - # View resource usage 204 - docker-compose top 205 - ``` 206 - 207 - ## Troubleshooting 208 - 209 - ### Database Connection Issues 210 - 1. Check if database container is running: 211 - ```bash 212 - docker-compose ps db 213 - ``` 214 - 2. Verify database credentials in environment files 215 - 3. Check database logs: 216 - ```bash 217 - docker-compose logs db 218 - ``` 219 - 220 - ### Frontend Build Issues 221 - 1. Clear node_modules and rebuild: 222 - ```bash 223 - docker-compose down 224 - rm -rf frontend/node_modules 225 - docker-compose up --build 226 - ``` 227 - 228 - ### Backend Issues 229 - 1. Check backend logs: 230 - ```bash 231 - docker-compose logs app 232 - ``` 233 - 2. Verify environment variables are set correctly 234 - 3. Check database connectivity 235 - 236 - ## Build Script Usage 237 - 238 - The `build.sh` script handles environment-specific builds and static file generation. 239 - 240 - ### Basic Usage 241 - ```bash 242 - # Default production build 243 - ./build.sh 244 - 245 - # Development build 246 - ENV=development ./build.sh 247 - 248 - # Staging build 249 - ENV=staging ./build.sh 250 - 251 - # Production build with custom API URL 252 - VITE_API_URL=https://api.example.com ./build.sh 253 - 254 - # Development build with custom API URL 255 - ENV=development VITE_API_URL=http://localhost:8080 ./build.sh 256 - 257 - # Show help 258 - ./build.sh --help
+12 -39
Dockerfile
··· 1 - # Frontend build stage 2 - FROM oven/bun:latest as frontend-builder 3 - WORKDIR /app/frontend 4 - 5 - # Install bun 6 - RUN curl -fsSL https://bun.sh/install | bash 7 - ENV PATH="/root/.bun/bin:${PATH}" 8 - 9 - # Copy frontend files 10 - COPY frontend/package.json frontend/bun.lock ./ 11 - RUN bun install 12 - 13 - COPY frontend/ ./ 14 - 15 - # Build frontend with environment variables 16 - # These can be overridden at build time 17 - ARG VITE_API_URL=http://localhost:3000 18 - ARG NODE_ENV=production 19 - ENV VITE_API_URL=$VITE_API_URL 20 - ENV NODE_ENV=$NODE_ENV 21 - 22 - RUN echo "VITE_API_URL=${VITE_API_URL}" > .env.production 23 - RUN bun run build 24 - 25 - # Rust build stage 26 - FROM rust:latest as backend-builder 1 + # Build stage 2 + FROM rust:latest as builder 27 3 28 4 # Install PostgreSQL client libraries and SSL dependencies 29 5 RUN apt-get update && \ ··· 40 16 COPY migrations/ migrations/ 41 17 COPY .sqlx/ .sqlx/ 42 18 43 - # Build application 44 - ARG RUST_ENV=release 45 - RUN cargo build --${RUST_ENV} 19 + # Build your application 20 + RUN cargo build --release 46 21 47 22 # Runtime stage 48 23 FROM debian:bookworm-slim ··· 54 29 55 30 WORKDIR /app 56 31 57 - # Copy the binary and migrations from backend builder 58 - COPY --from=backend-builder /usr/src/app/target/release/simplelink /app/simplelink 59 - COPY --from=backend-builder /usr/src/app/migrations /app/migrations 32 + # Copy the binary from builder 33 + COPY --from=builder /usr/src/app/target/release/simplelink /app/simplelink 34 + # Copy migrations folder for SQLx 35 + COPY --from=builder /usr/src/app/migrations /app/migrations 60 36 61 - # Copy static files from frontend builder 62 - COPY --from=frontend-builder /app/frontend/dist /app/static 63 - 64 - # Expose the port 65 - EXPOSE 3000 37 + # Expose the port (this is just documentation) 38 + EXPOSE 8080 66 39 67 40 # Set default network configuration 68 41 ENV SERVER_HOST=0.0.0.0 69 - ENV SERVER_PORT=3000 42 + ENV SERVER_PORT=8080 70 43 71 44 # Run the binary 72 - CMD ["./simplelink"] 45 + CMD ["./simplelink"]
-174
README.md
··· 1 - # SimpleLink 2 - 3 - A modern link shortening and tracking service built with Rust and React. 4 - 5 - ## Features 6 - 7 - - 🔗 Link shortening with custom codes 8 - - 📊 Click tracking and analytics 9 - - 🔒 User authentication 10 - - 📱 Responsive design 11 - - 🌓 Dark/light mode 12 - - 📈 Source attribution tracking 13 - 14 - ## Tech Stack 15 - 16 - ### Backend 17 - - Rust 18 - - Actix-web 19 - - SQLx 20 - - PostgreSQL 21 - - JWT Authentication 22 - 23 - ### Frontend 24 - - React 25 - - TypeScript 26 - - Tailwind CSS 27 - - Shadcn/ui 28 - - Vite 29 - - Recharts 30 - 31 - ## Development Setup 32 - 33 - ### Prerequisites 34 - - Rust (latest stable) 35 - - Bun (or Node.js) 36 - - PostgreSQL 37 - - Docker (optional) 38 - 39 - ### Environment Variables 40 - 41 - #### Backend (.env) 42 - ```env 43 - DATABASE_URL=postgres://user:password@localhost:5432/simplelink 44 - SERVER_HOST=127.0.0.1 45 - SERVER_PORT=3000 46 - JWT_SECRET=your-secret-key 47 - ``` 48 - 49 - #### Frontend Environment Files 50 - 51 - Development (.env.development): 52 - ```env 53 - VITE_API_URL=http://localhost:3000 54 - ``` 55 - 56 - Production (.env.production): 57 - ```env 58 - VITE_API_URL=https://your-production-domain.com 59 - ``` 60 - 61 - ### Local Development 62 - 63 - 1. Clone the repository: 64 - ```bash 65 - git clone https://github.com/yourusername/simplelink.git 66 - cd simplelink 67 - ``` 68 - 69 - 2. Set up the database: 70 - ```bash 71 - psql -U postgres 72 - CREATE DATABASE simplelink; 73 - ``` 74 - 75 - 3. Run database migrations: 76 - ```bash 77 - cargo run --bin migrate 78 - ``` 79 - 80 - 4. Start the backend server: 81 - ```bash 82 - cargo run 83 - ``` 84 - 85 - 5. In a new terminal, start the frontend development server: 86 - ```bash 87 - cd frontend 88 - bun install 89 - bun run dev 90 - ``` 91 - 92 - The app will be available at: 93 - - Frontend: http://localhost:5173 94 - - Backend API: http://localhost:3000 95 - 96 - ### Building for Production 97 - 98 - Use the build script to create a production build: 99 - ```bash 100 - ./build.sh 101 - ``` 102 - 103 - This will: 104 - 1. Build the frontend with production settings 105 - 2. Copy static files to the correct location 106 - 3. Prepare everything for deployment 107 - 108 - You can override the API URL during build: 109 - ```bash 110 - VITE_API_URL=https://api.yoursite.com ./build.sh 111 - ``` 112 - 113 - ### Docker Deployment 114 - 115 - Build and run using Docker Compose: 116 - ```bash 117 - docker-compose up --build 118 - ``` 119 - 120 - Or build and run the containers separately: 121 - ```bash 122 - # Build the image 123 - docker build -t simplelink . 124 - 125 - # Run the container 126 - docker run -p 3000:3000 \ 127 - -e DATABASE_URL=postgres://user:password@db:5432/simplelink \ 128 - -e JWT_SECRET=your-secret-key \ 129 - simplelink 130 - ``` 131 - 132 - ## Project Structure 133 - 134 - ``` 135 - simplelink/ 136 - ├── src/ # Rust backend code 137 - │ ├── handlers/ # Request handlers 138 - │ ├── models/ # Database models 139 - │ └── main.rs # Application entry point 140 - ├── migrations/ # Database migrations 141 - ├── frontend/ # React frontend 142 - │ ├── src/ 143 - │ │ ├── components/ # React components 144 - │ │ ├── api/ # API client 145 - │ │ └── types/ # TypeScript types 146 - │ └── vite.config.ts # Vite configuration 147 - ├── static/ # Built frontend files (generated) 148 - ├── Cargo.toml # Rust dependencies 149 - ├── docker-compose.yml # Docker composition 150 - └── build.sh # Build script 151 - ``` 152 - 153 - ## API Endpoints 154 - 155 - - `POST /api/auth/register` - Register new user 156 - - `POST /api/auth/login` - Login user 157 - - `POST /api/shorten` - Create short link 158 - - `GET /api/links` - Get all user links 159 - - `DELETE /api/links/{id}` - Delete link 160 - - `GET /api/links/{id}/clicks` - Get click statistics 161 - - `GET /api/links/{id}/sources` - Get source statistics 162 - - `GET /{short_code}` - Redirect to original URL 163 - 164 - ## Contributing 165 - 166 - 1. Fork the repository 167 - 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 168 - 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 169 - 4. Push to the branch (`git push origin feature/amazing-feature`) 170 - 5. Open a Pull Request 171 - 172 - ## License 173 - 174 - This project is licensed under the MIT License - see the LICENSE file for details.
-50
build.sh
··· 1 - #!/bin/bash 2 - set -e 3 - 4 - # Default environment is production 5 - ENV=${ENV:-production} 6 - echo "Building for environment: $ENV" 7 - 8 - # Load environment variables from the appropriate .env file 9 - if [ -f "frontend/.env.$ENV" ]; then 10 - echo "Loading environment variables from frontend/.env.$ENV" 11 - export $(cat frontend/.env.$ENV | grep -v '^#' | xargs) 12 - else 13 - echo "Warning: No .env.$ENV file found in frontend directory" 14 - fi 15 - 16 - # Allow override of VITE_API_URL through command line 17 - VITE_API_URL=${VITE_API_URL:-${VITE_API_URL:-http://localhost:3000}} 18 - echo "Using API URL: $VITE_API_URL" 19 - 20 - echo "Building frontend..." 21 - cd frontend 22 - bun install 23 - 24 - # Export variables for Vite to pick up 25 - export VITE_API_URL 26 - export NODE_ENV=$ENV 27 - 28 - echo "Running build..." 29 - bun run build 30 - 31 - echo "Copying static files..." 32 - cd .. 33 - rm -rf static 34 - mkdir -p static 35 - cp -r frontend/dist/* static/ 36 - 37 - echo "Build complete!" 38 - 39 - # Usage information if no arguments provided 40 - if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then 41 - echo "Usage:" 42 - echo " ./build.sh # Builds with production environment" 43 - echo " ENV=development ./build.sh # Builds with development environment" 44 - echo " VITE_API_URL=https://api.example.com ./build.sh # Overrides API URL" 45 - echo "" 46 - echo "Environment Variables:" 47 - echo " ENV - Build environment (development/staging/production)" 48 - echo " VITE_API_URL - Override the API URL" 49 - exit 0 50 - fi
+5 -33
docker-compose.yml
··· 1 1 version: '3.8' 2 - 3 - # Define reusable environment variables 4 - x-environment: &common-env 5 - SERVER_HOST: 0.0.0.0 6 - SERVER_PORT: 3000 7 - RUST_LOG: info 8 - 9 2 services: 10 - app: 11 - build: 12 - context: . 13 - dockerfile: Dockerfile 14 - args: 15 - # Build-time variables 16 - VITE_API_URL: ${VITE_API_URL:-http://localhost:3000} 17 - NODE_ENV: ${NODE_ENV:-production} 18 - RUST_ENV: ${RUST_ENV:-release} 19 - container_name: shortener-app 20 - ports: 21 - - "3000:3000" 22 - environment: 23 - <<: *common-env # Include common environment variables 24 - DATABASE_URL: postgres://shortener:shortener123@db:5432/shortener 25 - JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-me-in-production} 26 - depends_on: 27 - db: 28 - condition: service_healthy 29 - restart: unless-stopped 30 - 31 3 db: 32 4 image: postgres:15-alpine 33 5 container_name: shortener-db 34 6 environment: 35 - POSTGRES_DB: ${POSTGRES_DB:-shortener} 36 - POSTGRES_USER: ${POSTGRES_USER:-shortener} 37 - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-shortener123} 7 + POSTGRES_DB: shortener 8 + POSTGRES_USER: shortener 9 + POSTGRES_PASSWORD: shortener123 38 10 ports: 39 11 - "5432:5432" 40 12 volumes: 41 13 - shortener-data:/var/lib/postgresql/data 42 14 healthcheck: 43 - test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-shortener}" ] 15 + test: ["CMD-SHELL", "pg_isready -U shortener"] 44 16 interval: 5s 45 17 timeout: 5s 46 18 retries: 5 47 - restart: unless-stopped 48 19 49 20 volumes: 50 21 shortener-data: 22 +
+1 -89
frontend/bun.lock
··· 10 10 "@mantine/form": "^7.16.1", 11 11 "@mantine/hooks": "^7.16.1", 12 12 "@radix-ui/react-dialog": "^1.1.5", 13 - "@radix-ui/react-dropdown-menu": "^2.1.5", 14 13 "@radix-ui/react-label": "^2.1.1", 15 14 "@radix-ui/react-slot": "^1.1.1", 16 15 "@radix-ui/react-tabs": "^1.1.2", ··· 23 22 "react": "^18.3.1", 24 23 "react-dom": "^18.3.1", 25 24 "react-hook-form": "^7.54.2", 26 - "recharts": "^2.15.0", 27 25 "tailwind-merge": "^2.6.0", 28 26 "tailwindcss-animate": "^1.0.7", 29 27 "zod": "^3.24.1", ··· 222 220 223 221 "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], 224 222 225 - "@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=="], 226 - 227 223 "@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=="], 228 224 229 225 "@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=="], ··· 235 231 "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="], 236 232 237 233 "@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=="], 238 - 239 - "@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=="], 240 234 241 235 "@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=="], 242 236 ··· 246 240 247 241 "@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=="], 248 242 249 - "@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=="], 250 - 251 - "@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=="], 252 - 253 243 "@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=="], 254 244 255 245 "@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=="], ··· 272 262 273 263 "@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=="], 274 264 275 - "@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=="], 276 - 277 - "@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=="], 278 - 279 265 "@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=="], 280 - 281 - "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], 282 266 283 267 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.0", "", { "os": "android", "cpu": "arm" }, "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg=="], 284 268 ··· 356 340 357 341 "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], 358 342 359 - "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], 360 - 361 - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], 362 - 363 - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], 364 - 365 - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], 366 - 367 - "@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="], 368 - 369 - "@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="], 370 - 371 - "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], 372 - 373 - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], 374 - 375 - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], 376 - 377 343 "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], 378 344 379 345 "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], ··· 458 424 459 425 "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 460 426 461 - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], 462 - 463 - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], 464 - 465 - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], 466 - 467 - "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], 468 - 469 - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], 470 - 471 - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], 472 - 473 - "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=="], 474 - 475 - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], 476 - 477 - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], 478 - 479 - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], 480 - 481 - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], 482 - 483 427 "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 484 428 485 - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], 486 - 487 429 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 488 430 489 431 "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], ··· 491 433 "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], 492 434 493 435 "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], 494 - 495 - "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], 496 436 497 437 "electron-to-chromium": ["electron-to-chromium@1.5.88", "", {}, "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw=="], 498 438 ··· 526 466 527 467 "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 528 468 529 - "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], 530 - 531 469 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 532 470 533 - "fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], 534 - 535 471 "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=="], 536 472 537 473 "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], ··· 583 519 "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], 584 520 585 521 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 586 - 587 - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], 588 522 589 523 "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], 590 524 ··· 647 581 "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 648 582 649 583 "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 650 - 651 - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], 652 584 653 585 "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 654 586 ··· 676 608 677 609 "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], 678 610 679 - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 680 - 681 611 "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=="], 682 612 683 613 "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], ··· 704 634 705 635 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 706 636 707 - "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=="], 708 - 709 637 "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], 710 638 711 639 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], ··· 718 646 719 647 "react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="], 720 648 721 - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], 649 + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 722 650 723 651 "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=="], 724 652 ··· 728 656 729 657 "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=="], 730 658 731 - "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=="], 732 - 733 659 "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=="], 734 660 735 661 "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=="], 736 662 737 - "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=="], 738 - 739 - "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=="], 740 - 741 - "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], 742 - 743 663 "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], 744 664 745 665 "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=="], ··· 782 702 783 703 "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], 784 704 785 - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], 786 - 787 705 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 788 706 789 707 "ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="], ··· 814 732 815 733 "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=="], 816 734 817 - "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=="], 818 - 819 735 "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=="], 820 736 821 737 "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], ··· 845 761 "@typescript-eslint/typescript-estree/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], 846 762 847 763 "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 848 - 849 - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 850 - 851 - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], 852 764 853 765 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], 854 766 }
-1
frontend/package.json
··· 29 29 "react": "^18.3.1", 30 30 "react-dom": "^18.3.1", 31 31 "react-hook-form": "^7.54.2", 32 - "recharts": "^2.15.0", 33 32 "tailwind-merge": "^2.6.0", 34 33 "tailwindcss-animate": "^1.0.7", 35 34 "zod": "^3.24.1"
+2 -14
frontend/src/api/client.ts
··· 1 1 import axios from 'axios'; 2 - import { CreateLinkRequest, Link, AuthResponse, ClickStats, SourceStats } from '../types/api'; 2 + import { CreateLinkRequest, Link, AuthResponse } from '../types/api'; 3 3 4 4 // Create axios instance with default config 5 5 const api = axios.create({ ··· 45 45 46 46 export const deleteLink = async (id: number) => { 47 47 await api.delete(`/links/${id}`); 48 - }; 49 - 50 - export const getLinkClickStats = async (id: number) => { 51 - const response = await api.get<ClickStats[]>(`/links/${id}/clicks`); 52 - return response.data; 53 - }; 54 - 55 - export const getLinkSourceStats = async (id: number) => { 56 - const response = await api.get<SourceStats[]>(`/links/${id}/sources`); 57 - return response.data; 58 - }; 59 - 60 - export { api }; 48 + };
+1 -21
frontend/src/components/LinkList.tsx
··· 12 12 } from "@/components/ui/table" 13 13 import { Button } from "@/components/ui/button" 14 14 import { useToast } from "@/hooks/use-toast" 15 - import { Copy, Trash2, BarChart2 } from "lucide-react" 15 + import { Copy, Trash2 } from "lucide-react" 16 16 import { 17 17 Dialog, 18 18 DialogContent, ··· 21 21 DialogDescription, 22 22 DialogFooter, 23 23 } from "@/components/ui/dialog" 24 - 25 - import { StatisticsModal } from "./StatisticsModal" 26 24 27 25 interface LinkListProps { 28 26 refresh?: number; ··· 35 33 isOpen: false, 36 34 linkId: null, 37 35 }) 38 - const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({ 39 - isOpen: false, 40 - linkId: null, 41 - }); 42 36 const { toast } = useToast() 43 37 44 38 const fetchLinks = async () => { ··· 154 148 <Button 155 149 variant="ghost" 156 150 size="icon" 157 - className="h-8 w-8" 158 - onClick={() => setStatsModal({ isOpen: true, linkId: link.id })} 159 - > 160 - <BarChart2 className="h-4 w-4" /> 161 - <span className="sr-only">View statistics</span> 162 - </Button> 163 - <Button 164 - variant="ghost" 165 - size="icon" 166 151 className="h-8 w-8 text-destructive" 167 152 onClick={() => setDeleteModal({ isOpen: true, linkId: link.id })} 168 153 > ··· 178 163 </div> 179 164 </CardContent> 180 165 </Card> 181 - <StatisticsModal 182 - isOpen={statsModal.isOpen} 183 - onClose={() => setStatsModal({ isOpen: false, linkId: null })} 184 - linkId={statsModal.linkId!} 185 - /> 186 166 </> 187 167 ) 188 168 }
-115
frontend/src/components/StatisticsModal.tsx
··· 1 - import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 2 - import { 3 - LineChart, 4 - Line, 5 - XAxis, 6 - YAxis, 7 - CartesianGrid, 8 - Tooltip, 9 - ResponsiveContainer, 10 - } from "recharts"; 11 - import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 12 - import { useState, useEffect } from "react"; 13 - 14 - import { getLinkClickStats, getLinkSourceStats } from '../api/client'; 15 - import { ClickStats, SourceStats } from '../types/api'; 16 - 17 - interface StatisticsModalProps { 18 - isOpen: boolean; 19 - onClose: () => void; 20 - linkId: number; 21 - } 22 - 23 - export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) { 24 - const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]); 25 - const [sourcesData, setSourcesData] = useState<SourceStats[]>([]); 26 - const [loading, setLoading] = useState(true); 27 - 28 - useEffect(() => { 29 - if (isOpen && linkId) { 30 - const fetchData = async () => { 31 - try { 32 - setLoading(true); 33 - const [clicksData, sourcesData] = await Promise.all([ 34 - getLinkClickStats(linkId), 35 - getLinkSourceStats(linkId), 36 - ]); 37 - setClicksOverTime(clicksData); 38 - setSourcesData(sourcesData); 39 - } catch (error) { 40 - console.error("Failed to fetch statistics:", error); 41 - } finally { 42 - setLoading(false); 43 - } 44 - }; 45 - 46 - fetchData(); 47 - } 48 - }, [isOpen, linkId]); 49 - 50 - return ( 51 - <Dialog open={isOpen} onOpenChange={onClose}> 52 - <DialogContent className="max-w-3xl"> 53 - <DialogHeader> 54 - <DialogTitle>Link Statistics</DialogTitle> 55 - </DialogHeader> 56 - 57 - {loading ? ( 58 - <div className="flex items-center justify-center h-64">Loading...</div> 59 - ) : ( 60 - <div className="grid gap-4"> 61 - <Card> 62 - <CardHeader> 63 - <CardTitle>Clicks Over Time</CardTitle> 64 - </CardHeader> 65 - <CardContent> 66 - <div className="h-[300px]"> 67 - <ResponsiveContainer width="100%" height="100%"> 68 - <LineChart data={clicksOverTime}> 69 - <CartesianGrid strokeDasharray="3 3" /> 70 - <XAxis dataKey="date" /> 71 - <YAxis /> 72 - <Tooltip /> 73 - <Line 74 - type="monotone" 75 - dataKey="clicks" 76 - stroke="#8884d8" 77 - strokeWidth={2} 78 - /> 79 - </LineChart> 80 - </ResponsiveContainer> 81 - </div> 82 - </CardContent> 83 - </Card> 84 - 85 - <Card> 86 - <CardHeader> 87 - <CardTitle>Top Sources</CardTitle> 88 - </CardHeader> 89 - <CardContent> 90 - <ul className="space-y-2"> 91 - {sourcesData.map((source, index) => ( 92 - <li 93 - key={source.source} 94 - className="flex items-center justify-between py-2 border-b last:border-0" 95 - > 96 - <span className="text-sm"> 97 - <span className="font-medium text-muted-foreground mr-2"> 98 - {index + 1}. 99 - </span> 100 - {source.source} 101 - </span> 102 - <span className="text-sm font-medium"> 103 - {source.count} clicks 104 - </span> 105 - </li> 106 - ))} 107 - </ul> 108 - </CardContent> 109 - </Card> 110 - </div> 111 - )} 112 - </DialogContent> 113 - </Dialog> 114 - ); 115 - }
+1 -11
frontend/src/types/api.ts
··· 24 24 25 25 export interface ApiError { 26 26 error: string; 27 - } 28 - 29 - export interface ClickStats { 30 - date: string; 31 - clicks: number; 32 - } 33 - 34 - export interface SourceStats { 35 - source: string; 36 - count: number; 37 - } 27 + }
+17 -28
frontend/vite.config.ts
··· 1 - import { defineConfig, loadEnv } from 'vite' 1 + import { defineConfig } from 'vite' 2 2 import react from '@vitejs/plugin-react' 3 3 import tailwindcss from '@tailwindcss/vite' 4 4 import path from "path" 5 5 6 - // https://vitejs.dev/config/ 7 - export default defineConfig(({ mode }) => { 8 - // Load env file based on `mode` in the current working directory. 9 - const env = loadEnv(mode, process.cwd(), '') 10 - 11 - return { 12 - plugins: [ 13 - react(), 14 - tailwindcss(), 15 - ], 16 - server: { 17 - proxy: { 18 - '/api': { 19 - // Use environment variable with fallback 20 - target: env.VITE_API_URL || 'http://localhost:3000', 21 - changeOrigin: true, 22 - }, 23 - }, 24 - }, 25 - resolve: { 26 - alias: { 27 - "@": path.resolve(__dirname, "./src"), 6 + export default defineConfig({ 7 + plugins: [ 8 + react(), 9 + tailwindcss(), 10 + ], 11 + server: { 12 + proxy: { 13 + '/api': { 14 + target: 'http://localhost:8080', 15 + changeOrigin: true, 28 16 }, 29 17 }, 30 - base: '/', 31 - build: { 32 - outDir: 'dist', 33 - assetsDir: 'assets', 18 + }, 19 + resolve: { 20 + alias: { 21 + "@": path.resolve(__dirname, "./src"), 34 22 }, 35 - } 23 + }, 36 24 }) 25 +
+1 -84
src/handlers.rs
··· 2 2 use crate::{ 3 3 error::AppError, 4 4 models::{ 5 - AuthResponse, Claims, ClickStats, CreateLink, Link, LoginRequest, RegisterRequest, 6 - SourceStats, User, UserResponse, 5 + AuthResponse, Claims, CreateLink, Link, LoginRequest, RegisterRequest, User, UserResponse, 7 6 }, 8 7 AppState, 9 8 }; ··· 306 305 307 306 Ok(HttpResponse::NoContent().finish()) 308 307 } 309 - 310 - pub async fn get_link_clicks( 311 - state: web::Data<AppState>, 312 - user: AuthenticatedUser, 313 - path: web::Path<i32>, 314 - ) -> Result<impl Responder, AppError> { 315 - let link_id = path.into_inner(); 316 - 317 - // Verify the link belongs to the user 318 - let link = sqlx::query!( 319 - "SELECT id FROM links WHERE id = $1 AND user_id = $2", 320 - link_id, 321 - user.user_id 322 - ) 323 - .fetch_optional(&state.db) 324 - .await?; 325 - 326 - if link.is_none() { 327 - return Err(AppError::NotFound); 328 - } 329 - 330 - let clicks = sqlx::query_as!( 331 - ClickStats, 332 - r#" 333 - SELECT 334 - DATE(created_at)::date as "date!", 335 - COUNT(*)::bigint as "clicks!" 336 - FROM clicks 337 - WHERE link_id = $1 338 - GROUP BY DATE(created_at) 339 - ORDER BY DATE(created_at) ASC -- Changed from DESC to ASC 340 - LIMIT 30 341 - "#, 342 - link_id 343 - ) 344 - .fetch_all(&state.db) 345 - .await?; 346 - 347 - Ok(HttpResponse::Ok().json(clicks)) 348 - } 349 - 350 - pub async fn get_link_sources( 351 - state: web::Data<AppState>, 352 - user: AuthenticatedUser, 353 - path: web::Path<i32>, 354 - ) -> Result<impl Responder, AppError> { 355 - let link_id = path.into_inner(); 356 - 357 - // Verify the link belongs to the user 358 - let link = sqlx::query!( 359 - "SELECT id FROM links WHERE id = $1 AND user_id = $2", 360 - link_id, 361 - user.user_id 362 - ) 363 - .fetch_optional(&state.db) 364 - .await?; 365 - 366 - if link.is_none() { 367 - return Err(AppError::NotFound); 368 - } 369 - 370 - let sources = sqlx::query_as!( 371 - SourceStats, 372 - r#" 373 - SELECT 374 - query_source as "source!", 375 - COUNT(*)::bigint as "count!" 376 - FROM clicks 377 - WHERE link_id = $1 378 - AND query_source IS NOT NULL 379 - AND query_source != '' 380 - GROUP BY query_source 381 - ORDER BY COUNT(*) DESC 382 - LIMIT 10 383 - "#, 384 - link_id 385 - ) 386 - .fetch_all(&state.db) 387 - .await?; 388 - 389 - Ok(HttpResponse::Ok().json(sources)) 390 - }
+8 -24
src/main.rs
··· 1 1 use actix_cors::Cors; 2 - use actix_files::Files; 3 - use actix_web::{middleware::DefaultHeaders, web, App, HttpServer}; 2 + use actix_web::{web, App, HttpServer}; 4 3 use anyhow::Result; 5 - use simplelink::{handlers, AppState}; 4 + use simple_link::{handlers, AppState}; 6 5 use sqlx::postgres::PgPoolOptions; 7 6 use tracing::info; 8 7 9 - async fn index() -> Result<actix_files::NamedFile, actix_web::Error> { 10 - Ok(actix_files::NamedFile::open("./static/index.html")?) 11 - } 12 - 13 8 #[actix_web::main] 14 9 async fn main() -> Result<()> { 15 10 // Load environment variables from .env file ··· 31 26 // Run database migrations 32 27 sqlx::migrate!("./migrations").run(&pool).await?; 33 28 34 - let _state = AppState { db: pool }; 29 + let state = AppState { db: pool }; 35 30 36 31 let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 37 - let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "3000".to_string()); 32 + let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); 38 33 info!("Starting server at http://{}:{}", host, port); 39 34 40 35 // Start HTTP server ··· 47 42 48 43 App::new() 49 44 .wrap(cors) 50 - // Add headers to help with caching static assets 51 - .wrap(DefaultHeaders::new().add(("Cache-Control", "max-age=31536000"))) 52 - // API routes 45 + .app_data(web::Data::new(state.clone())) 53 46 .service( 54 47 web::scope("/api") 55 48 .route("/shorten", web::post().to(handlers::create_short_url)) 56 49 .route("/links", web::get().to(handlers::get_all_links)) 57 50 .route("/links/{id}", web::delete().to(handlers::delete_link)) 58 - .route( 59 - "/links/{id}/clicks", 60 - web::get().to(handlers::get_link_clicks), 61 - ) 62 - .route( 63 - "/links/{id}/sources", 64 - web::get().to(handlers::get_link_sources), 65 - ) 66 51 .route("/auth/register", web::post().to(handlers::register)) 67 52 .route("/auth/login", web::post().to(handlers::login)) 68 53 .route("/health", web::get().to(handlers::health_check)), 69 54 ) 70 - // Serve static files 71 - .service(Files::new("/assets", "./static/assets")) 72 - // Handle SPA routes - must be last 73 - .default_service(web::get().to(index)) 55 + .service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url))) 74 56 }) 57 + .workers(2) 58 + .backlog(10_000) 75 59 .bind(format!("{}:{}", host, port))? 76 60 .run() 77 61 .await?;
+6 -17
src/models.rs
··· 1 1 use std::time::{SystemTime, UNIX_EPOCH}; 2 2 3 - use chrono::NaiveDate; 4 3 use serde::{Deserialize, Serialize}; 5 4 use sqlx::FromRow; 6 5 ··· 15 14 let exp = SystemTime::now() 16 15 .duration_since(UNIX_EPOCH) 17 16 .unwrap() 18 - .as_secs() as usize 19 - + 24 * 60 * 60; // 24 hours from now 20 - 21 - Self { sub: user_id, exp } 17 + .as_secs() as usize + 24 * 60 * 60; // 24 hours from now 18 + 19 + Self { 20 + sub: user_id, 21 + exp, 22 + } 22 23 } 23 24 } 24 25 ··· 69 70 pub email: String, 70 71 pub password_hash: String, 71 72 } 72 - 73 - #[derive(sqlx::FromRow, Serialize)] 74 - pub struct ClickStats { 75 - pub date: NaiveDate, 76 - pub clicks: i64, 77 - } 78 - 79 - #[derive(sqlx::FromRow, Serialize)] 80 - pub struct SourceStats { 81 - pub source: String, 82 - pub count: i64, 83 - }