Web Components
Web components power BookWish's custom store websites built with Next.js 14 and Tailwind CSS.
Technology Stack
Framework
- Next.js 14: App Router with React Server Components
- React 18: Latest React features
- TypeScript: Full type safety
Styling
- Tailwind CSS: Utility-first CSS framework
- CSS Variables: Dynamic store branding
- Responsive Design: Mobile-first approach
Key Libraries
- next/image: Optimized image loading
- next/link: Client-side navigation
Tailwind Configuration
File Location
/Users/terryheath/Documents/bookwish_monorepo/stores/tailwind.config.ts
Configuration
export default {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
primary: "var(--store-primary, #1a1a1a)",
accent: "var(--store-accent, #e85d04)",
},
fontFamily: {
sans: ["var(--font-sans)", "system-ui", "sans-serif"],
serif: ["var(--font-serif)", "Georgia", "serif"],
},
},
},
plugins: [],
}
Dynamic Branding
Store-specific colors via CSS custom properties:
style={{
"--dynamic-primary": store.primaryColor || "#1a1a1a",
"--store-primary": store.primaryColor || "#1a1a1a",
"--store-accent": store.accentColor || "#e85d04",
} as React.CSSProperties}
BookCard (Web)
A card component for displaying inventory items on store websites.
File Location
/Users/terryheath/Documents/bookwish_monorepo/stores/components/BookCard.tsx
Usage
<BookCard item={inventoryItem} storeSlug={storeSlug} />
Props
| Property | Type | Required | Description |
|---|---|---|---|
item | InventoryItem | Yes | Inventory item with book, price, condition |
storeSlug | string | Yes | Store slug for routing |
Implementation
export function BookCard({ item, storeSlug }: BookCardProps) {
const { book, priceCents, condition, isUsed } = item;
return (
<Link
href={`/${storeSlug}/books/${item.id}`}
className="group bg-white rounded-lg border hover:shadow-md transition-shadow overflow-hidden"
>
{/* Cover Image */}
<div className="aspect-[2/3] relative bg-gray-100">
{book.coverImageUrl ? (
<Image
src={book.coverImageUrl}
alt={book.title}
fill
className="object-cover group-hover:scale-105 transition-transform"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-gray-400">
<span className="text-4xl">📖</span>
</div>
)}
{isUsed && (
<span className="absolute top-2 left-2 bg-amber-500 text-white text-xs px-2 py-1 rounded">
{conditionLabel}
</span>
)}
</div>
{/* Details */}
<div className="p-4">
<h3 className="font-medium text-gray-900 line-clamp-2 mb-1">
{book.title}
</h3>
<p className="text-sm text-gray-500 line-clamp-1">
{book.authors?.join(", ") || "Unknown Author"}
</p>
<p className="text-lg font-bold mt-2" style={{ color: "var(--store-primary)" }}>
{formatPrice(priceCents)}
</p>
</div>
</Link>
);
}
Features
- Image Optimization: Next.js Image component with automatic sizing
- Hover Effects: Scale image and add shadow on hover
- Condition Badge: Shows condition for used books
- Responsive: Adapts to grid layout
- Brand Colors: Uses store's primary color for price
Price Formatting
const formatPrice = (cents: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(cents / 100);
};
Condition Labels
const conditionLabel = {
new: "New",
like_new: "Like New",
good: "Good",
fair: "Fair",
}[condition];
Styling
- Aspect Ratio: 2:3 (portrait book cover)
- Border: Default Tailwind border
- Border Radius: Rounded-lg (8px)
- Hover: Shadow-md transition
- Image Transition: 300ms scale on hover
StoreHeader (Web)
Navigation header for store websites.
File Location
/Users/terryheath/Documents/bookwish_monorepo/stores/components/StoreHeader.tsx
Usage
<StoreHeader store={store} />
Props
| Property | Type | Required | Description |
|---|---|---|---|
store | Store | Yes | Store data (name, logo, slug, colors) |
Implementation
export function StoreHeader({ store }: StoreHeaderProps) {
return (
<header
className="border-b sticky top-0 z-10 bg-white/95 backdrop-blur-sm"
style={{
"--dynamic-primary": store.primaryColor || "#1a1a1a",
} as React.CSSProperties}
>
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
{/* Logo */}
<Link href={`/${store.slug}`} className="flex items-center gap-3">
{store.logoUrl ? (
<Image
src={store.logoUrl}
alt={store.name}
width={40}
height={40}
className="rounded-full"
/>
) : (
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold"
style={{ backgroundColor: store.primaryColor || "#1a1a1a" }}
>
{store.name.charAt(0)}
</div>
)}
<span className="font-serif text-xl font-bold">{store.name}</span>
</Link>
{/* Navigation */}
<nav className="flex items-center gap-4">
<Link href={`/${store.slug}/books`}>Browse Books</Link>
<Link href={`/${store.slug}/programs`}>Read With Us</Link>
<Link href={config.mainAppUrl}>Powered by BookWish</Link>
</nav>
</div>
</header>
);
}
Features
- Sticky Position: Remains at top while scrolling
- Backdrop Blur: Frosted glass effect
- Logo or Initial: Shows logo image or letter fallback
- Brand Colors: Dynamic primary color for logo fallback
- Navigation Links: Browse books, programs, app link
Styling
- Position: Sticky top-0, z-10
- Background: White 95% opacity with backdrop blur
- Max Width: 6xl (1152px)
- Padding: 16px (py-4, px-4)
- Font: Serif for store name, sans-serif for links
StoreFooter (Web)
Footer component with store information and links.
File Location
/Users/terryheath/Documents/bookwish_monorepo/stores/components/StoreFooter.tsx
Usage
<StoreFooter store={store} />
Props
| Property | Type | Required | Description |
|---|---|---|---|
store | Store | Yes | Store data (address, hours, phone) |
Implementation
export function StoreFooter({ store }: StoreFooterProps) {
return (
<footer className="bg-gray-900 text-gray-400 py-12 mt-16">
<div className="max-w-6xl mx-auto px-4">
<div className="grid md:grid-cols-3 gap-8">
{/* Store Info */}
<div>
<h3 className="text-white font-bold mb-4">{store.name}</h3>
{store.address && (
<address className="not-italic text-sm space-y-1">
<p>{store.address.line1}</p>
{store.address.line2 && <p>{store.address.line2}</p>}
<p>
{store.address.city}, {store.address.state}{" "}
{store.address.postalCode}
</p>
</address>
)}
{store.phone && <p className="text-sm mt-2">{store.phone}</p>}
</div>
{/* Hours */}
{store.hours && (
<div>
<h3 className="text-white font-bold mb-4">Hours</h3>
<div className="text-sm space-y-1">
{Object.entries(store.hours).map(([day, hours]) => (
<div key={day} className="flex justify-between">
<span className="capitalize">{day}</span>
<span>{hours}</span>
</div>
))}
</div>
</div>
)}
{/* Links */}
<div>
<h3 className="text-white font-bold mb-4">Links</h3>
<div className="space-y-2 text-sm">
<Link href={config.mainAppUrl}>Get the BookWish App</Link>
<Link href={`${config.mainAppUrl}/terms`}>Terms of Service</Link>
<Link href={`${config.mainAppUrl}/privacy`}>Privacy Policy</Link>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-gray-800 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center gap-4 text-sm">
<p>{config.copyrightNotice}</p>
<p>
Powered by{" "}
<Link href={config.mainAppUrl} className="text-amber-400 hover:text-amber-300">
BookWish
</Link>
</p>
</div>
</div>
</footer>
);
}
Features
- Three Columns: Store info, hours, links
- Conditional Hours: Only shows if available
- Address Formatting: Proper semantic HTML with
<address> - Responsive Grid: Stacks on mobile
- Brand Link: Amber-colored BookWish link
Styling
- Background: gray-900 (dark)
- Text Color: gray-400 (light gray)
- Headings: White, bold
- Grid: 3 columns desktop, 1 column mobile
- Padding: 48px vertical, 16px horizontal
- Top Margin: 64px from content
ProgramCard (Web)
Cards for displaying book clubs and reading challenges.
File Location
Inline components in /Users/terryheath/Documents/bookwish_monorepo/stores/app/[storeSlug]/programs/page.tsx
ClubCard Component
function ClubCard({ club }: { club: Club }) {
return (
<div className="bg-white rounded-xl border border-[#E0D7C8] p-5 hover:shadow-md transition-shadow">
{/* Club header with cover/icon */}
<div className="flex gap-4">
{club.coverImageUrl ? (
<img src={club.coverImageUrl} alt="" className="w-20 h-20 rounded-lg object-cover" />
) : (
<div className="w-20 h-20 rounded-lg bg-[#FAF4E8] flex items-center justify-center">
{/* People icon */}
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-[#233548] mb-1">{club.name}</h3>
<p className="text-sm text-gray-600 line-clamp-2">{club.description}</p>
<p className="text-xs text-gray-500">{club.memberCount} members</p>
</div>
</div>
{/* Current book section */}
{club.currentBook && (
<div className="mt-4 pt-4 border-t">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-2">
Currently Reading
</p>
<div className="flex items-center gap-3">
<img src={club.currentBook.coverUrl} className="w-10 h-14 rounded" />
<div>
<p className="font-medium text-sm">{club.currentBook.title}</p>
<p className="text-xs text-gray-500">{club.currentBook.authors.join(', ')}</p>
</div>
</div>
</div>
)}
{/* Join button */}
<a
href={`${config.mainAppUrl}/club/${club.id}`}
className="mt-4 block w-full text-center py-2 border rounded-full text-sm"
>
Join in App
</a>
</div>
);
}
ChallengeCard Component
function ChallengeCard({ challenge }: { challenge: Challenge }) {
const isActive = now >= startDate && now <= endDate;
const isUpcoming = now < startDate;
return (
<div className="bg-white rounded-xl border border-[#E0D7C8] p-5 hover:shadow-md transition-shadow">
{/* Header with status badge */}
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="font-semibold text-[#233548]">{challenge.name}</h3>
<p className="text-xs text-gray-500 mt-1">{challenge.bookCount} books</p>
</div>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
isActive
? 'bg-green-100 text-green-700'
: isUpcoming
? 'bg-[#FFC857]/20 text-[#233548]'
: 'bg-gray-100 text-gray-600'
}`}
>
{isActive ? 'Active' : isUpcoming ? 'Upcoming' : 'Ended'}
</span>
</div>
{/* Description */}
<p className="text-sm text-gray-600 line-clamp-2 mb-3">{challenge.description}</p>
{/* Date range and participant count */}
<div className="flex items-center justify-between text-xs text-gray-500 mb-4">
<span>{startDate.toLocaleDateString()} - {endDate.toLocaleDateString()}</span>
<span>{challenge.participantCount} participants</span>
</div>
{/* Join button */}
<a
href={`${config.mainAppUrl}/challenge/${challenge.id}`}
className="block w-full text-center py-2 border rounded-full text-sm"
>
Join in App
</a>
</div>
);
}
Features
- Status Badges: Color-coded active/upcoming/ended states
- Current Reading: Shows club's current book
- App Links: Deep links to BookWish app
- Hover Effects: Shadow on hover
- Line Clamping: Truncate long descriptions
Responsive Patterns
Breakpoints
Tailwind's default breakpoints:
sm: 640pxmd: 768pxlg: 1024pxxl: 1280px
Common Patterns
Grid Layouts
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{items.map(item => <Card key={item.id} item={item} />)}
</div>
Flex Layouts
<div className="flex flex-col md:flex-row gap-4">
{/* Stacks on mobile, horizontal on desktop */}
</div>
Text Sizing
<h1 className="text-2xl md:text-3xl lg:text-4xl">
Responsive Heading
</h1>
Spacing
<div className="px-4 md:px-6 lg:px-8">
{/* More padding on larger screens */}
</div>
Image Handling
Next.js Image Component
import Image from "next/image";
<Image
src={imageUrl}
alt={altText}
width={width}
height={height}
// OR
fill
className="object-cover"
/>
Optimization Features
- Automatic WebP: Serves WebP when supported
- Lazy Loading: Images load as they enter viewport
- Responsive Sizing: Serves appropriate size for screen
- Placeholder: Optional blur placeholder
Fallback Patterns
{imageUrl ? (
<Image src={imageUrl} alt="" fill className="object-cover" />
) : (
<div className="flex items-center justify-center">
<span className="text-4xl">📖</span>
</div>
)}
Typography
Font Configuration
Fonts loaded via Next.js font system:
fontFamily: {
sans: ["var(--font-sans)", "system-ui", "sans-serif"],
serif: ["var(--font-serif)", "Georgia", "serif"],
}
Usage
- Headings: Semibold weight, various sizes
- Store Name: Serif font for elegance
- Body Text: Sans-serif for readability
- Labels: Small text with uppercase tracking
Common Classes
font-serif- Serif fontfont-sans- Sans-serif fontfont-bold- Bold weightfont-semibold- Semibold weightfont-medium- Medium weight
Color System
BookWish Brand Colors
--ink-blue: #233548
--parchment: #FAF4E8
--amber: #FFC857
--border: #E0D7C8
Tailwind Classes
text-gray-900- Dark texttext-gray-600- Medium texttext-gray-500- Light textbg-white- White backgroundbg-gray-100- Light gray backgroundborder-gray-200- Light border
Dynamic Branding
Store-specific colors applied via inline styles:
style={{ color: "var(--store-primary)" }}
style={{ backgroundColor: store.primaryColor }}
Utility Classes
Layout
flex,flex-col,flex-row- Flexboxgrid,grid-cols-2- Grid layoutsgap-4- Spacing between itemsmax-w-6xl- Max width containermx-auto- Center horizontally
Spacing
p-4- Padding all sidespx-4,py-4- Padding horizontal/verticalmt-4,mb-4- Margin top/bottomspace-y-4- Vertical spacing between children
Effects
hover:shadow-md- Shadow on hovertransition-shadow- Smooth shadow transitionbackdrop-blur-sm- Backdrop blur effectrounded-lg,rounded-xl- Border radius
Text
line-clamp-2- Truncate to 2 linestext-sm,text-lg- Font sizeuppercase- Uppercase texttracking-wide- Letter spacing
Best Practices
Performance
- Use Next.js Image: Always use Image component for photos
- Lazy Load: Components below fold load as needed
- Static Generation: Pre-render pages at build time
- API Caching: Use
next: { revalidate: 300 }for API calls
Accessibility
- Alt Text: Always provide meaningful alt text
- Semantic HTML: Use proper HTML elements
- Color Contrast: Ensure sufficient contrast ratios
- Focus States: Maintain visible focus indicators
Responsive Design
- Mobile First: Design for mobile, enhance for desktop
- Touch Targets: Minimum 44x44px tap targets
- Test Breakpoints: Test all major breakpoint transitions
- Readable Text: Minimum 16px body text on mobile
Code Organization
- Component Files: One component per file
- Type Safety: Use TypeScript interfaces
- Reusable Components: Extract common patterns
- CSS Variables: Use for dynamic theming