parent
937b3fc811
commit
f3a61bfa99
21 changed files with 55 additions and 1071 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,5 +2,3 @@
|
||||||
**/node_modules
|
**/node_modules
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
||||||
/static
|
|
|
@ -1,3 +1,2 @@
|
||||||
.sqlx
|
.sqlx
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -34,29 +34,6 @@ dependencies = [
|
||||||
"smallvec",
|
"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]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.9.0"
|
version = "3.9.0"
|
||||||
|
@ -1056,12 +1033,6 @@ dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "http-range"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.5"
|
version = "1.9.5"
|
||||||
|
@ -1403,16 +1374,6 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -2058,7 +2019,6 @@ name = "simplelink"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
"actix-files",
|
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
@ -2627,12 +2587,6 @@ version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicase"
|
|
||||||
version = "2.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
|
@ -2711,12 +2665,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "v_htmlescape"
|
|
||||||
version = "0.15.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
|
@ -10,7 +10,6 @@ path = "src/lib.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
actix-web = "4.4"
|
actix-web = "4.4"
|
||||||
actix-files = "0.6"
|
|
||||||
actix-cors = "0.6"
|
actix-cors = "0.6"
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] }
|
||||||
|
|
258
DEPLOYMENT.md
258
DEPLOYMENT.md
|
@ -1,258 +0,0 @@
|
||||||
# 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
|
|
49
Dockerfile
49
Dockerfile
|
@ -1,29 +1,5 @@
|
||||||
# Frontend build stage
|
# Build stage
|
||||||
FROM oven/bun:latest as frontend-builder
|
FROM rust:latest as 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
|
# Install PostgreSQL client libraries and SSL dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
|
@ -40,9 +16,8 @@ COPY src/ src/
|
||||||
COPY migrations/ migrations/
|
COPY migrations/ migrations/
|
||||||
COPY .sqlx/ .sqlx/
|
COPY .sqlx/ .sqlx/
|
||||||
|
|
||||||
# Build application
|
# Build your application
|
||||||
ARG RUST_ENV=release
|
RUN cargo build --release
|
||||||
RUN cargo build --${RUST_ENV}
|
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
@ -54,19 +29,17 @@ RUN apt-get update && \
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy the binary and migrations from backend builder
|
# Copy the binary from builder
|
||||||
COPY --from=backend-builder /usr/src/app/target/release/simplelink /app/simplelink
|
COPY --from=builder /usr/src/app/target/release/simplelink /app/simplelink
|
||||||
COPY --from=backend-builder /usr/src/app/migrations /app/migrations
|
# Copy migrations folder for SQLx
|
||||||
|
COPY --from=builder /usr/src/app/migrations /app/migrations
|
||||||
|
|
||||||
# Copy static files from frontend builder
|
# Expose the port (this is just documentation)
|
||||||
COPY --from=frontend-builder /app/frontend/dist /app/static
|
EXPOSE 8080
|
||||||
|
|
||||||
# Expose the port
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Set default network configuration
|
# Set default network configuration
|
||||||
ENV SERVER_HOST=0.0.0.0
|
ENV SERVER_HOST=0.0.0.0
|
||||||
ENV SERVER_PORT=3000
|
ENV SERVER_PORT=8080
|
||||||
|
|
||||||
# Run the binary
|
# Run the binary
|
||||||
CMD ["./simplelink"]
|
CMD ["./simplelink"]
|
174
README.md
174
README.md
|
@ -1,174 +0,0 @@
|
||||||
# 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.
|
|
50
build.sh
50
build.sh
|
@ -1,50 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,50 +1,22 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
# Define reusable environment variables
|
|
||||||
x-environment: &common-env
|
|
||||||
SERVER_HOST: 0.0.0.0
|
|
||||||
SERVER_PORT: 3000
|
|
||||||
RUST_LOG: info
|
|
||||||
|
|
||||||
services:
|
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:
|
db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: shortener-db
|
container_name: shortener-db
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-shortener}
|
POSTGRES_DB: shortener
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-shortener}
|
POSTGRES_USER: shortener
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-shortener123}
|
POSTGRES_PASSWORD: shortener123
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- shortener-data:/var/lib/postgresql/data
|
- shortener-data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-shortener}" ]
|
test: ["CMD-SHELL", "pg_isready -U shortener"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
shortener-data:
|
shortener-data:
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
"@mantine/form": "^7.16.1",
|
"@mantine/form": "^7.16.1",
|
||||||
"@mantine/hooks": "^7.16.1",
|
"@mantine/hooks": "^7.16.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.5",
|
"@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-label": "^2.1.1",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
|
@ -23,7 +22,6 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"recharts": "^2.15.0",
|
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
|
@ -222,8 +220,6 @@
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
"@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-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=="],
|
"@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=="],
|
||||||
|
@ -236,8 +232,6 @@
|
||||||
|
|
||||||
"@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-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-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=="],
|
"@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=="],
|
||||||
|
@ -246,10 +240,6 @@
|
||||||
|
|
||||||
"@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-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-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=="],
|
"@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,14 +262,8 @@
|
||||||
|
|
||||||
"@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-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/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-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=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A=="],
|
||||||
|
@ -356,24 +340,6 @@
|
||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
|
"@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/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=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
@ -458,32 +424,8 @@
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
@ -492,8 +434,6 @@
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"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=="],
|
"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=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="],
|
||||||
|
@ -526,12 +466,8 @@
|
||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"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-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-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=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
@ -584,8 +520,6 @@
|
||||||
|
|
||||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
"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-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=="],
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
@ -648,8 +582,6 @@
|
||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
|
@ -676,8 +608,6 @@
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
"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=="],
|
"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=="],
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
@ -704,8 +634,6 @@
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"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=="],
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
@ -718,7 +646,7 @@
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="],
|
"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@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
@ -728,18 +656,10 @@
|
||||||
|
|
||||||
"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-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-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-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=="],
|
"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=="],
|
"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,8 +702,6 @@
|
||||||
|
|
||||||
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
|
"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=="],
|
"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=="],
|
"ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="],
|
||||||
|
@ -814,8 +732,6 @@
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
"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=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
@ -846,10 +762,6 @@
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"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=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"recharts": "^2.15.0",
|
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CreateLinkRequest, Link, AuthResponse, ClickStats, SourceStats } from '../types/api';
|
import { CreateLinkRequest, Link, AuthResponse } from '../types/api';
|
||||||
|
|
||||||
// Create axios instance with default config
|
// Create axios instance with default config
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
|
@ -46,15 +46,3 @@ export const getAllLinks = async () => {
|
||||||
export const deleteLink = async (id: number) => {
|
export const deleteLink = async (id: number) => {
|
||||||
await api.delete(`/links/${id}`);
|
await api.delete(`/links/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLinkClickStats = async (id: number) => {
|
|
||||||
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLinkSourceStats = async (id: number) => {
|
|
||||||
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { api };
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
import { Copy, Trash2, BarChart2 } from "lucide-react"
|
import { Copy, Trash2 } from "lucide-react"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
@ -22,8 +22,6 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
import { StatisticsModal } from "./StatisticsModal"
|
|
||||||
|
|
||||||
interface LinkListProps {
|
interface LinkListProps {
|
||||||
refresh?: number;
|
refresh?: number;
|
||||||
}
|
}
|
||||||
|
@ -35,10 +33,6 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
linkId: null,
|
linkId: null,
|
||||||
})
|
})
|
||||||
const [statsModal, setStatsModal] = useState<{ isOpen: boolean; linkId: number | null }>({
|
|
||||||
isOpen: false,
|
|
||||||
linkId: null,
|
|
||||||
});
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const fetchLinks = async () => {
|
const fetchLinks = async () => {
|
||||||
|
@ -151,15 +145,6 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
<span className="sr-only">Copy link</span>
|
<span className="sr-only">Copy link</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => setStatsModal({ isOpen: true, linkId: link.id })}
|
|
||||||
>
|
|
||||||
<BarChart2 className="h-4 w-4" />
|
|
||||||
<span className="sr-only">View statistics</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
@ -178,11 +163,6 @@ export function LinkList({ refresh = 0 }: LinkListProps) {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<StatisticsModal
|
|
||||||
isOpen={statsModal.isOpen}
|
|
||||||
onClose={() => setStatsModal({ isOpen: false, linkId: null })}
|
|
||||||
linkId={statsModal.linkId!}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,115 +0,0 @@
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
} from "recharts";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
import { getLinkClickStats, getLinkSourceStats } from '../api/client';
|
|
||||||
import { ClickStats, SourceStats } from '../types/api';
|
|
||||||
|
|
||||||
interface StatisticsModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
linkId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) {
|
|
||||||
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]);
|
|
||||||
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && linkId) {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const [clicksData, sourcesData] = await Promise.all([
|
|
||||||
getLinkClickStats(linkId),
|
|
||||||
getLinkSourceStats(linkId),
|
|
||||||
]);
|
|
||||||
setClicksOverTime(clicksData);
|
|
||||||
setSourcesData(sourcesData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch statistics:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}, [isOpen, linkId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Link Statistics</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-64">Loading...</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Clicks Over Time</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-[300px]">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<LineChart data={clicksOverTime}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis />
|
|
||||||
<Tooltip />
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="clicks"
|
|
||||||
stroke="#8884d8"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Top Sources</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{sourcesData.map((source, index) => (
|
|
||||||
<li
|
|
||||||
key={source.source}
|
|
||||||
className="flex items-center justify-between py-2 border-b last:border-0"
|
|
||||||
>
|
|
||||||
<span className="text-sm">
|
|
||||||
<span className="font-medium text-muted-foreground mr-2">
|
|
||||||
{index + 1}.
|
|
||||||
</span>
|
|
||||||
{source.source}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{source.count} clicks
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -25,13 +25,3 @@ export interface AuthResponse {
|
||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClickStats {
|
|
||||||
date: string;
|
|
||||||
clicks: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SourceStats {
|
|
||||||
source: string;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
export default defineConfig({
|
||||||
export default defineConfig(({ mode }) => {
|
|
||||||
// Load env file based on `mode` in the current working directory.
|
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
@ -16,8 +11,7 @@ export default defineConfig(({ mode }) => {
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// Use environment variable with fallback
|
target: 'http://localhost:8080',
|
||||||
target: env.VITE_API_URL || 'http://localhost:3000',
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -27,10 +21,5 @@ export default defineConfig(({ mode }) => {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
base: '/',
|
|
||||||
build: {
|
|
||||||
outDir: 'dist',
|
|
||||||
assetsDir: 'assets',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,7 @@ use crate::auth::AuthenticatedUser;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::AppError,
|
error::AppError,
|
||||||
models::{
|
models::{
|
||||||
AuthResponse, Claims, ClickStats, CreateLink, Link, LoginRequest, RegisterRequest,
|
AuthResponse, Claims, CreateLink, Link, LoginRequest, RegisterRequest, User, UserResponse,
|
||||||
SourceStats, User, UserResponse,
|
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
@ -306,85 +305,3 @@ pub async fn delete_link(
|
||||||
|
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_link_clicks(
|
|
||||||
state: web::Data<AppState>,
|
|
||||||
user: AuthenticatedUser,
|
|
||||||
path: web::Path<i32>,
|
|
||||||
) -> Result<impl Responder, AppError> {
|
|
||||||
let link_id = path.into_inner();
|
|
||||||
|
|
||||||
// Verify the link belongs to the user
|
|
||||||
let link = sqlx::query!(
|
|
||||||
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
|
|
||||||
link_id,
|
|
||||||
user.user_id
|
|
||||||
)
|
|
||||||
.fetch_optional(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if link.is_none() {
|
|
||||||
return Err(AppError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let clicks = sqlx::query_as!(
|
|
||||||
ClickStats,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
DATE(created_at)::date as "date!",
|
|
||||||
COUNT(*)::bigint as "clicks!"
|
|
||||||
FROM clicks
|
|
||||||
WHERE link_id = $1
|
|
||||||
GROUP BY DATE(created_at)
|
|
||||||
ORDER BY DATE(created_at) ASC -- Changed from DESC to ASC
|
|
||||||
LIMIT 30
|
|
||||||
"#,
|
|
||||||
link_id
|
|
||||||
)
|
|
||||||
.fetch_all(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(clicks))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_link_sources(
|
|
||||||
state: web::Data<AppState>,
|
|
||||||
user: AuthenticatedUser,
|
|
||||||
path: web::Path<i32>,
|
|
||||||
) -> Result<impl Responder, AppError> {
|
|
||||||
let link_id = path.into_inner();
|
|
||||||
|
|
||||||
// Verify the link belongs to the user
|
|
||||||
let link = sqlx::query!(
|
|
||||||
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
|
|
||||||
link_id,
|
|
||||||
user.user_id
|
|
||||||
)
|
|
||||||
.fetch_optional(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if link.is_none() {
|
|
||||||
return Err(AppError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sources = sqlx::query_as!(
|
|
||||||
SourceStats,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
query_source as "source!",
|
|
||||||
COUNT(*)::bigint as "count!"
|
|
||||||
FROM clicks
|
|
||||||
WHERE link_id = $1
|
|
||||||
AND query_source IS NOT NULL
|
|
||||||
AND query_source != ''
|
|
||||||
GROUP BY query_source
|
|
||||||
ORDER BY COUNT(*) DESC
|
|
||||||
LIMIT 10
|
|
||||||
"#,
|
|
||||||
link_id
|
|
||||||
)
|
|
||||||
.fetch_all(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(sources))
|
|
||||||
}
|
|
||||||
|
|
32
src/main.rs
32
src/main.rs
|
@ -1,15 +1,10 @@
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_files::Files;
|
use actix_web::{web, App, HttpServer};
|
||||||
use actix_web::{middleware::DefaultHeaders, web, App, HttpServer};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use simplelink::{handlers, AppState};
|
use simple_link::{handlers, AppState};
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
async fn index() -> Result<actix_files::NamedFile, actix_web::Error> {
|
|
||||||
Ok(actix_files::NamedFile::open("./static/index.html")?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
|
@ -31,10 +26,10 @@ async fn main() -> Result<()> {
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
let _state = AppState { db: pool };
|
let state = AppState { db: pool };
|
||||||
|
|
||||||
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "3000".to_string());
|
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
|
||||||
info!("Starting server at http://{}:{}", host, port);
|
info!("Starting server at http://{}:{}", host, port);
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
|
@ -47,31 +42,20 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
// Add headers to help with caching static assets
|
.app_data(web::Data::new(state.clone()))
|
||||||
.wrap(DefaultHeaders::new().add(("Cache-Control", "max-age=31536000")))
|
|
||||||
// API routes
|
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api")
|
web::scope("/api")
|
||||||
.route("/shorten", web::post().to(handlers::create_short_url))
|
.route("/shorten", web::post().to(handlers::create_short_url))
|
||||||
.route("/links", web::get().to(handlers::get_all_links))
|
.route("/links", web::get().to(handlers::get_all_links))
|
||||||
.route("/links/{id}", web::delete().to(handlers::delete_link))
|
.route("/links/{id}", web::delete().to(handlers::delete_link))
|
||||||
.route(
|
|
||||||
"/links/{id}/clicks",
|
|
||||||
web::get().to(handlers::get_link_clicks),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/links/{id}/sources",
|
|
||||||
web::get().to(handlers::get_link_sources),
|
|
||||||
)
|
|
||||||
.route("/auth/register", web::post().to(handlers::register))
|
.route("/auth/register", web::post().to(handlers::register))
|
||||||
.route("/auth/login", web::post().to(handlers::login))
|
.route("/auth/login", web::post().to(handlers::login))
|
||||||
.route("/health", web::get().to(handlers::health_check)),
|
.route("/health", web::get().to(handlers::health_check)),
|
||||||
)
|
)
|
||||||
// Serve static files
|
.service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))
|
||||||
.service(Files::new("/assets", "./static/assets"))
|
|
||||||
// Handle SPA routes - must be last
|
|
||||||
.default_service(web::get().to(index))
|
|
||||||
})
|
})
|
||||||
|
.workers(2)
|
||||||
|
.backlog(10_000)
|
||||||
.bind(format!("{}:{}", host, port))?
|
.bind(format!("{}:{}", host, port))?
|
||||||
.run()
|
.run()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
|
||||||
|
@ -15,10 +14,12 @@ impl Claims {
|
||||||
let exp = SystemTime::now()
|
let exp = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_secs() as usize
|
.as_secs() as usize + 24 * 60 * 60; // 24 hours from now
|
||||||
+ 24 * 60 * 60; // 24 hours from now
|
|
||||||
|
|
||||||
Self { sub: user_id, exp }
|
Self {
|
||||||
|
sub: user_id,
|
||||||
|
exp,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,15 +70,3 @@ pub struct User {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Serialize)]
|
|
||||||
pub struct ClickStats {
|
|
||||||
pub date: NaiveDate,
|
|
||||||
pub clicks: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(sqlx::FromRow, Serialize)]
|
|
||||||
pub struct SourceStats {
|
|
||||||
pub source: String,
|
|
||||||
pub count: i64,
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue