Archived
Merge branch 'main' into feature/customizable-mobile-reading-experience
Resolve merge conflicts: - backend/apps/books/: Keep main's models (EBook, BookChapter, etc.) - frontend/src/App.tsx: Keep /read/:id route + main's all routes - web/ files: Accept deletion (content ported to frontend/)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# 009 — Expo Mobile Application Integration
|
||||
|
||||
**Issue:** #16
|
||||
**Status:** Draft
|
||||
**Created:** 2026-05-29
|
||||
|
||||
## Objective
|
||||
|
||||
Integrate an Expo-based React Native mobile application into the `cloud-reader` monorepo, sharing types, API client patterns, and configuration with the existing web frontend.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
cloud-reader/
|
||||
├── mobile/ # Expo React Native app
|
||||
│ ├── package.json
|
||||
│ ├── app.json
|
||||
│ ├── tsconfig.json
|
||||
│ ├── babel.config.js
|
||||
│ ├── App.tsx # Root component
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API client (mirrors frontend/src/api/ pattern)
|
||||
│ │ │ ├── client.ts # Axios instance + JWT interceptor
|
||||
│ │ │ ├── books.ts # Book API calls
|
||||
│ │ │ └── annotations.ts
|
||||
│ │ ├── screens/ # Screen-level components
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── navigation/ # React Navigation setup
|
||||
│ │ ├── context/ # Auth context, etc.
|
||||
│ │ ├── hooks/ # Custom hooks
|
||||
│ │ └── types/ # Mobile-specific types
|
||||
│ └── assets/
|
||||
├── packages/
|
||||
│ └── shared/
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ └── src/
|
||||
│ ├── types.ts # Shared domain types (Book, User, Bookmark, Note)
|
||||
│ └── utils.ts # Shared utility functions
|
||||
└── package.json # Root — updated workspace config
|
||||
```
|
||||
|
||||
## Monorepo Workspace Config
|
||||
|
||||
Root `package.json` workspaces array updated to include `"mobile"`, `"packages/shared"` alongside existing `"frontend"` and `"backend"`.
|
||||
|
||||
## Shared `packages/shared`
|
||||
|
||||
- `@cloud-reader/shared` package published within the monorepo
|
||||
- Exports:
|
||||
- All domain types (`Book`, `BookSummary`, `Bookmark`, `Note`, `User`, `AnnotationEntry`, `PaginatedResponse`, `TokenResponse`)
|
||||
- API endpoint constants
|
||||
- Date formatting helpers
|
||||
- Validation utilities (email regex, password strength check)
|
||||
|
||||
## Mobile App Structure
|
||||
|
||||
### API Client (`mobile/src/api/client.ts`)
|
||||
- Axios instance configured with:
|
||||
- Base URL from environment variable (`EXPO_PUBLIC_API_URL`)
|
||||
- JWT token attachment via request interceptor
|
||||
- Token refresh response interceptor on 401
|
||||
- Uses `AsyncStorage` for token persistence (instead of `localStorage`)
|
||||
|
||||
### Navigation (`mobile/src/navigation/`)
|
||||
- React Navigation stack:
|
||||
1. `AuthStack` — Login, Register screens
|
||||
2. `MainTabs` — Library, Search, Settings tabs
|
||||
3. `BookReader` — Full-screen reading view
|
||||
|
||||
### Key Screens
|
||||
| Screen | Route | Purpose |
|
||||
|--------|-------|---------|
|
||||
| Login | `Auth/Login` | Email/password login |
|
||||
| Register | `Auth/Register` | User registration |
|
||||
| Library | `Main/Library` | Book list with filtering |
|
||||
| BookDetail | `Main/BookDetail` | Book metadata + actions |
|
||||
| Reader | `Reader/View` | EPUB/PDF rendering |
|
||||
| Search | `Main/Search` | Book discovery |
|
||||
| Settings | `Main/Settings` | Profile, theme, download mgmt |
|
||||
|
||||
## Backend Changes Required
|
||||
|
||||
None. The existing Django REST API already serves all endpoints needed by the mobile app. The mobile app communicates with the same backend via the shared API base URL.
|
||||
|
||||
## Docker
|
||||
|
||||
No changes to `docker-compose.yml` needed — the mobile app runs on-device or via Expo Go, not inside Docker.
|
||||
|
||||
## CI/CD Considerations
|
||||
|
||||
The monorepo structure supports a single pipeline that can:
|
||||
- `yarn install` at root (installs all workspaces)
|
||||
- `yarn workspace @cloud-reader/shared build`
|
||||
- `yarn workspace @cloud-reader/mobile build` (Expo EAS for mobile builds)
|
||||
- `yarn workspace @cloud-reader/frontend build` (Vite for web builds)
|
||||
@@ -0,0 +1,107 @@
|
||||
# Book Search & Discovery — Spec
|
||||
|
||||
## Overview
|
||||
Enable users to search books within the library and discover new books via filters and a dedicated detail view.
|
||||
|
||||
## Backend API Contracts
|
||||
|
||||
### Book List & Search
|
||||
`GET /api/books/`
|
||||
|
||||
**Query Parameters:**
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `q` | string | Full-text search across title, author, genre |
|
||||
| `genre` | string | Exact filter by genre |
|
||||
| `author` | string | Exact filter by author |
|
||||
| `reading_status` | string | Filter: `want_to_read`, `reading`, `finished`, `dnf` |
|
||||
| `ordering` | string | `title`, `author`, `genre`, `created_at` (prefix `-` for desc) |
|
||||
| `page` | int | Page number (default: 1) |
|
||||
|
||||
**Response (paginated):**
|
||||
```json
|
||||
{
|
||||
"count": 42,
|
||||
"next": "http://.../?page=2",
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Dune",
|
||||
"author": "Frank Herbert",
|
||||
"genre": "Science Fiction",
|
||||
"reading_status": "finished",
|
||||
"reading_status_display": "Finished",
|
||||
"cover_image": "https://..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Book Detail
|
||||
`GET /api/books/{id}/`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Dune",
|
||||
"author": "Frank Herbert",
|
||||
"genre": "Science Fiction",
|
||||
"description": "...",
|
||||
"reading_status": "finished",
|
||||
"reading_status_display": "Finished",
|
||||
"cover_image": "https://...",
|
||||
"total_pages": 688,
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
"updated_at": "2025-01-15T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Genre / Author Discovery
|
||||
`GET /api/books/genres/` → `["Fiction", "Science Fiction", ...]`
|
||||
|
||||
`GET /api/books/authors/` → `["Frank Herbert", "Ursula K. Le Guin", ...]`
|
||||
|
||||
## Frontend Components
|
||||
|
||||
### LibraryPage (enhanced)
|
||||
- **Search bar** at top: text input with debounced `onChange` → calls API with `q` param
|
||||
- **Filter row**: genre dropdown, author dropdown, reading status dropdown
|
||||
- Genre/Author dropdowns populated from `/api/books/genres/` and `/api/books/authors/`
|
||||
- Reading status uses static enum values (`READING_STATUS_OPTIONS`)
|
||||
- **Results grid**: card layout showing cover, title, author, reading status badge
|
||||
- **Empty state**: "No books found" with clear message when results are empty
|
||||
- **Loading state**: spinner/skeleton while fetching
|
||||
- **Click card → navigate to** `/books/{id}`
|
||||
|
||||
### BookDetailPage (new)
|
||||
- Shows full book info: cover, title, author, genre, description, reading status, total pages
|
||||
- Back button to return to library
|
||||
- Clean, mobile-responsive layout
|
||||
|
||||
### API Client — `frontend/src/api/books.ts`
|
||||
|
||||
| Method | Endpoint | Returns |
|
||||
|--------|----------|---------|
|
||||
| `searchBooks(params)` | `GET /api/books/` | `{count, results: BookListItem[]}` |
|
||||
| `getBook(id)` | `GET /api/books/{id}/` | `BookDetail` |
|
||||
| `getGenres()` | `GET /api/books/genres/` | `string[]` |
|
||||
| `getAuthors()` | `GET /api/books/authors/` | `string[]` |
|
||||
|
||||
## Routes (Frontend)
|
||||
| Path | Component | Auth |
|
||||
|------|-----------|------|
|
||||
| `/` | LibraryPage | Protected |
|
||||
| `/books/:id` | BookDetailPage | Protected |
|
||||
|
||||
## Data Flow
|
||||
1. User types in search bar → 300ms debounce → `GET /api/books/?q=...`
|
||||
2. User selects filter → `GET /api/books/?genre=...&author=...&reading_status=...`
|
||||
3. User clicks result → navigate to `/books/:id`
|
||||
4. BookDetailPage → `GET /api/books/{id}/`
|
||||
|
||||
## Mobile Optimizations
|
||||
- Filters collapse into a toggleable panel on small screens
|
||||
- Cards stack in single column on mobile
|
||||
- Touch-friendly tap targets (min 44px)
|
||||
@@ -0,0 +1,99 @@
|
||||
# Mobile Book Search & Discovery — Spec
|
||||
|
||||
## Overview
|
||||
Enhance the existing book search experience with mobile-first features: voice search via the Web Speech API, real-time autocomplete suggestions, and touch-optimized responsive layout.
|
||||
|
||||
## Prerequisites
|
||||
- Backend endpoints already exist (from `docs/backend/search-discovery-spec.md`):
|
||||
- `GET /api/books/?q=...&genre=...&author=...&reading_status=...` — paginated search
|
||||
- `GET /api/books/{id}/` — book detail
|
||||
- `GET /api/books/genres/` — genre discovery
|
||||
- `GET /api/books/authors/` — author discovery
|
||||
- Frontend `LibraryPage` and `BookDetailPage` components exist but lacked API client methods and types (fixed in this PR).
|
||||
|
||||
## Frontend API Client Additions
|
||||
|
||||
### `frontend/src/types/book.ts` — New exports
|
||||
|
||||
```typescript
|
||||
export interface BookSearchParams {
|
||||
q?: string;
|
||||
genre?: string;
|
||||
author?: string;
|
||||
reading_status?: string;
|
||||
ordering?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export const READING_STATUS_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "", label: "All Statuses" },
|
||||
{ value: "want_to_read", label: "Want to Read" },
|
||||
{ value: "reading", label: "Reading" },
|
||||
{ value: "finished", label: "Finished" },
|
||||
{ value: "dnf", label: "Did Not Finish" },
|
||||
];
|
||||
```
|
||||
|
||||
### `frontend/src/api/books.ts` — New methods on `booksApi`
|
||||
|
||||
| Method | Endpoint | Returns |
|
||||
|--------|----------|---------|
|
||||
| `searchBooks(params)` | `GET /api/books/` | `{ count, results: BookListItem[] }` |
|
||||
| `getBook(id)` | `GET /api/books/{id}/` | `BookDetail` |
|
||||
| `getGenres()` | `GET /api/books/genres/` | `string[]` |
|
||||
| `getAuthors()` | `GET /api/books/authors/` | `string[]` |
|
||||
|
||||
## Mobile Features
|
||||
|
||||
### 1. Voice Search
|
||||
- **Hook**: `useVoiceSearch` in `frontend/src/hooks/useVoiceSearch.ts`
|
||||
- Uses the Web Speech API (`SpeechRecognition` / `webkitSpeechRecognition`)
|
||||
- Returns: `{ isListening, transcript, isSupported, startListening, stopListening, hasError }`
|
||||
- Renders a microphone icon button next to the search input
|
||||
- On mobile, tapping the mic icon triggers the native speech recognition prompt
|
||||
- On success, populates the search input with the transcript and triggers a search
|
||||
- Graceful degradation: if SpeechRecognition API is unavailable, the mic button is hidden
|
||||
|
||||
### 2. Real-Time Suggestions (Autocomplete)
|
||||
- Component: `SearchSuggestions` rendered as a dropdown below the search input
|
||||
- On each keystroke (debounced 200ms), fetches `GET /api/books/?q=...&page_size=5` for suggestions
|
||||
- Shows up to 5 book title/author suggestions in a styled dropdown list
|
||||
- Clicking a suggestion navigates directly to `/books/{id}`
|
||||
- Clicking outside or pressing Escape dismisses the dropdown
|
||||
- Combines with existing full search results — suggestions are fast previews, not the main result list
|
||||
|
||||
### 3. Mobile-Responsive Enhancements
|
||||
- Filters panel is **collapsed by default** on mobile, toggleable via a "Filters" button
|
||||
- Touch targets minimum 44px (WCAG 2.1)
|
||||
- Results grid switches to **single column** below 600px viewport width
|
||||
- Search input and filters panel stack vertically on small screens
|
||||
- Add CSS breakpoints via inline styles and a `useMediaQuery` hook
|
||||
- Bottom navigation-style action buttons on mobile (Add Book, Bookmarks, Settings become icon-only)
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```
|
||||
LibraryPage
|
||||
├── Header (title, count, action buttons)
|
||||
├── SearchInput
|
||||
│ ├── TextInput (debounced 300ms)
|
||||
│ ├── VoiceSearchButton (microphone icon)
|
||||
│ └── SearchSuggestions (dropdown, debounced 200ms)
|
||||
├── FiltersButton (mobile: toggle; desktop: always visible)
|
||||
├── FiltersPanel (collapsible on mobile)
|
||||
│ ├── GenreSelect
|
||||
│ ├── AuthorSelect
|
||||
│ ├── StatusSelect
|
||||
│ └── ClearFiltersButton
|
||||
├── LoadingState (skeleton grid)
|
||||
├── ErrorState (message + retry button)
|
||||
├── EmptyState (no results / no books)
|
||||
└── ResultsGrid (responsive: auto-fill vs single column)
|
||||
```
|
||||
|
||||
## Mobile-First CSS Strategy
|
||||
- Use inline styles with `@media` queries in a shared `breakpoints.ts` utility
|
||||
- Breakpoints: sm = 480px, md = 768px, lg = 1024px
|
||||
- Base styles are mobile-first (single column, full width)
|
||||
- Media queries expand to multi-column grid and horizontal layout on larger screens
|
||||
Reference in New Issue
Block a user