From 9e5e074bbe7ec0b00a86d65e6e7c79ff0e514556 Mon Sep 17 00:00:00 2001 From: tech08mag Date: Sun, 13 Sep 2026 12:59:16 +0200 Subject: [PATCH] Initial commit --- .gitignore | 11 + README.md | 128 ++ config/library.db | Bin 0 -> 4096 bytes config/library.db-shm | Bin 0 -> 32768 bytes config/library.db-wal | Bin 0 -> 94792 bytes docker-compose.yml | 34 + frontend/.env | 1 + frontend/.gitignore | 24 + frontend/.oxlintrc.json | 8 + frontend/Dockerfile | 20 + frontend/README.md | 114 ++ frontend/index.html | 13 + frontend/nginx.conf | 50 + frontend/package-lock.json | 1725 +++++++++++++++++++++++ frontend/package.json | 29 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.css | 184 +++ frontend/src/App.tsx | 48 + frontend/src/api/client.ts | 44 + frontend/src/assets/hero.png | Bin 0 -> 13057 bytes frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/components/ChapterList.tsx | 69 + frontend/src/components/Feedback.tsx | 21 + frontend/src/components/Header.tsx | 50 + frontend/src/components/MangaCard.tsx | 27 + frontend/src/components/MangaGrid.tsx | 29 + frontend/src/components/Reader.tsx | 161 +++ frontend/src/index.css | 1010 +++++++++++++ frontend/src/main.tsx | 10 + frontend/src/pages/HomePage.tsx | 61 + frontend/src/pages/MangaDetailPage.tsx | 139 ++ frontend/src/pages/ReaderPage.tsx | 33 + frontend/src/pages/SearchPage.tsx | 70 + frontend/src/store/useStore.ts | 52 + frontend/src/types/index.ts | 36 + frontend/tsconfig.app.json | 26 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 23 + frontend/vite.config.ts | 18 + 41 files changed, 4302 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/library.db create mode 100644 config/library.db-shm create mode 100644 config/library.db-wal create mode 100644 docker-compose.yml create mode 100644 frontend/.env create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/components/ChapterList.tsx create mode 100644 frontend/src/components/Feedback.tsx create mode 100644 frontend/src/components/Header.tsx create mode 100644 frontend/src/components/MangaCard.tsx create mode 100644 frontend/src/components/MangaGrid.tsx create mode 100644 frontend/src/components/Reader.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/HomePage.tsx create mode 100644 frontend/src/pages/MangaDetailPage.tsx create mode 100644 frontend/src/pages/ReaderPage.tsx create mode 100644 frontend/src/pages/SearchPage.tsx create mode 100644 frontend/src/store/useStore.ts create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e472ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +downloads/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c6fb7c --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# MangaDL + +A web UI for browsing and downloading manga using [ifetch](https://github.com/FelixSiegel/ifetch) as the backend. + +## Quick Start + +### Using Docker Compose (Recommended) + +```bash +docker-compose up -d +``` + +This will start: +- **ifetch backend** on port 8080 +- **Frontend** on port 3000 + +Access the web UI at `http://localhost:3000` + +### Manual Setup + +#### 1. Start the Backend (ifetch) + +```bash +# Using Docker +docker run -d \ + --name ifetch \ + -p 8080:8080 \ + -v ./downloads:/app/downloads \ + -v ./config:/app/config \ + ghcr.io/felixsiegel/ifetch:latest + +# Or build from source +git clone https://github.com/FelixSiegel/ifetch.git +cd ifetch +cargo build --release +./target/release/ifetch --server --port 8080 --output ./downloads --config ./config +``` + +#### 2. Start the Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +Access the web UI at `http://localhost:3000` + +## Features + +- 🔍 **Search manga** from MangaKatana +- 📚 **Browse manga details** - cover, description, genres, authors, status +- 📖 **Read chapters** with a full-screen reader +- ⬇️ **Download chapters** via the ifetch backend +- 🌓 **Dark/Light mode** support +- 📱 **Responsive design** - works on mobile and desktop +- ⌨️ **Keyboard navigation** in reader (←/→ for pages, F for fullscreen, Esc to exit) + +## Project Structure + +``` +MangaDL/ +├── docker-compose.yml # Docker Compose for full stack +├── downloads/ # Manga downloads (mounted to ifetch) +├── config/ # ifetch config (mounted to ifetch) +├── frontend/ # React + TypeScript frontend +│ ├── src/ +│ │ ├── api/ # API client +│ │ ├── components/ # React components +│ │ ├── pages/ # Page components +│ │ ├── store/ # Zustand state management +│ │ └── types/ # TypeScript types +│ ├── Dockerfile # Frontend Dockerfile +│ ├── nginx.conf # Nginx config for production +│ └── package.json +└── README.md +``` + +## API Endpoints Used + +The frontend consumes these ifetch API endpoints: + +- `GET /api/search?q={query}` - Search manga +- `GET /api/manga/{id}` - Get manga details +- `GET /api/manga/{id}/chapters` - Get chapters (triggers background download) +- `GET /api/chapter/{id}::{number}/pages` - Get page URLs for a chapter +- `GET /api/image/{id}::{number}/{filename}` - Serve chapter images + +## Keyboard Shortcuts (Reader) + +| Key | Action | +|-----|--------| +| ← / → | Previous / Next page | +| F | Toggle fullscreen | +| Esc | Exit fullscreen / Close reader | + +## Configuration + +### Backend (ifetch) + +Environment variables for ifetch: +- `IFETCH_PORT` - Server port (default: 8080) +- `IFETCH_THREADS` - Download threads (default: 1) +- `IFETCH_CRON_HOURS` - Auto-update interval in hours (default: 12) +- `DISCORD_WEBHOOK_URL` - Optional Discord notifications + +### Frontend + +Environment variables (`.env`): +- `VITE_API_BASE` - Backend API base URL (default: `http://localhost:8080`) + +## Development + +```bash +# Frontend development +cd frontend +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview +``` + +## License + +MIT \ No newline at end of file diff --git a/config/library.db b/config/library.db new file mode 100644 index 0000000000000000000000000000000000000000..4a3492ca9ee32d110ce3484c72386f9ff8651a2c GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYC*>iWj6^fNV2HHI9bB nXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDP#6LLGPMVA literal 0 HcmV?d00001 diff --git a/config/library.db-shm b/config/library.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..59b903f724682b6ac69e5a51efc30c999bb2c31a GIT binary patch literal 32768 zcmeI)y-I^I6b9h8t*!rBf2ti_MciG)`%ou0uf)N@Yw(@~N5REia1zv{v>*yL}0yW6L$*YktN!P&#%<@@Q|%gz1xkNe9} zlNgon=eL;tPJddIyVEV%D_QH>!)#vb+5G2N_y4wD*8~9q1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfIzDPoypq~t#+p`0`nB;W{{PvWh2|!&E7n(ayEg+1bSJ_W@9~Rhrs^`^s`jn{94U= zhS|zaMyW4<2?7KN5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ U009C72oNAZfB*pk1X>dK0%YbXSO5S3 literal 0 HcmV?d00001 diff --git a/config/library.db-wal b/config/library.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..0cf4b7cf306f19e4be24c2bec75aa65a3454630a GIT binary patch literal 94792 zcmeI*eQXnD0LSsWwhTtLbtWnfG`l!3EYO)^9CVq3GMYKT&AljZ(Q#YPt+ngT_U1MT zArPYpz6BNiLrF+PCV@o~V-ztFUx*3&#}~*FalS=IqJ#jEpo==6>uqc&H85DUj_;Fh zZJ$1Oz2|e?AAR{su|PmCVw3W{uL9vG~YY|Jn9UEnl1#Rr1*S<>#I_ zzH+E^XI;&TkfA9L>akYUP;L`jI~>vyMGT%jGhEE=T_aD#ePVxOo+o?gkcze) z&UXJQIdZm0Ia}Pl$r@3I00IagfB*srAb2_AbH%MPn-shg9LjVB;5I_I{1Q0*~ z0R#|0VEh6XRR!-)`E0}fudKYl0m*$}{O`gR2q1s}0tg_000IagfB*sr zAdqr_a_e#g@?~6(KwOXD(Grmt7`z}a@YKxGj+~Fzy+vLi<>!w#KmY**5I_I{1Q0*~ z0R#|0;9nNFATKZ|4#1WdnE&>`&er0#`^>z6!`&tF0^%110R#|0009ILKmY**5I_I{ z1SVEsi6hhJnI%sX|Lt$lqgt1zNpICWiHAkrMLyr{cbc_|S*s*!tE0_&DB7GK-nG6w z+&$ZwHBos1r`fT_Gi#K-jKxRpul&USfI8S<<^>$?Udi3-9+=pUvo{0~KmY**5I_I{ z1Q0*~0R#|8fxt|s?;nO39C=P(X1qg>1wCra@aWM#zwfo)vi z_`r`%kH1>?8sh?~J~`|k0R#|0009ILKmY**5I_Kd6boG1xPWbafuC~y-|qRP?-te< zNb$L0_Xr?>00IagfB*srAb{PTa?gsm@N%L^0?oxbDXUDvAQ1+MOo zC%s1i0R#|0009ILKmY**5SRo4f6EJOOFoalmKQko^dBEo?Rn*MD=*M#UXP%25=hVm z1Q0*~0R#|0009ILKmY**(n`Q@U5-E=_vHw(#q|hMm>0P9ozq{sUeCIhyg*ugTsRm6 z5I_I{1Q0*~0R#|0009KXFK`)ofzsbw`Zi@0=8_i}KQ~(-fB*srAb + + + + + + frontend + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..25ead4b --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,50 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header X-XSS-Protection "1; mode=block"; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to ifetch backend + location /api/ { + proxy_pass http://ifetch:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + + # Health check + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..846e6cd --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1725 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.20.0", + "lucide-react": "^1.45.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.3", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.82.0.tgz", + "integrity": "sha512-a3LB+C5Dsj5b/qtmG/mv5WrzuiXEpg1KF5nXWcEvaoN5TYAqkIvxPOwTPp3Jy/FoGpRo8zsTFhMElMXfeoOEzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.82.0.tgz", + "integrity": "sha512-OBlhRgNqFblGpGenno/aqOfJLOkQ2B8Ig3iDAalfn0H8hJGZKXPeexCRTDm6uwv6YUjSA9Xnwt1y/Bgj5ZH8uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.82.0.tgz", + "integrity": "sha512-dsopxqtY5ZdyT9uLHyGt1SyiLop6hi7hWI3PKpePodkRQOkLaCm+OE4fR9CAz9qdfjiFO8531tX/QDyP/psjFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.82.0.tgz", + "integrity": "sha512-94Lu0SgTClKColU66g1VDuigV3HkcbkJBnTtZjGYfE8UPugaWDgKrm2icjC6HJVUYler2OXaHP/X0TBy8+CowQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.82.0.tgz", + "integrity": "sha512-hne/V06ewhh1i0w8+l7GDNROAGCGPmyFuOwiP7YTRu0JycyStJ4785dmF8xU5p0uUwt2emvIF9vc7Xjis+cJ0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.82.0.tgz", + "integrity": "sha512-aWY2xtbZf1LneW9Qsv/n2Sp8gOu74JrlQzEtj4coHX2SHFrCfhmAumaU+sI/A5nr+yoTRTSmI/pL2s6ADlNSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.82.0.tgz", + "integrity": "sha512-Fe+TtXCXMh/5f7kWlZ2VAwsMumZWtraFlKVk1NJlL52/beGwfDE7ov+/8gVirHzWokzGu7X65hSPq0ucPDskWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.82.0.tgz", + "integrity": "sha512-6azCZ6OJudlvipNttXCCQcyeFfcJ/NvUZdSN1z8elo73kCHtyQC7WTiUcSjWYvJ1jaq9KDUyMAoAS/vNzhBomA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.82.0.tgz", + "integrity": "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.82.0.tgz", + "integrity": "sha512-D94em/BwknNTn4vqxjHh5wb2oL566eFhArabqKIr0cNZMHOJuiraFp1A8tXpH05bbE5tqwEfLXTI0MWEGtn3Dw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.82.0.tgz", + "integrity": "sha512-MOprxBaoYU2D4VgxXCl3ghydThWtx7Um1lL51kGYNeQ5Al7WzsH7/tqGdNtbLrIWnjq3bsm13+nz/gRIxjrOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.82.0.tgz", + "integrity": "sha512-5h55QsfJ/luDXZzC20k6SNOY1Az+dCP9WvntKtcUWh2JhckAdwApY2ZusaBTwLENnReXU+A2fJtSrYvZJNKNPg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.82.0.tgz", + "integrity": "sha512-IE8NJNLlHr0CaXyGJPGVn0eTkUyoj1I2UfA8x7I4PSOYKsQ/6btVC7Pywrj5onk0cMH25r6Z38SoN3AvE5Zuog==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.82.0.tgz", + "integrity": "sha512-XUUUxaBo9XKl+J1B9EmP1cTGQPddzeURvoGkfwh/94PGnbW+hBprDljneoI2M1jzC1bzrIV3ihc7iM9UXl8+tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.82.0.tgz", + "integrity": "sha512-SWLSFulX9TDuH6yvbPYp4+VNn6jkkIvvI+KiujDM5rWBRHEfkesCC/pCneIIUr6ovkxZ5fRtpi2v5Cz5FrMJZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.82.0.tgz", + "integrity": "sha512-BQy35f6ZUdNr9a6c7B7orxQTcLjByGT2z3WAgmRovpRwmPYAaJ+NTplmMzhdjdJ4qSchfMNZy/Ukg+qRg6zseQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.82.0.tgz", + "integrity": "sha512-V4QhSTg5gctZue8RJjsGi7NpQPThr/p1/HfmiMC5kfe1KFEup9SQRVub4A6kijQjdHfxj7bLL1KO3QO7/5bwMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.82.0.tgz", + "integrity": "sha512-TUSCLaKB2yktpFAJ/r3HAUYsaV/3DT7JS4iNKyoh3a9YNwD0UG7Ezh4D8m23654vQcU6P/RQrCAjRPKe4peP/A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.82.0.tgz", + "integrity": "sha512-VTVoRIWJTb+wvUX8EYoPArfFH02whuR10goFXE/LHRRX33ajRrFgqbcONXZMiF4C5rnattfkm87HqYn8jb8hmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": 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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.45.0.tgz", + "integrity": "sha512-yH1ubCAduho9UR7oJhRXIQXogksRILBiTuZC4/bQIGeB9JOkxMlSuEHyyZpo1Z3S0yWJO2KTSUZbjiNvVxeOUw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.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.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "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/oxlint": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.82.0.tgz", + "integrity": "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.82.0", + "@oxlint/binding-android-arm64": "1.82.0", + "@oxlint/binding-darwin-arm64": "1.82.0", + "@oxlint/binding-darwin-x64": "1.82.0", + "@oxlint/binding-freebsd-x64": "1.82.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", + "@oxlint/binding-linux-arm-musleabihf": "1.82.0", + "@oxlint/binding-linux-arm64-gnu": "1.82.0", + "@oxlint/binding-linux-arm64-musl": "1.82.0", + "@oxlint/binding-linux-ppc64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-gnu": "1.82.0", + "@oxlint/binding-linux-riscv64-musl": "1.82.0", + "@oxlint/binding-linux-s390x-gnu": "1.82.0", + "@oxlint/binding-linux-x64-gnu": "1.82.0", + "@oxlint/binding-linux-x64-musl": "1.82.0", + "@oxlint/binding-openharmony-arm64": "1.82.0", + "@oxlint/binding-win32-arm64-msvc": "1.82.0", + "@oxlint/binding-win32-ia32-msvc": "1.82.0", + "@oxlint/binding-win32-x64-msvc": "1.82.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "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.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "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/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d24c38f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.20.0", + "lucide-react": "^1.45.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.3", + "zustand": "^5.0.15" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..6b9d610 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,48 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Header } from './components/Header'; +import { HomePage } from './pages/HomePage'; +import { SearchPage } from './pages/SearchPage'; +import { MangaDetailPage } from './pages/MangaDetailPage'; +import { ReaderPage } from './pages/ReaderPage'; +import { useStore } from './store/useStore'; +import { checkServerHealth } from './api/client'; +import { useEffect } from 'react'; + +function AppRoutes() { + const { setServerConnected } = useStore(); + + useEffect(() => { + const checkConnection = async () => { + const connected = await checkServerHealth(); + setServerConnected(connected); + }; + checkConnection(); + const interval = setInterval(checkConnection, 30000); + return () => clearInterval(interval); + }, [setServerConnected]); + + return ( + + } /> + } /> + } /> + } /> + } /> + + ); +} + +function App() { + return ( + +
+
+
+ +
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..ac59811 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; +import type { Manga, Chapter, SearchResult, ChapterPages } from '../types'; + +const API_BASE = import.meta.env.VITE_API_BASE; +const baseURL = API_BASE && API_BASE !== '' ? API_BASE : (typeof window !== 'undefined' ? window.location.origin : ''); + +const api = axios.create({ + baseURL, + timeout: 30000, +}); + +export const searchManga = async (query: string): Promise => { + const response = await api.get('/api/search', { params: { q: query } }); + return response.data; +}; + +export const getMangaDetails = async (id: string): Promise => { + const response = await api.get(`/api/manga/${id}`); + return response.data; +}; + +export const getMangaChapters = async (id: string): Promise => { + const response = await api.get(`/api/manga/${id}/chapters`); + return response.data; +}; + +export const getChapterPages = async (chapterId: string): Promise => { + const response = await api.get(`/api/chapter/${chapterId}/pages`); + return response.data; +}; + +export const getImageUrl = (chapterId: string, filename: string): string => { + const base = API_BASE && API_BASE !== '' ? API_BASE : (typeof window !== 'undefined' ? window.location.origin : ''); + return `${base}/api/image/${chapterId}/${filename}`; +}; + +export const checkServerHealth = async (): Promise => { + try { + await api.get('/api/search', { params: { q: 'test' }, timeout: 5000 }); + return true; + } catch { + return false; + } +}; \ No newline at end of file diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/ChapterList.tsx b/frontend/src/components/ChapterList.tsx new file mode 100644 index 0000000..4cc9462 --- /dev/null +++ b/frontend/src/components/ChapterList.tsx @@ -0,0 +1,69 @@ +import { useStore } from '../store/useStore'; +import type { Chapter } from '../types'; +import { Download, Check, Loader2 } from 'lucide-react'; + +interface ChapterListProps { + chapters: Chapter[]; + mangaId: string; + existingChapters: Chapter[]; + isDownloading: boolean; +} + +export function ChapterList({ chapters, mangaId, existingChapters, isDownloading }: ChapterListProps) { + const { setSelectedChapter, setChapterPages, setCurrentPageIndex, selectedChapter } = useStore(); + + const existingChapterNumbers = new Set(existingChapters.map((c) => c.number)); + const isExisting = (chapter: Chapter) => existingChapterNumbers.has(chapter.number); + + const handleChapterClick = async (chapter: Chapter) => { + if (!isExisting(chapter)) return; + + setSelectedChapter(chapter); + setCurrentPageIndex(0); + + try { + const apiBase = import.meta.env.VITE_API_BASE; + const base = apiBase && apiBase !== '' ? apiBase : window.location.origin; + const response = await fetch(`${base}/api/chapter/${mangaId}::${chapter.number}/pages`); + const data = await response.json(); + setChapterPages(data.pages || []); + window.location.href = `/reader/${mangaId}/${chapter.number}`; + } catch (error) { + console.error('Failed to load chapter pages:', error); + } + }; + + const sortedChapters = [...chapters].sort((a, b) => a.number - b.number); + + return ( +
+

Chapters ({chapters.length})

+
+ {sortedChapters.map((chapter) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Feedback.tsx b/frontend/src/components/Feedback.tsx new file mode 100644 index 0000000..b9fe067 --- /dev/null +++ b/frontend/src/components/Feedback.tsx @@ -0,0 +1,21 @@ +export function Loading() { + return ( +
+
+

Loading...

+
+ ); +} + +export function Error({ message, onRetry }: { message: string; onRetry?: () => void }) { + return ( +
+

{message}

+ {onRetry && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..44f2a34 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,50 @@ +import { useStore } from '../store/useStore'; +import { Search, Server, ServerOff, Loader2 } from 'lucide-react'; +import { useEffect } from 'react'; +import { checkServerHealth } from '../api/client'; + +export function Header() { + const { searchQuery, setSearchQuery, isSearching, serverConnected, setServerConnected } = useStore(); + + useEffect(() => { + const checkConnection = async () => { + const connected = await checkServerHealth(); + setServerConnected(connected); + }; + checkConnection(); + const interval = setInterval(checkConnection, 30000); + return () => clearInterval(interval); + }, [setServerConnected]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + window.location.href = `/search?q=${encodeURIComponent(searchQuery.trim())}`; + } + }; + + return ( +
+
+

MangaDL

+
+ + setSearchQuery(e.target.value)} + placeholder="Search manga..." + className="search-input" + disabled={isSearching} + /> + {isSearching && } + +
+ + {serverConnected ? : } + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/MangaCard.tsx b/frontend/src/components/MangaCard.tsx new file mode 100644 index 0000000..dd9623c --- /dev/null +++ b/frontend/src/components/MangaCard.tsx @@ -0,0 +1,27 @@ +import { Link } from 'react-router-dom'; +import type { SearchResult } from '../types'; +import { BookOpen } from 'lucide-react'; + +interface MangaCardProps { + manga: SearchResult; +} + +export function MangaCard({ manga }: MangaCardProps) { + return ( + +
+ {manga.cover_url ? ( + {manga.title} + ) : ( +
+ +
+ )} + {manga.status} +
+
+

{manga.title}

+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/components/MangaGrid.tsx b/frontend/src/components/MangaGrid.tsx new file mode 100644 index 0000000..23688b4 --- /dev/null +++ b/frontend/src/components/MangaGrid.tsx @@ -0,0 +1,29 @@ +import type { SearchResult } from '../types'; +import { MangaCard } from './MangaCard'; + +interface MangaGridProps { + mangas: SearchResult[]; + title?: string; + emptyMessage?: string; +} + +export function MangaGrid({ mangas, title, emptyMessage = 'No manga found' }: MangaGridProps) { + if (mangas.length === 0) { + return ( +
+

{emptyMessage}

+
+ ); + } + + return ( +
+ {title &&

{title}

} +
+ {mangas.map((manga) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Reader.tsx b/frontend/src/components/Reader.tsx new file mode 100644 index 0000000..07a5a3e --- /dev/null +++ b/frontend/src/components/Reader.tsx @@ -0,0 +1,161 @@ +import { useStore } from '../store/useStore'; +import { ChevronLeft, ChevronRight, Home, Fullscreen, X } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +export function Reader() { + const { chapterPages, currentPageIndex, setCurrentPageIndex, selectedChapter, selectedManga } = useStore(); + const [isFullscreen, setIsFullscreen] = useState(false); + const [showControls, setShowControls] = useState(true); + const controlsTimeoutRef = useRef | null>(null); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') goToPrevious(); + if (e.key === 'ArrowRight') goToNext(); + if (e.key === 'Escape') exitFullscreen(); + if (e.key === 'f') toggleFullscreen(); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const goToPrevious = () => { + if (currentPageIndex > 0) { + setCurrentPageIndex(currentPageIndex - 1); + } + }; + + const goToNext = () => { + if (currentPageIndex < chapterPages.length - 1) { + setCurrentPageIndex(currentPageIndex + 1); + } + }; + + const toggleFullscreen = async () => { + if (!isFullscreen) { + try { + await document.documentElement.requestFullscreen(); + setIsFullscreen(true); + } catch (err) { + console.error('Fullscreen failed:', err); + } + } else { + exitFullscreen(); + } + }; + + const exitFullscreen = () => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } + setIsFullscreen(false); + }; + + const resetControlsTimeout = () => { + setShowControls(true); + if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); + controlsTimeoutRef.current = setTimeout(() => setShowControls(false), 3000); + }; + + useEffect(() => { + resetControlsTimeout(); + return () => { + if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); + }; + }, [currentPageIndex, chapterPages.length]); + + if (chapterPages.length === 0 || !selectedChapter) { + return ( +
+
+

Loading chapter...

+
+ ); + } + + const currentPage = chapterPages[currentPageIndex]; + const apiBase = import.meta.env.VITE_API_BASE; + const base = apiBase && apiBase !== '' ? apiBase : window.location.origin; + const imageUrl = `${base}${currentPage}`; + + return ( +
+
+
+ + {selectedManga?.title} + Ch. {selectedChapter.number} +
+
+ + {currentPageIndex + 1} / {chapterPages.length} + + +
+
+ +
e.stopPropagation()}> + + +
+ {`Page +
+ + +
+ +
+
+ + +
+
+ {chapterPages.map((_, index) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..bfee4b8 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,1010 @@ +:root { + --primary: #6366f1; + --primary-hover: #4f46e5; + --primary-light: #eef2ff; + --secondary: #64748b; + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --bg: #f8fafc; + --bg-card: #ffffff; + --text: #1e293b; + --text-muted: #64748b; + --border: #e2e8f0; + --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --radius: 8px; + --radius-lg: 12px; + --transition: 150ms ease; +} + +@media (prefers-color-scheme: dark) { + :root { + --primary: #818cf8; + --primary-hover: #6366f1; + --primary-light: #1e1b4b; + --secondary: #94a3b8; + --success: #34d399; + --warning: #fbbf24; + --error: #f87171; + --bg: #0f172a; + --bg-card: #1e293b; + --text: #f1f5f9; + --text-muted: #94a3b8; + --border: #334155; + --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3); + } +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + height: 100%; + width: 100%; + max-width: none; + border: none; + text-align: left; +} + +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.main-content { + flex: 1; + padding: 24px; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +/* Header */ +.header { + background: var(--bg-card); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; + box-shadow: var(--shadow); +} + +.header-content { + max-width: 1400px; + margin: 0 auto; + padding: 16px 24px; + display: flex; + align-items: center; + gap: 24px; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary); + letter-spacing: -0.5px; +} + +.search-form { + flex: 1; + max-width: 500px; + position: relative; +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; + width: 18px; + height: 18px; +} + +.search-input { + width: 100%; + padding: 10px 16px 10px 44px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); + font-size: 14px; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-light); +} + +.search-input::placeholder { + color: var(--text-muted); +} + +.search-loader { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--primary); + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: translateY(-50%) rotate(0deg); } + to { transform: translateY(-50%) rotate(360deg); } +} + +.server-status { + display: flex; + align-items: center; + gap: 8px; +} + +.status-online { + color: var(--success); +} + +.status-offline { + color: var(--error); +} + +/* Page */ +.page { + animation: fadeIn 200ms ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.page-header { + margin-bottom: 24px; +} + +.page-header h1 { + font-size: 1.75rem; + font-weight: 600; + color: var(--text); +} + +.search-query { + color: var(--text-muted); + margin-top: 4px; +} + +/* Manga Grid */ +.manga-grid-container { + margin-bottom: 32px; +} + +.grid-title { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 16px; + color: var(--text); +} + +.manga-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 16px; +} + +.manga-card { + display: block; + text-decoration: none; + color: inherit; + border-radius: var(--radius-lg); + overflow: hidden; + background: var(--bg-card); + border: 1px solid var(--border); + transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition); +} + +.manga-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); + border-color: var(--primary); +} + +.manga-card-cover { + position: relative; + aspect-ratio: 2/3; + overflow: hidden; + background: var(--bg); +} + +.manga-card-cover img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 300ms ease; +} + +.manga-card:hover .manga-card-cover img { + transform: scale(1.05); +} + +.placeholder-cover { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); +} + +.placeholder-cover-large { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); +} + +.manga-status { + position: absolute; + top: 8px; + right: 8px; + padding: 2px 8px; + border-radius: 9999px; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + background: rgba(0, 0, 0, 0.7); + color: white; + backdrop-filter: blur(4px); +} + +.manga-card-info { + padding: 12px; +} + +.manga-title { + font-size: 0.875rem; + font-weight: 500; + color: var(--text); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Empty State */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 24px; + color: var(--text-muted); + text-align: center; +} + +.empty-state p { + margin: 0; +} + +/* Manga Detail */ +.manga-detail-header { + margin-bottom: 24px; +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + padding: 8px 12px; + border-radius: var(--radius); + transition: color var(--transition), background var(--transition); +} + +.back-link:hover { + color: var(--text); + background: var(--bg); +} + +.manga-detail-content { + display: grid; + grid-template-columns: 280px 1fr; + gap: 32px; + margin-bottom: 32px; +} + +.manga-cover-large { + border-radius: var(--radius-lg); + overflow: hidden; + aspect-ratio: 2/3; + background: var(--bg); + box-shadow: var(--shadow-lg); +} + +.manga-cover-large img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.manga-info { + display: flex; + flex-direction: column; +} + +.manga-title-large { + font-size: 2rem; + font-weight: 700; + margin-bottom: 8px; + line-height: 1.2; +} + +.manga-alt-names { + color: var(--text-muted); + font-size: 0.9375rem; + margin-bottom: 16px; +} + +.manga-meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 24px; +} + +.meta-item { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + font-size: 0.875rem; +} + +.meta-item svg { + color: var(--secondary); +} + +.manga-description { + margin-bottom: 24px; +} + +.manga-description h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 8px; +} + +.manga-description p { + color: var(--text-muted); + line-height: 1.6; +} + +.manga-actions { + display: flex; + gap: 12px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 24px; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + border: none; + cursor: pointer; + transition: all var(--transition); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background: var(--primary-hover); +} + +.btn-secondary { + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--bg-card); + border-color: var(--primary); +} + +/* Chapter List */ +.chapter-list { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.chapter-list-title { + padding: 16px 20px; + font-size: 1rem; + font-weight: 600; + border-bottom: 1px solid var(--border); + background: var(--bg); +} + +.chapters { + max-height: 500px; + overflow-y: auto; +} + +.chapter-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 12px 20px; + border: none; + background: transparent; + text-align: left; + cursor: pointer; + transition: background var(--transition); + border-bottom: 1px solid var(--border); +} + +.chapter-item:last-child { + border-bottom: none; +} + +.chapter-item:hover:not(.unavailable):not(:disabled) { + background: var(--bg); +} + +.chapter-item.unavailable { + opacity: 0.5; + cursor: not-allowed; +} + +.chapter-item.active { + background: var(--primary-light); +} + +.chapter-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.chapter-number { + font-weight: 500; + color: var(--text); +} + +.chapter-label { + font-size: 0.75rem; + color: var(--text-muted); +} + +.chapter-status { + display: flex; + align-items: center; +} + +.status-icon { + color: var(--text-muted); + transition: color var(--transition); +} + +.chapter-item.available .status-icon { + color: var(--success); +} + +.chapter-item.unavailable .status-icon { + color: var(--warning); +} + +.status-icon.spinning { + animation: spin 1s linear infinite; +} + +/* Reader */ +.reader-container { + height: 100vh; + width: 100vw; + background: #000; + color: white; + position: fixed; + top: 0; + left: 0; + z-index: 200; + display: flex; + flex-direction: column; +} + +.reader-header { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 16px 24px; + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(to bottom, rgba(0,0,0,0.7), transparent); + transition: opacity 200ms ease; + z-index: 10; +} + +.reader-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.reader-header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.manga-title { + font-size: 1rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.chapter-title { + font-size: 0.875rem; + color: rgba(255,255,255,0.7); +} + +.page-indicator { + font-size: 0.875rem; + font-variant-numeric: tabular-nums; + color: rgba(255,255,255,0.8); +} + +.icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + border-radius: 50%; + background: rgba(255,255,255,0.1); + color: white; + cursor: pointer; + transition: background var(--transition); + backdrop-filter: blur(8px); +} + +.icon-btn:hover:not(:disabled) { + background: rgba(255,255,255,0.2); +} + +.icon-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.reader-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} + +.nav-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 60px; + height: 60px; + border: none; + border-radius: 50%; + background: rgba(0,0,0,0.4); + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background var(--transition), transform var(--transition); + z-index: 5; +} + +.nav-btn:hover:not(.disabled) { + background: rgba(0,0,0,0.6); + transform: translateY(-50%) scale(1.1); +} + +.nav-btn.prev { + left: 24px; +} + +.nav-btn.next { + right: 24px; +} + +.nav-btn.disabled { + opacity: 0; + pointer-events: none; +} + +.reader-image-wrapper { + max-width: 100%; + max-height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.reader-image { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border-radius: 4px; + box-shadow: 0 0 40px rgba(0,0,0,0.5); +} + +.reader-footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 16px 24px; + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); + transition: opacity 200ms ease; + z-index: 10; +} + +.reader-controls { + display: flex; + gap: 8px; +} + +.page-thumbnails { + display: flex; + gap: 4px; + overflow-x: auto; + padding: 4px; + max-width: 60%; +} + +.thumbnail { + min-width: 36px; + height: 36px; + border: none; + border-radius: var(--radius); + background: rgba(255,255,255,0.1); + color: rgba(255,255,255,0.7); + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition); + flex-shrink: 0; +} + +.thumbnail:hover { + background: rgba(255,255,255,0.2); + color: white; +} + +.thumbnail.active { + background: var(--primary); + color: white; +} + +.reader-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: #000; + color: white; + gap: 16px; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(255,255,255,0.1); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +/* Loading & Error */ +.loading-container, +.error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 24px; + gap: 16px; + color: var(--text-muted); +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.error-message { + color: var(--error); +} + +.retry-btn { + padding: 10px 20px; + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius); + font-weight: 500; + cursor: pointer; + transition: background var(--transition); +} + +.retry-btn:hover { + background: var(--primary-hover); +} + +/* Home Page */ +.hero { + text-align: center; + padding: 80px 24px; + max-width: 800px; + margin: 0 auto; +} + +.hero h1 { + font-size: 3.5rem; + font-weight: 800; + background: linear-gradient(135deg, var(--primary), #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 16px; +} + +.hero-subtitle { + font-size: 1.25rem; + color: var(--text-muted); + margin-bottom: 40px; +} + +.hero-search { + position: relative; + max-width: 500px; + margin: 0 auto 48px; +} + +.search-icon-large { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + width: 24px; + height: 24px; +} + +.search-placeholder { + display: block; + width: 100%; + padding: 16px 20px 16px 56px; + border: 2px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-card); + color: var(--text-muted); + font-size: 1rem; + text-decoration: none; + text-align: left; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.search-placeholder:hover { + border-color: var(--primary); + box-shadow: 0 0 0 4px var(--primary-light); + color: var(--text); +} + +.hero-features { + display: flex; + justify-content: center; + gap: 32px; + flex-wrap: wrap; +} + +.feature { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + color: var(--text); + font-weight: 500; + transition: border-color var(--transition), box-shadow var(--transition); +} + +.feature:hover { + border-color: var(--primary); + box-shadow: var(--shadow); +} + +.feature svg { + color: var(--primary); +} + +.server-status-banner { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 16px; + margin: 0 auto 48px; + max-width: 800px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + font-size: 0.875rem; + color: var(--text-muted); +} + +.server-status-banner .online { + color: var(--success); +} + +.server-status-banner .offline { + color: var(--error); +} + +.getting-started { + max-width: 800px; + margin: 0 auto; + padding: 32px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); +} + +.getting-started h2 { + font-size: 1.25rem; + margin-bottom: 16px; +} + +.getting-started ol { + padding-left: 24px; + color: var(--text-muted); + line-height: 2; +} + +.getting-started code { + background: var(--bg); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.875rem; +} + +/* Responsive */ +@media (max-width: 1024px) { + .manga-detail-content { + grid-template-columns: 1fr; + max-width: 400px; + } + + .manga-cover-large { + max-width: 280px; + margin: 0 auto; + } + + .hero h1 { + font-size: 2.5rem; + } + + .hero-features { + gap: 16px; + } +} + +@media (max-width: 768px) { + .main-content { + padding: 16px; + } + + .header-content { + padding: 12px 16px; + gap: 16px; + } + + .search-form { + max-width: none; + } + + .manga-grid { + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 12px; + } + + .nav-btn { + width: 44px; + height: 44px; + } + + .nav-btn.prev { + left: 12px; + } + + .nav-btn.next { + right: 12px; + } + + .page-thumbnails { + max-width: 100%; + } +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg); +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..71af4d5 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,61 @@ +import { Link } from 'react-router-dom'; +import { Search, BookOpen, Download, Server, Library } from 'lucide-react'; +import { useStore } from '../store/useStore'; + +export function HomePage() { + const { serverConnected } = useStore(); + + return ( +
+
+
+

MangaDL

+

Browse, read, and download manga from MangaKatana

+ +
+ + + Search for manga... + +
+ +
+
+ + Browse Manga +
+
+ + Download Chapters +
+
+ + Read Online +
+
+ + Self-hosted Backend +
+
+
+
+ +
+ + + Backend: {serverConnected ? 'Connected' : 'Disconnected'} + {!serverConnected && ' - Start ifetch server on port 8080'} + +
+ +
+

Getting Started

+
    +
  1. Run ifetch --server --port 8080 to start the backend server
  2. +
  3. Search for manga using the search bar above
  4. +
  5. Click on a manga to view chapters and start reading
  6. +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/MangaDetailPage.tsx b/frontend/src/pages/MangaDetailPage.tsx new file mode 100644 index 0000000..d267ebf --- /dev/null +++ b/frontend/src/pages/MangaDetailPage.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { useStore } from '../store/useStore'; +import { getMangaDetails, getMangaChapters, getImageUrl } from '../api/client'; +import { ChapterList } from '../components/ChapterList'; +import { Loading, Error } from '../components/Feedback'; +import { BookOpen, Calendar, Tag, User, ArrowLeft } from 'lucide-react'; +import type { Manga, Chapter } from '../types'; + +export function MangaDetailPage() { + const { id } = useParams<{ id: string }>(); + const { + setSelectedManga, + setChapters, + chapters, + isLoading, setIsLoading, + setError, + serverConnected + } = useStore(); + + const [manga, setManga] = useState(null); + const [existingChapters, setExistingChapters] = useState([]); + const [isDownloading] = useState(false); + const [localError, setLocalError] = useState(null); + + useEffect(() => { + if (!id) return; + + const fetchManga = async () => { + setIsLoading(true); + setLocalError(null); + setError(null); + + try { + const [mangaData, chaptersData] = await Promise.all([ + getMangaDetails(id), + getMangaChapters(id) + ]); + + setManga(mangaData); + setSelectedManga(mangaData); + setChapters(chaptersData); + setExistingChapters(chaptersData); + } catch { + const message = 'Failed to load manga'; + setLocalError(message); + setError(message); + } finally { + setIsLoading(false); + } + }; + + fetchManga(); + }, [id, setSelectedManga, setChapters, setIsLoading, setError]); + + if (!id) return null; + + if (isLoading && !manga) return ; + if (localError && !manga) return window.location.reload()} />; + + if (!manga) return null; + + const coverUrl = manga.cover_url ? getImageUrl(manga.id, manga.cover_url.split('/').pop() || '') : null; + + return ( +
+
+ + + Back + +
+ +
+
+ {coverUrl ? ( + {manga.title} + ) : ( +
+ +
+ )} +
+ +
+

{manga.title}

+ + {manga.alt_names.length > 0 && ( +

{manga.alt_names.join(', ')}

+ )} + +
+ {manga.status && ( + + + {manga.status} + + )} + {manga.genres.length > 0 && ( + + + {manga.genres.join(', ')} + + )} + {manga.authors.length > 0 && ( + + + {manga.authors.join(', ')} + + )} +
+ + {manga.description && ( +
+

Description

+

{manga.description}

+
+ )} + +
+ + + View All Chapters + +
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/ReaderPage.tsx b/frontend/src/pages/ReaderPage.tsx new file mode 100644 index 0000000..303db03 --- /dev/null +++ b/frontend/src/pages/ReaderPage.tsx @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useStore } from '../store/useStore'; +import { getChapterPages } from '../api/client'; +import { Reader } from '../components/Reader'; +import { Loading } from '../components/Feedback'; + +export function ReaderPage() { + const { mangaId, chapterNum } = useParams<{ mangaId: string; chapterNum: string }>(); + const { setChapterPages, setCurrentPageIndex, selectedManga, selectedChapter } = useStore(); + + useEffect(() => { + if (!mangaId || !chapterNum) return; + + const fetchPages = async () => { + try { + const chapterId = `${mangaId}::${chapterNum}`; + const data = await getChapterPages(chapterId); + setChapterPages(data.pages || []); + setCurrentPageIndex(0); + } catch (error) { + console.error('Failed to load chapter pages:', error); + } + }; + + fetchPages(); + }, [mangaId, chapterNum, setChapterPages, setCurrentPageIndex]); + + if (!mangaId || !chapterNum) return null; + if (!selectedManga || !selectedChapter) return ; + + return ; +} \ No newline at end of file diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx new file mode 100644 index 0000000..c4925df --- /dev/null +++ b/frontend/src/pages/SearchPage.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useStore } from '../store/useStore'; +import { searchManga } from '../api/client'; +import { MangaGrid } from '../components/MangaGrid'; +import { Loading, Error } from '../components/Feedback'; +import type { SearchResult } from '../types'; + +export function SearchPage() { + const [searchParams] = useSearchParams(); + const query = searchParams.get('q') || ''; + const { setSearchResults, setIsSearching, setError, setSearchQuery } = useStore(); + const [localResults, setLocalResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setLocalError] = useState(null); + + useEffect(() => { + setSearchQuery(query); + }, [query, setSearchQuery]); + + const performSearch = async (searchQuery: string) => { + if (!searchQuery.trim()) return; + + setIsLoading(true); + setIsSearching(true); + setLocalError(null); + setError(null); + + try { + const results = await searchManga(searchQuery); + setLocalResults(results); + setSearchResults(results); + } catch { + const message = 'Search failed'; + setLocalError(message); + setError(message); + } finally { + setIsLoading(false); + setIsSearching(false); + } + }; + + useEffect(() => { + if (query) { + performSearch(query); + } else { + setLocalResults([]); + setSearchResults([]); + } + }, [query]); + + return ( +
+
+

Search Results

+ {query &&

for “{query}”

} +
+ + {isLoading && } + {error && performSearch(query)} />} + + {!isLoading && !error && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/store/useStore.ts b/frontend/src/store/useStore.ts new file mode 100644 index 0000000..c7d7c2f --- /dev/null +++ b/frontend/src/store/useStore.ts @@ -0,0 +1,52 @@ +import { create } from 'zustand'; +import type { Manga, Chapter, SearchResult } from '../types'; + +interface AppState { + searchQuery: string; + setSearchQuery: (query: string) => void; + searchResults: SearchResult[]; + setSearchResults: (results: SearchResult[]) => void; + isSearching: boolean; + setIsSearching: (loading: boolean) => void; + selectedManga: Manga | null; + setSelectedManga: (manga: Manga | null) => void; + chapters: Chapter[]; + setChapters: (chapters: Chapter[]) => void; + selectedChapter: Chapter | null; + setSelectedChapter: (chapter: Chapter | null) => void; + chapterPages: string[]; + setChapterPages: (pages: string[]) => void; + currentPageIndex: number; + setCurrentPageIndex: (index: number) => void; + isLoading: boolean; + setIsLoading: (loading: boolean) => void; + error: string | null; + setError: (error: string | null) => void; + serverConnected: boolean; + setServerConnected: (connected: boolean) => void; +} + +export const useStore = create((set) => ({ + searchQuery: '', + setSearchQuery: (query) => set({ searchQuery: query }), + searchResults: [], + setSearchResults: (results) => set({ searchResults: results }), + isSearching: false, + setIsSearching: (loading) => set({ isSearching: loading }), + selectedManga: null, + setSelectedManga: (manga) => set({ selectedManga: manga }), + chapters: [], + setChapters: (chapters) => set({ chapters }), + selectedChapter: null, + setSelectedChapter: (chapter) => set({ selectedChapter: chapter }), + chapterPages: [], + setChapterPages: (pages) => set({ chapterPages: pages }), + currentPageIndex: 0, + setCurrentPageIndex: (index) => set({ currentPageIndex: index }), + isLoading: false, + setIsLoading: (loading) => set({ isLoading: loading }), + error: null, + setError: (error) => set({ error }), + serverConnected: false, + setServerConnected: (connected) => set({ serverConnected: connected }), +})); \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..a757c3f --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,36 @@ +export interface Manga { + id: string; + title: string; + url: string; + cover_url: string; + status: string; + description: string; + genres: string[]; + authors: string[]; + alt_names: string[]; +} + +export interface Chapter { + number: number; + label: string; + url: string; +} + +export interface SearchResult { + id: string; + title: string; + cover_url: string; + status: string; +} + +export interface MangaWithChapters { + manga: Manga; + chapters: Chapter[]; + existingChapters: Chapter[]; + missingChapters: Chapter[]; + isDownloading: boolean; +} + +export interface ChapterPages { + pages: string[]; +} \ No newline at end of file diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..bab40a0 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..6661e50 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, + define: { + 'import.meta.env.VITE_API_BASE': JSON.stringify(''), + }, +}) \ No newline at end of file