# Frontend Architecture ## Component Architecture ### Component Organization ``` frontend/src/ ├── components/ │ ├── layout/ │ │ ├── Header.jsx │ │ ├── Footer.jsx │ │ └── Layout.jsx │ ├── books/ │ │ ├── BookCard.jsx │ │ ├── BookList.jsx │ │ ├── BookSearchResults.jsx │ │ └── AddBookForm.jsx │ ├── progress/ │ │ ├── LogProgressModal.jsx │ │ ├── ProgressDisplay.jsx │ │ └── StatusIndicator.jsx │ ├── calendar/ │ │ ├── Calendar.jsx │ │ ├── CalendarDay.jsx │ │ └── CalendarLegend.jsx │ └── common/ │ ├── Button.jsx │ ├── Input.jsx │ ├── Modal.jsx │ ├── Loading.jsx │ └── ErrorMessage.jsx ├── pages/ │ ├── Home.jsx │ ├── AddBook.jsx │ ├── BookDetail.jsx │ └── CalendarView.jsx ├── hooks/ │ ├── useBooks.js │ ├── useProgress.js │ └── useLogs.js ├── services/ │ ├── api.js (API client setup) │ ├── booksService.js │ └── logsService.js ├── context/ │ └── AppContext.jsx ├── utils/ │ ├── dateUtils.js │ ├── paceUtils.js │ └── validation.js ├── styles/ │ └── index.css (Tailwind directives) └── App.jsx ``` ### Component Template ```typescript // Example: BookCard.jsx import React from 'react'; import { useNavigate } from 'react-router-dom'; import StatusIndicator from '../progress/StatusIndicator'; import { formatDate } from '../../utils/dateUtils'; /** * BookCard - Displays a single book with progress metrics * @param {Object} book - Book object with progress data * @param {Function} onLogProgress - Handler for log progress action */ function BookCard({ book, onLogProgress }) { const navigate = useNavigate(); const handleCardClick = () => { navigate(`/books/${book.id}`); }; const handleLogClick = (e) => { e.stopPropagation(); // Prevent navigation when clicking log button onLogProgress(book); }; return (
{/* Book cover and title */}
{book.coverUrl ? ( {book.title} ) : (
No cover
)}

{book.title}

{book.author &&

{book.author}

}

Due: {formatDate(book.deadlineDate)}

{/* Progress metrics */}

Target: {book.requiredPace.toFixed(1)} pages/day

Your pace: {' '} {book.actualPace ? `${book.actualPace.toFixed(1)} pages/day` : 'No data yet'}

{book.pagesRemaining} pages left, {book.daysRemaining} days

{/* Log progress button */}
); } export default BookCard; ``` ## State Management Architecture ### State Structure ```typescript // context/AppContext.jsx import React, { createContext, useContext, useState, useEffect } from 'react'; import { booksService } from '../services/booksService'; const AppContext = createContext(); export function AppProvider({ children }) { const [books, setBooks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Fetch books on mount useEffect(() => { loadBooks(); }, []); const loadBooks = async () => { setLoading(true); setError(null); try { const data = await booksService.getActiveBooks(); setBooks(data.books); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const addBook = async (bookData) => { const newBook = await booksService.addBook(bookData); setBooks(prev => [...prev, newBook]); return newBook; }; const deleteBook = async (bookId) => { await booksService.deleteBook(bookId); setBooks(prev => prev.filter(b => b.id !== bookId)); }; const logProgress = async (bookId, currentPage, logDate) => { await booksService.logProgress(bookId, currentPage, logDate); // Refresh books to get updated progress await loadBooks(); }; const value = { books, loading, error, loadBooks, addBook, deleteBook, logProgress, }; return {children}; } export function useApp() { const context = useContext(AppContext); if (!context) { throw new Error('useApp must be used within AppProvider'); } return context; } ``` ### State Management Patterns - **Global State:** Books list, loading states, errors (via React Context) - **Local Component State:** Form inputs, modal open/closed, UI-only state (via useState) - **Server State:** Fetched data cached in Context, refetch on mutations - **No External State Library:** Context API sufficient for MVP (books list + UI state) - **State Updates:** Optimistic UI updates followed by data refetch for consistency ## Routing Architecture ### Route Organization ``` Routes: / → Home (Book List Screen) /add-book → Add Book Screen /books/:bookId → Book Detail Screen /calendar → Calendar View Screen (optional, may be in Book Detail) /* → 404 Not Found ``` ### Protected Route Pattern ```typescript // App.jsx with React Router import React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { AppProvider } from './context/AppContext'; import Layout from './components/layout/Layout'; import Home from './pages/Home'; import AddBook from './pages/AddBook'; import BookDetail from './pages/BookDetail'; import CalendarView from './pages/CalendarView'; import NotFound from './pages/NotFound'; function App() { return ( } /> } /> } /> } /> } /> ); } export default App; ``` **Note:** No protected routes needed for MVP (single-user, no authentication). Add in v1.1 if multi-user support is needed. ## Frontend Services Layer ### API Client Setup ```typescript // services/api.js const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; class ApiClient { async request(endpoint, options = {}) { const url = `${API_BASE_URL}${endpoint}`; const config = { headers: { 'Content-Type': 'application/json', ...options.headers, }, ...options, }; try { const response = await fetch(url, config); // Handle non-JSON responses (like 204 No Content) if (response.status === 204) { return null; } const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Request failed'); } return data; } catch (error) { console.error('API request failed:', error); throw error; } } get(endpoint) { return this.request(endpoint, { method: 'GET' }); } post(endpoint, body) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(body), }); } put(endpoint, body) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(body), }); } delete(endpoint) { return this.request(endpoint, { method: 'DELETE' }); } } export const apiClient = new ApiClient(); ``` ### Service Example ```typescript // services/booksService.js import { apiClient } from './api'; export const booksService = { // Search books via Open Library async searchBooks(query) { return apiClient.get(`/books/search?q=${encodeURIComponent(query)}`); }, // Get all active books with progress async getActiveBooks() { return apiClient.get('/books'); }, // Add a new book async addBook(bookData) { return apiClient.post('/books', bookData); }, // Get single book async getBook(bookId) { return apiClient.get(`/books/${bookId}`); }, // Delete book async deleteBook(bookId) { return apiClient.delete(`/books/${bookId}`); }, // Get book progress async getProgress(bookId) { return apiClient.get(`/books/${bookId}/progress`); }, // Get reading logs async getLogs(bookId) { return apiClient.get(`/books/${bookId}/logs`); }, // Log progress async logProgress(bookId, currentPage, logDate = null) { return apiClient.post(`/books/${bookId}/logs`, { currentPage, ...(logDate && { logDate }), }); }, }; ``` ---