Skip to main content

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

PropertyTypeRequiredDescription
itemInventoryItemYesInventory item with book, price, condition
storeSlugstringYesStore 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

PropertyTypeRequiredDescription
storeStoreYesStore 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

PropertyTypeRequiredDescription
storeStoreYesStore 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: 640px
  • md: 768px
  • lg: 1024px
  • xl: 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 font
  • font-sans - Sans-serif font
  • font-bold - Bold weight
  • font-semibold - Semibold weight
  • font-medium - Medium weight

Color System

BookWish Brand Colors

--ink-blue: #233548
--parchment: #FAF4E8
--amber: #FFC857
--border: #E0D7C8

Tailwind Classes

  • text-gray-900 - Dark text
  • text-gray-600 - Medium text
  • text-gray-500 - Light text
  • bg-white - White background
  • bg-gray-100 - Light gray background
  • border-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 - Flexbox
  • grid, grid-cols-2 - Grid layouts
  • gap-4 - Spacing between items
  • max-w-6xl - Max width container
  • mx-auto - Center horizontally

Spacing

  • p-4 - Padding all sides
  • px-4, py-4 - Padding horizontal/vertical
  • mt-4, mb-4 - Margin top/bottom
  • space-y-4 - Vertical spacing between children

Effects

  • hover:shadow-md - Shadow on hover
  • transition-shadow - Smooth shadow transition
  • backdrop-blur-sm - Backdrop blur effect
  • rounded-lg, rounded-xl - Border radius

Text

  • line-clamp-2 - Truncate to 2 lines
  • text-sm, text-lg - Font size
  • uppercase - Uppercase text
  • tracking-wide - Letter spacing

Best Practices

Performance

  1. Use Next.js Image: Always use Image component for photos
  2. Lazy Load: Components below fold load as needed
  3. Static Generation: Pre-render pages at build time
  4. API Caching: Use next: { revalidate: 300 } for API calls

Accessibility

  1. Alt Text: Always provide meaningful alt text
  2. Semantic HTML: Use proper HTML elements
  3. Color Contrast: Ensure sufficient contrast ratios
  4. Focus States: Maintain visible focus indicators

Responsive Design

  1. Mobile First: Design for mobile, enhance for desktop
  2. Touch Targets: Minimum 44x44px tap targets
  3. Test Breakpoints: Test all major breakpoint transitions
  4. Readable Text: Minimum 16px body text on mobile

Code Organization

  1. Component Files: One component per file
  2. Type Safety: Use TypeScript interfaces
  3. Reusable Components: Extract common patterns
  4. CSS Variables: Use for dynamic theming