From a8318e7c716d1f2319d5b3b9e67dc8d47b28fde3 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 21 Mar 2026 08:35:57 +0000 Subject: [PATCH 1/3] [#405] Book card and shelf layout redesign - StoryCard: 3D book with spine, page edges, shadow, hover lift - StoryGrid: Shelf rows with visible surface, edge, and shadow - Home hero: "The Bookshelf" heading with section subheading - StoryCardStats: Gold accent values instead of foreground - FilterBar: Rounded pill style with warm hover states Fixes #405 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/page.tsx | 19 ++++--- src/components/FilterBar.tsx | 10 ++-- src/components/StoryCard.tsx | 95 +++++++++++++++++++++---------- src/components/StoryCardStats.tsx | 8 +-- src/components/StoryGrid.tsx | 57 ++++++++++++++++++- 5 files changed, 139 insertions(+), 50 deletions(-) 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..9e3b9ea2 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..12d729ef 100644 --- a/src/components/StoryGrid.tsx +++ b/src/components/StoryGrid.tsx @@ -5,21 +5,72 @@ 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; +} + +/** + * A single bookshelf row: books sitting on a visible shelf surface. + */ +function ShelfRow({ children }: { children: React.ReactNode }) { + 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 row of 2 (mobile) or 3 (desktop) + * books sits on a visible shelf surface. */ export function StoryGrid({ storylines }: { storylines: Storyline[] }) { const tokenAddresses = storylines .map((s) => s.token_address) .filter((addr): addr is string => !!addr) as Address[]; + // We chunk by 3 (desktop cols) — CSS handles showing 2 cols on mobile + const shelves = chunk(storylines, 3); + return ( -
- {storylines.map((s) => ( - +
+ {shelves.map((shelf, i) => ( + + {shelf.map((s) => ( + + ))} + ))}
From adb4b7b471c5e8ff846d09dd3b0e9f8efe4a50d7 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 21 Mar 2026 08:38:37 +0000 Subject: [PATCH 2/3] [#405] Fix mobile shelf: adapt shelf rows to viewport columns Uses matchMedia to detect 2-col (mobile) vs 3-col (desktop) and chunks shelves accordingly, so each visual row of books sits on its own shelf surface. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/StoryGrid.tsx | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/StoryGrid.tsx b/src/components/StoryGrid.tsx index 12d729ef..e90b99b3 100644 --- a/src/components/StoryGrid.tsx +++ b/src/components/StoryGrid.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { type Address } from "viem"; import { type Storyline } from "../../lib/supabase"; import { BatchTokenDataProvider } from "./BatchTokenDataProvider"; @@ -16,14 +17,35 @@ function chunk(arr: T[], size: number): T[][] { 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 }: { children: React.ReactNode }) { +function ShelfRow({ children, cols }: { children: React.ReactNode; cols: number }) { return (
{/* Books */} -
+
{children}
@@ -51,22 +73,22 @@ function ShelfRow({ children }: { children: React.ReactNode }) { * 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 row of 2 (mobile) or 3 (desktop) - * books sits on a visible shelf surface. + * 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[]; - // We chunk by 3 (desktop cols) — CSS handles showing 2 cols on mobile - const shelves = chunk(storylines, 3); + const cols = useShelfSize(); + const shelves = chunk(storylines, cols); return (
{shelves.map((shelf, i) => ( - + {shelf.map((s) => ( ))} From 1fd8ed982bfe3b94070b6bfc82bdeddc6aea509c Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 21 Mar 2026 08:39:17 +0000 Subject: [PATCH 3/3] [#405] Add 3D rotateY tilt to book cards Books now have a subtle -3deg rotateY perspective tilt at rest, straightening on hover for a "picking up from shelf" effect. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/StoryCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index 9e3b9ea2..d5bf24d4 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -27,7 +27,7 @@ export function StoryCard({ {/* 3D Book with spine */} {/* Drop shadow beneath book — grows on hover */}