mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Frontend Components
|
||||
|
||||
**Status:** ⏳ In Development
|
||||
|
||||
React components for ReadMeABook UI built with Next.js 14+, TypeScript, and Tailwind CSS.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/app/
|
||||
├── (auth)/login/
|
||||
├── (user)/page.tsx, search/, requests/, profile/
|
||||
├── (admin)/admin/
|
||||
└── setup/
|
||||
|
||||
src/components/
|
||||
├── audiobooks/ # Audiobook display
|
||||
├── requests/ # Request cards, status
|
||||
├── layout/ # Header, nav, footer
|
||||
└── ui/ # Reusable primitives
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
**Layout**
|
||||
- **Header** - Top nav, search input, user menu
|
||||
- **Sidebar** - Admin side nav
|
||||
- **Footer** - Version, links
|
||||
|
||||
**Audiobooks**
|
||||
- **AudiobookCard** ✅ - Cover, title, author, narrator, duration, request button, clickable to open details modal. Shows "Requested by [username]" when someone else has requested the book, "Requested" when current user has requested it
|
||||
- **AudiobookGrid** - Responsive grid (1/2/3/4 cols)
|
||||
- **AudiobookDetailsModal** ✅ - Full-screen modal with comprehensive metadata (description, genres, rating, release date, narrator, request functionality). Shows requesting user's name when applicable
|
||||
|
||||
**Requests**
|
||||
- **RequestCard** ✅ - Cover, title, author, status badge, progress bar, timestamps, action buttons (cancel, manual search, interactive search)
|
||||
- **StatusBadge** - Color-coded status (pending=yellow, searching=blue, downloading=purple, downloaded=green, processing=orange, available=green, completed=green, failed=red, warn=orange, cancelled=gray). Shows "Initializing..." when downloading with 0% progress (fetching torrent info), "Downloading" when progress > 0%
|
||||
- **ProgressBar** - Animated fill with percentage
|
||||
- **InteractiveTorrentSearchModal** ✅ - Responsive table of ranked torrent results, uses ConfirmModal for downloads, hides columns on smaller screens (size on mobile, seeds on tablet, indexer on desktop)
|
||||
- Active indicator: "Setting up..." with spinner when progress = 0%, "Active" with pulsing dot when progress > 0%
|
||||
|
||||
**Forms**
|
||||
- **SearchBar** - Debounced input with suggestions
|
||||
- **Button** - Variants (primary/secondary/outline/ghost/danger), sizes (sm/md/lg), loading state
|
||||
- **Input** - Label, error display, validation, icons
|
||||
- **Select** - Custom styling, search/filter
|
||||
- **Modal** ✅ - Dialog overlay with backdrop, sizes (sm/md/lg/xl/full), ESC to close, body scroll lock
|
||||
- **ConfirmModal** ✅ - Confirmation dialog with customizable title, message, buttons, loading state, and variant (primary/danger)
|
||||
- **Pagination** ✅ - Traditional page navigation with prev/next buttons, smart ellipsis (shows 1...4 5 6...10)
|
||||
- **StickyPagination** ✅ - Minimal floating pill at bottom center with prev/next arrows, quick jump input, section label. Shows/hides based on section visibility (IntersectionObserver). Rounded-full design, backdrop blur, subtle shadow, auto-scroll on page change
|
||||
|
||||
**Auth**
|
||||
- **ProtectedRoute** ✅ - Auth check, loading state, redirects, admin role support
|
||||
- **LoginPage** ✅ - Full-screen design, floating covers, Plex OAuth popup
|
||||
|
||||
**Admin**
|
||||
- **MetricCard** - Icon, label, value, trend
|
||||
- **DataTable** - Sorting, filtering, pagination
|
||||
- **Chart** - Line/bar/pie
|
||||
|
||||
## Pages Implemented ✅
|
||||
|
||||
**Homepage** (`/`)
|
||||
- Popular Audiobooks and New Releases sections with distinct visual separation
|
||||
- Sticky section headers with rounded-2xl design matching section card aesthetic
|
||||
- Gradient accent bars for each section (blue/purple for Popular, emerald/teal for New Releases)
|
||||
- Headers use rounded cards (bg-white/90 dark:bg-gray-800/90) with backdrop blur
|
||||
- Section content wrapped in semi-transparent rounded cards (bg-white/40 dark:bg-gray-800/40)
|
||||
- Cohesive rounded design language throughout (rounded-2xl on headers and containers)
|
||||
- Floating pagination pill at bottom center of viewport
|
||||
- Minimal design: section label | ← | Page X of Y | →
|
||||
- Quick jump input (type page number + Enter)
|
||||
- Auto-shows when scrolling through a section (IntersectionObserver)
|
||||
- Auto-scrolls to section top on page change
|
||||
- Rounded-full design with backdrop blur and subtle shadow
|
||||
- Responsive grid layouts (1/2/3/4 cols)
|
||||
- Enhanced CTA section with gradient background (blue-to-indigo)
|
||||
|
||||
**Requests Page** (`/requests`)
|
||||
- Filter tabs: All, Active, Waiting, Completed, Failed, Cancelled
|
||||
- Auto-refresh every 5s (SWR)
|
||||
- Request counts per tab
|
||||
- Cancel functionality
|
||||
- Loading skeletons, empty states
|
||||
- Waiting filter shows awaiting_search and awaiting_import statuses
|
||||
|
||||
**Profile Page** (`/profile`)
|
||||
- User info card (avatar, username, email, role, Plex ID)
|
||||
- Stats: Total/Active/Waiting/Completed/Failed/Cancelled requests
|
||||
- Active downloads section
|
||||
- Recent requests (last 5)
|
||||
- Auto-refresh every 5s
|
||||
- Waiting stat shows awaiting_search and awaiting_import statuses
|
||||
|
||||
## Component APIs
|
||||
|
||||
```typescript
|
||||
interface AudiobookCardProps {
|
||||
audiobook: {asin, title, author, narrator?, coverArtUrl?, rating?, durationMinutes?, isRequested?, requestStatus?, requestedByUsername?};
|
||||
onRequest?: (asin: string) => void;
|
||||
isRequested?: boolean;
|
||||
requestStatus?: string;
|
||||
onRequestSuccess?: () => void;
|
||||
}
|
||||
|
||||
interface AudiobookDetailsModalProps {
|
||||
asin: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onRequestSuccess?: () => void;
|
||||
isRequested?: boolean;
|
||||
requestStatus?: string | null;
|
||||
isAvailable?: boolean;
|
||||
requestedByUsername?: string | null;
|
||||
}
|
||||
|
||||
interface RequestCardProps {
|
||||
request: {id, status, progress, audiobook: {title, author, coverArtUrl?}, createdAt, updatedAt};
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
|
||||
interface InteractiveTorrentSearchModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
requestId: string;
|
||||
audiobook: {title: string, author: string};
|
||||
}
|
||||
|
||||
interface ConfirmModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
isLoading?: boolean;
|
||||
variant?: 'danger' | 'primary';
|
||||
}
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface StickyPaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
sectionRef: React.RefObject<HTMLElement | null>;
|
||||
label: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
- **useAuth** - `{user, login, logout, isLoading}`
|
||||
- **useAudiobooks** - `{audiobooks, isLoading, error, totalPages, hasMore}`
|
||||
- **useAudiobookDetails** ✅ - `{audiobook, isLoading, error}` - Fetches individual audiobook by ASIN
|
||||
- **useRequest** - `{createRequest, cancelRequest, isLoading}`
|
||||
|
||||
## Styling
|
||||
|
||||
**Tailwind Patterns:**
|
||||
- Container: `container mx-auto px-4 py-8 max-w-7xl`
|
||||
- Card: `bg-white dark:bg-gray-800 rounded-lg shadow-md p-6`
|
||||
- Button: `bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md`
|
||||
- Grid: `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6`
|
||||
|
||||
**Dark Mode:** Use `dark:` variant
|
||||
|
||||
## Responsive Breakpoints
|
||||
|
||||
- Mobile: <768px (1 col)
|
||||
- Tablet: 768-1024px (2 cols)
|
||||
- Desktop: 1024-1280px (3 cols)
|
||||
- Large: >1280px (4 cols)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Next.js 14+ App Router
|
||||
- React 19
|
||||
- Tailwind CSS 4
|
||||
- Heroicons/Lucide React
|
||||
- React Hook Form + Zod
|
||||
- SWR (data fetching)
|
||||
- date-fns (formatting)
|
||||
@@ -0,0 +1,130 @@
|
||||
# Login Page
|
||||
|
||||
**Status:** ✅ Implemented | Real floating book covers with professional animations
|
||||
|
||||
Stylized entry point with Plex OAuth integration, animated floating popular audiobook covers, and prominent "Login with Plex" CTA.
|
||||
|
||||
## Design
|
||||
|
||||
- Full-screen immersive experience with gradient background
|
||||
- Centered hero with login button
|
||||
- Animated floating real audiobook covers (popular releases)
|
||||
- **100 randomly positioned covers** with varied sizes, animations, and depth
|
||||
- Multi-layer depth effect with z-index layering (0-20)
|
||||
- Dark theme optimized with glassmorphism card
|
||||
- Professional streaming service aesthetic
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. User visits protected route → redirected to `/login`
|
||||
2. Clicks "Login with Plex"
|
||||
3. `POST /api/auth/plex/login` → requests PIN
|
||||
4. Opens Plex OAuth in popup
|
||||
5. Polls `/api/auth/plex/callback` for authorization
|
||||
6. User authorizes in Plex popup
|
||||
7. Callback receives auth token
|
||||
8. Creates/updates user in DB
|
||||
9. Returns JWT tokens
|
||||
10. Client stores tokens in localStorage
|
||||
11. Redirects to originally requested page or homepage
|
||||
|
||||
## Book Covers
|
||||
|
||||
**Data Source:** `GET /api/audiobooks/covers`
|
||||
- Returns up to 200 popular audiobook covers
|
||||
- Uses cached thumbnails from `audible_cache` table
|
||||
- Shuffled on each request for variety
|
||||
- Fallback to placeholder elements if API fails
|
||||
|
||||
**Display:**
|
||||
- **100 covers** shown simultaneously for immersive experience
|
||||
- Varied sizes: 80-160px wide (1.5 aspect ratio)
|
||||
- Opacity range: 0.15-0.35 for subtle layering and depth
|
||||
- Staggered animation delays (0-10s) for natural movement
|
||||
- Z-index layering (0-20) for depth perception
|
||||
- Programmatic positioning using seeded random for consistency
|
||||
- Lazy loading (first 10 eager, rest lazy) for performance
|
||||
- Hover pauses animation and scales for interaction
|
||||
|
||||
**Positioning Algorithm:**
|
||||
- Seeded random function ensures consistent positions per cover index
|
||||
- Random distribution across full viewport (0-100% both axes)
|
||||
- Each cover gets unique: size, position, opacity, delay, z-index, animation type
|
||||
- Seed multipliers (7, 13, 17, 23, 29, 31) prevent pattern repetition
|
||||
- Math.sin() based pseudo-random for deterministic results
|
||||
|
||||
## State
|
||||
|
||||
```typescript
|
||||
interface LoginPageState {
|
||||
isLoggingIn: boolean;
|
||||
error: string | null;
|
||||
pinId: number | null;
|
||||
authWindow: Window | null;
|
||||
bookCovers: BookCover[];
|
||||
showAdminLogin: boolean;
|
||||
adminUsername: string;
|
||||
adminPassword: string;
|
||||
}
|
||||
|
||||
interface BookCover {
|
||||
asin: string;
|
||||
title: string;
|
||||
author: string;
|
||||
coverUrl: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Popup Blocked:** "Popup was blocked. Please allow popups."
|
||||
**Login Timeout:** 2 min polling timeout
|
||||
**Plex Unavailable:** "Plex services currently unavailable."
|
||||
**Covers Fail:** Silent fallback to placeholder gradient elements
|
||||
|
||||
## Animations
|
||||
|
||||
Three animation speeds with realistic floating motion:
|
||||
|
||||
```css
|
||||
@keyframes float-slow {
|
||||
/* 22s cycle with 4 keyframes */
|
||||
0%, 100% { transform: translateY(0) translateX(0) rotate(0deg) scale(1); }
|
||||
25% { transform: translateY(-25px) translateX(15px) rotate(2deg) scale(1.03); }
|
||||
50% { transform: translateY(-35px) translateX(25px) rotate(4deg) scale(1.05); }
|
||||
75% { transform: translateY(-20px) translateX(-10px) rotate(-2deg) scale(1.02); }
|
||||
}
|
||||
|
||||
@keyframes float-medium {
|
||||
/* 16s cycle with 3 keyframes */
|
||||
0%, 100% { transform: translateY(0) translateX(0) rotate(0deg) scale(1); }
|
||||
33% { transform: translateY(-30px) translateX(-20px) rotate(-3deg) scale(1.04); }
|
||||
66% { transform: translateY(-15px) translateX(10px) rotate(3deg) scale(1.02); }
|
||||
}
|
||||
|
||||
@keyframes float-fast {
|
||||
/* 12s cycle with 2 keyframes */
|
||||
0%, 100% { transform: translateY(0) translateX(0) rotate(0deg) scale(1); }
|
||||
50% { transform: translateY(-28px) translateX(18px) rotate(5deg) scale(1.06); }
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Scale transformations (1.02-1.06) for depth
|
||||
- Rotation (-5° to +5°) for natural movement
|
||||
- X/Y translation for floating effect
|
||||
- Hover pauses animation
|
||||
- Shadow-2xl for 3D depth
|
||||
|
||||
## Security
|
||||
|
||||
- Tokens in localStorage (access 1hr, refresh 7d)
|
||||
- Tokens cleared on logout
|
||||
- OAuth state parameter validation
|
||||
- SameSite cookie attributes
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Next.js 14+ Client Component
|
||||
- Tailwind CSS with custom animations
|
||||
- Plex OAuth via AuthContext
|
||||
@@ -0,0 +1,138 @@
|
||||
# Route Authentication and Protection
|
||||
|
||||
**Status:** ✅ Implemented | Token expiry validation, auto-refresh, 401 handling
|
||||
|
||||
Authentication and authorization system protecting routes, ensuring only authenticated users can access protected pages.
|
||||
|
||||
## Protection Strategy
|
||||
|
||||
**Client-Side:** React components check auth state, redirect to login if needed, preserve original URL
|
||||
**Server-Side:** API routes validate JWT tokens via middleware, return 401/403 for unauthorized
|
||||
|
||||
## Routes
|
||||
|
||||
**Public:** `/login`, `/setup`, `/api/*` (handle auth independently)
|
||||
**Protected:** `/` (home), `/search`, `/requests`, `/profile`
|
||||
**Admin:** `/admin/*` - requires admin role
|
||||
|
||||
## ProtectedRoute Component
|
||||
|
||||
**Location:** `src/components/auth/ProtectedRoute.tsx`
|
||||
|
||||
**Behavior:**
|
||||
1. Check auth state from AuthContext
|
||||
2. Optionally check admin role
|
||||
3. Show loading spinner while checking
|
||||
4. Redirect to `/login` if unauthenticated
|
||||
5. Redirect to `/` if admin required but not admin
|
||||
6. Render children if authorized
|
||||
|
||||
## API Middleware
|
||||
|
||||
**Location:** `src/lib/middleware/auth.ts`
|
||||
|
||||
**Server-side validation:**
|
||||
- `requireAuth()` - validates JWT, adds user to request
|
||||
- `requireAdmin()` - checks admin role, chains after requireAuth
|
||||
- Returns 401 for invalid/expired tokens
|
||||
- Returns 403 for insufficient permissions
|
||||
|
||||
## Token Management
|
||||
|
||||
**Location:** `src/contexts/AuthContext.tsx`, `src/lib/utils/jwt-client.ts`
|
||||
|
||||
**Token Validation on Mount:**
|
||||
- Decodes access token to check expiry
|
||||
- If expired but refresh token valid → auto-refresh
|
||||
- If both expired → clear storage, redirect to login
|
||||
- Cross-tab logout sync via storage events
|
||||
|
||||
**Auto-Refresh (5 mins before expiry):**
|
||||
```typescript
|
||||
const refreshTimeMs = getRefreshTimeMs(token);
|
||||
setTimeout(() => refreshToken(), refreshTimeMs);
|
||||
```
|
||||
|
||||
**Schedule:**
|
||||
- After login → schedule first refresh
|
||||
- After token refresh → schedule next refresh
|
||||
- Cleanup on logout or unmount
|
||||
|
||||
## API Client with 401 Handling
|
||||
|
||||
**Location:** `src/lib/utils/api.ts`
|
||||
|
||||
**fetchWithAuth():**
|
||||
- Adds Authorization header automatically
|
||||
- Catches 401 responses
|
||||
- Attempts token refresh once
|
||||
- Retries original request with new token
|
||||
- Logs out if refresh fails
|
||||
- Prevents duplicate refresh requests
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
// In hooks/components
|
||||
import { fetchWithAuth, fetchJSON } from '@/lib/utils/api';
|
||||
|
||||
// GET request
|
||||
const response = await fetchWithAuth('/api/requests');
|
||||
|
||||
// POST with JSON
|
||||
const data = await fetchJSON('/api/requests', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ audiobook }),
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**401 Unauthorized:**
|
||||
1. Attempt token refresh automatically
|
||||
2. Retry original request with new token
|
||||
3. If still 401 or refresh fails → logout (clears storage + redirects to /login)
|
||||
|
||||
**403 Forbidden:**
|
||||
- Valid token but insufficient permissions
|
||||
- Return error, don't logout
|
||||
|
||||
## Logout Behavior
|
||||
|
||||
**Global redirect on logout:**
|
||||
- `logout()` from AuthContext → clears storage + redirects to /login
|
||||
- API 401 errors → `performLogout()` → clears storage + redirects to /login
|
||||
- Cross-tab logout → storage event triggers redirect to /login
|
||||
- Ensures user never remains on authenticated pages after logout
|
||||
|
||||
## Cross-Tab Sync
|
||||
|
||||
**Storage Events:**
|
||||
- Logout in one tab → logout + redirect to login in all tabs
|
||||
- Login in one tab → sync auth state to all tabs
|
||||
- Prevents stale sessions across browser tabs
|
||||
|
||||
## Security
|
||||
|
||||
- Never log tokens
|
||||
- HTTPS only in production
|
||||
- Short access token expiry (1hr)
|
||||
- Auto-refresh 5 mins before expiry
|
||||
- Token expiry validation on mount
|
||||
- Prevent duplicate refresh requests
|
||||
- SameSite cookies for CSRF protection
|
||||
- Client-side token decode (signature verified server-side only)
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
- **Expired tokens not logging out:** Added token expiry validation on mount
|
||||
- **No auto-refresh:** Scheduled refresh 5 mins before token expires
|
||||
- **401 errors not handled:** Added global 401 interceptor with token refresh
|
||||
- **Logged-out sessions persisting:** Token validation clears expired sessions immediately
|
||||
- **Logout not redirecting:** Added automatic redirect to /login on all logout scenarios (manual, API 401, cross-tab)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Next.js 14+ App Router
|
||||
- JWT via AuthContext
|
||||
- React Context API
|
||||
- Custom fetch wrapper for 401 handling
|
||||
Reference in New Issue
Block a user