- 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>
9.8 KiB
9.8 KiB
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
// 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
// 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
// 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
// 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
// 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 }),
});
},
};