From 546c6e75ea9554285fb544d960e2c9e62b406d55 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Wed, 18 Mar 2026 16:38:37 +0000 Subject: [PATCH 1/3] [#331] Combine create + chain into tabbed page with Add Plot shortcut - Unified /create page with [New] and [Add Plot] tabs - New tab: full create storyline form (unchanged behavior) - Add Plot tab: chain plot form with storyline selector - ?tab=chain and ?storyline= query params auto-select and prefill - /chain now redirects to /create?tab=chain - Removed "chain" link from NavBar - Added AddPlotButton client component on story detail page, visible only to the connected writer, links to /create?tab=chain Fixes #331 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/chain/page.tsx | 243 +------------ src/app/create/page.tsx | 521 ++++++++++++++++++++------- src/app/story/[storylineId]/page.tsx | 4 + src/components/AddPlotButton.tsx | 24 ++ src/components/NavBar.tsx | 1 - 5 files changed, 421 insertions(+), 372 deletions(-) create mode 100644 src/components/AddPlotButton.tsx diff --git a/src/app/chain/page.tsx b/src/app/chain/page.tsx index 07172ae4..1c1e6b2c 100644 --- a/src/app/chain/page.tsx +++ b/src/app/chain/page.tsx @@ -1,234 +1,11 @@ -"use client"; - -import { useState } from "react"; -import { useAccount } from "wagmi"; -import { useQuery } from "@tanstack/react-query"; -import { - validateContentLength, - MIN_CONTENT_LENGTH, - MAX_CONTENT_LENGTH, -} from "../../../lib/content"; -import { supabase, type Storyline } from "../../../lib/supabase"; -import { STORY_FACTORY } from "../../../lib/contracts/constants"; -import { useChainPlot } from "../../hooks/useChainPlot"; -import { usePublishIntent } from "../../hooks/usePublishIntent"; -import { RecoveryBanner } from "../../components/RecoveryBanner"; -import type { PublishState } from "../../hooks/usePublish"; -import Link from "next/link"; -import { ConnectWallet } from "../../components/ConnectWallet"; -import { Select } from "../../components/Select"; - -const STATE_LABELS: Record = { - idle: "", - uploading: "Uploading to IPFS...", - confirming: "Confirm in wallet...", - pending: "Publishing to Base...", - indexing: "Indexing...", - published: "Published!", - error: "Error", -}; - -async function fetchWriterStorylines(address: string): Promise { - if (!supabase) return []; - const { data } = await supabase - .from("storylines") - .select("*") - .eq("writer_address", address.toLowerCase()) - .eq("hidden", false) - .eq("sunset", false) - .eq("contract_address", STORY_FACTORY.toLowerCase()) - .order("block_timestamp", { ascending: false }) - .returns(); - return data ?? []; -} - -export default function ChainPlotPage() { - const { address, isConnected } = useAccount(); - const [storylineId, setStorylineId] = useState(null); - const [title, setTitle] = useState(""); - const [content, setContent] = useState(""); - - const { data: storylines = [], isLoading: loadingStorylines } = useQuery({ - queryKey: ["writer-active-storylines", address], - queryFn: () => fetchWriterStorylines(address!), - enabled: isConnected && !!address, - }); - - const { pendingIntent, saveIntent, persistTxHash, clearIntent, attemptRetry } = - usePublishIntent(); - const { state, error, chainPlot, reset } = useChainPlot({ - onIntentSave: saveIntent, - onTxConfirmed: persistTxHash, - onIndexed: clearIntent, - }); - const { valid, charCount } = validateContentLength(content); - const titleValid = title.trim().length > 0; - const canSubmit = - (state === "idle" || state === "error") && - storylineId !== null && - titleValid && - valid; - - if (!isConnected) { - return ( -
-

- Connect your wallet to chain a plot. -

- -
- ); - } - - if (state === "published") { - return ( -
-

Plot chained!

-
- {storylineId && ( - - View story - - )} - -
-
- ); - } - - const busy = state !== "idle" && state !== "error"; - const noStoryline = storylineId === null; - - return ( -
-

- Chain Plot -

- - {pendingIntent && ( -
- -
- )} - -
{ - e.preventDefault(); - if (canSubmit) chainPlot(storylineId, content, title); - }} - className="mt-8 space-y-6" - > - {/* Storyline selector */} -
- - {loadingStorylines ? ( -

Loading storylines...

- ) : storylines.length === 0 ? ( -

- No active storylines.{" "} - - Create one - -

- ) : ( - setTitle(e.target.value.slice(0, 100))} - disabled={busy || noStoryline} - placeholder={noStoryline ? "Select a storyline first" : "e.g. The Silent Storm"} - maxLength={100} - className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50" - /> -
- {!titleValid && content.length > 0 ? ( - Title is required - ) : ( - - )} - {title.length} / 100 chars -
-
- - {/* Content */} -
- -