books/docs/architecture/frontend-architecture.md
Greg fa8acef423 Epic 1, Story 1.1: Project Initialization & Repository Setup
- Initialize Git repository with main branch
- Create comprehensive .gitignore for Node.js, React, and environment files
- Set up directory structure (frontend/, backend/, docs/)
- Create detailed README.md with project overview and setup instructions
- Add .env.example with all required environment variables
- Configure Prettier for consistent code formatting

All acceptance criteria met:
 Git repository initialized with appropriate .gitignore
 Directory structure matches Technical Assumptions
 README.md created with project overview and setup docs
 .env.example file with all required environment variables
 Prettier config files added for code formatting consistency

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 15:12:30 +01:00

390 lines
9.8 KiB
Markdown

# 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 (
<div
onClick={handleCardClick}
className="bg-white rounded-lg shadow-md p-4 cursor-pointer hover:shadow-lg transition-shadow"
>
{/* Book cover and title */}
<div className="flex gap-4">
{book.coverUrl ? (
<img
src={book.coverUrl}
alt={book.title}
className="w-16 h-24 object-cover rounded"
/>
) : (
<div className="w-16 h-24 bg-gray-200 rounded flex items-center justify-center">
<span className="text-gray-400 text-xs">No cover</span>
</div>
)}
<div className="flex-1">
<h3 className="font-bold text-lg">{book.title}</h3>
{book.author && <p className="text-gray-600 text-sm">{book.author}</p>}
<p className="text-gray-500 text-xs mt-1">
Due: {formatDate(book.deadlineDate)}
</p>
</div>
</div>
{/* Progress metrics */}
<div className="mt-4 space-y-2">
<StatusIndicator status={book.status} />
<div className="text-sm">
<p><strong>Target:</strong> {book.requiredPace.toFixed(1)} pages/day</p>
<p>
<strong>Your pace:</strong> {' '}
{book.actualPace ? `${book.actualPace.toFixed(1)} pages/day` : 'No data yet'}
</p>
<p className="text-gray-600">
{book.pagesRemaining} pages left, {book.daysRemaining} days
</p>
</div>
</div>
{/* Log progress button */}
<button
onClick={handleLogClick}
className="mt-4 w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition-colors"
>
Log Progress
</button>
</div>
);
}
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 <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
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 (
<BrowserRouter>
<AppProvider>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/add-book" element={<AddBook />} />
<Route path="/books/:bookId" element={<BookDetail />} />
<Route path="/calendar" element={<CalendarView />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</AppProvider>
</BrowserRouter>
);
}
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 }),
});
},
};
```
---