From 63925333872cecabb8c58a030f441e8d4eb53fe8 Mon Sep 17 00:00:00 2001 From: sbilketay Date: Mon, 10 Nov 2025 00:14:49 +0300 Subject: [PATCH] first commit --- .env.example | 14 + .gitignore | 63 + Dockerfile | 7 + doc/api.md | 173 + docker-compose.yml | 65 + frontend/.env.example | 0 frontend/.gitignore | 24 + frontend/.npmrc | 1 + frontend/Dockerfile | 7 + frontend/README.md | 16 + frontend/eslint.config.js | 29 + frontend/index.html | 13 + frontend/package.json | 37 + frontend/public/asset/apple.png | Bin 0 -> 612 bytes frontend/public/asset/google.png | Bin 0 -> 1623 bytes frontend/public/vite.svg | 1 + frontend/src/App.css | 384 ++ frontend/src/App.jsx | 57 + frontend/src/assets/apple.png | Bin 0 -> 84815 bytes frontend/src/assets/booklibra-logo.png | Bin 0 -> 199639 bytes frontend/src/assets/booklibra-logo.svg | 702 +++ frontend/src/assets/booklibra-logo_2.svg | 4616 ++++++++++++++++++++ frontend/src/assets/google.png | Bin 0 -> 17720 bytes frontend/src/assets/pngwing.apple.png | Bin 0 -> 84815 bytes frontend/src/assets/react.svg | 1 + frontend/src/components/BookCard.jsx | 33 + frontend/src/components/SocialButton.css | 37 + frontend/src/components/SocialButton.jsx | 23 + frontend/src/context/SavedBooksContext.jsx | 44 + frontend/src/index.css | 23 + frontend/src/main.jsx | 10 + frontend/src/pages/AddBooksPage.jsx | 210 + frontend/src/pages/HomePage.jsx | 7 + frontend/src/pages/MyBooksPage.jsx | 60 + frontend/src/pages/ProfilePage.jsx | 7 + frontend/vite.config.js | 15 + package.json | 38 + src/app.js | 34 + src/config/env.js | 37 + src/config/services.js | 49 + src/config/socket.js | 32 + src/controllers/authController.js | 41 + src/controllers/bookController.js | 156 + src/middleware/auth.js | 17 + src/middleware/errorHandler.js | 13 + src/middleware/notFound.js | 6 + src/middleware/requestLogger.js | 37 + src/routes/authRoutes.js | 12 + src/routes/bookRoutes.js | 14 + src/server.js | 17 + src/services/amazonService.js | 143 + src/services/cacheService.js | 76 + src/services/userService.js | 80 + src/utils/isbn.js | 47 + src/utils/logger.js | 69 + src/utils/mediaCache.js | 65 + src/utils/request.js | 31 + src/validators/authValidators.js | 14 + 58 files changed, 7707 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 doc/api.md create mode 100644 docker-compose.yml create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/asset/apple.png create mode 100644 frontend/public/asset/google.png create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/apple.png create mode 100644 frontend/src/assets/booklibra-logo.png create mode 100644 frontend/src/assets/booklibra-logo.svg create mode 100644 frontend/src/assets/booklibra-logo_2.svg create mode 100644 frontend/src/assets/google.png create mode 100644 frontend/src/assets/pngwing.apple.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/BookCard.jsx create mode 100644 frontend/src/components/SocialButton.css create mode 100644 frontend/src/components/SocialButton.jsx create mode 100644 frontend/src/context/SavedBooksContext.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/AddBooksPage.jsx create mode 100644 frontend/src/pages/HomePage.jsx create mode 100644 frontend/src/pages/MyBooksPage.jsx create mode 100644 frontend/src/pages/ProfilePage.jsx create mode 100644 frontend/vite.config.js create mode 100644 package.json create mode 100644 src/app.js create mode 100644 src/config/env.js create mode 100644 src/config/services.js create mode 100644 src/config/socket.js create mode 100644 src/controllers/authController.js create mode 100644 src/controllers/bookController.js create mode 100644 src/middleware/auth.js create mode 100644 src/middleware/errorHandler.js create mode 100644 src/middleware/notFound.js create mode 100644 src/middleware/requestLogger.js create mode 100644 src/routes/authRoutes.js create mode 100644 src/routes/bookRoutes.js create mode 100644 src/server.js create mode 100644 src/services/amazonService.js create mode 100644 src/services/cacheService.js create mode 100644 src/services/userService.js create mode 100644 src/utils/isbn.js create mode 100644 src/utils/logger.js create mode 100644 src/utils/mediaCache.js create mode 100644 src/utils/request.js create mode 100644 src/validators/authValidators.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..23fd975 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +NODE_ENV=development +PORT=8080 +GEMINI_API_KEY=your_gemini_key +ALLOWED_ORIGINS=* +REQUEST_LOGGING=true +REDIS_URL=redis://redis:6379 +ISBN_CACHE_TTL_SECONDS=21600 +JWT_SECRET=please_change_me +JWT_EXPIRES_IN=1h +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=bookibra +POSTGRES_USER=bookibra +POSTGRES_PASSWORD=bookibra diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8e5b3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Node.js +node_modules/ +.svelte-kit/ +.serena/ +.claude/ +.vscode +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +package-lock.json +.pnpm-debug.log + +# Build output +/build +/.svelte-kit +/dist +/public/build +/.output +/cache + +# Environment files +.env +.env.* +!.env.example + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*.sublime-project +*.sublime-workspace + +# OS generated files +.DS_Store +Thumbs.db + +# TypeScript +*.tsbuildinfo + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# Misc +coverage/ +.cache/ +.sass-cache/ +.eslintcache +.stylelintcache + +# SvelteKit specific +.vercel +.netlify + +# Database files +*.db +db/*.db +yakit_takip.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ecfd841 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install --legacy-peer-deps +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..a3c3746 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,173 @@ +# Bookibra API Dokumani + +## Genel Bakis +Bookibra, Amazon kaynakli kitap verilerini ISBN, kitap adi ve yayin tarihi filtreleri ile sunan bir backend servisidir. Servis, gemini tabanli aciklama uretimi, Socket.IO altyapisi ve Redis/PostgreSQL baglanti noktalarini Docker uzerinden hazir halde getirir. ISBN tabanli tum sorgular otomatik olarak Redis uzerinde cache'lenir ve tekrar eden istekler Amazon'a gitmeden hizlica cevaplanir. + +## Calistirma +1. `.env` dosyanizi olusturun veya mevcut dosyada gerekli alanlari doldurun. Ornek icin `.env.example` goz atabilirsiniz. `ISBN_CACHE_TTL_SECONDS` degiskeni ile Redis cache omrunu saniye cinsinden ayarlayabilirsiniz (varsayilan 21600 sn / 6 saat). JWT tabanli oturum icin `JWT_SECRET` ve `JWT_EXPIRES_IN` alanlarini doldurun. +2. Docker ortamini acmak icin proje kok dizininde `docker compose up --build` komutunu calistirin. +3. API varsayilan olarak `http://localhost:8080`, React frontend ise `http://localhost:5173` adresinde yayin yapar. + +> Not: Docker ortamina eklenen Redis (6379) ve PostgreSQL (5432) servisleri su anda veri saklama icin zorunlu degildir, ancak ileride kullanima hazir sekilde ayaga kalkar. Socket.IO, backend konteyneri icinde hazirlanmistir ve istemci baglantilarini kabul eder. + +## Ortak Parametreler +Her endpoint icin asagidaki sorgu parametreleri desteklenir: + +| Parametre | Tip | Varsayilan | Aciklama | +|-----------|-----|------------|----------| +| `locales` | string | `en,tr` | `en`, `tr` veya `en,tr` seklinde virgulle ayrilmis diller. Her dil icin ayri Amazon kaynagi kullanilir. | +| `withGemini` | boolean | `false` | `true` oldugunda GEMINI_API_KEY kullanilarak kitap aciklamasi Gemini ile yeniden uretilir. | +| `limit` | integer | `3` | Liste bazli aramalarda donulecek maksimum kayit sayisi. ISBN aramasinda dikkate alinmaz. | + +`withGemini=true` parametresi kullanilirsa `.env` icinde `GEMINI_API_KEY` tanimli olmalidir, aksi halde API 400 hatasi dondurur. + +### Standart Yanıt Alanları +- `cacheHit`: (boolean) Yalnızca ISBN endpointinde görünür. Yanıt Redis'ten geldiyse `true` olur. +- `normalizedIsbn`: (string) Arama yaparken Amazon'a gönderilen normalize edilmis ISBN (ör: `605842285X` -> `9786058422854`). Kullanıcı girişi `isbn` alanında korunur. +- `cachedThumbnail`: (string) Thumbnail görselinin proje kokundaki `cache/` dizininde saklanan göreli yolu. Frontend bu yolu kullanarak yereldeki görseli isteyebilir. +- `cachedAt`: (ISO tarih) Cache'e kaydedildiği zamanı gösterir. Cache'ten dönülen yanıtta da aynı değer gönderilir. + +## Kimlik Dogrulama Endpointleri + +### 0. Kullanici Kaydi +- **URL:** `POST /api/auth/register` +- **Body:** `{ "email": "user@example.com", "password": "enAz6Karakter" }` +- **Donus:** `{ "token": "", "user": { "id": "...", "email": "..." } }` + +### 0.1 Giris +- **URL:** `POST /api/auth/login` +- **Body:** Kayit endpointi ile ayni. +- **Donus:** Basarili olursa JWT ve temel kullanici bilgileri. + +### 0.2 Profil +- **URL:** `GET /api/auth/profile` +- **Header:** `Authorization: Bearer ` +- **Donus:** `{ "user": { "id": "...", "email": "..." } }` + +> Not: Tum JWT'ler `.env` icindeki `JWT_SECRET` ile uretilir. Varsayilan sure `JWT_EXPIRES_IN` degiskeniyle (or. `1h`, `2d`) belirlenir. + +## Endpointler + +### 1. ISBN ile Arama +- **URL:** `GET /api/books/isbn/:isbn` +- **Aciklama:** Verilen ISBN numarasi icin ingilizce (`amazon.com`) ve turkce (`amazon.com.tr`) kayitlarindan detaya ulasir. Ilk sorgu Amazon'dan alinip Redis'e kaydedilir; takip eden ayni parametreli sorgular cache'ten servis edilir. +- **Ornek Istek:** + ```http + GET /api/books/isbn/9789750719383?locales=en,tr&withGemini=true HTTP/1.1 + Host: localhost:8080 + ``` +- **Ornek Donus:** + ```json + { + "isbn": "9789750719383", + "locales": ["en", "tr"], + "includeGemini": true, + "cacheHit": false, + "cachedAt": "2024-01-10T12:34:56.789Z", + "data": { + "en": { + "title": "Book Title", + "authorName": "Author", + "description": "Gemini tarafindan iyilestirilmis aciklama", + "date": "September 5, 2023", + "publisher": "Publisher", + "rate": "4.7", + "thumbImage": "https://..." + }, + "tr": { + "title": "Kitap Basligi", + "description": "Gemini aciklamasi", + "date": "5 Eylul 2023", + "publisher": "Yayinevi" + } + } + } + ``` +- **Hata Durumlari:** + - 400: ISBN formati hatali veya Gemini anahtari eksik. + - 404: Amazon tarafinda kayit bulunamazsa ilgili dil icin `error` alaninda bilgi gelir. + +### 2. Kitap Adi ile Arama +- **URL:** `GET /api/books/title` +- **Gerekli Parametre:** `title` +- **Aciklama:** Amazon arama sayfalarindan belirlenen `limit` kadar kaydi bulur, her biri icin detay sayfasina giderek bilgileri toplar. Bu endpointte cache mekanizması devrede değildir. +- **Ornek Istek:** + ```http + GET /api/books/title?title=atomic%20habits&limit=2&locales=en HTTP/1.1 + Host: localhost:8080 + ``` +- **Ornek Donus:** + ```json + { + "title": "atomic habits", + "locales": ["en"], + "limit": 2, + "includeGemini": false, + "data": [ + { + "locale": "en", + "items": [ + { + "asin": "0593189647", + "locale": "en", + "title": "Atomic Habits", + "authorName": "James Clear", + "date": "October 16, 2018", + "publisher": "Avery", + "rate": "4.8", + "summary": { + "asin": "0593189647", + "title": "Atomic Habits", + "price": "$16.20", + "image": "https://...", + "author": "James Clear" + } + } + ] + } + ] + } + ``` + +### 3. Kitap Adi + Tarih Filtreli Arama +- **URL:** `GET /api/books/filter` +- **Gerekli Parametreler:** `title`, `published` +- **Aciklama:** Ilgili kitabi basliktan arar, donen kayitlari `published` degerini icerenler ile sinirlar. `published` alanina yil (`2020`) veya tam tarih (`2020-05-01`, `May 2020`, `Eylul 2020`) verilebilir. Bu endpointte de cache mekanizması devre dışıdır. +- **Ornek Istek:** + ```http + GET /api/books/filter?title=lord%20of%20the%20rings&published=1954&locales=en,tr HTTP/1.1 + Host: localhost:8080 + ``` +- **Ornek Donus:** `title` endpointi ile ayni yapida olur ancak `items` listesi sadece `published` bilgisini iceren kayitlari tutar. + +## Health Check +- **URL:** `GET /health` +- **Aciklama:** Servisin ayakta oldugunu dogrulamak icin kullanilir. + +## Socket.IO +- Sunucu baglantilarini kabul eder ve `connection:ack` etkinligi ile basit bir karsilama mesaji gonderir. +- Ileride frontend istemcileri bu kanal uzerinden bildirim alabilir. + +## Redis & Thumbnail Cache ve Loglama +- ISBN sorgulari `book:isbn:{ISBN}:loc={lokaller}:gem={flag}` formundaki anahtarlarla varsayilan olarak 6 saat (21600 sn) boyunca Redis'te saklanir. Sure `.env` icindeki `ISBN_CACHE_TTL_SECONDS` degiskeniyle degistirilebilir. Yeni bir sorgu cache olusturdukten sonra tum parametre uyumlu talepler direk Redis'ten 3xx/4xx/5xx durumlarina bakilmaksizin cevaplanir. +- Amazon'dan donen her `thumbImage` url'si indirilip proje kokundaki `cache/` klasorune `{isbn}_{locale}_thumbnail.jpg/png` formatinda kaydedilir. Aynı istek tekrarlandiginda yerel dosya kullanilir ve API yanitina `cachedThumbnail` alanı olarak eklenir. Frontend bu yolu kullanarak dosyayi servis edebilir. +- Docker konsolunda `🚀`, `📦` gibi emojili ve chalk tabanli renkli loglar gorunur. Her istegin suresi parantez icinde gosterilir (ornegin `(152.3ms)`), cache'den gelen yanitlarda `[cache-hit 📦]`, Amazon'a gidilenlerde `[cache-miss 🆕]` etiketi yer alir. +- Redis veya thumbnail indirme surecinde olusan uyari/hata mesajlari da renklendirilmis bicimde loglanir; boylece sistem sagligi anlik olarak izlenebilir. + +## Frontend (React) +- `frontend/` dizininde Vite + React 18 tabanli bir login ekranı bulunur. Google/Apple sosyal butonlari ve Booklibra baykus logosu ile uyumlu pastel renk paleti kullanir. +- Varsayilan olarak tarayicinin bulundugu hostname uzerinden `:8080` portuna istek atar. Farkli bir domain kullanacaksaniz `VITE_API_BASE_URL` degiskenini `http(s)://...` formatinda build aninda girin. +- Gelistirme icin `cd frontend && npm run dev -- --host` komutu ile uygulamayi baslatabilirsiniz. + +## Altyapi +- **Redis:** `redis://redis:6379` adresinde, `docker-compose` ile otomatik calisir. +- **PostgreSQL:** Varsayilan `bookibra` veritabani ve kullanicisi ile ayaga kalkar. +- **Gemini:** `.env` dosyasina `GEMINI_API_KEY` eklendiginde aciklama zenginlestirme ozelligi aktive olur. + +## Hata Mesajlari +- Tum hatalar JSON formatinda `message` ve `code` alanlariyla dondurulur. +- Uygulama `NODE_ENV=development` iken `stack` bilgisi eklenir. + +## Gelistirme Ipuclari +- Lokal gelistirme icin `npm run dev` komutu Socket.IO ve API'yi hot-reload modunda calistirir. +- Test yazmadiginiz sürece `npm test` komutu sadece bos bir mesaj basar ancak CI hataya dusmez. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..862b517 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +services: + frontend: + build: ./frontend + container_name: bookibra_frontend + restart: unless-stopped + ports: + - "5173:5173" + depends_on: + api: + condition: service_started + volumes: + - ./frontend:/app + - frontend-node_modules:/app/node_modules + command: sh -c "npm install && npm run dev -- --host 0.0.0.0 --port 5173" + + api: + build: . + container_name: bookibra_api + restart: unless-stopped + env_file: + - .env + ports: + - "8080:8080" + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + volumes: + - ./:/app + command: npm run dev + + redis: + image: redis:7-alpine + container_name: bookibra_redis + restart: unless-stopped + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + postgres: + image: postgres:15-alpine + container_name: bookibra_postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-bookibra} + POSTGRES_USER: ${POSTGRES_USER:-bookibra} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-bookibra} + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bookibra}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres-data: + frontend-node_modules: diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..ecfd841 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install --legacy-peer-deps +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..cee1e2c --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c20fbd3 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4d714f1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.0", + "color-thief-react": "^2.1.0", + "framer-motion": "^12.23.24", + "prop-types": "^15.8.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-image-glow": "^1.0.6", + "react-qr-barcode-scanner": "^2.1.18", + "react-router-dom": "^7.9.5" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "vite": "^7.1.7" + } +} diff --git a/frontend/public/asset/apple.png b/frontend/public/asset/apple.png new file mode 100644 index 0000000000000000000000000000000000000000..37e50d49eedb97f7347c8108adede00b8f07cbb5 GIT binary patch literal 612 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VAA$!md}|O$G)`sN%H}@m22zMY?9AcSKm7K{g0~elt)v5ni-TfE%m(def#}e`%nHn z{5|b$9@uu?P-1`Yc;tim7l}PfI}-CvoIi*u#a&`Zk4+ZnTpRbXF^olH~DN}&UJea08-GA!!eR2CInOm?+G{ZF+au~2N9#a4s z2ozk;Wb;RR`qUlM)p=o}Kx;Z9pw>KyWoTb$H}9!CbC0Hw1NSYBlf7Vvid_kuCC+QG z%!@&H0@&@k4PqyrU6xsP?egX4NBEy_)A1|ke=c`!Ufi{1KV=Rq^ib%=-k4@Ci9TnAyMG4biODN(ChVPgg&ebxsLQ01c@SAOHXW literal 0 HcmV?d00001 diff --git a/frontend/public/asset/google.png b/frontend/public/asset/google.png new file mode 100644 index 0000000000000000000000000000000000000000..662d505580a97a0c9c6a4a05ab924ba86417cc23 GIT binary patch literal 1623 zcmV-d2B`UoP)ql4fPsQY6{Qeqr59R+^iJvCW@pCC5Y*jS+P$2eIWy;c$!62dZfF1R`)}tm zdjJj&4h{|u4h|L}z#(Yedt1x(Gt#>y)Y~Vxg>0vaBCtR@&0+{F5>Jy50?WkHAcW-? zq>*?#g|O^GP9WY6Aw*aJCz5Z~_b#f!P9@&TXee3)xQYB?+EWw(+(v#e?IJFKTgfk$ zeMALtJNd=1lb8UkkWboPA_A~RK1sU^7JyaqN!eeJ0IZWwf`_~X;N+9x!;+~84#EO( z?YdCjR zK}WZO{t*@95e*a58iIZS<#PlqDHpJ=!E5}F(vaD_ktq#34n&P-8qyGTDX6hZ#QQG> z;q#=v9+!b{&&p`)NFXMgKPN{uOhq;Hj{v8xC`PS#b&VgJSNn|OeHRPn3fT2*0J{#w zKm(|X$C99c-5JOq7*(Aw8fBAQ(xcG;s*_&Mu4h$0-kS9?J@Pf{(~cN+=>>K zuPD33^{Emu>L{oO5nZt z6lodZZd{d0#_%l-$1Y@<@Uy@~M8l~o$$$UWyP!nU{xJa+UC`=!uTg;ikM{F2l7t7Z z-=9w!>FE4b1--*$BFhy5JYHw+NRD6r&s$01_W{Lt&;%+{&?C!I1sEDrabv>%^uUAw z;ecRjJ-deJNDoX1aQ}Vg5&FOg7r-)9mYTmpzJo3&!gc||{>&3@tibjzDCjq*phS+1 z%0r7q?0Y?w*Y)?b$MNGO@*S6BB0$bC81mCGPsWA-Q8~M)a4T#VU?Rg3VZ~f_1n|zZ zf@8&m+`y>0<*c5cn*f)FRBZTYde-;p!nVB~h8?)qikR&YFfyg#cG`kTV--6^z;*$2 zTo_1=f`%#)a{_FnAu0s;`LEPAP!#mJdN!hf3ITrlQ!y=;q_2)KDS%Z!Tr?TcOw-T$ za^Z%RwvQ8uU?v`^h=99GhH6^R_2x!DDoWj53#bvGcUZ;I&eT!ayg35)yc{&PC>9ER zy?M)%bnJx4NdPN9!D_X|g`0&Bh-!5uigK z54?9cZki3%x1oQ&J%k;ba!W~fcG(56Z(GO^GE?p8vw3YeYiq3Brm~`1#NN%BQ;&Wh zKxdzVGd&8f{H|IDFPz^r;rW4iP*ej2kmvTkAupz!_4V>>n;BvHy6&lDlTI zt({ai+KLK%eOkuR&Lo~|_TuR#^WM?fpx=-obtUoLFEW)oEpm1%yKh-qzECvw5$VrM zD@4o>3&ze0JzAm(%^-YmOf%}kZ~ZDR4l2g2N>(D5&XMW}$OL`2wEl1lL?`~_JI%t$ z;Cu6v^jJAb&j~_EESx1tqYx5{r>Ul+m7ga}4_f(&+Vr8-rDw`>aBy&NaB#pL`~$_R VUP7cBs$~EG002ovPDHLkV1ff;{$2n8 literal 0 HcmV?d00001 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..c628794 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,384 @@ +:root { + font-family: 'DM Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: #1f1f1f; + background: #f8f5f0; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: #f8f5f0; +} + +.mobile-shell { + max-width: 420px; + margin: 0 auto; + min-height: 100vh; + display: flex; + flex-direction: column; + background: #fefefe; + box-shadow: 0 12px 35px rgba(69, 52, 35, 0.12); +} + +.page-container { + flex: 1; + width: 100%; + padding: 1.5rem 1.5rem 8rem; + display: flex; + align-items: flex-start; + justify-content: center; +} + +.page-heading { + text-align: center; +} + +.page-heading h1 { + margin: 0; + font-size: 2.4rem; + letter-spacing: 0.03em; + color: #413122; +} + +.add-books-page { + width: 100%; + display: flex; + flex-direction: column; + gap: 1.2rem; + min-height: 100%; +} + +.search-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + position: sticky; + top: 0.25rem; + background: #fefefe; + padding-bottom: 1rem; + z-index: 2; +} + +.search-field { + display: flex; + align-items: center; + border-radius: 18px; + border: 1px solid rgba(0, 0, 0, 0.08); + background: #fff; + padding: 0.6rem 0.75rem; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05); +} + +.search-field input { + flex: 1; + border: none; + outline: none; + background: transparent; + font-size: 1rem; +} + +.search-field button { + border: none; + background: transparent; + color: #f2994a; + font-size: 1.2rem; +} + +.primary-btn { + border: none; + border-radius: 18px; + padding: 0.9rem; + font-size: 1rem; + font-weight: 600; + color: #fff; + background: linear-gradient(135deg, #f2994a, #f46b45); + cursor: pointer; +} + +.ghost-copy { + text-align: center; + color: rgba(0, 0, 0, 0.15); + font-size: 1.8rem; + letter-spacing: 0.1em; +} + +.status-text { + text-align: center; + font-weight: 600; +} + +.status-text.loading { + color: #555; +} + +.status-text.error { + color: #c0392b; +} + +.status-text.info { + color: #857c72; +} + +.book-list { + display: flex; + flex-direction: column; + gap: 0.9rem; + width: 100%; +} + +.book-card { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.8rem; + align-items: flex-start; + background: #fff; + padding: 1rem; + border-radius: 22px; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.08); + border: none; + text-align: left; + cursor: pointer; + outline: none; +} + +.book-card::after { + content: ''; + width: 100%; + height: 3px; + border-radius: 999px; + background: linear-gradient(90deg, #f2994a, #f46b45); + opacity: 0.5; + display: block; + margin-top: 0.2rem; +} + +.book-card:focus-visible { + box-shadow: 0 0 0 3px rgba(242, 153, 74, 0.35); +} + +.book-thumb { + width: 72px; + height: 108px; + object-fit: cover; + border-radius: 14px; + box-shadow: 0 12px 25px rgba(0, 0, 0, 0.18); + align-self: center; +} + +.book-info h3 { + margin: 0; + font-size: 1.05rem; + color: #1f1f1f; +} + +.book-info { + width: 100%; +} + +.book-meta { + margin: 0.2rem 0; + color: #8b8175; + font-size: 0.9rem; +} + +.mybooks-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.mybooks-detail { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.back-btn { + border: none; + background: transparent; + color: #4f3823; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.detail-hero { + border-radius: 28px; + overflow: hidden; + min-height: 220px; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem 0; + background: linear-gradient(135deg, #f0eade, #f9f4ec); +} + +.detail-hero-glow { + width: 100%; + display: flex; + justify-content: center; +} + +.detail-hero-img { + width: 130px; + border-radius: 16px; + box-shadow: 0 20px 45px rgba(0, 0, 0, 0.25); +} + +.placeholder-thumb { + width: 110px; + height: 150px; + border-radius: 16px; + border: 1px dashed rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + color: rgba(0, 0, 0, 0.5); + font-weight: 600; +} + +.detail-body { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.book-description { + color: #5c5046; + line-height: 1.6; +} + +.secondary-btn { + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 14px; + padding: 0.85rem; + background: #fff; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + cursor: pointer; +} + +.bottom-nav { + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%); + width: min(420px, 100%); + background: #ffffff; + border-top-left-radius: 24px; + border-top-right-radius: 24px; + box-shadow: 0 -10px 35px rgba(0, 0, 0, 0.12); + padding: 0.65rem 1rem; + display: grid; + grid-template-columns: repeat(4, 1fr); + z-index: 20; +} + +.nav-item { + border: none; + background: transparent; + color: #a6a6a6; + font-weight: 600; + font-size: 0.8rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.4rem 0; + text-decoration: none; +} + +.nav-item svg { + font-size: 1.1rem; +} + +.nav-item.active { + color: #f2994a; +} + +.scanner-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.85); + z-index: 50; +} + +.scanner-fullscreen { + position: relative; + width: 100%; + height: 100%; +} + +.scanner-fullscreen canvas, +.scanner-fullscreen video { + width: 100% !important; + height: 100% !important; + object-fit: cover; +} + +.scanner-overlay-line { + position: absolute; + left: 10%; + right: 10%; + top: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #4ade80, transparent); + box-shadow: 0 0 12px rgba(74, 222, 128, 0.8); +} + +.scanner-overlay-info { + position: absolute; + bottom: 36px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.55); + color: #fff; + padding: 0.8rem 1.2rem; + border-radius: 16px; + text-align: center; + display: flex; + gap: 0.75rem; + align-items: center; +} + +.scanner-overlay-info button { + border: none; + background: rgba(255, 255, 255, 0.15); + color: #fff; + padding: 0.3rem 0.8rem; + border-radius: 999px; +} + +.save-toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 0.5rem 1rem; + border-radius: 16px; + font-weight: 600; + z-index: 70; +} + +@media (max-width: 480px) { + .mobile-shell { + max-width: 100%; + box-shadow: none; + } + + .page-container { + padding: 1rem 1rem 8rem; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..f20c3ca --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,57 @@ +import { BrowserRouter, NavLink, Route, Routes, useLocation } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faHouse, faBookOpen, faPlusCircle, faUser } from '@fortawesome/free-solid-svg-icons'; +import HomePage from './pages/HomePage.jsx'; +import MyBooksPage from './pages/MyBooksPage.jsx'; +import AddBooksPage from './pages/AddBooksPage.jsx'; +import ProfilePage from './pages/ProfilePage.jsx'; +import { SavedBooksProvider } from './context/SavedBooksContext.jsx'; +import './App.css'; + +const tabs = [ + { path: '/', label: 'Home', icon: faHouse }, + { path: '/my-books', label: 'My Books', icon: faBookOpen }, + { path: '/add-books', label: 'Add Books', icon: faPlusCircle }, + { path: '/profile', label: 'Profile', icon: faUser } +]; + +function BottomNav() { + const location = useLocation(); + + return ( + + ); +} + +function App() { + return ( + + +
+
+ + } /> + } /> + } /> + } /> + +
+ +
+
+
+ ); +} + +export default App; diff --git a/frontend/src/assets/apple.png b/frontend/src/assets/apple.png new file mode 100644 index 0000000000000000000000000000000000000000..2f98743b6de78fa85ea0efbf5f710ebc523cc17d GIT binary patch literal 84815 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