diff --git a/src/app/page.tsx b/src/app/page.tsx index f4a697ed..f74f42ab 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -42,17 +42,22 @@ export default async function Home({ return (
- {/* Compact hero */} -
-

- Your story is a token. + {/* Hero: featured section */} +
+

+ The Bookshelf

-

- Every plot you publish drives the market — and every trade pays you. Write more, earn more. +

+ Every plot you publish drives the market — and every trade pays you. Browse the shelf, pick a story.

- {/* Filter bar */} + {/* Section heading + filter bar */} +
+

+ {tab === "trending" ? "Trending" : "New & Noteworthy"} +

+
{/* Story grid — batched multicall for price/TVL */} diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index ad1c30cc..572ac841 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -64,7 +64,7 @@ export function FilterBar({ writer, genre, lang, tab }: FilterBarProps) { return (
-
+
{/* Writer token + dropdown */}
- {shortLabel}: - {label}: - {value} + {shortLabel}: + {label}: + {value} ); } diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index e9d4ba36..d5bf24d4 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -23,61 +23,94 @@ export function StoryCard({ : false; return ( -
- {/* Book cover with page-thickness lines */} -
- {/* Page layer 2 (furthest back) */} -
- {/* Page layer 1 */} -
+
+ {/* 3D Book with spine */} + + {/* Drop shadow beneath book — grows on hover */} +
- {/* Main card (front cover) */} - - {/* Spine edge — thicker left border */} -
+ {/* Spine — left edge with depth */} +
- {/* Top edge — page block thickness */} -
+ {/* Page edges visible on right side */} +
-
- {/* Top: genre tag + completion badge */} + {/* Page edges visible on bottom */} +
+ + {/* Front cover */} +
+ {/* Spine inner shadow overlay */} +
+ + {/* Content */} +
+ {/* Top: genre badge + completion */}
- + {displayGenre || "Uncategorized"} {storyline.sunset && ( - + complete )}
- {/* Center: title */} -
-

+ {/* Center: title displayed like printed book cover */} +
+

{storyline.title}

{storyline.language && storyline.language !== "English" && ( - + {storyline.language} )}
- {/* Bottom: author */} -
+ {/* Bottom: author name like a printed book */} +
{storyline.writer_type === 1 && }
- -

- {/* Metadata below card */} -
+ {/* Decorative horizontal rule near bottom */} +
+
+ + + {/* Metadata below book */} +
{storyline.token_address && ( @@ -85,7 +118,7 @@ export function StoryCard({ )} {storyline.plot_count} {storyline.plot_count === 1 ? "plot" : "plots"} linked - {isNew && NEW} + {isNew && NEW}
diff --git a/src/components/StoryCardStats.tsx b/src/components/StoryCardStats.tsx index 1c652c86..facc7a45 100644 --- a/src/components/StoryCardStats.tsx +++ b/src/components/StoryCardStats.tsx @@ -40,9 +40,9 @@ export function StoryCardStats({ tokenAddress }: { tokenAddress: string }) { : "—"; return ( -
- Price: {price} {RESERVE_LABEL} - TVL: {tvl} {RESERVE_LABEL} +
+ Price: {price} {RESERVE_LABEL} + TVL: {tvl} {RESERVE_LABEL}
); } @@ -65,6 +65,6 @@ export function StoryCardTVL({ tokenAddress }: { tokenAddress: string }) { const tvl = tvlData ? formatCompact(tvlData.tvl) : "—"; return ( - TVL: {tvl} {RESERVE_LABEL} + TVL: {tvl} {RESERVE_LABEL} ); } diff --git a/src/components/StoryGrid.tsx b/src/components/StoryGrid.tsx index 11efa088..e90b99b3 100644 --- a/src/components/StoryGrid.tsx +++ b/src/components/StoryGrid.tsx @@ -1,25 +1,98 @@ "use client"; +import { useState, useEffect } from "react"; import { type Address } from "viem"; import { type Storyline } from "../../lib/supabase"; import { BatchTokenDataProvider } from "./BatchTokenDataProvider"; import { StoryCard } from "./StoryCard"; +/** + * Groups an array into chunks of the given size. + */ +function chunk(arr: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; +} + +/** + * Hook that returns the current shelf size (columns per row). + * Uses matchMedia to stay in sync with the CSS breakpoint. + */ +function useShelfSize(): number { + const [cols, setCols] = useState(3); + + useEffect(() => { + const mql = window.matchMedia("(min-width: 1024px)"); + const update = () => setCols(mql.matches ? 3 : 2); + update(); + mql.addEventListener("change", update); + return () => mql.removeEventListener("change", update); + }, []); + + return cols; +} + +/** + * A single bookshelf row: books sitting on a visible shelf surface. + */ +function ShelfRow({ children, cols }: { children: React.ReactNode; cols: number }) { + return ( +
+ {/* Books */} +
+ {children} +
+ + {/* Shelf surface */} +
+ {/* Shelf front edge */} +
+
+ ); +} + /** * Story card grid wrapped in BatchTokenDataProvider. * Fetches price + TVL for all visible stories in a single multicall * instead of 4 individual RPC calls per card. + * + * Books are displayed on shelves — each visual row of books sits on + * a visible shelf surface. Shelf size adapts to viewport (2 on mobile, 3 on desktop). */ export function StoryGrid({ storylines }: { storylines: Storyline[] }) { const tokenAddresses = storylines .map((s) => s.token_address) .filter((addr): addr is string => !!addr) as Address[]; + const cols = useShelfSize(); + const shelves = chunk(storylines, cols); + return ( -
- {storylines.map((s) => ( - +
+ {shelves.map((shelf, i) => ( + + {shelf.map((s) => ( + + ))} + ))}