Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/books/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default async function BookGuidePage({
</div>
<p className="mt-4 max-w-2xl text-lg text-white/90">{book.hook}</p>
<div className="mt-5 flex flex-wrap gap-2">
{[...book.genres, book.era, ...book.themes].map((tag) => (
{book.themes.map((tag) => (
<span
key={tag}
className="rounded-full bg-white/15 px-3 py-1 text-sm font-medium text-white"
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './globals.css';
import { ProgressProvider } from '@/lib/progress';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import FloatingMenu from '@/components/FloatingMenu';

export const metadata: Metadata = {
title: {
Expand All @@ -21,6 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Header />
<main className="flex-1">{children}</main>
<Footer />
<FloatingMenu />
</ProgressProvider>
</body>
</html>
Expand Down
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function HomePage() {
<BrowseSection books={books} />

{/* Key Highlights — card-like posts at the bottom */}
<section className="mx-auto mt-16 max-w-6xl px-4">
<section id="highlights" className="mx-auto mt-16 max-w-6xl scroll-mt-20 px-4">
<div className="flex items-end justify-between">
<div>
<h2 className="font-serif text-3xl font-black text-ink">Key highlights</h2>
Expand Down
7 changes: 1 addition & 6 deletions src/components/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ export default function BookCard({ book }: { book: Book }) {
<p className="mt-2 flex-1 text-sm leading-snug text-ink/75">{book.hook}</p>

<div className="mt-3 flex flex-wrap gap-1.5">
{book.genres.slice(0, 2).map((g) => (
<Pill key={g} size="sm">
{g}
</Pill>
))}
{book.themes.slice(0, 1).map((t) => (
{book.themes.slice(0, 3).map((t) => (
<Pill key={t} size="sm">
{t}
</Pill>
Expand Down
44 changes: 19 additions & 25 deletions src/components/FilterPills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import Pill from './Pill';
import {
allEras,
allGenres,
allThemes,
countActive,
type ActiveFilters,
Expand All @@ -16,36 +14,32 @@ interface FilterPillsProps {
onClear: () => void;
}

const GROUPS: { key: FilterGroup; label: string; values: string[] }[] = [
{ key: 'genre', label: 'Genre', values: allGenres() },
{ key: 'theme', label: 'Theme', values: allThemes() },
{ key: 'era', label: 'Era', values: allEras() },
];
// Browsing is filtered by theme only — genre and era pills were removed to
// keep the bar focused and uncluttered.
const THEMES = allThemes();

/** The pill-box filter bar. Multi-select within and across groups. */
/** The theme pill-box filter bar. Multi-select. */
export default function FilterPills({ active, onToggle, onClear }: FilterPillsProps) {
const activeCount = countActive(active);

return (
<div className="space-y-4">
{GROUPS.map((group) => (
<div key={group.key} className="flex flex-col gap-2 sm:flex-row sm:items-start">
<span className="w-16 shrink-0 pt-1.5 text-xs font-semibold uppercase tracking-wide text-ink/50">
{group.label}
</span>
<div className="flex flex-wrap gap-2">
{group.values.map((value) => (
<Pill
key={value}
active={active[group.key].includes(value)}
onClick={() => onToggle(group.key, value)}
>
{value}
</Pill>
))}
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
<span className="w-16 shrink-0 pt-1.5 text-xs font-semibold uppercase tracking-wide text-ink/50">
Theme
</span>
<div className="flex flex-wrap gap-2">
{THEMES.map((value) => (
<Pill
key={value}
active={active.theme.includes(value)}
onClick={() => onToggle('theme', value)}
>
{value}
</Pill>
))}
</div>
))}
</div>

{activeCount > 0 && (
<button
Expand Down
99 changes: 99 additions & 0 deletions src/components/FloatingMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import { useEffect, useState } from 'react';
import Link from 'next/link';

interface MenuLink {
href: string;
label: string;
emoji: string;
}

const LINKS: MenuLink[] = [
{ href: '/', label: 'Home', emoji: '🏠' },
{ href: '/#browse', label: 'Browse books', emoji: '📚' },
{ href: '/#highlights', label: 'Key highlights', emoji: '✨' },
{ href: '/rewards', label: 'Trophy Case', emoji: '🏆' },
];

/**
* A floating action button (bottom-right) that expands into a quick-nav menu.
* Also offers a "back to top" jump once the user has scrolled down.
*/
export default function FloatingMenu() {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);

useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 400);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);

// Close the menu on Escape for accessibility.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false);
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open]);

return (
<div className="fixed bottom-5 right-5 z-50 flex flex-col items-end gap-3">
{/* Expanded menu */}
{open && (
<>
{/* Click-away backdrop */}
<button
type="button"
aria-label="Close menu"
onClick={() => setOpen(false)}
className="fixed inset-0 -z-10 cursor-default bg-ink/20 backdrop-blur-[1px]"
/>
<nav className="animate-pop flex flex-col gap-2 rounded-2xl bg-white p-2 shadow-2xl ring-1 ring-ink/10">
{LINKS.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setOpen(false)}
className="flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm font-semibold text-ink transition-colors hover:bg-spark/15"
>
<span aria-hidden className="text-lg">
{link.emoji}
</span>
{link.label}
</Link>
))}
{scrolled && (
<button
type="button"
onClick={() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
setOpen(false);
}}
className="flex items-center gap-3 rounded-xl px-4 py-2.5 text-sm font-semibold text-ink transition-colors hover:bg-spark/15"
>
<span aria-hidden className="text-lg">
⬆️
</span>
Back to top
</button>
)}
</nav>
</>
)}

{/* The floating action button */}
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
aria-label={open ? 'Close menu' : 'Open menu'}
className="flex h-14 w-14 items-center justify-center rounded-full bg-spark text-2xl text-ink shadow-2xl ring-1 ring-spark-deep/30 transition-transform hover:scale-105 active:scale-95"
>
<span aria-hidden>{open ? '✕' : '⚡'}</span>
</button>
</div>
);
}
16 changes: 12 additions & 4 deletions src/data/books.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ import { classics3 } from './books/classics-3';
import { classics4 } from './books/classics-4';
import { modern } from './books/modern';
import { contemporary } from './books/contemporary';
import type { BookDeepDive } from './types';
import { deepDives } from './deepdives';
import { deepDives2 } from './deepdives-2';
import { deepDives3 } from './deepdives-3';
import { deepDives4 } from './deepdives-4';

export type { Book, GuideSection, Quote, Highlight } from './types';

// Deep-dive guides are authored in batches; merge them into one lookup keyed by slug.
const allDeepDives: Record<string, BookDeepDive> = {
...deepDives,
...deepDives2,
...deepDives3,
...deepDives4,
};

/** The full collection, sorted alphabetically by title for stable browsing. */
export const books: Book[] = [
...classics1,
Expand All @@ -19,10 +30,7 @@ export const books: Book[] = [
...modern,
...contemporary,
]
.map((b) => {
const dd = deepDives[b.slug] ?? deepDives2[b.slug];
return dd ? { ...b, deepDive: dd } : b;
})
.map((b) => (allDeepDives[b.slug] ? { ...b, deepDive: allDeepDives[b.slug] } : b))
.sort((a, b) => a.title.localeCompare(b.title));

export function getBook(slug: string): Book | undefined {
Expand Down
Loading
Loading