Skip to main content

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-serif for 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 any type
  • 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