Shared Web Components
This document outlines the shared components, patterns, and utilities used across BookWish web projects (stores website, future public wishlist site, etc.).
Design System
Color Palette
BookWish uses a consistent color palette across web projects:
// Brand Colors
const colors = {
primary: '#233548', // Dark blue-gray
secondary: '#FFC857', // Warm yellow
accent: '#4BB4C8', // Teal
success: '#3FA37B', // Green
background: '#FAF4E8', // Cream
border: '#E0D7C8', // Light tan
};
Typography
- Headings:
font-seriffor elegant book-focused design - Body: System font stack for readability
- Font weights: Regular (400), Medium (500), Semibold (600), Bold (700)
Tailwind Configuration
// tailwind.config.ts
theme: {
extend: {
colors: {
'bw-primary': '#233548',
'bw-secondary': '#FFC857',
'bw-accent': '#4BB4C8',
'bw-success': '#3FA37B',
'bw-background': '#FAF4E8',
'bw-border': '#E0D7C8',
},
},
}
Reusable Components
BookCard
Universal component for displaying books in grid layouts:
interface BookCardProps {
item: InventoryItem;
storeSlug?: string; // Optional for store context
onClick?: () => void; // Optional custom handler
showStore?: boolean; // Show store name badge
}
export function BookCard({ item, storeSlug }: BookCardProps) {
const { book, priceCents, condition, isUsed } = item;
return (
<Link href={`/${storeSlug}/books/${item.id}`}>
{/* Cover image with aspect ratio */}
<div className="aspect-[2/3] relative bg-gray-100">
<Image src={book.coverImageUrl} fill className="object-cover" />
{isUsed && <ConditionBadge condition={condition} />}
</div>
{/* Book info */}
<div className="p-4">
<h3 className="font-medium line-clamp-2">{book.title}</h3>
<p className="text-sm text-gray-500 line-clamp-1">
{book.authors?.join(', ')}
</p>
<p className="text-lg font-bold">{formatPrice(priceCents)}</p>
</div>
</Link>
);
}
Features:
- Responsive aspect ratio (2:3 for book covers)
- Hover effects with scale transform
- Condition badge for used books
- Price formatting
- Line clamping for title overflow
StoreHeader
Navigation header used on all store pages:
interface StoreHeaderProps {
store: Store;
}
export function StoreHeader({ store }: StoreHeaderProps) {
return (
<header className="border-b sticky top-0 z-10 bg-white/95 backdrop-blur-sm">
{/* Logo & name */}
<Link href={`/${store.slug}`}>
{store.logoUrl ? (
<Image src={store.logoUrl} width={40} height={40} />
) : (
<div className="w-10 h-10 rounded-full bg-primary text-white">
{store.name.charAt(0)}
</div>
)}
<span className="font-serif text-xl font-bold">{store.name}</span>
</Link>
{/* Navigation */}
<nav>
<Link href={`/${store.slug}/books`}>Browse Books</Link>
<Link href={`/${store.slug}/programs`}>Read With Us</Link>
<Link href={mainAppUrl}>Powered by BookWish</Link>
</nav>
</header>
);
}
Features:
- Sticky positioning
- Backdrop blur effect
- Logo fallback (first letter circle)
- Responsive layout
StoreFooter
Consistent footer across store pages:
export function StoreFooter({ store }: { store: Store }) {
return (
<footer className="border-t bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* Store info */}
{/* Links */}
{/* Copyright */}
</div>
</footer>
);
}
Utility Functions
Price Formatting
export function formatPrice(cents: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(cents / 100);
}
Usage: Consistent currency display across all pages
Condition Labels
export const CONDITION_LABELS = {
new: 'New',
like_new: 'Like New',
good: 'Good',
fair: 'Fair',
} as const;
export type Condition = keyof typeof CONDITION_LABELS;
Date Formatting
export function formatDate(date: string | Date): string {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
Common Patterns
Responsive Grids
Book grids adapt across breakpoints:
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
{items.map(item => <BookCard key={item.id} item={item} />)}
</div>
Breakpoints:
- Mobile: 2 columns
- Tablet (md): 4 columns
- Desktop (lg): 6 columns
Loading States
export function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
);
}
Empty States
export function EmptyState({
icon,
title,
description,
action
}: EmptyStateProps) {
return (
<div className="text-center py-16">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-background flex items-center justify-center">
{icon}
</div>
<h2 className="text-xl font-medium text-primary mb-2">{title}</h2>
<p className="text-gray-500 mb-6">{description}</p>
{action}
</div>
);
}
Error States
export function ErrorState({ message, retry }: ErrorStateProps) {
return (
<div className="text-center py-12">
<p className="text-red-600 mb-4">{message}</p>
{retry && (
<button onClick={retry} className="btn-primary">
Try Again
</button>
)}
</div>
);
}
API Client Pattern
Type-Safe API Client
class ApiClient {
private baseUrl: string;
constructor() {
this.baseUrl = config.apiUrl;
}
private async fetch<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async getStoreBySlug(slug: string): Promise<Store | null> {
try {
return await this.fetch<Store>(`/stores/${slug}`);
} catch {
return null;
}
}
}
export const api = new ApiClient();
Features:
- Centralized error handling
- TypeScript generics for type safety
- Graceful error degradation (returns null)
- Consistent headers
Authentication Pattern
Client-Side Auth Context
interface AuthContextType {
user: User | null;
wishlists: Wishlist[];
isLoading: boolean;
isAuthenticated: boolean;
refreshWishlists: () => Promise<void>;
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
const token = getToken();
if (!token) {
setIsLoading(false);
return;
}
const response = await fetch(`${API_BASE_URL}/users/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
setIsLoading(false);
}
return (
<AuthContext.Provider value={{ user, isLoading, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
}
Token Utilities
function getToken(): string | null {
if (typeof window === 'undefined') return null;
// Check localStorage
const localToken = localStorage.getItem('bookwish_token');
if (localToken) return localToken;
// Check cookies
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'bookwish_token') {
return decodeURIComponent(value);
}
}
return null;
}
CSS Utilities
Custom Classes
/* Dynamic theming */
.store-branded {
--store-primary: var(--dynamic-primary);
}
/* Hover effects */
.book-card-hover {
@apply hover:shadow-md transition-shadow group;
}
.book-card-hover img {
@apply group-hover:scale-105 transition-transform;
}
/* Backdrop effects */
.header-blur {
@apply bg-white/95 backdrop-blur-sm;
}
Line Clamping
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.line-clamp-2 {
-webkit-line-clamp: 2;
}
Image Handling
Next.js Image Component
<Image
src={coverImageUrl}
alt={book.title}
fill // Fill parent container
className="object-cover" // Cover aspect ratio
priority={isAboveFold} // Prioritize above-fold images
sizes="(max-width: 768px) 50vw, 25vw" // Responsive sizes
/>
Fallback Pattern
{book.coverImageUrl ? (
<Image src={book.coverImageUrl} alt={book.title} fill />
) : (
<div className="absolute inset-0 flex items-center justify-center text-gray-400">
<span className="text-4xl">📖</span>
</div>
)}
Form Patterns
Search Forms
<form className="flex gap-4">
<input
type="search"
name="search"
placeholder="Search by title or author..."
defaultValue={search}
className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
<button type="submit" className="btn-primary">
Search
</button>
</form>
Button Styles
Primary Button
.btn-primary {
@apply px-6 py-2 bg-primary text-white rounded-full
hover:opacity-90 transition-opacity font-medium;
}
Secondary Button
.btn-secondary {
@apply px-6 py-2 border border-primary text-primary rounded-full
hover:bg-primary/5 transition-colors font-medium;
}
Pagination Pattern
{totalPages > 1 && (
<div className="mt-12 flex justify-center gap-2">
{currentPage > 1 && (
<Link href={`?page=${currentPage - 1}`} className="px-4 py-2 border rounded">
Previous
</Link>
)}
<span className="px-4 py-2 text-gray-600">
Page {currentPage} of {totalPages}
</span>
{currentPage < totalPages && (
<Link href={`?page=${currentPage + 1}`} className="px-4 py-2 border rounded">
Next
</Link>
)}
</div>
)}
Configuration Management
Environment Variables
export const config = {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
mainAppUrl: process.env.NEXT_PUBLIC_MAIN_APP_URL || 'https://bookwish.io',
storesBaseUrl: process.env.NEXT_PUBLIC_STORES_URL || 'https://bookwish.shop',
supportEmail: 'support@bookwish.io',
companyName: 'Willow Tree Creative LLC',
get currentYear(): number {
return new Date().getFullYear();
},
get copyrightNotice(): string {
return `© ${this.currentYear} ${this.companyName}. All rights reserved.`;
},
};
Best Practices
Performance
- Use Next.js
<Image>for all images - Implement proper caching strategies
- Lazy load below-the-fold content
- Minimize client-side JavaScript
Accessibility
- Semantic HTML elements
- ARIA labels where needed
- Keyboard navigation support
- Alt text for all images
TypeScript
- Strict type checking
- Interface definitions for all data
- Avoid
anytype - Use generics for reusable functions
Responsive Design
- Mobile-first approach
- Touch-friendly tap targets (min 44px)
- Responsive images with sizes attribute
- Test on multiple devices