From 55c42f1f99239d94abce976ccd8c00423034d551 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 21 Jan 2026 20:01:18 -0500 Subject: [PATCH] feat: Add comprehensive backend validation, toast notifications, and Tokyo Night theme - Backend: Add server-side validation with go-playground/validator/v10 - Frontend: Add toast notifications for API errors with @zerodevx/svelte-toast - UI: Complete Tokyo Night theme redesign with modern animations - Docs: Update COMPLETE_DOCUMENTATION.md and README.md with all enhancements - Validation: Email format, password strength, and input sanitization - UX: Real-time error feedback, loading states, and responsive design --- COMPLETE_DOCUMENTATION.md | 736 +++++ README.md | 151 +- backend/.dockerignore | 10 + backend/Dockerfile | 58 + backend/cmd/server/main.go | 74 + backend/go.mod | 33 + backend/go.sum | 74 + backend/internal/config/config.go | 42 + backend/internal/database/connection.go | 20 + backend/internal/database/db.go | 32 + backend/internal/database/models.go | 41 + backend/internal/database/querier.go | 29 + backend/internal/database/queries.sql.go | 356 ++ backend/internal/database/queries/queries.sql | 57 + backend/internal/handlers/auth.go | 166 + backend/internal/handlers/ebook.go | 252 ++ backend/main.REMOVED.git-id | 1 + backend/migrations/001_create_tables.up.sql | 43 + backend/package-lock.json | 977 ++++++ backend/package.json | 16 + backend/server.REMOVED.git-id | 1 + backend/server.js | 107 + backend/sqlc.yaml | 17 + backend/static/_app/env.js | 1 + .../_app/immutable/assets/0.DLollcCP.css | 1 + .../_app/immutable/assets/2.D5pqjzFo.css | 1 + .../static/_app/immutable/chunks/6OCXa8_L.js | 1 + .../static/_app/immutable/chunks/BOU_Z_Ye.js | 1 + .../static/_app/immutable/chunks/C9HAc536.js | 1 + .../static/_app/immutable/chunks/CKL1QNnB.js | 1 + .../static/_app/immutable/chunks/DUCk1qN8.js | 1 + .../static/_app/immutable/chunks/DZQwV0xP.js | 2 + .../static/_app/immutable/chunks/DnPHkIdi.js | 1 + .../static/_app/immutable/chunks/VM9GfMNA.js | 1 + .../static/_app/immutable/chunks/kSfNJqxT.js | 1 + .../_app/immutable/entry/app.o34c63v3.js | 2 + .../_app/immutable/entry/start.BrST8BG9.js | 1 + .../static/_app/immutable/nodes/0.Cf73ubsW.js | 1 + .../static/_app/immutable/nodes/1.Mwi0d7FS.js | 1 + .../static/_app/immutable/nodes/2.BQz_jWEl.js | 1 + backend/static/_app/version.json | 1 + backend/static/index.html | 37 + backend/static/robots.txt | 3 + bruno/README.md | 47 + bruno/auth/Get Profile.yml | 23 + bruno/auth/Login User.yml | 29 + bruno/auth/Register User.yml | 31 + bruno/collection.yml | 4 + bruno/ebooks/Create Ebook.yml | 43 + bruno/ebooks/Delete Ebook.yml | 27 + bruno/ebooks/Get Ebook.yml | 27 + bruno/ebooks/List Ebooks.yml | 23 + bruno/ebooks/Update Ebook.yml | 43 + bruno/environments/localhost.yml | 4 + bruno/progress/Get Reading Progress.yml | 31 + bruno/progress/Update Reading Progress.yml | 38 + bruno/workspace.yml | 3 + docker-compose.yml | 51 + frontend/.gitignore | 23 + frontend/.npmrc | 1 + frontend/README.md | 42 + frontend/package-lock.json | 2895 +++++++++++++++++ frontend/package.json | 33 + frontend/postcss.config.js | 6 + frontend/src/app.css | 135 + frontend/src/app.d.ts | 13 + frontend/src/app.html | 14 + frontend/src/lib/api.ts | 170 + frontend/src/lib/assets/favicon.svg | 1 + frontend/src/lib/auth.ts | 53 + frontend/src/lib/index.ts | 1 + frontend/src/lib/toast.ts | 21 + frontend/src/routes/+layout.svelte | 93 + frontend/src/routes/+page.svelte | 105 + frontend/src/routes/login/+page.svelte | 105 + frontend/src/routes/register/+page.server.ts | 38 + frontend/src/routes/register/+page.svelte | 124 + frontend/static/robots.txt | 3 + frontend/svelte.config.js | 16 + frontend/tailwind.config.js | 45 + frontend/tsconfig.json | 20 + frontend/vite.config.ts | 6 + go.mod | 5 +- go.sum | 8 + 84 files changed, 7749 insertions(+), 4 deletions(-) create mode 100644 COMPLETE_DOCUMENTATION.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/cmd/server/main.go create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/database/connection.go create mode 100644 backend/internal/database/db.go create mode 100644 backend/internal/database/models.go create mode 100644 backend/internal/database/querier.go create mode 100644 backend/internal/database/queries.sql.go create mode 100644 backend/internal/database/queries/queries.sql create mode 100644 backend/internal/handlers/auth.go create mode 100644 backend/internal/handlers/ebook.go create mode 100644 backend/main.REMOVED.git-id create mode 100644 backend/migrations/001_create_tables.up.sql create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/server.REMOVED.git-id create mode 100644 backend/server.js create mode 100644 backend/sqlc.yaml create mode 100644 backend/static/_app/env.js create mode 100644 backend/static/_app/immutable/assets/0.DLollcCP.css create mode 100644 backend/static/_app/immutable/assets/2.D5pqjzFo.css create mode 100644 backend/static/_app/immutable/chunks/6OCXa8_L.js create mode 100644 backend/static/_app/immutable/chunks/BOU_Z_Ye.js create mode 100644 backend/static/_app/immutable/chunks/C9HAc536.js create mode 100644 backend/static/_app/immutable/chunks/CKL1QNnB.js create mode 100644 backend/static/_app/immutable/chunks/DUCk1qN8.js create mode 100644 backend/static/_app/immutable/chunks/DZQwV0xP.js create mode 100644 backend/static/_app/immutable/chunks/DnPHkIdi.js create mode 100644 backend/static/_app/immutable/chunks/VM9GfMNA.js create mode 100644 backend/static/_app/immutable/chunks/kSfNJqxT.js create mode 100644 backend/static/_app/immutable/entry/app.o34c63v3.js create mode 100644 backend/static/_app/immutable/entry/start.BrST8BG9.js create mode 100644 backend/static/_app/immutable/nodes/0.Cf73ubsW.js create mode 100644 backend/static/_app/immutable/nodes/1.Mwi0d7FS.js create mode 100644 backend/static/_app/immutable/nodes/2.BQz_jWEl.js create mode 100644 backend/static/_app/version.json create mode 100644 backend/static/index.html create mode 100644 backend/static/robots.txt create mode 100644 bruno/README.md create mode 100644 bruno/auth/Get Profile.yml create mode 100644 bruno/auth/Login User.yml create mode 100644 bruno/auth/Register User.yml create mode 100644 bruno/collection.yml create mode 100644 bruno/ebooks/Create Ebook.yml create mode 100644 bruno/ebooks/Delete Ebook.yml create mode 100644 bruno/ebooks/Get Ebook.yml create mode 100644 bruno/ebooks/List Ebooks.yml create mode 100644 bruno/ebooks/Update Ebook.yml create mode 100644 bruno/environments/localhost.yml create mode 100644 bruno/progress/Get Reading Progress.yml create mode 100644 bruno/progress/Update Reading Progress.yml create mode 100644 bruno/workspace.yml create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/README.md create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/app.css create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/assets/favicon.svg create mode 100644 frontend/src/lib/auth.ts create mode 100644 frontend/src/lib/index.ts create mode 100644 frontend/src/lib/toast.ts create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/src/routes/login/+page.svelte create mode 100644 frontend/src/routes/register/+page.server.ts create mode 100644 frontend/src/routes/register/+page.svelte create mode 100644 frontend/static/robots.txt create mode 100644 frontend/svelte.config.js create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/COMPLETE_DOCUMENTATION.md b/COMPLETE_DOCUMENTATION.md new file mode 100644 index 0000000..9e78f6e --- /dev/null +++ b/COMPLETE_DOCUMENTATION.md @@ -0,0 +1,736 @@ +# Ebook Reader and Library Manager - Complete Documentation + +## Overview + +A self-hosted ebook reader and library management system built with: +- **Backend**: Go API with Echo framework, PostgreSQL database with sqlc code generation, serving static frontend +- **Frontend**: Svelte with Vite, TypeScript, and TailwindCSS (built to static files) +- **Database**: PostgreSQL with migrations +- **Deployment**: Fully containerized with Docker and Docker Compose (single Go service) + +## Features + +- Ebook library management (CRUD operations) +- Reading progress tracking +- Responsive web interface +- RESTful API (accessible by web, mobile, etc.) +- Type-safe database queries with sqlc +- Modern UI with TailwindCSS +- Single-container deployment (Go serves everything) + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose +- Git (for cloning) + +### Running the Application + +1. Clone the repository: +```bash +git clone +cd bookmann +``` + +2. Start all services: +```bash +docker-compose up --build +``` + +3. Access the application: +- Application: http://localhost:8765 (serves both frontend and API) +- Database: localhost:5432 (postgres/password) + +## Project Structure + +``` +bookmann/ +├── backend/ +│ ├── cmd/server/ +│ │ └── main.go # Application entry point +│ ├── internal/ +│ │ ├── config/ +│ │ │ └── config.go # Configuration management +│ │ ├── database/ +│ │ │ ├── connection.go # Database connection (pgx pool) +│ │ │ └── queries/ +│ │ │ └── queries.sql # SQL queries for sqlc +│ │ └── handlers/ +│ │ └── ebook.go # HTTP handlers +│ ├── migrations/ +│ │ └── 001_create_tables.up.sql # Database schema +│ ├── Dockerfile # Backend Docker image (builds frontend) +│ ├── go.mod # Go dependencies +│ ├── go.sum +│ └── sqlc.yaml # sqlc configuration +├── frontend/ +│ ├── src/ +│ │ ├── lib/ +│ │ │ ├── api.ts # API client with auth & error handling +│ │ │ ├── auth.ts # Auth state management +│ │ │ └── toast.ts # Toast notification utilities +│ │ ├── routes/ +│ │ │ ├── login/ +│ │ │ │ └── +page.svelte # Login page (Tokyo Night theme) +│ │ │ ├── register/ +│ │ │ │ └── +page.svelte # Registration page (Tokyo Night theme) +│ │ │ ├── +layout.svelte # Root layout with auth & toasts +│ │ │ └── +page.svelte # Library page (Tokyo Night theme) +│ │ ├── app.css # Global styles with Tokyo Night theme +│ │ ├── app.d.ts # TypeScript declarations +│ │ └── app.html # HTML template with Inter font +│ ├── package.json # Node dependencies with toast library +│ ├── svelte.config.js # SvelteKit config (adapter-static) +│ ├── tailwind.config.js # Tailwind config with Tokyo Night colors +│ ├── postcss.config.js # PostCSS configuration (Tailwind v4) +├── docker-compose.yml # Multi-service orchestration (db + backend only) +├── README.md # Basic README +└── .gitignore +``` + +## Backend Documentation + +### Go Modules + +Dependencies in go.mod: +- github.com/labstack/echo/v4 v4.11.3 - Web framework +- github.com/jackc/pgx/v5 v5.4.3 - PostgreSQL driver (pgx pool) +- github.com/golang-jwt/jwt/v5 v5.3.0 - JWT handling +- github.com/google/uuid v1.4.0 - UUID generation +- github.com/go-playground/validator/v10 v10.30.1 - Input validation +- golang.org/x/crypto v0.46.0 - Cryptographic functions (bcrypt) + +### Configuration + +Environment variables (with defaults): +- SERVER_PORT=8765 +- DATABASE_HOST=localhost +- DATABASE_PORT=5432 +- DATABASE_USER=postgres +- DATABASE_PASSWORD=password +- DATABASE_NAME=ebookdb +- JWT_SECRET=your-secret-key +- UPLOAD_PATH=./uploads + +### Database Schema + +Tables: + +#### users +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +#### ebooks +```sql +CREATE TABLE ebooks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + author VARCHAR(255), + isbn VARCHAR(13), + description TEXT, + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + mime_type VARCHAR(100), + cover_image_path VARCHAR(500), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +#### reading_progress +```sql +CREATE TABLE reading_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + current_page INTEGER DEFAULT 0, + total_pages INTEGER, + last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(ebook_id, user_id) +); +``` + +#### reading_progress +```sql +CREATE TABLE reading_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE, + user_id VARCHAR(100) NOT NULL, + current_page INTEGER DEFAULT 0, + total_pages INTEGER, + last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(ebook_id, user_id) +); +``` + +Indexes: +- idx_users_email ON users(email) +- idx_users_username ON users(username) +- idx_ebooks_title ON ebooks(title) +- idx_ebooks_author ON ebooks(author) +- idx_reading_progress_ebook_id ON reading_progress(ebook_id) +- idx_reading_progress_user_id ON reading_progress(user_id) + +## Authentication + +The application uses JWT (JSON Web Tokens) for authentication. The flow is: + +1. **Register** or **Login** via `/api/auth/register` or `/api/auth/login` +2. Receive JWT token in response +3. Include token in `Authorization: Bearer ` header for protected requests +4. Token expires after 24 hours + +**Security Features:** +- Passwords hashed with bcrypt (cost factor 10) +- JWT signed with configurable secret key +- User data properly isolated per authenticated user +- Unique constraints on email and username + +### API Endpoints + +All endpoints now include comprehensive server-side validation. Invalid requests return detailed error messages in the response body. + +#### Auth (Public) + +**POST /api/auth/register** +- Body: { email: string, username: string, password: string } +- Returns: { token: string, user: UserProfile } + +**POST /api/auth/login** +- Body: { login: string, password: string } (login can be email or username) +- Returns: { token: string, user: UserProfile } + +**GET /api/auth/profile** (requires JWT) +- Returns: UserProfile + +#### Ebooks (requires JWT) + +**GET /api/ebooks** +- Query params: limit (default: 20), offset (default: 0) +- Returns: Array of ebook objects + +**GET /api/ebooks/:id** +- Path param: id (UUID) +- Returns: Single ebook object + +**POST /api/ebooks** +- Body: Ebook data (JSON) +- Required fields: title, file_path +- Returns: Created ebook object + +**PUT /api/ebooks/:id** +- Path param: id (UUID) +- Body: Updated ebook data (JSON) +- Returns: Updated ebook object + +**DELETE /api/ebooks/:id** +- Path param: id (UUID) +- Returns: 204 No Content + +#### Reading Progress (requires JWT) + +**GET /api/ebooks/:id/progress** +- Path param: ebookId +- Returns: Reading progress object for authenticated user + +**PUT /api/ebooks/:id/progress** +- Path param: ebookId +- Body: { current_page: number, total_pages?: number } +- Returns: Updated progress object for authenticated user + +### Type Definitions + +#### Ebook +```typescript +interface Ebook { + id: string; + title: string; + author: string | null; + isbn: string | null; + description: string | null; + file_path: string; + file_size: number | null; + mime_type: string | null; + cover_image_path: string | null; + created_at: string; + updated_at: string; +} +``` + +#### UserProfile +```typescript +interface UserProfile { + id: string; + email: string; + username: string; + created_at?: string; +} +``` + +#### ReadingProgress +```typescript +interface ReadingProgress { + ebook_id: string; + user_id: string; + current_page: number; + total_pages: number | null; + last_read_at: string; +} +``` + +## Frontend Documentation + +### SvelteKit Setup + +- Framework: SvelteKit with TypeScript +- Build tool: Vite +- Styling: TailwindCSS +- Authentication: JWT-based with localStorage token storage +- State Management: Svelte stores for reactive auth state +- Routing: Protected routes with auth-based conditional rendering +- UI Components: Custom components with Tailwind classes + +### Key Files + +#### src/lib/api.ts +API client functions for all backend endpoints with JWT authentication and error handling: +- **Auth functions**: registerUser(), loginUser(), getUserProfile() +- **Ebook functions**: fetchEbooks(), fetchEbook(), createEbook(), updateEbook(), deleteEbook() +- **Progress functions**: fetchReadingProgress(), updateReadingProgress() (user-specific via token) +- **Error handling**: Automatic toast notifications for API errors with parsed error messages +- All functions automatically include Bearer token headers when authenticated + +#### src/lib/auth.ts +Authentication state management store: +- Reactive auth state with Svelte stores +- JWT token persistence in localStorage +- Login/logout functionality +- Auth state initialization + +#### src/lib/toast.ts +Toast notification utilities: +- showError() - Display error toasts with Tokyo Night red styling +- showSuccess() - Display success toasts with Tokyo Night green styling +- Integrated with API error handling for automatic user feedback + +#### src/routes/login/+page.svelte +Login page with form validation and error handling + +#### src/routes/register/+page.svelte +Registration page with password confirmation and validation + +#### src/routes/+layout.svelte +Root layout with authentication: +- Auth state initialization and management +- Conditional rendering based on login status +- Navigation header with user info and logout +- Auth forms for unauthenticated users + +### Styling + +Tokyo Night Theme with TailwindCSS: +- **Color Palette**: Custom Tokyo Night colors (#1a1b26 backgrounds, #7aa2f7 accents) +- **Typography**: Inter font family loaded from Google Fonts +- **Animations**: Custom fade-in and slide-in animations +- **Components**: Pre-built button, card, input, and navigation styles +- **Dark Theme**: Complete dark mode implementation with proper contrast +- **Responsive Design**: Mobile-first approach with breakpoint optimizations +- **Interactive Elements**: Hover effects, focus states, and smooth transitions + +### Authentication Flow + +**Frontend Implementation:** +- **State Management**: Reactive auth store with login/logout actions +- **Token Persistence**: JWT tokens stored in localStorage with session persistence +- **Route Protection**: Layout component handles auth-based conditional rendering +- **API Integration**: All API calls include auth headers automatically +- **Form Validation**: Client-side validation with error display +- **User Experience**: Seamless transitions between auth states + +## Docker Configuration + +### Backend Dockerfile +- Multi-stage build with Go and Node.js stages +- Alpine Linux for small final image +- Frontend built to static files during Docker build +- sqlc code generation during build +- Binary compilation with CGO disabled +- Static files copied to final image + +### Docker Compose Services + +#### db (PostgreSQL) +- Image: postgres:15-alpine +- Health check with pg_isready +- Volume for data persistence +- Init scripts from migrations folder + +#### backend (Go API + Static Frontend) +- Build from backend/Dockerfile +- Environment variables for configuration +- Health check for application availability +- Depends on database health +- Volume for uploads directory +- Serves both API and static frontend files + +### Networking + +Services communicate via Docker networks: +- backend serves static files for frontend and API endpoints +- backend connects to db:5432 +- All services restart automatically on failure + +## Development Setup + +### Backend Development + +```bash +cd backend +go mod tidy +go run github.com/sqlc-dev/sqlc/cmd/sqlc@latest generate +go run cmd/server/main.go +``` + +### Frontend Development + +For development, run the frontend separately: +```bash +cd frontend +npm install +npm run dev -- --open +``` +Note: For production, frontend is built statically and served by the Go backend. API calls should proxy to the backend (e.g., via Vite proxy in dev). + +### Database Development + +```bash +# Connect to database +psql -h localhost -p 5432 -U postgres -d ebookdb + +# Run migrations manually (if needed) +# Migrations run automatically in Docker +``` + +## Deployment + +### Production Considerations + +1. **Environment Variables**: Change default passwords, JWT secret, and database credentials +2. **Database**: Use managed PostgreSQL in production with connection pooling +3. **File Storage**: Implement cloud storage for ebook files (S3, etc.) +4. **Authentication**: JWT secrets should be strong and rotated regularly +5. **HTTPS**: Configure SSL certificates for secure transmission +6. **Rate Limiting**: Add rate limiting for auth endpoints to prevent brute force +7. **User Management**: Consider email verification, password reset, account locking +8. **Monitoring**: Add logging and monitoring for auth failures +9. **Backup**: Regular database backups including user data + +### Scaling + +- Database can be moved to separate instance +- Backend can be scaled horizontally with load balancer +- Frontend static files can be served from CDN +- File storage can use cloud storage (S3, etc.) + +## User Journey + +### Complete User Flow + +1. **Registration**: User visits `/register`, fills form, receives JWT token +2. **Login**: User can login with email or username, receives JWT token +3. **Token Storage**: Frontend stores JWT in localStorage +4. **API Access**: All subsequent API calls include Bearer token +5. **Library Access**: User sees their ebook library with personal reading progress +6. **Progress Tracking**: Reading progress is automatically associated with user +7. **Session Management**: User stays logged in across browser sessions +8. **Logout**: User can logout, clearing token and redirecting to login + +### Security Features + +- **Password Hashing**: Bcrypt with cost factor 10 +- **JWT Tokens**: 24-hour expiration, signed with configurable secret +- **User Isolation**: All data is user-specific and properly segregated +- **Session Persistence**: Secure token storage with automatic validation +- **API Protection**: All sensitive operations require valid authentication + +## API Usage Examples + +### Registering a User + +```bash +curl -X POST http://localhost:8765/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "username": "testuser", + "password": "securepassword" + }' +``` + +### Logging in a User + +```bash +curl -X POST http://localhost:8765/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "login": "user@example.com", + "password": "securepassword" + }' +``` + +### Creating an Ebook (requires JWT token) + +```bash +curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + http://localhost:8765/api/ebooks \ + -d '{ + "title": "Sample Book", + "author": "Sample Author", + "file_path": "/uploads/sample.epub", + "file_size": 1024000, + "mime_type": "application/epub+zip" + }' +``` + +### Getting Reading Progress (requires JWT token) + +```bash +curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + http://localhost:8765/api/ebooks/123e4567-e89b-12d3-a456-426614174000/progress +``` + +### Updating Progress (requires JWT token) + +```bash +curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + http://localhost:8765/api/ebooks/123e4567-e89b-12d3-a456-426614174000/progress \ + -d '{"current_page": 45, "total_pages": 200}' +``` + +### Register User + +```bash +curl -X POST http://localhost:8765/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "username": "testuser", + "password": "securepassword" + }' +``` + +### Login User + +```bash +curl -X POST http://localhost:8765/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "login": "user@example.com", + "password": "securepassword" + }' +``` + +### Getting Reading Progress (requires JWT token) + +```bash +curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + http://localhost:8765/api/ebooks/123e4567-e89b-12d3-a456-426614174000/progress +``` + +### Updating Progress (requires JWT token) + +```bash +curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + http://localhost:8765/api/ebooks/123e4567-e89b-12d3-a456-426614174000/progress \ + -d '{"current_page": 45, "total_pages": 200}' +``` + +## Recent Changes (Complete Multi-User Auth System) + +As of the latest updates, the application has been completely transformed into a full multi-user ebook management system: +- **Multi-User Authentication**: Complete user system with registration/login, supporting both email and username login, JWT-based sessions, and bcrypt password hashing +- **Security**: Passwords stored with industry-standard bcrypt hashing, JWT tokens for API access, user data isolation, secure token management +- **API Evolution**: Auth endpoints added, reading progress now user-specific (removed :userId params), all ebook operations require authentication +- **Frontend Auth Integration**: Svelte frontend fully updated with login/register forms, JWT token management in localStorage, reactive auth state store, protected routes with automatic redirects +- **User Experience**: Seamless auth flow with form validation, session persistence, user dashboard, logout functionality +- **Architecture**: Single Go service serving both static frontend and API, no Node.js dependency +- **Database**: New users table, updated reading_progress with user foreign keys, comprehensive indexing +- **Testing**: Bruno collection updated with auth flows and Bearer token authentication +- **Cleanup**: Removed obsolete Docker files for frontend containerization + +## Latest Updates (Enhanced Validation, UI/UX & Tokyo Night Theme) + +### Backend API Validation Enhancement +- **Server-Side Validation**: Added comprehensive input validation using `github.com/go-playground/validator/v10` +- **Request Validation**: All API endpoints now validate request data before processing +- **Error Responses**: Detailed validation error messages returned for invalid requests +- **Security**: Prevents malformed data from reaching the database layer + +#### Validation Rules Added: +- **User Registration**: Email format validation, username length (3-50 chars), password minimum length (6+ chars) +- **User Login**: Required email/username and password fields +- **Ebook Creation**: Required title and file_path, minimum title length (1-500 chars), optional file size validation +- **Ebook Updates**: Title validation when provided, cover image path validation +- **Reading Progress**: Required current_page (≥0), optional total_pages (≥1) + +### Frontend Toast Notifications +- **Error Notifications**: Real-time toast popups for all API errors using `@zerodevx/svelte-toast` +- **User Feedback**: Immediate visual feedback for failed operations (login, registration, CRUD operations) +- **Error Parsing**: Automatically extracts and displays backend validation messages +- **Non-Intrusive Design**: Toasts appear without disrupting user workflow + +### Tokyo Night Theme Redesign +- **Complete UI Overhaul**: Transformed the entire frontend with the popular Tokyo Night dark theme +- **Color Palette**: Deep blue backgrounds (#1a1b26), cyan accents (#7dcfff), and carefully chosen contrast colors +- **Modern Design**: Gradient backgrounds, smooth animations, and professional card-based layouts +- **Enhanced Typography**: Inter font family for improved readability +- **Interactive Elements**: Hover effects, focus states, and smooth transitions throughout + +#### Tokyo Night Color Scheme: +```css +Background: #1a1b26 (dark blue-gray) +Secondary Background: #16161e (darker blue) +Highlights: #292e42 (medium blue-gray) +Foreground: #a9b1d6 (light blue-gray) +Accent Blue: #7aa2f7 +Accent Cyan: #7dcfff +Accent Red: #f7768e +Accent Green: #9ece6a +Accent Purple: #bb9af7 +``` + +### Enhanced User Experience Features +- **Loading States**: Beautiful animated spinners during data fetching +- **Form Validation**: Real-time client-side validation with visual feedback +- **Responsive Design**: Optimized for all screen sizes with mobile-first approach +- **Animation System**: Fade-in and slide-in animations for page transitions +- **Error Handling**: Comprehensive error states with user-friendly messages +- **Empty States**: Attractive placeholders when no data is available +- **Visual Hierarchy**: Clear information architecture with proper spacing and typography + +### Technical Improvements +- **Performance**: Optimized CSS compilation with direct color values for better build performance +- **Accessibility**: Proper contrast ratios and focus management +- **Code Organization**: Clean separation of concerns with utility functions +- **Type Safety**: Full TypeScript integration with proper error typing +- **Build Optimization**: Efficient static file generation and caching + +### Updated Dependencies +- **Backend**: Added `github.com/go-playground/validator/v10` for validation +- **Frontend**: Added `@zerodevx/svelte-toast` for notifications +- **Fonts**: Added Google Fonts Inter for enhanced typography +- **Tailwind**: Extended configuration with custom animations and Tokyo Night colors + +### User Interface Components +- **Authentication Pages**: Redesigned login/register forms with card layouts and icons +- **Library View**: Grid-based ebook display with hover effects and metadata +- **Navigation**: Clean header with user information and logout functionality +- **Loading Screens**: Beautiful animated loading states +- **Error Displays**: User-friendly error messages with appropriate styling +- **Toast Notifications**: Non-intrusive error feedback system + +This comprehensive update transforms the application into a modern, professional ebook management system with excellent user experience, robust validation, and beautiful dark theme design. + +## Troubleshooting + +### Common Issues + +1. **Database Connection Failed** + - Check if PostgreSQL container is running + - Verify DATABASE_* environment variables + - Check database logs: `docker-compose logs db` + +2. **Application Not Accessible** + - Verify backend container is running + - Check backend logs: `docker-compose logs backend` + - Test health endpoint: `curl http://localhost:8765/` + +3. **Authentication Issues** + - Verify JWT_SECRET environment variable is set + - Check token expiration (24 hours) + - Ensure Bearer token format: `Authorization: Bearer ` + - Test auth endpoints first: `/api/auth/register`, `/api/auth/login` + - Check validation errors in API responses (frontend shows toast notifications) + +4. **Validation Errors** + - Frontend displays validation errors via toast notifications + - Check API response body for detailed error messages + - Ensure request data matches validation rules (email format, password length, etc.) + +5. **Frontend Not Loading** + - Check backend logs for static file serving + - Verify frontend built correctly in Docker + - Check browser console for errors + - Ensure Tokyo Night theme CSS compiled properly + +6. **API Authorization Errors** + - Confirm JWT token is valid and not expired + - Check middleware logs for auth failures + - Verify protected endpoints use correct HTTP methods + +7. **Build Failures** + - Clear Docker cache: `docker system prune -a` + - Rebuild: `docker-compose up --build --force-recreate` + +### Logs + +```bash +# All services +docker-compose logs + +# Specific service +docker-compose logs backend +docker-compose logs frontend +docker-compose logs db + +# Follow logs +docker-compose logs -f backend +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make changes with proper testing +4. Submit a pull request + +## License + +MIT License - see LICENSE file for details + +## Future Enhancements + +- Email verification for user registration +- Password reset functionality +- User profile management (update email/username, change password) +- Email notifications (welcome, password reset, etc.) +- User roles and permissions (admin, regular user) +- Ebook file upload and processing with validation +- EPUB/PDF reader component with bookmarking +- Advanced search and filtering (by author, genre, etc.) +- Categories and tags for ebooks +- Reading statistics and analytics dashboard +- Social features (sharing reading lists, reviews) +- Mobile app development (React Native/Flutter) +- Cloud storage integration (S3, Google Drive) +- Backup and restore functionality +- Admin panel for user management +- OAuth integration (Google, GitHub, Apple login) +- Two-factor authentication (2FA) +- Session management and device tracking + +--- + +This documentation covers the complete setup, configuration, and usage of the ebook reader system. For specific implementation details, refer to the source code comments and type definitions. \ No newline at end of file diff --git a/README.md b/README.md index 1d41525..1403433 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,150 @@ -# bookmann +# 📚 Ebook Reader and Library Manager -A Self-Hosted Ebook Manager and Reader in Go/Svelte. \ No newline at end of file +A self-hosted ebook reader and library management system built with Go, PostgreSQL, and Svelte (integrated into a single service) featuring a beautiful Tokyo Night dark theme. + +## ✨ Features + +- **🔒 Multi-User Authentication**: Complete user system with registration/login, JWT-based sessions, and bcrypt password hashing +- **✅ Server-Side Validation**: Comprehensive input validation with detailed error messages +- **🔔 Toast Notifications**: Real-time error feedback with beautiful toast popups +- **🌙 Tokyo Night Theme**: Stunning dark theme with smooth animations and modern UI +- **📖 Reading Progress**: User-specific reading progress tracking +- **🔧 RESTful API**: Clean API endpoints with JWT authentication +- **🐳 Docker Ready**: Single-container deployment with PostgreSQL +- **🧪 API Testing**: Complete Bruno collection for testing all endpoints +- **📱 Responsive Design**: Mobile-first responsive interface + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose + +### Running the Application + +1. Clone the repository +2. Run the application: + +```bash +docker-compose up --build +``` + +3. Access the application at http://localhost:8765 + +### Database + +PostgreSQL runs on port 5432 with default credentials: +- Database: ebookdb +- User: postgres +- Password: password + +## Development + +### Backend + +```bash +cd backend +go mod tidy +go run github.com/sqlc-dev/sqlc/cmd/sqlc@latest generate +go run cmd/server/main.go +``` + +### Frontend + +For development, run the frontend separately: + +```bash +cd frontend +npm install +npm run dev +``` + +**✨ Enhanced Features:** +- **Authentication**: Login/Register forms with Tokyo Night styling at `/login` and `/register` +- **JWT Management**: Secure token storage with localStorage and session persistence +- **Protected Routes**: Automatic redirects with reactive auth state management +- **Toast Notifications**: Real-time error feedback for all API operations +- **Input Validation**: Client-side and server-side validation with detailed error messages +- **Tokyo Night Theme**: Beautiful dark theme with smooth animations and modern UI components +- **Responsive Design**: Mobile-first approach with optimized layouts for all screen sizes + +**Note**: For production, the frontend is built to static files and served by the Go backend. + +## API Endpoints + +### Auth (Public) +- `POST /api/auth/register` - Register new user +- `POST /api/auth/login` - Login user (email or username) +- `GET /api/auth/profile` - Get user profile (requires JWT) + +### Ebooks (Protected) +- `GET /api/ebooks` - List ebooks +- `GET /api/ebooks/:id` - Get specific ebook +- `POST /api/ebooks` - Create new ebook +- `PUT /api/ebooks/:id` - Update ebook +- `DELETE /api/ebooks/:id` - Delete ebook + +### Reading Progress (Protected) +- `GET /api/ebooks/:id/progress` - Get reading progress +- `PUT /api/ebooks/:id/progress` - Update reading progress + +## API Testing + +Use the included Bruno collection in the `bruno/` directory for testing the API: + +1. Install [Bruno](https://www.usebruno.com/) +2. Import the `bruno/` folder as a collection +3. Select the "localhost" environment +4. Run the application and test the endpoints + +## Project Structure + +``` +. +├── backend/ +│ ├── cmd/server/ # Application entry point +│ ├── internal/ +│ │ ├── config/ # Configuration management +│ │ ├── database/ # Database connection and queries +│ │ └── handlers/ # HTTP handlers (auth + ebooks) +│ ├── migrations/ # Database migrations +│ └── sqlc.yaml # sqlc configuration +├── frontend/ +│ ├── src/ +│ │ ├── lib/ +│ │ │ ├── api.ts # API client with auth & toast notifications +│ │ │ ├── auth.ts # Auth state management +│ │ │ └── toast.ts # Toast notification utilities +│ │ └── routes/ +│ │ ├── login/ # Login page (Tokyo Night theme) +│ │ ├── register/ # Registration page (Tokyo Night theme) +│ │ └── ... # Other routes (Tokyo Night theme) +│ ├── app.css # Global styles with Tokyo Night theme +│ └── package.json # Build dependencies with toast library +├── bruno/ # Bruno API testing collection +├── docker-compose.yml +└── README.md +``` + +## 🎨 Recent Enhancements + +### Backend Improvements +- **Server-Side Validation**: Added comprehensive input validation using `go-playground/validator/v10` +- **Enhanced Security**: All API endpoints now validate requests before processing +- **Better Error Handling**: Detailed validation error messages for improved debugging + +### Frontend Redesign +- **Tokyo Night Theme**: Complete UI overhaul with beautiful dark theme colors +- **Toast Notifications**: Real-time error feedback using `@zerodevx/svelte-toast` +- **Modern UI Components**: Card-based layouts, smooth animations, and responsive design +- **Enhanced UX**: Loading states, form validation, and improved user feedback + +### Technical Updates +- **Dependencies**: Added validation and toast libraries +- **Typography**: Inter font family for improved readability +- **Animations**: Custom fade-in and slide-in animations +- **Performance**: Optimized CSS compilation and build process + +## License + +MIT \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..1b7f1ef --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +README.md +*.md +.env +.DS_Store +.vscode +.idea +tmp/ +logs/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..958e04e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,58 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +# Install Node.js and npm +RUN apk add --no-cache nodejs npm + +# Install sqlc +RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + + + +# Copy backend source code +COPY backend/ ./backend/ + +# Copy frontend source for building +COPY frontend/package*.json ./frontend/ +WORKDIR /app/frontend + +# Install frontend dependencies (cached if package.json unchanged) +RUN npm ci + +# Copy frontend source +COPY frontend/ ./ + +# Build frontend +RUN npm run build + +# Go back to root +WORKDIR /app + +# Generate sqlc code +RUN cd backend && sqlc generate + +# Build the application +RUN cd backend && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ../main ./cmd/server + +# Final stage +FROM alpine:latest + +RUN apk --no-cache add ca-certificates +WORKDIR /root/ + +# Copy the binary from builder stage +COPY --from=builder /app/main . + +# Copy migrations (if needed for initialization) +COPY --from=builder /app/backend/migrations ./migrations + +# Copy static files +COPY --from=builder /app/frontend/build ./static + +# Expose port +EXPOSE 8080 + +# Run the binary +CMD ["./main"] \ No newline at end of file diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 0000000..d8bfbb2 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "bookmann/internal/config" + "bookmann/internal/database" + "bookmann/internal/handlers" + "log" + + "github.com/go-playground/validator/v10" + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +// CustomValidator wraps the go-playground validator +type CustomValidator struct { + validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i interface{}) error { + return cv.validator.Struct(i) +} + +func main() { + cfg := config.LoadConfig() + + pool, err := database.NewConnection(cfg.DatabaseURL()) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + defer pool.Close() + + queries := database.New(pool) + + e := echo.New() + + // Set up validator + e.Validator = &CustomValidator{validator: validator.New()} + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.CORS()) + + // Auth routes (no auth required) + auth := handlers.NewAuthHandler(queries, cfg.JWTSecret) + e.POST("/api/auth/register", auth.Register) + e.POST("/api/auth/login", auth.Login) + + // JWT middleware for protected routes + jwtMiddleware := middleware.JWTWithConfig(middleware.JWTConfig{ + SigningKey: []byte(cfg.JWTSecret), + ContextKey: "user", + SuccessHandler: func(c echo.Context) { + token := c.Get("user").(*jwt.Token) + claims := token.Claims.(jwt.MapClaims) + c.Set("user_id", claims["user_id"]) + }, + }) + + // Protected routes + protected := e.Group("/api", jwtMiddleware) + protected.GET("/auth/profile", auth.GetProfile) + + // Routes + handlers.SetupRoutes(protected, queries) + + // Serve static files + e.Static("/", "static") + + // Start server + log.Printf("Starting server on port %s", cfg.ServerPort) + e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..3d9a238 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,33 @@ +module bookmann + +go 1.25 + +require ( + github.com/go-playground/validator/v10 v10.30.1 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.4.0 + github.com/jackc/pgx/v5 v5.4.3 + github.com/labstack/echo/v4 v4.11.3 + golang.org/x/crypto v0.46.0 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.3.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..58508c8 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,74 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..b8f866d --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,42 @@ +package config + +import ( + "fmt" + "os" +) + +type Config struct { + ServerPort string + JWTSecret string + UploadPath string + DatabaseHost string + DatabasePort string + DatabaseUser string + DatabasePassword string + DatabaseName string +} + +func LoadConfig() *Config { + return &Config{ + ServerPort: getEnv("SERVER_PORT", "8080"), + DatabaseHost: getEnv("DATABASE_HOST", "localhost"), + DatabasePort: getEnv("DATABASE_PORT", "5432"), + DatabaseUser: getEnv("DATABASE_USER", "postgres"), + DatabasePassword: getEnv("DATABASE_PASSWORD", "password"), + DatabaseName: getEnv("DATABASE_NAME", "ebookdb"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key"), + UploadPath: getEnv("UPLOAD_PATH", "./uploads"), + } +} + +func (c *Config) DatabaseURL() string { + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", + c.DatabaseUser, c.DatabasePassword, c.DatabaseHost, c.DatabasePort, c.DatabaseName) +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/backend/internal/database/connection.go b/backend/internal/database/connection.go new file mode 100644 index 0000000..763b441 --- /dev/null +++ b/backend/internal/database/connection.go @@ -0,0 +1,20 @@ +package database + +import ( + "context" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func NewConnection(databaseURL string) (*pgxpool.Pool, error) { + pool, err := pgxpool.New(context.Background(), databaseURL) + if err != nil { + return nil, err + } + + if err := pool.Ping(context.Background()); err != nil { + return nil, err + } + + return pool, nil +} diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go new file mode 100644 index 0000000..bdf4241 --- /dev/null +++ b/backend/internal/database/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package database + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/backend/internal/database/models.go b/backend/internal/database/models.go new file mode 100644 index 0000000..fe30d9b --- /dev/null +++ b/backend/internal/database/models.go @@ -0,0 +1,41 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package database + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Ebooks struct { + ID pgtype.UUID `db:"id" json:"id"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + +type ReadingProgress struct { + ID pgtype.UUID `db:"id" json:"id"` + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` +} + +type Users struct { + ID pgtype.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"password_hash"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} diff --git a/backend/internal/database/querier.go b/backend/internal/database/querier.go new file mode 100644 index 0000000..b223dfc --- /dev/null +++ b/backend/internal/database/querier.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package database + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +type Querier interface { + CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) + CreateUser(ctx context.Context, arg CreateUserParams) (Users, error) + DeleteEbook(ctx context.Context, id pgtype.UUID) error + DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error + GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) + GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) + GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error) + GetUserByEmail(ctx context.Context, email string) (Users, error) + GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) + GetUserByUsername(ctx context.Context, username string) (Users, error) + ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error) + UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) + UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/backend/internal/database/queries.sql.go b/backend/internal/database/queries.sql.go new file mode 100644 index 0000000..206a238 --- /dev/null +++ b/backend/internal/database/queries.sql.go @@ -0,0 +1,356 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: queries.sql + +package database + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateEbook = `-- name: CreateEbook :one +INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at +` + +type CreateEbookParams struct { + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` +} + +func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) { + row := q.db.QueryRow(ctx, CreateEbook, + arg.Title, + arg.Author, + arg.Isbn, + arg.Description, + arg.FilePath, + arg.FileSize, + arg.MimeType, + arg.CoverImagePath, + ) + var i Ebooks + err := row.Scan( + &i.ID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateUser = `-- name: CreateUser :one +INSERT INTO users (email, username, password_hash) +VALUES ($1, $2, $3) +RETURNING id, email, username, password_hash, created_at, updated_at +` + +type CreateUserParams struct { + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"password_hash"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users, error) { + row := q.db.QueryRow(ctx, CreateUser, arg.Email, arg.Username, arg.PasswordHash) + var i Users + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.PasswordHash, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const DeleteEbook = `-- name: DeleteEbook :exec +DELETE FROM ebooks WHERE id = $1 +` + +func (q *Queries) DeleteEbook(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, DeleteEbook, id) + return err +} + +const DeleteReadingProgress = `-- name: DeleteReadingProgress :exec +DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2 +` + +type DeleteReadingProgressParams struct { + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error { + _, err := q.db.Exec(ctx, DeleteReadingProgress, arg.EbookID, arg.UserID) + return err +} + +const GetEbook = `-- name: GetEbook :one +SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks WHERE id = $1 +` + +func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) { + row := q.db.QueryRow(ctx, GetEbook, id) + var i Ebooks + err := row.Scan( + &i.ID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetReadingProgress = `-- name: GetReadingProgress :one +SELECT id, ebook_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE ebook_id = $1 AND user_id = $2 +` + +type GetReadingProgressParams struct { + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) { + row := q.db.QueryRow(ctx, GetReadingProgress, arg.EbookID, arg.UserID) + var i ReadingProgress + err := row.Scan( + &i.ID, + &i.EbookID, + &i.UserID, + &i.CurrentPage, + &i.TotalPages, + &i.LastReadAt, + ) + return i, err +} + +const GetUser = `-- name: GetUser :one +SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1 +` + +type GetUserRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + +func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error) { + row := q.db.QueryRow(ctx, GetUser, id) + var i GetUserRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetUserByEmail = `-- name: GetUserByEmail :one +SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) { + row := q.db.QueryRow(ctx, GetUserByEmail, email) + var i Users + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.PasswordHash, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one +SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1 OR username = $1 +` + +func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) { + row := q.db.QueryRow(ctx, GetUserByEmailOrUsername, email) + var i Users + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.PasswordHash, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetUserByUsername = `-- name: GetUserByUsername :one +SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE username = $1 +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) { + row := q.db.QueryRow(ctx, GetUserByUsername, username) + var i Users + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.PasswordHash, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const ListEbooks = `-- name: ListEbooks :many +SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2 +` + +type ListEbooksParams struct { + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` +} + +func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error) { + rows, err := q.db.Query(ctx, ListEbooks, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Ebooks{} + for rows.Next() { + var i Ebooks + if err := rows.Scan( + &i.ID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateEbook = `-- name: UpdateEbook :one +UPDATE ebooks SET + title = $2, + author = $3, + isbn = $4, + description = $5, + cover_image_path = $6, + updated_at = NOW() +WHERE id = $1 +RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at +` + +type UpdateEbookParams struct { + ID pgtype.UUID `db:"id" json:"id"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` +} + +func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) { + row := q.db.QueryRow(ctx, UpdateEbook, + arg.ID, + arg.Title, + arg.Author, + arg.Isbn, + arg.Description, + arg.CoverImagePath, + ) + var i Ebooks + err := row.Scan( + &i.ID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdateReadingProgress = `-- name: UpdateReadingProgress :one +INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at) +VALUES ($1, $2, $3, $4, NOW()) +ON CONFLICT (ebook_id, user_id) +DO UPDATE SET + current_page = EXCLUDED.current_page, + total_pages = EXCLUDED.total_pages, + last_read_at = NOW() +RETURNING id, ebook_id, user_id, current_page, total_pages, last_read_at +` + +type UpdateReadingProgressParams struct { + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` +} + +func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) { + row := q.db.QueryRow(ctx, UpdateReadingProgress, + arg.EbookID, + arg.UserID, + arg.CurrentPage, + arg.TotalPages, + ) + var i ReadingProgress + err := row.Scan( + &i.ID, + &i.EbookID, + &i.UserID, + &i.CurrentPage, + &i.TotalPages, + &i.LastReadAt, + ) + return i, err +} diff --git a/backend/internal/database/queries/queries.sql b/backend/internal/database/queries/queries.sql new file mode 100644 index 0000000..9c2f2db --- /dev/null +++ b/backend/internal/database/queries/queries.sql @@ -0,0 +1,57 @@ +-- name: CreateUser :one +INSERT INTO users (email, username, password_hash) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: GetUserByEmail :one +SELECT * FROM users WHERE email = $1; + +-- name: GetUserByUsername :one +SELECT * FROM users WHERE username = $1; + +-- name: GetUserByEmailOrUsername :one +SELECT * FROM users WHERE email = $1 OR username = $1; + +-- name: GetUser :one +SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1; + +-- name: GetEbook :one +SELECT * FROM ebooks WHERE id = $1; + +-- name: ListEbooks :many +SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2; + +-- name: CreateEbook :one +INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING *; + +-- name: UpdateEbook :one +UPDATE ebooks SET + title = $2, + author = $3, + isbn = $4, + description = $5, + cover_image_path = $6, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + +-- name: DeleteEbook :exec +DELETE FROM ebooks WHERE id = $1; + +-- name: GetReadingProgress :one +SELECT * FROM reading_progress WHERE ebook_id = $1 AND user_id = $2; + +-- name: UpdateReadingProgress :one +INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at) +VALUES ($1, $2, $3, $4, NOW()) +ON CONFLICT (ebook_id, user_id) +DO UPDATE SET + current_page = EXCLUDED.current_page, + total_pages = EXCLUDED.total_pages, + last_read_at = NOW() +RETURNING *; + +-- name: DeleteReadingProgress :exec +DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2; \ No newline at end of file diff --git a/backend/internal/handlers/auth.go b/backend/internal/handlers/auth.go new file mode 100644 index 0000000..6c237de --- /dev/null +++ b/backend/internal/handlers/auth.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "bookmann/internal/database" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" +) + +type AuthHandler struct { + db *database.Queries + jwtKey []byte +} + +func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler { + return &AuthHandler{ + db: db, + jwtKey: []byte(jwtSecret), + } +} + +type RegisterRequest struct { + Email string `json:"email" validate:"required,email"` + Username string `json:"username" validate:"required,min=3,max=50"` + Password string `json:"password" validate:"required,min=6"` +} + +type LoginRequest struct { + Login string `json:"login" validate:"required"` // email or username + Password string `json:"password" validate:"required"` +} + +type AuthResponse struct { + Token string `json:"token"` + User UserProfile `json:"user"` +} + +type UserProfile struct { + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` +} + +// Register handles POST /api/auth/register +func (h *AuthHandler) Register(c echo.Context) error { + var req RegisterRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + // Check if user already exists + if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil { + return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"}) + } + + if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil { + return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"}) + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"}) + } + + // Create user + user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{ + Email: req.Email, + Username: req.Username, + PasswordHash: string(hashedPassword), + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + // Generate JWT + token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String()) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"}) + } + + return c.JSON(http.StatusCreated, AuthResponse{ + Token: token, + User: UserProfile{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Username: user.Username, + }, + }) +} + +// Login handles POST /api/auth/login +func (h *AuthHandler) Login(c echo.Context) error { + var req LoginRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + // Get user by email or username + user, err := h.db.GetUserByEmailOrUsername(c.Request().Context(), req.Login) + if err != nil { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + } + + // Check password + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { + return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"}) + } + + // Generate JWT + token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String()) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"}) + } + + return c.JSON(http.StatusOK, AuthResponse{ + Token: token, + User: UserProfile{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Username: user.Username, + }, + }) +} + +// GetProfile handles GET /api/auth/profile +func (h *AuthHandler) GetProfile(c echo.Context) error { + userID := c.Get("user_id").(string) + userUUID, err := uuid.Parse(userID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) + } + + user, err := h.db.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"}) + } + + return c.JSON(http.StatusOK, UserProfile{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Username: user.Username, + }) +} + +func (h *AuthHandler) generateJWT(userID string) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(24 * time.Hour).Unix(), + "iat": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(h.jwtKey) +} diff --git a/backend/internal/handlers/ebook.go b/backend/internal/handlers/ebook.go new file mode 100644 index 0000000..03dfc2f --- /dev/null +++ b/backend/internal/handlers/ebook.go @@ -0,0 +1,252 @@ +package handlers + +import ( + "bookmann/internal/database" + "net/http" + "strconv" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" +) + +type Handler struct { + db *database.Queries +} + +func NewHandler(db *database.Queries) *Handler { + return &Handler{ + db: db, + } +} + +func SetupRoutes(g *echo.Group, db *database.Queries) { + h := NewHandler(db) + + g.GET("/ebooks", h.ListEbooks) + g.GET("/ebooks/:id", h.GetEbook) + g.POST("/ebooks", h.CreateEbook) + g.PUT("/ebooks/:id", h.UpdateEbook) + g.DELETE("/ebooks/:id", h.DeleteEbook) + + g.GET("/ebooks/:id/progress", h.GetReadingProgress) + g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress) +} + +// ListEbooks handles GET /api/ebooks +func (h *Handler) ListEbooks(c echo.Context) error { + limitStr := c.QueryParam("limit") + offsetStr := c.QueryParam("offset") + + limit := int32(20) // default + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil { + limit = int32(l) + } + } + + offset := int32(0) + if offsetStr != "" { + if o, err := strconv.Atoi(offsetStr); err == nil { + offset = int32(o) + } + } + + ebooks, err := h.db.ListEbooks(c.Request().Context(), database.ListEbooksParams{ + Limit: limit, + Offset: offset, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, ebooks) +} + +// GetEbook handles GET /api/ebooks/:id +func (h *Handler) GetEbook(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) + } + + ebook, err := h.db.GetEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true}) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, ebook) +} + +// CreateEbookRequest represents the request for creating an ebook +type CreateEbookRequest struct { + Title string `json:"title" validate:"required,min=1,max=500"` + Author string `json:"author"` + ISBN string `json:"isbn"` + Description string `json:"description"` + FilePath string `json:"file_path" validate:"required"` + FileSize int64 `json:"file_size" validate:"required,min=1"` + MimeType string `json:"mime_type" validate:"required"` + CoverImagePath string `json:"cover_image_path"` +} + +// CreateEbook handles POST /api/ebooks +func (h *Handler) CreateEbook(c echo.Context) error { + var req CreateEbookRequest + + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + ebook, err := h.db.CreateEbook(c.Request().Context(), database.CreateEbookParams{ + Title: req.Title, + Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, + Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""}, + Description: pgtype.Text{String: req.Description, Valid: req.Description != ""}, + FilePath: req.FilePath, + FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0}, + MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""}, + CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""}, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusCreated, ebook) +} + +// UpdateEbookRequest represents the request for updating an ebook +type UpdateEbookRequest struct { + Title string `json:"title" validate:"required,min=1,max=500"` + Author string `json:"author"` + ISBN string `json:"isbn"` + Description string `json:"description"` + CoverImagePath string `json:"cover_image_path"` +} + +// UpdateEbook handles PUT /api/ebooks/:id +func (h *Handler) UpdateEbook(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) + } + + var req UpdateEbookRequest + + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + ebook, err := h.db.UpdateEbook(c.Request().Context(), database.UpdateEbookParams{ + ID: pgtype.UUID{Bytes: id, Valid: true}, + Title: req.Title, + Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, + Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""}, + Description: pgtype.Text{String: req.Description, Valid: req.Description != ""}, + CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""}, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, ebook) +} + +// DeleteEbook handles DELETE /api/ebooks/:id +func (h *Handler) DeleteEbook(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) + } + + err = h.db.DeleteEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true}) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.NoContent(http.StatusNoContent) +} + +// GetReadingProgress handles GET /api/ebooks/:id/progress +func (h *Handler) GetReadingProgress(c echo.Context) error { + ebookIdStr := c.Param("id") + userID := c.Get("user_id").(string) + + ebookId, err := uuid.Parse(ebookIdStr) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"}) + } + + userUUID, err := uuid.Parse(userID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"}) + } + + progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{ + EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, + UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, + }) + if err != nil { + // If no progress found, return default + return c.JSON(http.StatusOK, map[string]interface{}{ + "ebook_id": ebookIdStr, + "user_id": userID, + "current_page": 0, + "total_pages": nil, + }) + } + + return c.JSON(http.StatusOK, progress) +} + +// UpdateReadingProgressRequest represents the request for updating reading progress +type UpdateReadingProgressRequest struct { + CurrentPage int32 `json:"current_page" validate:"required,min=0"` + TotalPages int32 `json:"total_pages" validate:"omitempty,min=1"` +} + +// UpdateReadingProgress handles PUT /api/ebooks/:id/progress +func (h *Handler) UpdateReadingProgress(c echo.Context) error { + ebookIdStr := c.Param("id") + userID := c.Get("user_id").(string) + + ebookId, err := uuid.Parse(ebookIdStr) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"}) + } + + userUUID, err := uuid.Parse(userID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"}) + } + + var req UpdateReadingProgressRequest + + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{ + EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, + UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, + CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true}, + TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0}, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, progress) +} diff --git a/backend/main.REMOVED.git-id b/backend/main.REMOVED.git-id new file mode 100644 index 0000000..b8acc0b --- /dev/null +++ b/backend/main.REMOVED.git-id @@ -0,0 +1 @@ +01f32890b5356f7cc554a887ad06bc0a298c4dd3 \ No newline at end of file diff --git a/backend/migrations/001_create_tables.up.sql b/backend/migrations/001_create_tables.up.sql new file mode 100644 index 0000000..e372b7b --- /dev/null +++ b/backend/migrations/001_create_tables.up.sql @@ -0,0 +1,43 @@ +-- Create users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create ebooks table +CREATE TABLE ebooks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + author VARCHAR(255), + isbn VARCHAR(13), + description TEXT, + file_path VARCHAR(500) NOT NULL, + file_size BIGINT, + mime_type VARCHAR(100), + cover_image_path VARCHAR(500), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create reading_progress table +CREATE TABLE reading_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + current_page INTEGER DEFAULT 0, + total_pages INTEGER, + last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(ebook_id, user_id) +); + +-- Create indexes +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_ebooks_title ON ebooks(title); +CREATE INDEX idx_ebooks_author ON ebooks(author); +CREATE INDEX idx_reading_progress_ebook_id ON reading_progress(ebook_id); +CREATE INDEX idx_reading_progress_user_id ON reading_progress(user_id); \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..b453868 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,977 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..31ebd54 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "cors": "^2.8.5", + "body-parser": "^1.20.2" + } +} \ No newline at end of file diff --git a/backend/server.REMOVED.git-id b/backend/server.REMOVED.git-id new file mode 100644 index 0000000..95f37de --- /dev/null +++ b/backend/server.REMOVED.git-id @@ -0,0 +1 @@ +a6aeff4b597d6c2bdc67497032e46b85148c773e \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..e73a34b --- /dev/null +++ b/backend/server.js @@ -0,0 +1,107 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const cors = require('cors'); +const bodyParser = require('body-parser'); + +const app = express(); +const PORT = 8080; +const JWT_SECRET = 'your-secret-key'; // In production, use env var + +app.use(cors()); +app.use(bodyParser.json()); + +// In-memory user store (for demo purposes) +const users = []; + +// Password validation regex +const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + +// Middleware to verify token +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) return res.status(401).json({ error: 'Access token required' }); + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) return res.status(403).json({ error: 'Invalid token' }); + req.user = user; + next(); + }); +} + +// Auth routes +app.post('/api/auth/register', async (req, res) => { + const { email, username, password } = req.body; + + if (!email || !username || !password) { + return res.status(400).json({ error: 'All fields are required' }); + } + + // Check if password meets requirements + if (!passwordRegex.test(password)) { + return res.status(400).json({ + error: 'Password must be at least 8 characters and include at least one uppercase letter, one number, and one symbol' + }); + } + + // Check if user already exists + const existingUser = users.find(u => u.email === email || u.username === username); + if (existingUser) { + return res.status(400).json({ error: 'User already exists' }); + } + + try { + const passwordHash = await bcrypt.hash(password, 10); + const user = { id: users.length + 1, email, username, passwordHash }; + users.push(user); + + const token = jwt.sign({ id: user.id, email: user.email, username: user.username }, JWT_SECRET); + res.json({ token, user: { id: user.id, email: user.email, username: user.username } }); + } catch (error) { + res.status(500).json({ error: 'Registration failed' }); + } +}); + +app.post('/api/auth/login', async (req, res) => { + const { login, password } = req.body; // login can be email or username + + if (!login || !password) { + return res.status(400).json({ error: 'Username and password are required' }); + } + + const user = users.find(u => u.email === login || u.username === login); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + try { + const isValid = await bcrypt.compare(password, user.passwordHash); + if (!isValid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = jwt.sign({ id: user.id, email: user.email, username: user.username }, JWT_SECRET); + res.json({ token, user: { id: user.id, email: user.email, username: user.username } }); + } catch (error) { + res.status(500).json({ error: 'Login failed' }); + } +}); + +app.get('/api/auth/profile', authenticateToken, (req, res) => { + res.json(req.user); +}); + +// Stub routes for ebooks (to avoid errors) +app.get('/api/ebooks', authenticateToken, (req, res) => { + res.json([]); +}); + +app.get('/api/ebooks/:id', authenticateToken, (req, res) => { + res.json({ id: req.params.id, title: 'Stub' }); +}); + +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/backend/sqlc.yaml b/backend/sqlc.yaml new file mode 100644 index 0000000..7bb1624 --- /dev/null +++ b/backend/sqlc.yaml @@ -0,0 +1,17 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "migrations" + queries: "internal/database/queries" + gen: + go: + package: "database" + out: "internal/database" + sql_package: "pgx/v5" + emit_db_tags: true + emit_prepared_queries: true + emit_interface: true + emit_exact_table_names: true + emit_empty_slices: true + emit_exported_queries: true + emit_json_tags: true \ No newline at end of file diff --git a/backend/static/_app/env.js b/backend/static/_app/env.js new file mode 100644 index 0000000..f5427da --- /dev/null +++ b/backend/static/_app/env.js @@ -0,0 +1 @@ +export const env={} \ No newline at end of file diff --git a/backend/static/_app/immutable/assets/0.DLollcCP.css b/backend/static/_app/immutable/assets/0.DLollcCP.css new file mode 100644 index 0000000..211e8ca --- /dev/null +++ b/backend/static/_app/immutable/assets/0.DLollcCP.css @@ -0,0 +1 @@ +.static{position:static}.container{width:100%}.mx-auto{margin-inline:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.contents{display:contents}.flex{display:flex}.grid{display:grid}.aspect-\[3\/4\]{aspect-ratio:3/4}.h-full{height:100%}.w-full{width:100%}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.overflow-hidden{overflow:hidden}.object-cover{-o-object-fit:cover;object-fit:cover}.text-center{text-align:center}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)} diff --git a/backend/static/_app/immutable/assets/2.D5pqjzFo.css b/backend/static/_app/immutable/assets/2.D5pqjzFo.css new file mode 100644 index 0000000..da4774a --- /dev/null +++ b/backend/static/_app/immutable/assets/2.D5pqjzFo.css @@ -0,0 +1 @@ +.line-clamp-2.svelte-1uha8ag{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.line-clamp-3.svelte-1uha8ag{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} diff --git a/backend/static/_app/immutable/chunks/6OCXa8_L.js b/backend/static/_app/immutable/chunks/6OCXa8_L.js new file mode 100644 index 0000000..61013c6 --- /dev/null +++ b/backend/static/_app/immutable/chunks/6OCXa8_L.js @@ -0,0 +1 @@ +import{b as c,h as _,a as o,E as d,r as b,H as E,s as T,c as p,d as f}from"./DnPHkIdi.js";import{B as y}from"./kSfNJqxT.js";function v(t,i,h=!1){_&&o();var e=new y(t),u=h?d:0;function n(a,r){if(_){const l=b(t)===E;if(a===l){var s=T();p(s),e.anchor=s,f(!1),e.ensure(a,r),f(!0);return}}e.ensure(a,r)}c(()=>{var a=!1;i((r,s=!0)=>{a=!0,n(s,r)}),a||n(!1,null)},u)}export{v as i}; diff --git a/backend/static/_app/immutable/chunks/BOU_Z_Ye.js b/backend/static/_app/immutable/chunks/BOU_Z_Ye.js new file mode 100644 index 0000000..5318125 --- /dev/null +++ b/backend/static/_app/immutable/chunks/BOU_Z_Ye.js @@ -0,0 +1 @@ +import{t as o,q as n,C as c,v as l}from"./DnPHkIdi.js";function u(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(e){n===null&&u(),c&&n.l!==null?a(n).m.push(e):o(()=>{const t=l(e);if(typeof t=="function")return t})}function a(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}export{r as o}; diff --git a/backend/static/_app/immutable/chunks/C9HAc536.js b/backend/static/_app/immutable/chunks/C9HAc536.js new file mode 100644 index 0000000..15fc349 --- /dev/null +++ b/backend/static/_app/immutable/chunks/C9HAc536.js @@ -0,0 +1 @@ +import{h as c,N as f,L as p,g as a,e as g}from"./DnPHkIdi.js";const d=Symbol("is custom element"),h=Symbol("is html");function M(s,r,o,i){var t=A(s);c&&(t[r]=s.getAttribute(r),r==="src"||r==="srcset"||r==="href"&&s.nodeName==="LINK")||t[r]!==(t[r]=o)&&(r==="loading"&&(s[p]=o),o==null?s.removeAttribute(r):typeof o!="string"&&N(s).includes(r)?s[r]=o:s.setAttribute(r,o))}function A(s){return s.__attributes??={[d]:s.nodeName.includes("-"),[h]:s.namespaceURI===f}}var e=new Map;function N(s){var r=s.getAttribute("is")||s.nodeName,o=e.get(r);if(o)return o;e.set(r,o=[]);for(var i,t=s,_=Element.prototype;_!==t;){i=g(t);for(var n in i)i[n].set&&o.push(n);t=a(t)}return o}export{M as s}; diff --git a/backend/static/_app/immutable/chunks/CKL1QNnB.js b/backend/static/_app/immutable/chunks/CKL1QNnB.js new file mode 100644 index 0000000..858e7fa --- /dev/null +++ b/backend/static/_app/immutable/chunks/CKL1QNnB.js @@ -0,0 +1 @@ +import{aT as he,aU as pt,aQ as U,y as T,a2 as I,aR as ee,aV as gt}from"./DnPHkIdi.js";import{o as qe}from"./BOU_Z_Ye.js";const V=[];function Se(e,t=he){let n=null;const a=new Set;function r(s){if(pt(e,s)&&(e=s,n)){const c=!V.length;for(const l of a)l[1](),V.push(l,e);if(c){for(let l=0;l{a.delete(l),a.size===0&&n&&(n(),n=null)}}return{set:r,update:i,subscribe:o}}class Ee{constructor(t,n){this.status=t,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${t}`}}toString(){return JSON.stringify(this.body)}}class Re{constructor(t,n){this.status=t,this.location=n}}class xe extends Error{constructor(t,n,a){super(a),this.status=t,this.text=n}}new URL("sveltekit-internal://");function _t(e,t){return e==="/"||t==="ignore"?e:t==="never"?e.endsWith("/")?e.slice(0,-1):e:t==="always"&&!e.endsWith("/")?e+"/":e}function mt(e){return e.split("%25").map(decodeURI).join("%25")}function wt(e){for(const t in e)e[t]=decodeURIComponent(e[t]);return e}function pe({href:e}){return e.split("#")[0]}function vt(...e){let t=5381;for(const n of e)if(typeof n=="string"){let a=n.length;for(;a;)t=t*33^n.charCodeAt(--a)}else if(ArrayBuffer.isView(n)){const a=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let r=a.length;for(;r;)t=t*33^a[--r]}else throw new TypeError("value must be a string or TypedArray");return(t>>>0).toString(36)}new TextEncoder;new TextDecoder;function yt(e){const t=atob(e),n=new Uint8Array(t.length);for(let a=0;a((e instanceof Request?e.method:t?.method||"GET")!=="GET"&&F.delete(Ae(e)),bt(e,t));const F=new Map;function kt(e,t){const n=Ae(e,t),a=document.querySelector(n);if(a?.textContent){a.remove();let{body:r,...i}=JSON.parse(a.textContent);const o=a.getAttribute("data-ttl");return o&&F.set(n,{body:r,init:i,ttl:1e3*Number(o)}),a.getAttribute("data-b64")!==null&&(r=yt(r)),Promise.resolve(new Response(r,i))}return window.fetch(e,t)}function St(e,t,n){if(F.size>0){const a=Ae(e,n),r=F.get(a);if(r){if(performance.now(){const r=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(a);if(r)return t.push({name:r[1],matcher:r[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const i=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(a);if(i)return t.push({name:i[1],matcher:i[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!a)return;const o=a.split(/\[(.+?)\](?!\])/);return"/"+o.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return ge(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return ge(String.fromCharCode(...c.slice(2).split("-").map(m=>parseInt(m,16))));const f=Et.exec(c),[,h,w,u,g]=f;return t.push({name:u,matcher:g,optional:!!h,rest:!!w,chained:w?l===1&&o[0]==="":!1}),w?"([^]*?)":h?"([^/]*)?":"([^/]+?)"}return ge(c)}).join("")}).join("")}/?$`),params:t}}function xt(e){return e!==""&&!/^\([^)]+\)$/.test(e)}function At(e){return e.slice(1).split("/").filter(xt)}function Lt(e,t,n){const a={},r=e.slice(1),i=r.filter(s=>s!==void 0);let o=0;for(let s=0;sf).join("/"),o=0),l===void 0){c.rest&&(a[c.name]="");continue}if(!c.matcher||n[c.matcher](l)){a[c.name]=l;const f=t[s+1],h=r[s+1];f&&!f.rest&&f.optional&&h&&c.chained&&(o=0),!f&&!h&&Object.keys(a).length===i.length&&(o=0);continue}if(c.optional&&c.chained){o++;continue}return}if(!o)return a}function ge(e){return e.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function Ut({nodes:e,server_loads:t,dictionary:n,matchers:a}){const r=new Set(t);return Object.entries(n).map(([s,[c,l,f]])=>{const{pattern:h,params:w}=Rt(s),u={id:s,exec:g=>{const m=h.exec(g);if(m)return Lt(m,w,a)},errors:[1,...f||[]].map(g=>e[g]),layouts:[0,...l||[]].map(o),leaf:i(c)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function i(s){const c=s<0;return c&&(s=~s),[c,e[s]]}function o(s){return s===void 0?s:[r.has(s),e[s]]}}function Ge(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function De(e,t,n=JSON.stringify){const a=n(t);try{sessionStorage[e]=a}catch{}}const x=globalThis.__sveltekit_1nk8k67?.base??"",Tt=globalThis.__sveltekit_1nk8k67?.assets??x??"",It="1768960079047",We="sveltekit:snapshot",Ye="sveltekit:scroll",He="sveltekit:states",Ot="sveltekit:pageurl",K="sveltekit:history",W="sveltekit:navigation",j={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},Le=location.origin;function Je(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){const n=document.getElementsByTagName("base");t=n.length?n[0].href:document.URL}return new URL(e,t)}function le(){return{x:pageXOffset,y:pageYOffset}}function B(e,t){return e.getAttribute(`data-sveltekit-${t}`)}const Ve={...j,"":j.hover};function Xe(e){let t=e.assignedSlot??e.parentNode;return t?.nodeType===11&&(t=t.host),t}function Qe(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=Xe(e)}}function we(e,t,n){let a;try{if(a=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI),n&&a.hash.match(/^#[^/]/)){const s=location.hash.split("#")[1]||"/";a.hash=`#${s}${a.hash}`}}catch{}const r=e instanceof SVGAElement?e.target.baseVal:e.target,i=!a||!!r||ue(a,t,n)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),o=a?.origin===Le&&e.hasAttribute("download");return{url:a,external:i,target:r,download:o}}function te(e){let t=null,n=null,a=null,r=null,i=null,o=null,s=e;for(;s&&s!==document.documentElement;)a===null&&(a=B(s,"preload-code")),r===null&&(r=B(s,"preload-data")),t===null&&(t=B(s,"keepfocus")),n===null&&(n=B(s,"noscroll")),i===null&&(i=B(s,"reload")),o===null&&(o=B(s,"replacestate")),s=Xe(s);function c(l){switch(l){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:Ve[a??"off"],preload_data:Ve[r??"off"],keepfocus:c(t),noscroll:c(n),reload:c(i),replace_state:c(o)}}function Be(e){const t=Se(e);let n=!0;function a(){n=!0,t.update(o=>o)}function r(o){n=!1,t.set(o)}function i(o){let s;return t.subscribe(c=>{(s===void 0||n&&c!==s)&&o(s=c)})}return{notify:a,set:r,subscribe:i}}const Ze={v:()=>{}};function Pt(){const{set:e,subscribe:t}=Se(!1);let n;async function a(){clearTimeout(n);try{const r=await fetch(`${Tt}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!r.ok)return!1;const o=(await r.json()).version!==It;return o&&(e(!0),Ze.v(),clearTimeout(n)),o}catch{return!1}}return{subscribe:t,check:a}}function ue(e,t,n){return e.origin!==Le||!e.pathname.startsWith(t)?!0:n?e.pathname!==location.pathname:!1}function cn(e){}const et=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...et];const $t=new Set([...et]);[...$t];function Ct(e){return e.filter(t=>t!=null)}function Ue(e){return e instanceof Ee||e instanceof xe?e.status:500}function jt(e){return e instanceof xe?e.text:"Internal Error"}let k,Y,_e;const Nt=qe.toString().includes("$$")||/function \w+\(\) \{\}/.test(qe.toString());Nt?(k={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},Y={current:null},_e={current:!1}):(k=new class{#e=U({});get data(){return T(this.#e)}set data(t){I(this.#e,t)}#t=U(null);get form(){return T(this.#t)}set form(t){I(this.#t,t)}#n=U(null);get error(){return T(this.#n)}set error(t){I(this.#n,t)}#a=U({});get params(){return T(this.#a)}set params(t){I(this.#a,t)}#r=U({id:null});get route(){return T(this.#r)}set route(t){I(this.#r,t)}#o=U({});get state(){return T(this.#o)}set state(t){I(this.#o,t)}#s=U(-1);get status(){return T(this.#s)}set status(t){I(this.#s,t)}#i=U(new URL("https://example.com"));get url(){return T(this.#i)}set url(t){I(this.#i,t)}},Y=new class{#e=U(null);get current(){return T(this.#e)}set current(t){I(this.#e,t)}},_e=new class{#e=U(!1);get current(){return T(this.#e)}set current(t){I(this.#e,t)}},Ze.v=()=>_e.current=!0);function qt(e){Object.assign(k,e)}const Dt=new Set(["icon","shortcut icon","apple-touch-icon"]),q=Ge(Ye)??{},H=Ge(We)??{},C={url:Be({}),page:Be({}),navigating:Se(null),updated:Pt()};function Te(e){q[e]=le()}function Vt(e,t){let n=e+1;for(;q[n];)delete q[n],n+=1;for(n=t+1;H[n];)delete H[n],n+=1}function J(e,t=!1){return t?location.replace(e.href):location.href=e.href,new Promise(()=>{})}async function tt(){if("serviceWorker"in navigator){const e=await navigator.serviceWorker.getRegistration(x||"/");e&&await e.update()}}function Ke(){}let Ie,ve,ne,O,ye,v;const ae=[],re=[];let A=null;function be(){A?.fork?.then(e=>e?.discard()),A=null}const Z=new Map,nt=new Set,Bt=new Set,G=new Set;let _={branch:[],error:null,url:null},at=!1,oe=!1,Me=!0,X=!1,z=!1,rt=!1,Oe=!1,ot,y,R,N;const se=new Set,ze=new Map;async function dn(e,t,n){globalThis.__sveltekit_1nk8k67?.data&&globalThis.__sveltekit_1nk8k67.data,document.URL!==location.href&&(location.href=location.href),v=e,await e.hooks.init?.(),Ie=Ut(e),O=document.documentElement,ye=t,ve=e.nodes[0],ne=e.nodes[1],ve(),ne(),y=history.state?.[K],R=history.state?.[W],y||(y=R=Date.now(),history.replaceState({...history.state,[K]:y,[W]:R},""));const a=q[y];function r(){a&&(history.scrollRestoration="manual",scrollTo(a.x,a.y))}n?(r(),await tn(ye,n)):(await M({type:"enter",url:Je(v.hash?rn(new URL(location.href)):location.href),replace_state:!0}),r()),en()}function Kt(){ae.length=0,Oe=!1}function st(e){re.some(t=>t?.snapshot)&&(H[e]=re.map(t=>t?.snapshot?.capture()))}function it(e){H[e]?.forEach((t,n)=>{re[n]?.snapshot?.restore(t)})}function Fe(){Te(y),De(Ye,q),st(R),De(We,H)}async function Mt(e,t,n,a){let r;t.invalidateAll&&be(),await M({type:"goto",url:Je(e),keepfocus:t.keepFocus,noscroll:t.noScroll,replace_state:t.replaceState,state:t.state,redirect_count:n,nav_token:a,accept:()=>{t.invalidateAll&&(Oe=!0,r=[...ze.keys()]),t.invalidate&&t.invalidate.forEach(Zt)}}),t.invalidateAll&&ee().then(ee).then(()=>{ze.forEach(({resource:i},o)=>{r?.includes(o)&&i.refresh?.()})})}async function zt(e){if(e.id!==A?.id){be();const t={};se.add(t),A={id:e.id,token:t,promise:lt({...e,preload:t}).then(n=>(se.delete(t),n.type==="loaded"&&n.state.error&&be(),n)),fork:null}}return A.promise}async function me(e){const t=(await fe(e,!1))?.route;t&&await Promise.all([...t.layouts,t.leaf].map(n=>n?.[1]()))}async function ct(e,t,n){_=e.state;const a=document.querySelector("style[data-sveltekit]");if(a&&a.remove(),Object.assign(k,e.props.page),ot=new v.root({target:t,props:{...e.props,stores:C,components:re},hydrate:n,sync:!1}),await Promise.resolve(),it(R),n){const r={from:null,to:{params:_.params,route:{id:_.route?.id??null},url:new URL(location.href)},willUnload:!1,type:"enter",complete:Promise.resolve()};G.forEach(i=>i(r))}oe=!0}function ie({url:e,params:t,branch:n,status:a,error:r,route:i,form:o}){let s="never";if(x&&(e.pathname===x||e.pathname===x+"/"))s="always";else for(const u of n)u?.slash!==void 0&&(s=u.slash);e.pathname=_t(e.pathname,s),e.search=e.search;const c={type:"loaded",state:{url:e,params:t,branch:n,error:r,route:i},props:{constructors:Ct(n).map(u=>u.node.component),page:Ne(k)}};o!==void 0&&(c.props.form=o);let l={},f=!k,h=0;for(let u=0;us(new URL(o))))return!0;return!1}function $e(e,t){return e?.type==="data"?e:e?.type==="skip"?t??null:null}function Wt(e,t){if(!e)return new Set(t.searchParams.keys());const n=new Set([...e.searchParams.keys(),...t.searchParams.keys()]);for(const a of n){const r=e.searchParams.getAll(a),i=t.searchParams.getAll(a);r.every(o=>i.includes(o))&&i.every(o=>r.includes(o))&&n.delete(a)}return n}function Yt({error:e,url:t,route:n,params:a}){return{type:"loaded",state:{error:e,url:t,route:n,params:a,branch:[]},props:{page:Ne(k),constructors:[]}}}async function lt({id:e,invalidating:t,url:n,params:a,route:r,preload:i}){if(A?.id===e)return se.delete(A.token),A.promise;const{errors:o,layouts:s,leaf:c}=r,l=[...s,c];o.forEach(p=>p?.().catch(()=>{})),l.forEach(p=>p?.[1]().catch(()=>{}));const f=_.url?e!==ce(_.url):!1,h=_.route?r.id!==_.route.id:!1,w=Wt(_.url,n);let u=!1;const g=l.map(async(p,d)=>{if(!p)return;const S=_.branch[d];return p[1]===S?.loader&&!Gt(u,h,f,w,S.universal?.uses,a)?S:(u=!0,Pe({loader:p[1],url:n,params:a,route:r,parent:async()=>{const P={};for(let L=0;L{});const m=[];for(let p=0;pPromise.resolve({}),server_data_node:$e(i)}),s={node:await ne(),loader:ne,universal:null,server:null,data:null};return ie({url:n,params:r,branch:[o,s],status:e,error:t,route:null})}catch(o){if(o instanceof Re)return Mt(new URL(o.location,location.href),{},0);throw o}}async function Jt(e){const t=e.href;if(Z.has(t))return Z.get(t);let n;try{const a=(async()=>{let r=await v.hooks.reroute({url:new URL(e),fetch:async(i,o)=>Ft(i,o,e).promise})??e;if(typeof r=="string"){const i=new URL(e);v.hash?i.hash=r:i.pathname=r,r=i}return r})();Z.set(t,a),n=await a}catch{Z.delete(t);return}return n}async function fe(e,t){if(e&&!ue(e,x,v.hash)){const n=await Jt(e);if(!n)return;const a=Xt(n);for(const r of Ie){const i=r.exec(a);if(i)return{id:ce(e),invalidating:t,route:r,params:wt(i),url:e}}}}function Xt(e){return mt(v.hash?e.hash.replace(/^#/,"").replace(/[?#].+/,""):e.pathname.slice(x.length))||"/"}function ce(e){return(v.hash?e.hash.replace(/^#/,""):e.pathname)+e.search}function ut({url:e,type:t,intent:n,delta:a,event:r}){let i=!1;const o=je(_,n,e,t);a!==void 0&&(o.navigation.delta=a),r!==void 0&&(o.navigation.event=r);const s={...o.navigation,cancel:()=>{i=!0,o.reject(new Error("navigation cancelled"))}};return X||nt.forEach(c=>c(s)),i?null:o}async function M({type:e,url:t,popped:n,keepfocus:a,noscroll:r,replace_state:i,state:o={},redirect_count:s=0,nav_token:c={},accept:l=Ke,block:f=Ke,event:h}){const w=N;N=c;const u=await fe(t,!1),g=e==="enter"?je(_,u,t,e):ut({url:t,type:e,delta:n?.delta,intent:u,event:h});if(!g){f(),N===c&&(N=w);return}const m=y,p=R;l(),X=!0,oe&&g.navigation.type!=="enter"&&C.navigating.set(Y.current=g.navigation);let d=u&&await lt(u);if(!d){if(ue(t,x,v.hash))return await J(t,i);d=await ft(t,{id:null},await Q(new xe(404,"Not Found",`Not found: ${t.pathname}`),{url:t,params:{},route:{id:null}}),404,i)}if(t=u?.url||t,N!==c)return g.reject(new Error("navigation aborted")),!1;if(d.type==="redirect"){if(s<20){await M({type:e,url:new URL(d.location,t),popped:n,keepfocus:a,noscroll:r,replace_state:i,state:o,redirect_count:s+1,nav_token:c}),g.fulfil(void 0);return}d=await Ce({status:500,error:await Q(new Error("Redirect loop"),{url:t,params:{},route:{id:null}}),url:t,route:{id:null}})}else d.props.page.status>=400&&await C.updated.check()&&(await tt(),await J(t,i));if(Kt(),Te(m),st(p),d.props.page.url.pathname!==t.pathname&&(t.pathname=d.props.page.url.pathname),o=n?n.state:o,!n){const b=i?0:1,D={[K]:y+=b,[W]:R+=b,[He]:o};(i?history.replaceState:history.pushState).call(history,D,"",t),i||Vt(y,R)}const S=u&&A?.id===u.id?A.fork:null;A=null,d.props.page.state=o;let E;if(oe){const b=(await Promise.all(Array.from(Bt,$=>$(g.navigation)))).filter($=>typeof $=="function");if(b.length>0){let $=function(){b.forEach(de=>{G.delete(de)})};b.push($),b.forEach(de=>{G.add(de)})}_=d.state,d.props.page&&(d.props.page.url=t);const D=S&&await S;D?E=D.commit():(ot.$set(d.props),qt(d.props.page),E=gt?.()),rt=!0}else await ct(d,ye,!1);const{activeElement:P}=document;await E,await ee(),await ee();let L=n?n.scroll:r?le():null;if(Me){const b=t.hash&&document.getElementById(dt(t));if(L)scrollTo(L.x,L.y);else if(b){b.scrollIntoView();const{top:D,left:$}=b.getBoundingClientRect();L={x:pageXOffset+$,y:pageYOffset+D}}else scrollTo(0,0)}const ht=document.activeElement!==P&&document.activeElement!==document.body;!a&&!ht&&an(t,L),Me=!0,d.props.page&&Object.assign(k,d.props.page),X=!1,e==="popstate"&&it(R),g.fulfil(void 0),G.forEach(b=>b(g.navigation)),C.navigating.set(Y.current=null)}async function ft(e,t,n,a,r){return e.origin===Le&&e.pathname===location.pathname&&!at?await Ce({status:a,error:n,url:e,route:t}):await J(e,r)}function Qt(){let e,t,n;O.addEventListener("mousemove",s=>{const c=s.target;clearTimeout(e),e=setTimeout(()=>{i(c,j.hover)},20)});function a(s){s.defaultPrevented||i(s.composedPath()[0],j.tap)}O.addEventListener("mousedown",a),O.addEventListener("touchstart",a,{passive:!0});const r=new IntersectionObserver(s=>{for(const c of s)c.isIntersecting&&(me(new URL(c.target.href)),r.unobserve(c.target))},{threshold:0});async function i(s,c){const l=Qe(s,O),f=l===t&&c>=n;if(!l||f)return;const{url:h,external:w,download:u}=we(l,x,v.hash);if(w||u)return;const g=te(l),m=h&&ce(_.url)===ce(h);if(!(g.reload||m))if(c<=g.preload_data){t=l,n=j.tap;const p=await fe(h,!1);if(!p)return;zt(p)}else c<=g.preload_code&&(t=l,n=c,me(h))}function o(){r.disconnect();for(const s of O.querySelectorAll("a")){const{url:c,external:l,download:f}=we(s,x,v.hash);if(l||f)continue;const h=te(s);h.reload||(h.preload_code===j.viewport&&r.observe(s),h.preload_code===j.eager&&me(c))}}G.add(o),o()}function Q(e,t){if(e instanceof Ee)return e.body;const n=Ue(e),a=jt(e);return v.hooks.handleError({error:e,event:t,status:n,message:a})??{message:a}}function Zt(e){if(typeof e=="function")ae.push(e);else{const{href:t}=new URL(e,location.href);ae.push(n=>n.href===t)}}function en(){history.scrollRestoration="manual",addEventListener("beforeunload",t=>{let n=!1;if(Fe(),!X){const a=je(_,void 0,null,"leave"),r={...a.navigation,cancel:()=>{n=!0,a.reject(new Error("navigation cancelled"))}};nt.forEach(i=>i(r))}n?(t.preventDefault(),t.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&Fe()}),navigator.connection?.saveData||Qt(),O.addEventListener("click",async t=>{if(t.button||t.which!==1||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||t.defaultPrevented)return;const n=Qe(t.composedPath()[0],O);if(!n)return;const{url:a,external:r,target:i,download:o}=we(n,x,v.hash);if(!a)return;if(i==="_parent"||i==="_top"){if(window.parent!==window)return}else if(i&&i!=="_self")return;const s=te(n);if(!(n instanceof SVGAElement)&&a.protocol!==location.protocol&&!(a.protocol==="https:"||a.protocol==="http:")||o)return;const[l,f]=(v.hash?a.hash.replace(/^#/,""):a.href).split("#"),h=l===pe(location);if(r||s.reload&&(!h||!f)){ut({url:a,type:"link",event:t})?X=!0:t.preventDefault();return}if(f!==void 0&&h){const[,w]=_.url.href.split("#");if(w===f){if(t.preventDefault(),f===""||f==="top"&&n.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const u=n.ownerDocument.getElementById(decodeURIComponent(f));u&&(u.scrollIntoView(),u.focus())}return}if(z=!0,Te(y),e(a),!s.replace_state)return;z=!1}t.preventDefault(),await new Promise(w=>{requestAnimationFrame(()=>{setTimeout(w,0)}),setTimeout(w,100)}),await M({type:"link",url:a,keepfocus:s.keepfocus,noscroll:s.noscroll,replace_state:s.replace_state??a.href===location.href,event:t})}),O.addEventListener("submit",t=>{if(t.defaultPrevented)return;const n=HTMLFormElement.prototype.cloneNode.call(t.target),a=t.submitter;if((a?.formTarget||n.target)==="_blank"||(a?.formMethod||n.method)!=="get")return;const o=new URL(a?.hasAttribute("formaction")&&a?.formAction||n.action);if(ue(o,x,!1))return;const s=t.target,c=te(s);if(c.reload)return;t.preventDefault(),t.stopPropagation();const l=new FormData(s,a);o.search=new URLSearchParams(l).toString(),M({type:"form",url:o,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??o.href===location.href,event:t})}),addEventListener("popstate",async t=>{if(!ke){if(t.state?.[K]){const n=t.state[K];if(N={},n===y)return;const a=q[n],r=t.state[He]??{},i=new URL(t.state[Ot]??location.href),o=t.state[W],s=_.url?pe(location)===pe(_.url):!1;if(o===R&&(rt||s)){r!==k.state&&(k.state=r),e(i),q[y]=le(),a&&scrollTo(a.x,a.y),y=n;return}const l=n-y;await M({type:"popstate",url:i,popped:{state:r,scroll:a,delta:l},accept:()=>{y=n,R=o},block:()=>{history.go(-l)},nav_token:N,event:t})}else if(!z){const n=new URL(location.href);e(n),v.hash&&location.reload()}}}),addEventListener("hashchange",()=>{z&&(z=!1,history.replaceState({...history.state,[K]:++y,[W]:R},"",location.href))});for(const t of document.querySelectorAll("link"))Dt.has(t.rel)&&(t.href=t.href);addEventListener("pageshow",t=>{t.persisted&&C.navigating.set(Y.current=null)});function e(t){_.url=k.url=t,C.page.set(Ne(k)),C.page.notify()}}async function tn(e,{status:t=200,error:n,node_ids:a,params:r,route:i,server_route:o,data:s,form:c}){at=!0;const l=new URL(location.href);let f;({params:r={},route:i={id:null}}=await fe(l,!1)||{}),f=Ie.find(({id:u})=>u===i.id);let h,w=!0;try{const u=a.map(async(m,p)=>{const d=s[p];return d?.uses&&(d.uses=nn(d.uses)),Pe({loader:v.nodes[m],url:l,params:r,route:i,parent:async()=>{const S={};for(let E=0;E{const s=history.state;ke=!0,location.replace(`#${a}`),v.hash&&location.replace(e.hash),history.replaceState(s,"",e.hash),scrollTo(i,o),ke=!1})}else{const i=document.body,o=i.getAttribute("tabindex");i.tabIndex=-1,i.focus({preventScroll:!0,focusVisible:!1}),o!==null?i.setAttribute("tabindex",o):i.removeAttribute("tabindex")}const r=getSelection();if(r&&r.type!=="None"){const i=[];for(let o=0;o{if(r.rangeCount===i.length){for(let o=0;o{r=c,i=l});return o.catch(()=>{}),{navigation:{from:{params:e.params,route:{id:e.route?.id??null},url:e.url},to:n&&{params:t?.params??null,route:{id:t?.route?.id??null},url:n},willUnload:!t,type:a,complete:o},fulfil:r,reject:i}}function Ne(e){return{data:e.data,error:e.error,form:e.form,params:e.params,route:e.route,state:e.state,status:e.status,url:e.url}}function rn(e){const t=new URL(e);return t.hash=decodeURIComponent(e.hash),t}function dt(e){let t;if(v.hash){const[,,n]=e.hash.split("#",3);t=n??""}else t=e.hash.slice(1);return decodeURIComponent(t)}export{dn as a,cn as l,k as p,C as s}; diff --git a/backend/static/_app/immutable/chunks/DUCk1qN8.js b/backend/static/_app/immutable/chunks/DUCk1qN8.js new file mode 100644 index 0000000..58ffcd1 --- /dev/null +++ b/backend/static/_app/immutable/chunks/DUCk1qN8.js @@ -0,0 +1 @@ +import{k as d,O as f,a3 as v,a4 as u,a5 as E,a6 as p,h as i,m as o,a7 as h,a as T,a8 as N,c as g}from"./DnPHkIdi.js";function y(r){var n=document.createElement("template");return n.innerHTML=r.replaceAll("",""),n.content}function a(r,n){var e=u;e.nodes===null&&(e.nodes={start:r,end:n,a:null,t:null})}function A(r,n){var e=(n&E)!==0,l=(n&p)!==0,t,_=!r.startsWith("");return()=>{if(i)return a(o,null),o;t===void 0&&(t=y(_?r:""+r),e||(t=f(t)));var s=l||v?document.importNode(t,!0):t.cloneNode(!0);if(e){var c=f(s),m=s.lastChild;a(c,m)}else a(s,s);return s}}function M(r=""){if(!i){var n=d(r+"");return a(n,n),n}var e=o;return e.nodeType!==N&&(e.before(e=d()),g(e)),a(e,e),e}function O(){if(i)return a(o,null),o;var r=document.createDocumentFragment(),n=document.createComment(""),e=d();return r.append(n,e),a(n,e),r}function C(r,n){if(i){var e=u;((e.f&h)===0||e.nodes.end===null)&&(e.nodes.end=o),T();return}r!==null&&r.before(n)}const w="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(w);export{C as a,a as b,O as c,A as f,M as t}; diff --git a/backend/static/_app/immutable/chunks/DZQwV0xP.js b/backend/static/_app/immutable/chunks/DZQwV0xP.js new file mode 100644 index 0000000..3533c19 --- /dev/null +++ b/backend/static/_app/immutable/chunks/DZQwV0xP.js @@ -0,0 +1,2 @@ +import{aa as U,y as P,V as j,ab as G,v as J,ac as B,ad as W,m as d,h as c,a4 as b,b as K,a as X,Q as x,H as Z,l as _,ae as T,p as k,k as $,af as ee,ag as S,ah as y,ai as C,aj as te,ak as O,q as z,n as se,al as M,am as ie,an as V,ao as re,S as ne,j as A,c as D,ap as ae,s as he,aq as q,ar as oe,E as fe,as as le,at as ue,au as _e,av as de,aw as F,O as ce,ax as pe,a0 as ge,ay as I,d as w,az as ve,a1 as ye,T as me,aA as be,D as Ee,R as Te,aB as we,I as Re}from"./DnPHkIdi.js";import{b as Se}from"./DUCk1qN8.js";function De(t){let e=0,i=j(0),r;return()=>{U()&&(P(i),G(()=>(e===0&&(r=J(()=>t(()=>B(i)))),e+=1,()=>{W(()=>{e-=1,e===0&&(r?.(),r=void 0,B(i))})})))}}var Ne=fe|le|ue;function ke(t,e,i){new Ae(t,e,i)}class Ae{parent;is_pending=!1;#t;#v=c?d:null;#i;#l;#r;#s=null;#e=null;#n=null;#a=null;#o=null;#u=0;#h=0;#_=!1;#d=new Set;#c=new Set;#f=null;#b=De(()=>(this.#f=j(this.#u),()=>{this.#f=null}));constructor(e,i,r){this.#t=e,this.#i=i,this.#l=r,this.parent=b.b,this.is_pending=!!this.#i.pending,this.#r=K(()=>{if(b.b=this,c){const s=this.#v;X(),s.nodeType===x&&s.data===Z?this.#T():(this.#E(),this.#h===0&&(this.is_pending=!1))}else{var a=this.#y();try{this.#s=_(()=>r(a))}catch(s){this.error(s)}this.#h>0?this.#g():this.is_pending=!1}return()=>{this.#o?.remove()}},Ne),c&&(this.#t=d)}#E(){try{this.#s=_(()=>this.#l(this.#t))}catch(e){this.error(e)}}#T(){const e=this.#i.pending;e&&(this.#e=_(()=>e(this.#t)),T.enqueue(()=>{var i=this.#y();this.#s=this.#p(()=>(T.ensure(),_(()=>this.#l(i)))),this.#h>0?this.#g():(k(this.#e,()=>{this.#e=null}),this.is_pending=!1)}))}#y(){var e=this.#t;return this.is_pending&&(this.#o=$(),this.#t.before(this.#o),e=this.#o),e}defer_effect(e){ee(e,this.#d,this.#c)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#i.pending}#p(e){var i=b,r=O,a=z;S(this.#r),y(this.#r),C(this.#r.ctx);try{return e()}catch(s){return te(s),null}finally{S(i),y(r),C(a)}}#g(){const e=this.#i.pending;this.#s!==null&&(this.#a=document.createDocumentFragment(),this.#a.append(this.#o),se(this.#s,this.#a)),this.#e===null&&(this.#e=_(()=>e(this.#t)))}#m(e){if(!this.has_pending_snippet()){this.parent&&this.parent.#m(e);return}if(this.#h+=e,this.#h===0){this.is_pending=!1;for(const i of this.#d)M(i,ie),V(i);for(const i of this.#c)M(i,re),V(i);this.#d.clear(),this.#c.clear(),this.#e&&k(this.#e,()=>{this.#e=null}),this.#a&&(this.#t.before(this.#a),this.#a=null)}}update_pending_count(e){this.#m(e),this.#u+=e,this.#f&&ne(this.#f,this.#u)}get_effect_pending(){return this.#b(),P(this.#f)}error(e){var i=this.#i.onerror;let r=this.#i.failed;if(this.#_||!i&&!r)throw e;this.#s&&(A(this.#s),this.#s=null),this.#e&&(A(this.#e),this.#e=null),this.#n&&(A(this.#n),this.#n=null),c&&(D(this.#v),ae(),D(he()));var a=!1,s=!1;const o=()=>{if(a){_e();return}a=!0,s&&oe(),T.ensure(),this.#u=0,this.#n!==null&&k(this.#n,()=>{this.#n=null}),this.is_pending=this.has_pending_snippet(),this.#s=this.#p(()=>(this.#_=!1,_(()=>this.#l(this.#t)))),this.#h>0?this.#g():this.is_pending=!1};var l=O;try{y(null),s=!0,i?.(e,o),s=!1}catch(f){q(f,this.#r&&this.#r.parent)}finally{y(l)}r&&W(()=>{this.#n=this.#p(()=>{T.ensure(),this.#_=!0;try{return _(()=>{r(this.#t,()=>e,()=>o)})}catch(f){return q(f,this.#r.parent),null}finally{this.#_=!1}})})}}const Oe=["touchstart","touchmove"];function Fe(t){return Oe.includes(t)}const Ie=new Set,H=new Set;let L=null;function R(t){var e=this,i=e.ownerDocument,r=t.type,a=t.composedPath?.()||[],s=a[0]||t.target;L=t;var o=0,l=L===t&&t.__root;if(l){var f=a.indexOf(l);if(f!==-1&&(e===document||e===window)){t.__root=e;return}var p=a.indexOf(e);if(p===-1)return;f<=p&&(o=f)}if(s=a[o]||t.target,s!==e){de(t,"currentTarget",{configurable:!0,get(){return s||i}});var N=O,u=b;y(null),S(null);try{for(var n,h=[];s!==null;){var g=s.assignedSlot||s.parentNode||s.host||null;try{var m=s["__"+r];m!=null&&(!s.disabled||t.target===s)&&m.call(s,t)}catch(E){n?h.push(E):n=E}if(t.cancelBubble||g===e||g===null)break;s=g}if(n){for(let E of h)queueMicrotask(()=>{throw E});throw n}}finally{t.__root=e,delete t.currentTarget,y(N),S(u)}}}function Ce(t,e){var i=e==null?"":typeof e=="object"?e+"":e;i!==(t.__t??=t.nodeValue)&&(t.__t=i,t.nodeValue=i+"")}function Ye(t,e){return Q(t,e)}function Me(t,e){F(),e.intro=e.intro??!1;const i=e.target,r=c,a=d;try{for(var s=ce(i);s&&(s.nodeType!==x||s.data!==pe);)s=ge(s);if(!s)throw I;w(!0),D(s);const o=Q(t,{...e,anchor:s});return w(!1),o}catch(o){if(o instanceof Error&&o.message.split(` +`).some(l=>l.startsWith("https://svelte.dev/e/")))throw o;return o!==I&&console.warn("Failed to hydrate: ",o),e.recover===!1&&ve(),F(),ye(i),w(!1),Ye(t,e)}finally{w(r),D(a)}}const v=new Map;function Q(t,{target:e,anchor:i,props:r={},events:a,context:s,intro:o=!0}){F();var l=new Set,f=u=>{for(var n=0;n{var u=i??e.appendChild($());return ke(u,{pending:()=>{}},n=>{if(s){Ee({});var h=z;h.c=s}if(a&&(r.$$events=a),c&&Se(n,null),p=t(n,r)||{},c&&(b.nodes.end=d,d===null||d.nodeType!==x||d.data!==Te))throw we(),I;s&&Re()}),()=>{for(var n of l){e.removeEventListener(n,R);var h=v.get(n);--h===0?(document.removeEventListener(n,R),v.delete(n)):v.set(n,h)}H.delete(f),u!==i&&u.parentNode?.removeChild(u)}});return Y.set(p,N),p}let Y=new WeakMap;function Ve(t,e){const i=Y.get(t);return i?(Y.delete(t),i(e)):Promise.resolve()}export{Me as h,Ye as m,Ce as s,Ve as u}; diff --git a/backend/static/_app/immutable/chunks/DnPHkIdi.js b/backend/static/_app/immutable/chunks/DnPHkIdi.js new file mode 100644 index 0000000..6916856 --- /dev/null +++ b/backend/static/_app/immutable/chunks/DnPHkIdi.js @@ -0,0 +1 @@ +var Ft=Array.isArray,Mt=Array.prototype.indexOf,bn=Array.from,Tn=Object.defineProperty,ae=Object.getOwnPropertyDescriptor,jt=Object.getOwnPropertyDescriptors,Lt=Object.prototype,qt=Array.prototype,We=Object.getPrototypeOf,ze=Object.isExtensible;const An=()=>{};function Rn(e){return e()}function Yt(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}const w=2,Ee=4,_e=8,tt=1<<24,M=16,j=32,te=64,nt=128,k=512,E=1024,R=2048,B=4096,P=8192,q=16384,je=32768,ge=65536,$e=1<<17,rt=1<<18,ve=1<<19,st=1<<20,Sn=1<<25,X=32768,Ne=1<<21,Le=1<<22,Y=1<<23,le=Symbol("$state"),xn=Symbol("legacy props"),On=Symbol(""),ne=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},qe=3,ft=8;function Ut(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Ht(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Bt(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Vt(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Gt(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Dn(){throw new Error("https://svelte.dev/e/hydration_failed")}function Nn(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Kt(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function zt(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function $t(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Pn(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const In=1,Cn=2,Fn=16,Mn=1,jn=2,Ln=4,qn=8,Yn=16,Un=1,Hn=2,Xt="[",Zt="[!",Jt="]",Ye={},y=Symbol(),Bn="http://www.w3.org/1999/xhtml";function Ue(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Vn(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let Z=!1;function Gn(e){Z=e}let A;function re(e){if(e===null)throw Ue(),Ye;return A=e}function Kn(){return re(V(A))}function zn(e){if(Z){if(V(A)!==null)throw Ue(),Ye;A=e}}function $n(e=1){if(Z){for(var t=e,n=A;t--;)n=V(n);A=n}}function Xn(e=!0){for(var t=0,n=A;;){if(n.nodeType===ft){var r=n.data;if(r===Jt){if(t===0)return n;t-=1}else(r===Xt||r===Zt)&&(t+=1)}var s=V(n);e&&n.remove(),n=s}}function Zn(e){if(!e||e.nodeType!==ft)throw Ue(),Ye;return e.data}function it(e){return e===this.v}function Qt(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function at(e){return!Qt(e,this.v)}let xe=!1;function Jn(){xe=!0}let b=null;function me(e){b=e}function Qn(e,t=!1,n){b={p:b,i:!1,c:null,e:null,s:e,x:null,l:xe&&!t?{s:null,u:null,$:[]}:null}}function Wn(e){var t=b,n=t.e;if(n!==null){t.e=null;for(var r of n)bt(r)}return t.i=!0,b=t.p,{}}function de(){return!xe||b!==null&&b.l===null}let K=[];function lt(){var e=K;K=[],Yt(e)}function Wt(e){if(K.length===0&&!ue){var t=K;queueMicrotask(()=>{t===K&<()})}K.push(e)}function en(){for(;K.length>0;)lt()}function tn(e){var t=h;if(t===null)return _.f|=Y,e;if((t.f&je)===0){if((t.f&nt)===0)throw e;t.b.error(e)}else be(e,t)}function be(e,t){for(;t!==null;){if((t.f&nt)!==0)try{t.b.error(e);return}catch(n){e=n}t=t.parent}throw e}const nn=-7169;function g(e,t){e.f=e.f&nn|t}function He(e){(e.f&k)!==0||e.deps===null?g(e,E):g(e,B)}function ut(e){if(e!==null)for(const t of e)(t.f&w)===0||(t.f&X)===0||(t.f^=X,ut(t.deps))}function rn(e,t,n){(e.f&R)!==0?t.add(e):(e.f&B)!==0&&n.add(e),ut(e.deps),g(e,E)}const we=new Set;let p=null,D=null,O=[],Oe=null,Pe=!1,ue=!1;class J{committed=!1;current=new Map;previous=new Map;#n=new Set;#r=new Set;#e=0;#t=0;#i=null;#s=new Set;#f=new Set;skipped_effects=new Set;is_fork=!1;is_deferred(){return this.is_fork||this.#t>0}process(t){O=[],this.apply();var n=[],r=[];for(const s of t)this.#a(s,n,r);this.is_fork||this.#u(),this.is_deferred()?(this.#l(r),this.#l(n)):(p=null,Xe(r),Xe(n),this.#i?.resolve()),D=null}#a(t,n,r){t.f^=E;for(var s=t.first,f=null;s!==null;){var i=s.f,l=(i&(j|te))!==0,a=l&&(i&E)!==0,u=a||(i&P)!==0||this.skipped_effects.has(s);if(!u&&s.fn!==null){l?s.f^=E:f!==null&&(i&(Ee|_e|tt))!==0?f.b.defer_effect(s):(i&Ee)!==0?n.push(s):he(s)&&((i&M)!==0&&this.#s.add(s),ce(s));var o=s.first;if(o!==null){s=o;continue}}var c=s.parent;for(s=s.next;s===null&&c!==null;)c===f&&(f=null),s=c.next,c=c.parent}}#l(t){for(var n=0;n0){if(ot(),p!==null&&p!==this)return}else this.#e===0&&this.process([]);this.deactivate()}discard(){for(const t of this.#r)t(this);this.#r.clear()}#u(){if(this.#t===0){for(const t of this.#n)t();this.#n.clear()}this.#e===0&&this.#o()}#o(){if(we.size>1){this.previous.clear();var t=D,n=!0;for(const s of we){if(s===this){n=!1;continue}const f=[];for(const[l,a]of this.current){if(s.current.has(l))if(n&&a!==s.current.get(l))s.current.set(l,a);else continue;f.push(l)}if(f.length===0)continue;const i=[...s.current.keys()].filter(l=>!this.current.has(l));if(i.length>0){var r=O;O=[];const l=new Set,a=new Map;for(const u of f)ct(u,i,l,a);if(O.length>0){p=s,s.apply();for(const u of O)s.#a(u,[],[]);s.deactivate()}O=r}}p=null,D=t}this.committed=!0,we.delete(this)}increment(t){this.#e+=1,t&&(this.#t+=1)}decrement(t){this.#e-=1,t&&(this.#t-=1),this.revive()}revive(){for(const t of this.#s)this.#f.delete(t),g(t,R),Q(t);for(const t of this.#f)g(t,B),Q(t);this.flush()}oncommit(t){this.#n.add(t)}ondiscard(t){this.#r.add(t)}settled(){return(this.#i??=et()).promise}static ensure(){if(p===null){const t=p=new J;we.add(p),ue||J.enqueue(()=>{p===t&&t.flush()})}return p}static enqueue(t){Wt(t)}apply(){}}function sn(e){var t=ue;ue=!0;try{for(var n;;){if(en(),O.length===0&&(p?.flush(),O.length===0))return Oe=null,n;ot()}}finally{ue=t}}function ot(){var e=z;Pe=!0;var t=null;try{var n=0;for(Re(!0);O.length>0;){var r=J.ensure();if(n++>1e3){var s,f;fn()}r.process(O),U.clear()}}finally{Pe=!1,Re(e),Oe=null}}function fn(){try{Gt()}catch(e){be(e,Oe)}}let C=null;function Xe(e){var t=e.length;if(t!==0){for(var n=0;n0)){U.clear();for(const s of C){if((s.f&(q|P))!==0)continue;const f=[s];let i=s.parent;for(;i!==null;)C.has(i)&&(C.delete(i),f.push(i)),i=i.parent;for(let l=f.length-1;l>=0;l--){const a=f[l];(a.f&(q|P))===0&&ce(a)}}C.clear()}}C=null}}function ct(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const s of e.reactions){const f=s.f;(f&w)!==0?ct(s,t,n,r):(f&(Le|M))!==0&&(f&R)===0&&_t(s,t,r)&&(g(s,R),Q(s))}}function _t(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const s of e.deps){if(t.includes(s))return!0;if((s.f&w)!==0&&_t(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function Q(e){for(var t=Oe=e;t.parent!==null;){t=t.parent;var n=t.f;if(Pe&&t===h&&(n&M)!==0&&(n&rt)===0)return;if((n&(te|j))!==0){if((n&E)===0)return;t.f^=E}}O.push(t)}function an(e,t,n,r){const s=de()?Be:on;if(n.length===0&&e.length===0){r(t.map(s));return}var f=p,i=h,l=ln();function a(){Promise.all(n.map(u=>un(u))).then(u=>{l();try{r([...t.map(s),...u])}catch(o){(i.f&q)===0&&be(o,i)}f?.deactivate(),Te()}).catch(u=>{be(u,i)})}e.length>0?Promise.all(e).then(()=>{l();try{return a()}finally{f?.deactivate(),Te()}}):a()}function ln(){var e=h,t=_,n=b,r=p;return function(f=!0){se(e),H(t),me(n),f&&r?.activate()}}function Te(){se(null),H(null),me(null)}function Be(e){var t=w|R,n=_!==null&&(_.f&w)!==0?_:null;return h!==null&&(h.f|=ve),{ctx:b,deps:null,effects:null,equals:it,f:t,fn:e,reactions:null,rv:0,v:y,wv:0,parent:n??h,ac:null}}function un(e,t,n){let r=h;r===null&&Ut();var s=r.b,f=void 0,i=Ge(y),l=!_,a=new Map;return pn(()=>{var u=et();f=u.promise;try{Promise.resolve(e()).then(u.resolve,u.reject).then(()=>{o===p&&o.committed&&o.deactivate(),Te()})}catch(d){u.reject(d),Te()}var o=p;if(l){var c=s.is_rendered();s.update_pending_count(1),o.increment(c),a.get(o)?.reject(ne),a.delete(o),a.set(o,u)}const v=(d,S=void 0)=>{if(o.activate(),S)S!==ne&&(i.f|=Y,Ce(i,S));else{(i.f&Y)!==0&&(i.f^=Y),Ce(i,d);for(const[pe,ye]of a){if(a.delete(pe),pe===o)break;ye.reject(ne)}}l&&(s.update_pending_count(-1),o.decrement(c))};u.promise.then(v,d=>v(null,d||"unknown"))}),hn(()=>{for(const u of a.values())u.reject(ne)}),new Promise(u=>{function o(c){function v(){c===f?u(i):o(f)}c.then(v,v)}o(f)})}function er(e){const t=Be(e);return Ot(t),t}function on(e){const t=Be(e);return t.equals=at,t}function vt(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!ht&&_n()}return t}function _n(){ht=!1;var e=z;Re(!0);const t=Array.from(Ie);try{for(const n of t)(n.f&E)!==0&&g(n,B),he(n)&&ce(n)}finally{Re(e)}Ie.clear()}function De(e){G(e,e.v+1)}function pt(e,t){var n=e.reactions;if(n!==null)for(var r=de(),s=n.length,f=0;f{if($===f)return l();var a=_,u=$;H(null),Qe(f);var o=l();return H(a),Qe(u),o};return r&&n.set("length",L(e.length)),new Proxy(e,{defineProperty(l,a,u){(!("value"in u)||u.configurable===!1||u.enumerable===!1||u.writable===!1)&&Kt();var o=n.get(a);return o===void 0?o=i(()=>{var c=L(u.value);return n.set(a,c),c}):G(o,u.value,!0),!0},deleteProperty(l,a){var u=n.get(a);if(u===void 0){if(a in l){const o=i(()=>L(y));n.set(a,o),De(s)}}else G(u,y),De(s);return!0},get(l,a,u){if(a===le)return e;var o=n.get(a),c=a in l;if(o===void 0&&(!c||ae(l,a)?.writable)&&(o=i(()=>{var d=fe(c?l[a]:y),S=L(d);return S}),n.set(a,o)),o!==void 0){var v=ie(o);return v===y?void 0:v}return Reflect.get(l,a,u)},getOwnPropertyDescriptor(l,a){var u=Reflect.getOwnPropertyDescriptor(l,a);if(u&&"value"in u){var o=n.get(a);o&&(u.value=ie(o))}else if(u===void 0){var c=n.get(a),v=c?.v;if(c!==void 0&&v!==y)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return u},has(l,a){if(a===le)return!0;var u=n.get(a),o=u!==void 0&&u.v!==y||Reflect.has(l,a);if(u!==void 0||h!==null&&(!o||ae(l,a)?.writable)){u===void 0&&(u=i(()=>{var v=o?fe(l[a]):y,d=L(v);return d}),n.set(a,u));var c=ie(u);if(c===y)return!1}return o},set(l,a,u,o){var c=n.get(a),v=a in l;if(r&&a==="length")for(var d=u;dL(y)),n.set(d+"",S))}if(c===void 0)(!v||ae(l,a)?.writable)&&(c=i(()=>L(void 0)),G(c,fe(u)),n.set(a,c));else{v=c.v!==y;var pe=i(()=>fe(u));G(c,pe)}var ye=Reflect.getOwnPropertyDescriptor(l,a);if(ye?.set&&ye.set.call(o,u),!v){if(r&&typeof a=="string"){var Ke=n.get("length"),ke=Number(a);Number.isInteger(ke)&&ke>=Ke.v&&G(Ke,ke+1)}De(s)}return!0},ownKeys(l){ie(s);var a=Reflect.ownKeys(l).filter(c=>{var v=n.get(c);return v===void 0||v.v!==y});for(var[u,o]of n)o.v!==y&&!(u in l)&&a.push(u);return a},setPrototypeOf(){zt()}})}var Ze,vn,yt,wt;function nr(){if(Ze===void 0){Ze=window,vn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;yt=ae(t,"firstChild").get,wt=ae(t,"nextSibling").get,ze(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),ze(n)&&(n.__t=void 0)}}function Ae(e=""){return document.createTextNode(e)}function Fe(e){return yt.call(e)}function V(e){return wt.call(e)}function rr(e,t){if(!Z)return Fe(e);var n=Fe(A);if(n===null)n=A.appendChild(Ae());else if(t&&n.nodeType!==qe){var r=Ae();return n?.before(r),re(r),r}return re(n),n}function sr(e,t=!1){if(!Z){var n=Fe(e);return n instanceof Comment&&n.data===""?V(n):n}if(t&&A?.nodeType!==qe){var r=Ae();return A?.before(r),re(r),r}return A}function fr(e,t=1,n=!1){let r=Z?A:e;for(var s;t--;)s=r,r=V(r);if(!Z)return r;if(n&&r?.nodeType!==qe){var f=Ae();return r===null?s?.after(f):r.before(f),re(f),f}return re(r),r}function ir(e){e.textContent=""}function ar(){return!1}function Et(e){var t=_,n=h;H(null),se(null);try{return e()}finally{H(t),se(n)}}function gt(e){h===null&&(_===null&&Vt(),Bt()),ee&&Ht()}function dn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function I(e,t,n){var r=h;r!==null&&(r.f&P)!==0&&(e|=P);var s={ctx:b,deps:null,nodes:null,f:e|R|k,first:null,fn:t,last:null,next:null,parent:r,b:r&&r.b,prev:null,teardown:null,wv:0,ac:null};if(n)try{ce(s),s.f|=je}catch(l){throw W(s),l}else t!==null&&Q(s);var f=s;if(n&&f.deps===null&&f.teardown===null&&f.nodes===null&&f.first===f.last&&(f.f&ve)===0&&(f=f.first,(e&M)!==0&&(e&ge)!==0&&f!==null&&(f.f|=ge)),f!==null&&(f.parent=r,r!==null&&dn(f,r),_!==null&&(_.f&w)!==0&&(e&te)===0)){var i=_;(i.effects??=[]).push(f)}return s}function mt(){return _!==null&&!N}function hn(e){const t=I(_e,null,!1);return g(t,E),t.teardown=e,t}function lr(e){gt();var t=h.f,n=!_&&(t&j)!==0&&(t&je)===0;if(n){var r=b;(r.e??=[]).push(e)}else return bt(e)}function bt(e){return I(Ee|st,e,!1)}function ur(e){return gt(),I(_e|st,e,!0)}function or(e){J.ensure();const t=I(te|ve,e,!0);return(n={})=>new Promise(r=>{n.outro?En(t,()=>{W(t),r(void 0)}):(W(t),r(void 0))})}function cr(e){return I(Ee,e,!1)}function pn(e){return I(Le|ve,e,!0)}function _r(e,t=0){return I(_e|t,e,!0)}function vr(e,t=[],n=[],r=[]){an(r,t,n,s=>{I(_e,()=>e(...s.map(ie)),!0)})}function dr(e,t=0){var n=I(M|t,e,!0);return n}function hr(e){return I(j|ve,e,!0)}function Tt(e){var t=e.teardown;if(t!==null){const n=ee,r=_;Je(!0),H(null);try{t.call(null)}finally{Je(n),H(r)}}}function At(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&Et(()=>{s.abort(ne)});var r=n.next;(n.f&te)!==0?n.parent=null:W(n,t),n=r}}function yn(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&j)===0&&W(t),t=n}}function W(e,t=!0){var n=!1;(t||(e.f&rt)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(wn(e.nodes.start,e.nodes.end),n=!0),At(e,t&&!n),Se(e,0),g(e,q);var r=e.nodes&&e.nodes.t;if(r!==null)for(const f of r)f.stop();Tt(e);var s=e.parent;s!==null&&s.first!==null&&Rt(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function wn(e,t){for(;e!==null;){var n=e===t?null:V(e);e.remove(),e=n}}function Rt(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function En(e,t,n=!0){var r=[];St(e,r,!0);var s=()=>{n&&W(e),t&&t()},f=r.length;if(f>0){var i=()=>--f||s();for(var l of r)l.out(i)}else s()}function St(e,t,n){if((e.f&P)===0){e.f^=P;var r=e.nodes&&e.nodes.t;if(r!==null)for(const l of r)(l.is_global||n)&&t.push(l);for(var s=e.first;s!==null;){var f=s.next,i=(s.f&ge)!==0||(s.f&j)!==0&&(e.f&M)!==0;St(s,t,i?n:!1),s=f}}}function pr(e){xt(e,!0)}function xt(e,t){if((e.f&P)!==0){e.f^=P,(e.f&E)===0&&(g(e,R),Q(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&ge)!==0||(n.f&j)!==0;xt(n,s?t:!1),n=r}var f=e.nodes&&e.nodes.t;if(f!==null)for(const i of f)(i.is_global||t)&&i.in()}}function yr(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:V(n);t.append(n),n=s}}let z=!1;function Re(e){z=e}let ee=!1;function Je(e){ee=e}let _=null,N=!1;function H(e){_=e}let h=null;function se(e){h=e}let F=null;function Ot(e){_!==null&&(F===null?F=[e]:F.push(e))}let m=null,T=0,x=null;function gn(e){x=e}let kt=1,oe=0,$=oe;function Qe(e){$=e}function Dt(){return++kt}function he(e){var t=e.f;if((t&R)!==0)return!0;if(t&w&&(e.f&=~X),(t&B)!==0){for(var n=e.deps,r=n.length,s=0;se.wv)return!0}(t&k)!==0&&D===null&&g(e,E)}return!1}function Nt(e,t,n=!0){var r=e.reactions;if(r!==null&&!F?.includes(e))for(var s=0;s{e.ac.abort(ne)}),e.ac=null);try{e.f|=Ne;var o=e.fn,c=o(),v=e.deps;if(m!==null){var d;if(Se(e,T),v!==null&&T>0)for(v.length=T+m.length,d=0;dv(s.s);if(n){let a=0,t={};const _=y(()=>{let l=!1;const r=s.s;for(const o in r)r[o]!==t[o]&&(t[o]=r[o],l=!0);return l&&a++,a});f=()=>p(_)}e.b.length&&g(()=>{u(s,f),i(e.b)}),c(()=>{const a=m(()=>e.m.map(b));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}h();export{x as i}; diff --git a/backend/static/_app/immutable/chunks/kSfNJqxT.js b/backend/static/_app/immutable/chunks/kSfNJqxT.js new file mode 100644 index 0000000..148fa12 --- /dev/null +++ b/backend/static/_app/immutable/chunks/kSfNJqxT.js @@ -0,0 +1 @@ +import{f as n,i as p,j as o,p as u,k as d,l,h as m,m as _,n as v,o as b}from"./DnPHkIdi.js";class w{anchor;#t=new Map;#s=new Map;#e=new Map;#i=new Set;#a=!0;constructor(e,s=!0){this.anchor=e,this.#a=s}#f=()=>{var e=n;if(this.#t.has(e)){var s=this.#t.get(e),t=this.#s.get(s);if(t)p(t),this.#i.delete(s);else{var a=this.#e.get(s);a&&(this.#s.set(s,a.effect),this.#e.delete(s),a.fragment.lastChild.remove(),this.anchor.before(a.fragment),t=a.effect)}for(const[i,f]of this.#t){if(this.#t.delete(i),i===e)break;const h=this.#e.get(f);h&&(o(h.effect),this.#e.delete(f))}for(const[i,f]of this.#s){if(i===s||this.#i.has(i))continue;const h=()=>{if(Array.from(this.#t.values()).includes(i)){var c=document.createDocumentFragment();v(f,c),c.append(d()),this.#e.set(i,{effect:f,fragment:c})}else o(f);this.#i.delete(i),this.#s.delete(i)};this.#a||!t?(this.#i.add(i),u(f,h,!1)):h()}}};#h=e=>{this.#t.delete(e);const s=Array.from(this.#t.values());for(const[t,a]of this.#e)s.includes(t)||(o(a.effect),this.#e.delete(t))};ensure(e,s){var t=n,a=b();if(s&&!this.#s.has(e)&&!this.#e.has(e))if(a){var i=document.createDocumentFragment(),f=d();i.append(f),this.#e.set(e,{effect:l(()=>s(f)),fragment:i})}else this.#s.set(e,l(()=>s(this.anchor)));if(this.#t.set(t,e),a){for(const[h,r]of this.#s)h===e?t.skipped_effects.delete(r):t.skipped_effects.add(r);for(const[h,r]of this.#e)h===e?t.skipped_effects.delete(r.effect):t.skipped_effects.add(r.effect);t.oncommit(this.#f),t.ondiscard(this.#h)}else m&&(this.anchor=_),this.#f()}}export{w as B}; diff --git a/backend/static/_app/immutable/entry/app.o34c63v3.js b/backend/static/_app/immutable/entry/app.o34c63v3.js new file mode 100644 index 0000000..e3ba7e5 --- /dev/null +++ b/backend/static/_app/immutable/entry/app.o34c63v3.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.Cf73ubsW.js","../chunks/DUCk1qN8.js","../chunks/DnPHkIdi.js","../chunks/kSfNJqxT.js","../chunks/C9HAc536.js","../assets/0.DLollcCP.css","../nodes/1.Mwi0d7FS.js","../chunks/VM9GfMNA.js","../chunks/DZQwV0xP.js","../chunks/CKL1QNnB.js","../chunks/BOU_Z_Ye.js","../nodes/2.BQz_jWEl.js","../chunks/6OCXa8_L.js","../assets/2.D5pqjzFo.css"])))=>i.map(i=>d[i]); +import{h as z,a as H,b as Q,E as Z,aC as X,ab as p,v as U,ad as $,aD as q,aE as ee,aF as te,aG as re,y as v,A as ae,P as ne,aH as se,a2 as O,aI as ie,a4 as oe,aJ as ce,aK as ue,C as fe,aL as le,aM as de,aN as _e,aO as F,aP as me,av as ve,W as he,D as ge,u as be,t as ye,aQ as I,aR as Ee,F as w,M as Pe,I as Se,J as Re,K as Oe,aS as L,G as we}from"../chunks/DnPHkIdi.js";import{h as Ae,m as Ie,u as Le,s as Te}from"../chunks/DZQwV0xP.js";import{f as Y,a as R,c as T,t as xe}from"../chunks/DUCk1qN8.js";import{o as Ce}from"../chunks/BOU_Z_Ye.js";import{i as x}from"../chunks/6OCXa8_L.js";import{B as De}from"../chunks/kSfNJqxT.js";function C(t,e,a){z&&H();var c=new De(t);Q(()=>{var s=e()??null;c.ensure(s,s&&(r=>a(r,s)))},Z)}function k(t,e){return t===e||t?.[q]===e}function D(t={},e,a,c){return X(()=>{var s,r;return p(()=>{s=r,r=[],U(()=>{t!==a(...r)&&(e(t,...r),s&&k(a(...s),t)&&e(null,...s))})}),()=>{$(()=>{r&&k(a(...r),t)&&e(null,...r)})}}),t}let A=!1;function Me(t){var e=A;try{return A=!1,[t(),A]}finally{A=e}}function M(t,e,a,c){var s=!fe||(a&le)!==0,r=(a&ue)!==0,n=(a&_e)!==0,i=c,y=!0,P=()=>(y&&(y=!1,i=n?U(c):c),i),u;if(r){var h=q in t||F in t;u=ee(t,e)?.set??(h&&e in t?o=>t[e]=o:void 0)}var d,_=!1;r?[d,_]=Me(()=>t[e]):d=t[e],d===void 0&&c!==void 0&&(d=P(),u&&(s&&te(),u(d)));var l;if(s?l=()=>{var o=t[e];return o===void 0?P():(y=!0,o)}:l=()=>{var o=t[e];return o!==void 0&&(i=void 0),o===void 0?i:o},s&&(a&re)===0)return l;if(u){var f=t.$$legacy;return(function(o,b){return arguments.length>0?((!s||!b||f||_)&&u(b?l():o),o):l()})}var g=!1,m=((a&de)!==0?ae:ne)(()=>(g=!1,l()));r&&v(m);var S=oe;return(function(o,b){if(arguments.length>0){const E=b?v(m):s&&r?se(o):o;return O(m,E),g=!0,i!==void 0&&(i=E),o}return ie&&g||(S.f&ce)!==0?m.v:v(m)})}function Ne(t){return class extends je{constructor(e){super({component:t,...e})}}}class je{#t;#e;constructor(e){var a=new Map,c=(r,n)=>{var i=he(n,!1,!1);return a.set(r,i),i};const s=new Proxy({...e.props||{},$$events:{}},{get(r,n){return v(a.get(n)??c(n,Reflect.get(r,n)))},has(r,n){return n===F?!0:(v(a.get(n)??c(n,Reflect.get(r,n))),Reflect.has(r,n))},set(r,n,i){return O(a.get(n)??c(n,i),i),Reflect.set(r,n,i)}});this.#e=(e.hydrate?Ae:Ie)(e.component,{target:e.target,anchor:e.anchor,props:s,context:e.context,intro:e.intro??!1,recover:e.recover}),(!e?.props?.$$host||e.sync===!1)&&me(),this.#t=s.$$events;for(const r of Object.keys(this.#e))r==="$set"||r==="$destroy"||r==="$on"||ve(this,r,{get(){return this.#e[r]},set(n){this.#e[r]=n},enumerable:!0});this.#e.$set=r=>{Object.assign(s,r)},this.#e.$destroy=()=>{Le(this.#e)}}$set(e){this.#e.$set(e)}$on(e,a){this.#t[e]=this.#t[e]||[];const c=(...s)=>a.call(this,...s);return this.#t[e].push(c),()=>{this.#t[e]=this.#t[e].filter(s=>s!==c)}}$destroy(){this.#e.$destroy()}}const ke="modulepreload",Be=function(t,e){return new URL(t,e).href},B={},N=function(e,a,c){let s=Promise.resolve();if(a&&a.length>0){let P=function(u){return Promise.all(u.map(h=>Promise.resolve(h).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const n=document.getElementsByTagName("link"),i=document.querySelector("meta[property=csp-nonce]"),y=i?.nonce||i?.getAttribute("nonce");s=P(a.map(u=>{if(u=Be(u,c),u in B)return;B[u]=!0;const h=u.endsWith(".css"),d=h?'[rel="stylesheet"]':"";if(c)for(let l=n.length-1;l>=0;l--){const f=n[l];if(f.href===u&&(!h||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${u}"]${d}`))return;const _=document.createElement("link");if(_.rel=h?"stylesheet":ke,h||(_.as="script"),_.crossOrigin="",_.href=u,y&&_.setAttribute("nonce",y),document.head.appendChild(_),h)return new Promise((l,f)=>{_.addEventListener("load",l),_.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${u}`)))})}))}function r(n){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=n,window.dispatchEvent(i),!i.defaultPrevented)throw n}return s.then(n=>{for(const i of n||[])i.status==="rejected"&&r(i.reason);return e().catch(r)})},Qe={};var Ue=Y('
'),qe=Y(" ",1);function Fe(t,e){ge(e,!0);let a=M(e,"components",23,()=>[]),c=M(e,"data_0",3,null),s=M(e,"data_1",3,null);be(()=>e.stores.page.set(e.page)),ye(()=>{e.stores,e.page,e.constructors,a(),e.form,c(),s(),e.stores.page.notify()});let r=I(!1),n=I(!1),i=I(null);Ce(()=>{const f=e.stores.page.subscribe(()=>{v(r)&&(O(n,!0),Ee().then(()=>{O(i,document.title||"untitled page",!0)}))});return O(r,!0),f});const y=L(()=>e.constructors[1]);var P=qe(),u=w(P);{var h=f=>{const g=L(()=>e.constructors[0]);var m=T(),S=w(m);C(S,()=>v(g),(o,b)=>{D(b(o,{get data(){return c()},get form(){return e.form},get params(){return e.page.params},children:(E,Ge)=>{var j=T(),V=w(j);C(V,()=>v(y),(J,K)=>{D(K(J,{get data(){return s()},get form(){return e.form},get params(){return e.page.params}}),W=>a()[1]=W,()=>a()?.[1])}),R(E,j)},$$slots:{default:!0}}),E=>a()[0]=E,()=>a()?.[0])}),R(f,m)},d=f=>{const g=L(()=>e.constructors[0]);var m=T(),S=w(m);C(S,()=>v(g),(o,b)=>{D(b(o,{get data(){return c()},get form(){return e.form},get params(){return e.page.params}}),E=>a()[0]=E,()=>a()?.[0])}),R(f,m)};x(u,f=>{e.constructors[1]?f(h):f(d,!1)})}var _=Pe(u,2);{var l=f=>{var g=Ue(),m=Re(g);{var S=o=>{var b=xe();we(()=>Te(b,v(i))),R(o,b)};x(m,o=>{v(n)&&o(S)})}Oe(g),R(f,g)};x(_,f=>{v(r)&&f(l)})}R(t,P),Se()}const Ze=Ne(Fe),Xe=[()=>N(()=>import("../nodes/0.Cf73ubsW.js"),__vite__mapDeps([0,1,2,3,4,5]),import.meta.url),()=>N(()=>import("../nodes/1.Mwi0d7FS.js"),__vite__mapDeps([6,1,2,7,8,9,10]),import.meta.url),()=>N(()=>import("../nodes/2.BQz_jWEl.js"),__vite__mapDeps([11,1,2,7,10,8,12,3,4,13]),import.meta.url)],pe=[],$e={"/":[2]},G={handleError:(({error:t})=>{console.error(t)}),reroute:(()=>{}),transport:{}},Ye=Object.fromEntries(Object.entries(G.transport).map(([t,e])=>[t,e.decode])),et=Object.fromEntries(Object.entries(G.transport).map(([t,e])=>[t,e.encode])),tt=!1,rt=(t,e)=>Ye[t](e);export{rt as decode,Ye as decoders,$e as dictionary,et as encoders,tt as hash,G as hooks,Qe as matchers,Xe as nodes,Ze as root,pe as server_loads}; diff --git a/backend/static/_app/immutable/entry/start.BrST8BG9.js b/backend/static/_app/immutable/entry/start.BrST8BG9.js new file mode 100644 index 0000000..df22380 --- /dev/null +++ b/backend/static/_app/immutable/entry/start.BrST8BG9.js @@ -0,0 +1 @@ +import{l as o,a as r}from"../chunks/CKL1QNnB.js";export{o as load_css,r as start}; diff --git a/backend/static/_app/immutable/nodes/0.Cf73ubsW.js b/backend/static/_app/immutable/nodes/0.Cf73ubsW.js new file mode 100644 index 0000000..d329039 --- /dev/null +++ b/backend/static/_app/immutable/nodes/0.Cf73ubsW.js @@ -0,0 +1 @@ +import{c as p,a as r,f as v}from"../chunks/DUCk1qN8.js";import{b as m,E as g,k as h,a9 as u,h as n,O as _,Q as y,a0 as o,d as f,c,m as w,F as E,G as x}from"../chunks/DnPHkIdi.js";import{B as T}from"../chunks/kSfNJqxT.js";import{s as b}from"../chunks/C9HAc536.js";function A(l,s,...a){var i=new T(l);m(()=>{const t=s()??null;i.ensure(t,t&&(e=>t(e,...a)))},g)}function F(l,s){let a=null,i=n;var t;if(n){a=w;for(var e=_(document.head);e!==null&&(e.nodeType!==y||e.data!==l);)e=o(e);if(e===null)f(!1);else{var d=o(e);e.remove(),c(d)}}n||(t=document.head.appendChild(h()));try{m(()=>s(t),u)}finally{i&&(f(!0),c(a))}}const M="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='107'%20height='128'%20viewBox='0%200%20107%20128'%3e%3ctitle%3esvelte-logo%3c/title%3e%3cpath%20d='M94.157%2022.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282%2029.608A29.92%2029.92%200%200%200%208.764%2049.65a31.5%2031.5%200%200%200%203.108%2020.231%2030%2030%200%200%200-4.477%2011.183%2031.9%2031.9%200%200%200%205.448%2024.116c10.402%2014.887%2030.942%2019.297%2045.791%209.835l26.083-16.624A29.92%2029.92%200%200%200%2098.235%2078.35a31.53%2031.53%200%200%200-3.105-20.232%2030%2030%200%200%200%204.474-11.182%2031.88%2031.88%200%200%200-5.447-24.116'%20style='fill:%23ff3e00'/%3e%3cpath%20d='M45.817%20106.582a20.72%2020.72%200%200%201-22.237-8.243%2019.17%2019.17%200%200%201-3.277-14.503%2018%2018%200%200%201%20.624-2.435l.49-1.498%201.337.981a33.6%2033.6%200%200%200%2010.203%205.098l.97.294-.09.968a5.85%205.85%200%200%200%201.052%203.878%206.24%206.24%200%200%200%206.695%202.485%205.8%205.8%200%200%200%201.603-.704L69.27%2076.28a5.43%205.43%200%200%200%202.45-3.631%205.8%205.8%200%200%200-.987-4.371%206.24%206.24%200%200%200-6.698-2.487%205.7%205.7%200%200%200-1.6.704l-9.953%206.345a19%2019%200%200%201-5.296%202.326%2020.72%2020.72%200%200%201-22.237-8.243%2019.17%2019.17%200%200%201-3.277-14.502%2017.99%2017.99%200%200%201%208.13-12.052l26.081-16.623a19%2019%200%200%201%205.3-2.329%2020.72%2020.72%200%200%201%2022.237%208.243%2019.17%2019.17%200%200%201%203.277%2014.503%2018%2018%200%200%201-.624%202.435l-.49%201.498-1.337-.98a33.6%2033.6%200%200%200-10.203-5.1l-.97-.294.09-.968a5.86%205.86%200%200%200-1.052-3.878%206.24%206.24%200%200%200-6.696-2.485%205.8%205.8%200%200%200-1.602.704L37.73%2051.72a5.42%205.42%200%200%200-2.449%203.63%205.79%205.79%200%200%200%20.986%204.372%206.24%206.24%200%200%200%206.698%202.486%205.8%205.8%200%200%200%201.602-.704l9.952-6.342a19%2019%200%200%201%205.295-2.328%2020.72%2020.72%200%200%201%2022.237%208.242%2019.17%2019.17%200%200%201%203.277%2014.503%2018%2018%200%200%201-8.13%2012.053l-26.081%2016.622a19%2019%200%200%201-5.3%202.328'%20style='fill:%23fff'/%3e%3c/svg%3e";var k=v('');function O(l,s){var a=p();F("12qhfyh",t=>{var e=k();x(()=>b(e,"href",M)),r(t,e)});var i=E(a);A(i,()=>s.children),r(l,a)}export{O as component}; diff --git a/backend/static/_app/immutable/nodes/1.Mwi0d7FS.js b/backend/static/_app/immutable/nodes/1.Mwi0d7FS.js new file mode 100644 index 0000000..254420c --- /dev/null +++ b/backend/static/_app/immutable/nodes/1.Mwi0d7FS.js @@ -0,0 +1 @@ +import{f as u,a as h}from"../chunks/DUCk1qN8.js";import{i as g}from"../chunks/VM9GfMNA.js";import{D as l,F as v,G as d,I as _,J as a,K as e,M as x}from"../chunks/DnPHkIdi.js";import{s as o}from"../chunks/DZQwV0xP.js";import{s as $,p}from"../chunks/CKL1QNnB.js";const k={get error(){return p.error},get status(){return p.status}};$.updated.check;const m=k;var b=u("

",1);function J(i,f){l(f,!1),g();var t=b(),r=v(t),n=a(r,!0);e(r);var s=x(r,2),c=a(s,!0);e(s),d(()=>{o(n,m.status),o(c,m.error?.message)}),h(i,t),_()}export{J as component}; diff --git a/backend/static/_app/immutable/nodes/2.BQz_jWEl.js b/backend/static/_app/immutable/nodes/2.BQz_jWEl.js new file mode 100644 index 0000000..356dbd8 --- /dev/null +++ b/backend/static/_app/immutable/nodes/2.BQz_jWEl.js @@ -0,0 +1 @@ +import{f as N,a as T,c as te}from"../chunks/DUCk1qN8.js";import{i as _e}from"../chunks/VM9GfMNA.js";import{o as he}from"../chunks/BOU_Z_Ye.js";import{c as J,h as H,O as me,k as K,a as xe,b as Ee,y as _,P as be,r as ye,H as we,s as se,d as Q,m as V,Q as Te,R as Ae,S as ie,f as Ie,l as $,o as Ne,T as ee,U as ke,V as ne,W as P,X as Ce,Y as Se,Z as Me,_ as F,i as ve,p as de,$ as W,a0 as Re,a1 as Fe,j as De,D as He,I as Oe,a2 as Z,M as Y,J as M,K as S,F as le,G as z}from"../chunks/DnPHkIdi.js";import{s as B}from"../chunks/DZQwV0xP.js";import{i as O}from"../chunks/6OCXa8_L.js";import{s as oe}from"../chunks/C9HAc536.js";function ze(e,i){return i}function Le(e,i,s){for(var u=[],h=i.length,n,l=i.length,t=0;t{if(n){if(n.pending.delete(f),n.done.add(f),n.pending.size===0){var m=e.outrogroups;j(ee(n.done)),m.delete(n),m.size===0&&(e.outrogroups=null)}}else l-=1},!1)}if(l===0){var v=u.length===0&&s!==null;if(v){var a=s,o=a.parentNode;Fe(o),o.append(a),e.items.clear()}j(i,!v)}else n={pending:new Set(i),done:new Set},(e.outrogroups??=new Set).add(n)}function j(e,i=!0){for(var s=0;s{var p=s();return ke(p)?p:p==null?[]:ee(p)}),f,m=!0;function D(){c.fallback=a,Ye(c,f,l,i,u),a!==null&&(f.length===0?(a.f&F)===0?ve(a):(a.f^=F,L(a,null,l)):de(a,()=>{a=null}))}var r=Ee(()=>{f=_(o);var p=f.length;let I=!1;if(H){var y=ye(l)===we;y!==(p===0)&&(l=se(),J(l),Q(!1),I=!0)}for(var w=new Set,x=Ie,k=Ne(),d=0;dn(l)):(a=$(()=>n(fe??=K())),a.f|=F)),H&&p>0&&J(se()),!m)if(k){for(const[U,X]of t)w.has(U)||x.skipped_effects.add(X.e);x.oncommit(D),x.ondiscard(()=>{})}else D();I&&Q(!0),_(o)}),c={effect:r,items:t,outrogroups:null,fallback:a};m=!1,H&&(l=V)}function Ye(e,i,s,u,h){var n=i.length,l=e.items,t=e.effect.first,v,a=null,o=[],f=[],m,D,r,c;for(c=0;c0){var C=n===0?s:null;Le(e,k,C)}}}function Be(e,i,s,u,h,n,l,t){var v=(l&Se)!==0?(l&Me)===0?P(s,!1,!1):ne(s):null,a=(l&Ce)!==0?ne(h):null;return{v,i:a,e:$(()=>(n(i,v??s,a??h,t),()=>{e.delete(u)}))}}function L(e,i,s){if(e.nodes)for(var u=e.nodes.start,h=e.nodes.end,n=i&&(i.f&F)===0?i.nodes.start:s;u!==null;){var l=Re(u);if(n.before(u),u===h)return;u=l}}function R(e,i,s){i===null?e.effect.first=s:i.next=s,s===null?e.effect.last=i:s.prev=i}const Pe="/api";async function Ue(e=20,i=0){const s=await fetch(`${Pe}/ebooks?limit=${e}&offset=${i}`);if(!s.ok)throw new Error("Failed to fetch ebooks");return s.json()}var Xe=N('
Loading ebooks...
'),qe=N('
'),Ge=N('
No ebooks found. Add some ebooks to get started.
'),Je=N(''),Ke=N('
📖
No Cover
'),Qe=N('

'),We=N('

'),Ze=N('

'),$e=N('
'),je=N('

Ebook Library

');function lr(e,i){He(i,!1);let s=P([]),u=P(!0),h=P(null);he(async()=>{try{Z(s,await Ue())}catch(a){Z(h,a instanceof Error?a.message:"Failed to load ebooks")}finally{Z(u,!1)}}),_e();var n=je(),l=Y(M(n),2);{var t=a=>{var o=Xe();T(a,o)},v=a=>{var o=te(),f=le(o);{var m=r=>{var c=qe(),p=M(c,!0);S(c),z(()=>B(p,_(h))),T(r,c)},D=r=>{var c=te(),p=le(c);{var I=w=>{var x=Ge();T(w,x)},y=w=>{var x=$e();Ve(x,5,()=>_(s),ze,(k,d)=>{var C=Ze(),g=M(C),A=M(g);{var U=E=>{var b=Je();z(()=>{oe(b,"src",_(d).cover_image_path),oe(b,"alt",`${_(d).title??""} cover`)}),T(E,b)},X=E=>{var b=Ke();T(E,b)};O(A,E=>{_(d).cover_image_path?E(U):E(X,!1)})}S(g);var re=Y(g,2),q=M(re),ue=M(q,!0);S(q);var ae=Y(q,2);{var ce=E=>{var b=Qe(),G=M(b);S(b),z(()=>B(G,`by ${_(d).author??""}`)),T(E,b)};O(ae,E=>{_(d).author&&E(ce)})}var pe=Y(ae,2);{var ge=E=>{var b=We(),G=M(b,!0);S(b),z(()=>B(G,_(d).description)),T(E,b)};O(pe,E=>{_(d).description&&E(ge)})}S(re),S(C),z(()=>B(ue,_(d).title)),T(k,C)}),S(x),T(w,x)};O(p,w=>{_(s).length===0?w(I):w(y,!1)},!0)}T(r,c)};O(f,r=>{_(h)?r(m):r(D,!1)},!0)}T(a,o)};O(l,a=>{_(u)?a(t):a(v,!1)})}S(n),T(e,n),Oe()}export{lr as component}; diff --git a/backend/static/_app/version.json b/backend/static/_app/version.json new file mode 100644 index 0000000..58c810e --- /dev/null +++ b/backend/static/_app/version.json @@ -0,0 +1 @@ +{"version":"1768960079047"} \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html new file mode 100644 index 0000000..89211e6 --- /dev/null +++ b/backend/static/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + +
+ +
+ + diff --git a/backend/static/robots.txt b/backend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/backend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/bruno/README.md b/bruno/README.md new file mode 100644 index 0000000..550b8fd --- /dev/null +++ b/bruno/README.md @@ -0,0 +1,47 @@ +# Bruno API Tests for Ebook Reader + +This directory contains Bruno collection for testing the Ebook Reader API with comprehensive REST documentation. + +## Setup + +1. Install Bruno: https://www.usebruno.com/ +2. Open Bruno and import this collection folder +3. Select the "localhost" environment +4. Start the application with `docker-compose up --build` +5. Register/Login first, then use Bearer token for protected endpoints + +## Available Tests + +### Auth (Public Endpoints) +- **Register User**: POST /api/auth/register - Create new account +- **Login User**: POST /api/auth/login - Authenticate (email or username) +- **Get Profile**: GET /api/auth/profile - Get user info (requires token) + +### Ebooks (Protected - JWT Required) +- **List Ebooks**: GET /api/ebooks - Paginated ebook list +- **Get Ebook**: GET /api/ebooks/:id - Single ebook details +- **Create Ebook**: POST /api/ebooks - Add new ebook +- **Update Ebook**: PUT /api/ebooks/:id - Modify ebook metadata +- **Delete Ebook**: DELETE /api/ebooks/:id - Remove ebook + +### Reading Progress (Protected - JWT Required) +- **Get Reading Progress**: GET /api/ebooks/:id/progress - User's progress +- **Update Reading Progress**: PUT /api/ebooks/:id/progress - Update progress + +## Documentation Features + +Each request includes: +- **Detailed descriptions** of functionality +- **Parameter specifications** (required/optional, types) +- **Request/Response examples** +- **Error response codes** and meanings +- **Authentication requirements** + +## Notes + +- **Authentication Flow**: Register → Login → Use Bearer token for all other requests +- **JWT Tokens**: Valid for 24 hours, include in `Authorization: Bearer ` header +- **User Isolation**: Progress and data are user-specific +- **Variables**: Update `ebook_id` for testing specific ebooks +- **Security**: Passwords hashed with bcrypt, unique email/username constraints +- **JSON**: All requests/responses use JSON format \ No newline at end of file diff --git a/bruno/auth/Get Profile.yml b/bruno/auth/Get Profile.yml new file mode 100644 index 0000000..7dd622d --- /dev/null +++ b/bruno/auth/Get Profile.yml @@ -0,0 +1,23 @@ +meta: + name: Get Profile + seq: 3 +http: + method: get + url: "{{base_url}}/api/auth/profile" + auth: + type: bearer +docs: | + ## Get User Profile + + Retrieves the profile of the authenticated user. + + **Authentication:** Required (Bearer token) + + **Response:** + - `id` (string): User UUID + - `email` (string): User's email address + - `username` (string): User's username + + **Error Responses:** + - 401: Invalid or missing JWT token + - 404: User not found \ No newline at end of file diff --git a/bruno/auth/Login User.yml b/bruno/auth/Login User.yml new file mode 100644 index 0000000..2939fbb --- /dev/null +++ b/bruno/auth/Login User.yml @@ -0,0 +1,29 @@ +meta: + name: Login User + seq: 2 +http: + method: post + url: "{{base_url}}/api/auth/login" + body: + type: json + data: | + { + "login": "test@example.com", + "password": "password123" + } +docs: | + ## Login User + + Authenticates a user and returns a JWT token. + + **Request Body:** + - `login` (string, required): Email address or username + - `password` (string, required): User's password + + **Response:** + - `token` (string): JWT authentication token (24h expiry) + - `user` (object): User profile with id, email, username + + **Error Responses:** + - 401: Invalid credentials + - 400: Invalid request data \ No newline at end of file diff --git a/bruno/auth/Register User.yml b/bruno/auth/Register User.yml new file mode 100644 index 0000000..58727e4 --- /dev/null +++ b/bruno/auth/Register User.yml @@ -0,0 +1,31 @@ +meta: + name: Register User + seq: 1 +http: + method: post + url: "{{base_url}}/api/auth/register" + body: + type: json + data: | + { + "email": "test@example.com", + "username": "testuser", + "password": "password123" + } +docs: | + ## Register User + + Creates a new user account with email, username, and password. + + **Request Body:** + - `email` (string, required): Valid email address + - `username` (string, required): Unique username (3-50 chars) + - `password` (string, required): Password (min 6 chars) + + **Response:** + - `token` (string): JWT authentication token + - `user` (object): User profile with id, email, username + + **Error Responses:** + - 409: Email or username already exists + - 400: Invalid request data \ No newline at end of file diff --git a/bruno/collection.yml b/bruno/collection.yml new file mode 100644 index 0000000..481de34 --- /dev/null +++ b/bruno/collection.yml @@ -0,0 +1,4 @@ +version: "1" +name: "Ebook Reader API" +type: collection +items: [] \ No newline at end of file diff --git a/bruno/ebooks/Create Ebook.yml b/bruno/ebooks/Create Ebook.yml new file mode 100644 index 0000000..7f1f781 --- /dev/null +++ b/bruno/ebooks/Create Ebook.yml @@ -0,0 +1,43 @@ +meta: + name: Create Ebook + seq: 3 +http: + method: post + url: "{{base_url}}/api/ebooks" + auth: + type: bearer + body: + type: json + data: | + { + "title": "Sample Book", + "author": "Sample Author", + "isbn": "1234567890", + "description": "A sample ebook", + "file_path": "/uploads/sample.epub", + "file_size": 1024000, + "mime_type": "application/epub+zip", + "cover_image_path": "/uploads/cover.jpg" + } +docs: | + ## Create Ebook + + Creates a new ebook entry in the library. + + **Authentication:** Required (Bearer token) + + **Request Body:** + - `title` (string, required): Book title + - `author` (string, optional): Book author + - `isbn` (string, optional): ISBN number + - `description` (string, optional): Book description + - `file_path` (string, required): Path to ebook file + - `file_size` (number, optional): File size in bytes + - `mime_type` (string, optional): MIME type + - `cover_image_path` (string, optional): Path to cover image + + **Response:** Created ebook object + + **Error Responses:** + - 401: Invalid authentication + - 400: Invalid request data \ No newline at end of file diff --git a/bruno/ebooks/Delete Ebook.yml b/bruno/ebooks/Delete Ebook.yml new file mode 100644 index 0000000..4e54fee --- /dev/null +++ b/bruno/ebooks/Delete Ebook.yml @@ -0,0 +1,27 @@ +meta: + name: Delete Ebook + seq: 5 +http: + method: delete + url: "{{base_url}}/api/ebooks/{{ebook_id}}" + auth: + type: bearer +vars: + pre-request: + - name: ebook_id + value: "123e4567-e89b-12d3-a456-426614174000" +docs: | + ## Delete Ebook + + Removes an ebook from the library. + + **Authentication:** Required (Bearer token) + + **Path Parameters:** + - `id` (string): Ebook UUID + + **Response:** 204 No Content + + **Error Responses:** + - 401: Invalid authentication + - 404: Ebook not found \ No newline at end of file diff --git a/bruno/ebooks/Get Ebook.yml b/bruno/ebooks/Get Ebook.yml new file mode 100644 index 0000000..b5f2d34 --- /dev/null +++ b/bruno/ebooks/Get Ebook.yml @@ -0,0 +1,27 @@ +meta: + name: Get Ebook + seq: 2 +http: + method: get + url: "{{base_url}}/api/ebooks/{{ebook_id}}" + auth: + type: bearer +vars: + pre-request: + - name: ebook_id + value: "123e4567-e89b-12d3-a456-426614174000" +docs: | + ## Get Ebook + + Retrieves details of a specific ebook. + + **Authentication:** Required (Bearer token) + + **Path Parameters:** + - `id` (string): Ebook UUID + + **Response:** Complete ebook object with all metadata + + **Error Responses:** + - 401: Invalid authentication + - 404: Ebook not found \ No newline at end of file diff --git a/bruno/ebooks/List Ebooks.yml b/bruno/ebooks/List Ebooks.yml new file mode 100644 index 0000000..f771842 --- /dev/null +++ b/bruno/ebooks/List Ebooks.yml @@ -0,0 +1,23 @@ +meta: + name: List Ebooks + seq: 1 +http: + method: get + url: "{{base_url}}/api/ebooks" + auth: + type: bearer +docs: | + ## List Ebooks + + Retrieves a paginated list of ebooks. + + **Authentication:** Required (Bearer token) + + **Query Parameters:** + - `limit` (number, optional): Number of results (default: 20, max: 100) + - `offset` (number, optional): Pagination offset (default: 0) + + **Response:** Array of ebook objects with id, title, author, etc. + + **Error Responses:** + - 401: Invalid authentication \ No newline at end of file diff --git a/bruno/ebooks/Update Ebook.yml b/bruno/ebooks/Update Ebook.yml new file mode 100644 index 0000000..f488612 --- /dev/null +++ b/bruno/ebooks/Update Ebook.yml @@ -0,0 +1,43 @@ +meta: + name: Update Ebook + seq: 4 +http: + method: put + url: "{{base_url}}/api/ebooks/{{ebook_id}}" + auth: + type: bearer + body: + type: json + data: | + { + "title": "Updated Book Title", + "author": "Updated Author", + "description": "Updated description" + } +vars: + pre-request: + - name: ebook_id + value: "123e4567-e89b-12d3-a456-426614174000" +docs: | + ## Update Ebook + + Updates an existing ebook's metadata. + + **Authentication:** Required (Bearer token) + + **Path Parameters:** + - `id` (string): Ebook UUID + + **Request Body:** (all fields optional) + - `title` (string): Book title + - `author` (string): Book author + - `isbn` (string): ISBN number + - `description` (string): Book description + - `cover_image_path` (string): Path to cover image + + **Response:** Updated ebook object + + **Error Responses:** + - 401: Invalid authentication + - 404: Ebook not found + - 400: Invalid request data \ No newline at end of file diff --git a/bruno/environments/localhost.yml b/bruno/environments/localhost.yml new file mode 100644 index 0000000..7f0e49e --- /dev/null +++ b/bruno/environments/localhost.yml @@ -0,0 +1,4 @@ +name: localhost +variables: + - name: base_url + value: http://localhost:8765 \ No newline at end of file diff --git a/bruno/progress/Get Reading Progress.yml b/bruno/progress/Get Reading Progress.yml new file mode 100644 index 0000000..64bdc10 --- /dev/null +++ b/bruno/progress/Get Reading Progress.yml @@ -0,0 +1,31 @@ +meta: + name: Get Reading Progress + seq: 6 +http: + method: get + url: "{{base_url}}/api/ebooks/{{ebook_id}}/progress" + auth: + type: bearer +vars: + pre-request: + - name: ebook_id + value: "123e4567-e89b-12d3-a456-426614174000" +docs: | + ## Get Reading Progress + + Retrieves the authenticated user's reading progress for an ebook. + + **Authentication:** Required (Bearer token) + + **Path Parameters:** + - `id` (string): Ebook UUID + + **Response:** + - `ebook_id` (string): Ebook UUID + - `user_id` (string): User UUID + - `current_page` (number): Current page number + - `total_pages` (number, nullable): Total pages + - `last_read_at` (string): Last read timestamp + + **Error Responses:** + - 401: Invalid authentication \ No newline at end of file diff --git a/bruno/progress/Update Reading Progress.yml b/bruno/progress/Update Reading Progress.yml new file mode 100644 index 0000000..4bea3f6 --- /dev/null +++ b/bruno/progress/Update Reading Progress.yml @@ -0,0 +1,38 @@ +meta: + name: Update Reading Progress + seq: 7 +http: + method: put + url: "{{base_url}}/api/ebooks/{{ebook_id}}/progress" + auth: + type: bearer + body: + type: json + data: | + { + "current_page": 45, + "total_pages": 200 + } +vars: + pre-request: + - name: ebook_id + value: "123e4567-e89b-12d3-a456-426614174000" +docs: | + ## Update Reading Progress + + Updates the authenticated user's reading progress for an ebook. + + **Authentication:** Required (Bearer token) + + **Path Parameters:** + - `id` (string): Ebook UUID + + **Request Body:** + - `current_page` (number, required): Current page number + - `total_pages` (number, optional): Total pages in book + + **Response:** Updated progress object + + **Error Responses:** + - 401: Invalid authentication + - 400: Invalid request data \ No newline at end of file diff --git a/bruno/workspace.yml b/bruno/workspace.yml new file mode 100644 index 0000000..e755d59 --- /dev/null +++ b/bruno/workspace.yml @@ -0,0 +1,3 @@ +version: "1" +name: "Ebook Reader API Workspace" +type: workspace \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0785bd8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: "3.8" + +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_DB: ebookdb + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DBPASS} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backend/migrations:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + env_file: + - .env + + backend: + build: + context: . + dockerfile: ./backend/Dockerfile + environment: + DATABASE_HOST: db + DATABASE_PORT: 5432 + DATABASE_USER: postgres + DATABASE_PASSWORD: ${DBPASS} + DATABASE_NAME: ebookdb + JWT_SECRET: ${JWT_SECRET} + SERVER_PORT: 8765 + ports: + - "8765:8765" + depends_on: + db: + condition: service_healthy + volumes: + - ./backend/uploads:/app/uploads + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8765/"] + interval: 30s + timeout: 10s + retries: 3 + env_file: + - .env + +volumes: + postgres_data: \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..eb63507 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +npx sv create --template minimal --types ts --install npm . +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9c20b82 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2895 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "@zerodevx/svelte-toast": "^0.9.6", + "sveltekit-superforms": "^2.29.1", + "zod": "^4.3.5" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.1", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/postcss": "^4.1.18", + "@tailwindcss/typography": "^0.5.19", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.2.6" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ark/schema": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz", + "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/util": "0.56.0" + } + }, + "node_modules/@ark/util": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz", + "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==", + "license": "MIT", + "optional": true + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT", + "optional": true + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@poppinss/macroable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.1.0.tgz", + "integrity": "sha512-y/YKzZDuG8XrpXpM7Z1RdQpiIc0MAKyva24Ux1PB4aI7RiSI/79K8JVDcdyubriTm7vJ1LhFs8CrZpmPnx/8Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.0.tgz", + "integrity": "sha512-Hj8sR8O27p2zshFEIJzsvfhLzxga/hWw6tRLnBjMYw70m1aS9BSYCqAUtzDBjRREtX1EvLMYgaC0mYE3Hz4KWA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT", + "optional": true + }, + "node_modules/@typeschema/class-validator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@typeschema/class-validator/-/class-validator-0.3.0.tgz", + "integrity": "sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@typeschema/core": "0.14.0" + }, + "peerDependencies": { + "class-validator": "^0.14.1" + }, + "peerDependenciesMeta": { + "class-validator": { + "optional": true + } + } + }, + "node_modules/@typeschema/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@typeschema/core/-/core-0.14.0.tgz", + "integrity": "sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + }, + "peerDependenciesMeta": { + "@types/json-schema": { + "optional": true + } + } + }, + "node_modules/@valibot/to-json-schema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.5.0.tgz", + "integrity": "sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "valibot": "^1.2.0" + } + }, + "node_modules/@vinejs/compiler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-3.0.0.tgz", + "integrity": "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@vinejs/vine": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.1.tgz", + "integrity": "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@poppinss/macroable": "^1.0.4", + "@types/validator": "^13.12.2", + "@vinejs/compiler": "^3.0.0", + "camelcase": "^8.0.0", + "dayjs": "^1.11.13", + "dlv": "^1.1.3", + "normalize-url": "^8.0.1", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@zerodevx/svelte-toast": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.9.6.tgz", + "integrity": "sha512-nHlTrCjverlPK9yukK6fqbG3e/R+f10ldrc4nJHOe2qNDScuPTuYVSFEk2dDDtzWAwTN5pmdEXgA3M2RbT8jiw==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3.57.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arkregex": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz", + "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/util": "0.56.0" + } + }, + "node_modules/arktype": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.29.tgz", + "integrity": "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/schema": "0.56.0", + "@ark/util": "0.56.0", + "arkregex": "0.0.5" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz", + "integrity": "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT", + "optional": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", + "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT", + "optional": true + }, + "node_modules/effect": { + "version": "3.19.14", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.19.14.tgz", + "integrity": "sha512-3vwdq0zlvQOxXzXNKRIPKTqZNMyGCdaFUBfMPqpsyzZDre67kgC1EEHDV4EoQTovJ4w5fmJW756f86kkuz7WFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz", + "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.34", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.34.tgz", + "integrity": "sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==", + "license": "MIT", + "optional": true + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoize-weak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/memoize-weak/-/memoize-weak-1.0.2.tgz", + "integrity": "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==", + "license": "ISC" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT", + "optional": true + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/svelte": { + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.47.1.tgz", + "integrity": "sha512-MhSWfWEpG5T57z0Oyfk9D1GhAz/KTZKZZlWtGEsy9zNk2fafpuU7sJQlXNSA8HtvwKxVC9XlDyl5YovXUXjjHA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.2", + "esm-env": "^1.2.1", + "esrap": "^2.2.1", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz", + "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/sveltekit-superforms": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.29.1.tgz", + "integrity": "sha512-9Cv1beOVPgm8rb8NZBqLdlZ9cBqRBTk0+6/oHn7DWvHQoAFie1EPjh1e4NHO3Qouv1Zq9QTGrZNDbYcetkuOVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ciscoheat" + }, + { + "type": "ko-fi", + "url": "https://ko-fi.com/ciscoheat" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=NY7F5ALHHSVQS" + } + ], + "license": "MIT", + "dependencies": { + "devalue": "^5.6.1", + "memoize-weak": "^1.0.2", + "ts-deepmerge": "^7.0.3" + }, + "optionalDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@typeschema/class-validator": "^0.3.0", + "@valibot/to-json-schema": "^1.5.0", + "@vinejs/vine": "^3.0.1", + "arktype": "^2.1.29", + "class-validator": "^0.14.3", + "effect": "^3.19.12", + "joi": "^17.13.3", + "json-schema-to-ts": "^3.1.1", + "superstruct": "^2.0.2", + "typebox": "^1.0.62", + "valibot": "^1.2.0", + "yup": "^1.7.1", + "zod": "^4.1.13", + "zod-v3-to-json-schema": "^4.0.0" + }, + "peerDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@sveltejs/kit": "1.x || 2.x", + "@typeschema/class-validator": "^0.3.0", + "@vinejs/vine": "^1.8.0 || ^2.0.0 || ^3.0.0", + "arktype": ">=2.0.0-rc.23", + "class-validator": "^0.14.1", + "effect": "^3.13.7", + "joi": "^17.13.1", + "superstruct": "^2.0.2", + "svelte": "3.x || 4.x || >=5.0.0-next.51", + "typebox": "^1.0.36", + "valibot": "^1.2.0", + "yup": "^1.4.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@exodus/schemasafe": { + "optional": true + }, + "@typeschema/class-validator": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "arktype": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "effect": { + "optional": true + }, + "joi": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT", + "optional": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz", + "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typebox": { + "version": "1.0.79", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.0.79.tgz", + "integrity": "sha512-luG4ORaG70S8mK00kCQR3Ow2xXaqzHv03/PHJJGs9Mjg1Q+0k3Kwz6Z5ORjoqSwWl3ixDUKgJajZ2vvBGs3ECA==", + "license": "MIT", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-v3-to-json-schema": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/zod-v3-to-json-schema/-/zod-v3-to-json-schema-4.0.0.tgz", + "integrity": "sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w==", + "license": "ISC", + "optional": true, + "peerDependencies": { + "zod": "^3.25 || ^4.0.14" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..97abee0 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.1", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/postcss": "^4.1.18", + "@tailwindcss/typography": "^0.5.19", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "svelte": "^5.45.6", + "svelte-check": "^4.3.4", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.2.6" + }, + "dependencies": { + "@zerodevx/svelte-toast": "^0.9.6", + "sveltekit-superforms": "^2.29.1", + "zod": "^4.3.5" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..af9d8dc --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..42360de --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,135 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + background-color: #1a1b26; + color: #a9b1d6; + } + + body { + background-color: #1a1b26; + color: #a9b1d6; + font-family: 'Inter', system-ui, sans-serif; + } + + /* Scrollbar styling */ + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + background-color: #16161e; + } + + ::-webkit-scrollbar-thumb { + background-color: #292e42; + border-radius: 9999px; + } + + ::-webkit-scrollbar-thumb:hover { + background-color: #565f89; + } +} + +@layer components { + /* Custom button styles */ + .btn-primary { + background-color: #7aa2f7; + color: white; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.2s; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + } + + .btn-primary:hover { + background-color: #3b82f6; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + } + + .btn-secondary { + background-color: #292e42; + color: #a9b1d6; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.2s; + } + + .btn-secondary:hover { + background-color: #364a82; + } + + .btn-danger { + background-color: #f7768e; + color: white; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.2s; + } + + .btn-danger:hover { + background-color: #dc2626; + } + + /* Card styles */ + .card { + background-color: #16161e; + border: 1px solid #292e42; + border-radius: 0.75rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + transition: all 0.3s; + } + + .card:hover { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + } + + .card-header { + border-bottom: 1px solid #292e42; + padding: 1.5rem; + } + + .card-body { + padding: 1.5rem; + } + + /* Input styles */ + .input-field { + background-color: #1a1b26; + border: 1px solid #292e42; + border-radius: 0.5rem; + padding: 0.5rem 0.75rem; + color: #a9b1d6; + transition: all 0.2s; + } + + .input-field:focus { + outline: none; + ring: 2px; + ring-color: #7aa2f7; + border-color: transparent; + } + + .input-field::placeholder { + color: #565f89; + } + + /* Navigation styles */ + .nav-link { + color: #a9b1d6; + transition: color 0.2s; + } + + .nav-link:hover { + color: #7aa2f7; + } + + .nav-link.active { + color: #7aa2f7; + } +} \ No newline at end of file diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..93528a0 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,14 @@ + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..43337d6 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,170 @@ +import { showError } from './toast'; + +const API_BASE = import.meta.env.DEV ? 'http://localhost:8080/api' : '/api'; + +// Helper function to handle API responses and show error toasts +async function handleResponse(response: Response, errorMessage: string): Promise { + if (!response.ok) { + let message = errorMessage; + try { + const errorData = await response.json(); + if (errorData.error) { + message = errorData.error; + } + } catch (e) { + // If we can't parse JSON, use the default message + } + showError(message); + throw new Error(message); + } + return response.json(); +} + +export interface User { + id: string; + email: string; + username: string; +} + +export interface AuthResponse { + token: string; + user: User; +} + +export interface Ebook { + id: string; + title: string; + author: string | null; + isbn: string | null; + description: string | null; + file_path: string; + file_size: number | null; + mime_type: string | null; + cover_image_path: string | null; + created_at: string; + updated_at: string; +} + +export interface ReadingProgress { + ebook_id: string; + user_id: string; + current_page: number; + total_pages: number | null; + last_read_at: string; +} + +// Get stored token +function getToken(): string | null { + if (typeof window !== 'undefined') { + return localStorage.getItem('auth_token'); + } + return null; +} + +// Create headers with auth +function createHeaders(): Record { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + const token = getToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + return headers; +} + +// Auth functions +export async function registerUser(email: string, username: string, password: string): Promise { + const response = await fetch(`${API_BASE}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, username, password }), + }); + return handleResponse(response, 'Registration failed'); +} + +export async function loginUser(login: string, password: string): Promise { + const response = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ login, password }), + }); + return handleResponse(response, 'Login failed'); +} + +export async function getUserProfile(): Promise { + const response = await fetch(`${API_BASE}/auth/profile`, { + headers: createHeaders(), + }); + return handleResponse(response, 'Failed to get profile'); +} + +export async function fetchEbooks(limit: number = 20, offset: number = 0): Promise { + const response = await fetch(`${API_BASE}/ebooks?limit=${limit}&offset=${offset}`, { + headers: createHeaders(), + }); + return handleResponse(response, 'Failed to fetch ebooks'); +} + +export async function fetchEbook(id: string): Promise { + const response = await fetch(`${API_BASE}/ebooks/${id}`, { + headers: createHeaders(), + }); + return handleResponse(response, 'Failed to fetch ebook'); +} + +export async function createEbook(data: Partial): Promise { + const response = await fetch(`${API_BASE}/ebooks`, { + method: 'POST', + headers: createHeaders(), + body: JSON.stringify(data), + }); + return handleResponse(response, 'Failed to create ebook'); +} + +export async function updateEbook(id: string, data: Partial): Promise { + const response = await fetch(`${API_BASE}/ebooks/${id}`, { + method: 'PUT', + headers: createHeaders(), + body: JSON.stringify(data), + }); + return handleResponse(response, 'Failed to update ebook'); +} + +export async function deleteEbook(id: string): Promise { + const response = await fetch(`${API_BASE}/ebooks/${id}`, { + method: 'DELETE', + headers: createHeaders(), + }); + if (!response.ok) { + let message = 'Failed to delete ebook'; + try { + const errorData = await response.json(); + if (errorData.error) { + message = errorData.error; + } + } catch (e) { + // If we can't parse JSON, use the default message + } + showError(message); + throw new Error(message); + } +} + +export async function fetchReadingProgress(ebookId: string): Promise { + const response = await fetch(`${API_BASE}/ebooks/${ebookId}/progress`, { + headers: createHeaders(), + }); + return handleResponse(response, 'Failed to fetch reading progress'); +} + +export async function updateReadingProgress(ebookId: string, currentPage: number, totalPages?: number): Promise { + const response = await fetch(`${API_BASE}/ebooks/${ebookId}/progress`, { + method: 'PUT', + headers: createHeaders(), + body: JSON.stringify({ current_page: currentPage, total_pages: totalPages }), + }); + return handleResponse(response, 'Failed to update reading progress'); +} \ No newline at end of file diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts new file mode 100644 index 0000000..9bf2ce8 --- /dev/null +++ b/frontend/src/lib/auth.ts @@ -0,0 +1,53 @@ +import { writable } from 'svelte/store'; +import type { User } from './api'; + +export interface AuthState { + user: User | null; + token: string | null; + loading: boolean; +} + +function createAuthStore() { + const initialState: AuthState = { + user: null, + token: null, + loading: true, + }; + + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + login: (token: string, user: User) => { + if (typeof window !== 'undefined') { + localStorage.setItem('auth_token', token); + } + set({ user, token, loading: false }); + }, + logout: () => { + if (typeof window !== 'undefined') { + localStorage.removeItem('auth_token'); + } + set({ user: null, token: null, loading: false }); + }, + setLoading: (loading: boolean) => { + update(state => ({ ...state, loading })); + }, + initialize: () => { + if (typeof window !== 'undefined') { + const token = localStorage.getItem('auth_token'); + if (token) { + // Token exists, but we need to validate it by fetching profile + // For now, just set loading to false and let components handle + set({ user: null, token, loading: false }); + } else { + set({ user: null, token: null, loading: false }); + } + } else { + set({ user: null, token: null, loading: false }); + } + }, + }; +} + +export const authStore = createAuthStore(); \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/toast.ts b/frontend/src/lib/toast.ts new file mode 100644 index 0000000..f379d30 --- /dev/null +++ b/frontend/src/lib/toast.ts @@ -0,0 +1,21 @@ +import { toast } from '@zerodevx/svelte-toast'; + +export function showError(message: string) { + toast.push(message, { + theme: { + '--toastBackground': '#f7768e', + '--toastBarBackground': '#f7768e', + '--toastColor': '#1a1b26', + } + }); +} + +export function showSuccess(message: string) { + toast.push(message, { + theme: { + '--toastBackground': '#9ece6a', + '--toastBarBackground': '#9ece6a', + '--toastColor': '#1a1b26', + } + }); +} \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..09c2853 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,93 @@ + + + + + + +{#if auth.loading} +
+
+
+

Loading your library...

+
+
+{:else if !auth.token} + +
+
+
+

📚 Ebook Reader

+

Your personal digital library

+
+ +
+

Organize • Read • Enjoy

+
+
+
+{:else} + +
+
+
+
+

📚 Ebook Reader

+ + +
+
+ Hello, {auth.user?.username} + +
+
+
+
+ +
+ {@render children()} +
+{/if} + + + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..fd7ef72 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,105 @@ + + +
+
+

📚 Your Ebook Library

+

Discover and organize your digital reading collection

+
+ + {#if loading} +
+
+

Loading your ebooks...

+
+ {:else if error} +
+
+
⚠️
+

Oops! Something went wrong

+

{error}

+
+
+ {:else if ebooks.length === 0} +
+
+
📖
+

Welcome to your library!

+

You haven't added any ebooks yet. Start building your collection by uploading your favorite books.

+ +
+
+ {:else} +
+ {#each ebooks as ebook (ebook.id)} +
+
+ {#if ebook.cover_image_path} + {ebook.title} cover +
+ {:else} +
+
📚
+
No Cover
+
+ {/if} +
+
+

+ {ebook.title} +

+ {#if ebook.author} +

by {ebook.author}

+ {/if} + {#if ebook.description} +

{ebook.description}

+ {/if} +
+
+ {new Date(ebook.created_at).toLocaleDateString()} + {#if ebook.file_size} + {Math.round(ebook.file_size / 1024 / 1024)}MB + {/if} +
+
+
+
+ {/each} +
+ {/if} +
+ + diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..e2ea56c --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,105 @@ + + + + Login - Ebook Reader + + +
+
+
+
+
🔐
+

+ Welcome Back +

+

Sign in to access your library

+
+
+
+
+
+ + +
+
+ + +
+
+ + {#if error} +
+
+
⚠️
+
{error}
+
+
+ {/if} + + + + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/routes/register/+page.server.ts b/frontend/src/routes/register/+page.server.ts new file mode 100644 index 0000000..7d0c2b9 --- /dev/null +++ b/frontend/src/routes/register/+page.server.ts @@ -0,0 +1,38 @@ +import { superValidate } from 'sveltekit-superforms/server'; +import { z } from 'zod'; +import { registerUser } from '$lib/api'; +import { redirect, fail } from '@sveltejs/kit'; + +const schema = z.object({ + email: z.string().email('Invalid email address'), + username: z.string().min(3, 'Username must be at least 3 characters'), + password: z.string().min(8, 'Password must be at least 8 characters').regex(/^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, 'Password must include at least one uppercase letter, one number, and one symbol'), + confirmPassword: z.string(), +}).refine(data => data.password === data.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], +}); + +type FormData = z.infer; + +export const load = async () => { + const form = await superValidate(schema); + return { form }; +}; + +export const actions = { + default: async ({ request }) => { + const form = await superValidate(request, schema); + if (!form.valid) { + return fail(400, { form }); + } + const data = form.data as FormData; + try { + await registerUser(data.email, data.username, data.password); + throw redirect(302, '/login'); + } catch (err) { + if (err instanceof Response) throw err; + return fail(500, { form, error: err instanceof Error ? err.message : 'Registration failed' }); + } + }, +}; \ No newline at end of file diff --git a/frontend/src/routes/register/+page.svelte b/frontend/src/routes/register/+page.svelte new file mode 100644 index 0000000..0372e68 --- /dev/null +++ b/frontend/src/routes/register/+page.svelte @@ -0,0 +1,124 @@ + + + + Register - Ebook Reader + + +
+
+
+
+
+

+ Join Our Library +

+

Create your account to start reading

+
+
+
+
+
+ + + {#if $errors.email} +

{$errors.email}

+ {/if} +
+
+ + + {#if $errors.username} +

{$errors.username}

+ {/if} +
+
+ + + {#if $errors.password} +

{$errors.password}

+ {/if} +
+
+ + + {#if $errors.confirmPassword} +

{$errors.confirmPassword}

+ {/if} +
+
+ + {#if $page.data.error} +
+
+
⚠️
+
{$page.data.error}
+
+
+ {/if} + + + + +
+
+
+
+
\ No newline at end of file diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..c56f7eb --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,16 @@ +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }) + } +}; + +export default config; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..92524a4 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,45 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: { + colors: { + tokyo: { + bg: '#1a1b26', + 'bg-dark': '#16161e', + 'bg-highlight': '#292e42', + 'bg-selection': '#364a82', + fg: '#a9b1d6', + 'fg-dark': '#565f89', + 'fg-light': '#c0caf5', + red: '#f7768e', + orange: '#ff9e64', + yellow: '#e0af68', + green: '#9ece6a', + cyan: '#7dcfff', + blue: '#7aa2f7', + purple: '#bb9af7', + magenta: '#c0caf5', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, + animation: { + 'fade-in': 'fadeIn 0.5s ease-in-out', + 'slide-in': 'slideIn 0.3s ease-out', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideIn: { + '0%': { transform: 'translateY(-10px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' }, + }, + }, + }, + }, + plugins: [], +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/go.mod b/go.mod index 50a503f..e129aff 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module bookmann -go 1.25.5 +go 1.25 + +require github.com/labstack/echo/v4 v4.15.0 require ( - github.com/labstack/echo/v4 v4.15.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 6928997..32930de 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24= github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -6,6 +8,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -19,3 +25,5 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=