From 0e76ca1895d5892853530435aa998622c7141869 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 16 May 2026 11:41:24 +0200 Subject: [PATCH] clean up --- Cargo.lock | 6 +- .../plans/2026-05-15-frontend-overhaul.md | 1425 ----------------- .../2026-05-16-federated-hashtag-indexing.md | 402 ----- .../plans/2026-05-16-suspense-streaming.md | 579 ------- .../2026-05-15-frontend-overhaul-design.md | 304 ---- ...05-16-federated-hashtag-indexing-design.md | 84 - .../2026-05-16-suspense-streaming-design.md | 173 -- 7 files changed, 4 insertions(+), 2969 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-15-frontend-overhaul.md delete mode 100644 docs/superpowers/plans/2026-05-16-federated-hashtag-indexing.md delete mode 100644 docs/superpowers/plans/2026-05-16-suspense-streaming.md delete mode 100644 docs/superpowers/specs/2026-05-15-frontend-overhaul-design.md delete mode 100644 docs/superpowers/specs/2026-05-16-federated-hashtag-indexing-design.md delete mode 100644 docs/superpowers/specs/2026-05-16-suspense-streaming-design.md diff --git a/Cargo.lock b/Cargo.lock index bdff7c1..f9a52ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,9 +427,11 @@ dependencies = [ "bcrypt", "chrono", "domain", + "hex", "jsonwebtoken", "rand 0.8.6", "serde", + "sha2", "thiserror 2.0.18", "tokio", "uuid", @@ -1056,7 +1058,9 @@ dependencies = [ "async-trait", "chrono", "futures", + "hex", "serde", + "sha2", "thiserror 2.0.18", "tokio", "url", @@ -2525,11 +2529,9 @@ dependencies = [ "axum", "chrono", "domain", - "hex", "http-body-util", "serde", "serde_json", - "sha2", "tokio", "tower", "tower-http", diff --git a/docs/superpowers/plans/2026-05-15-frontend-overhaul.md b/docs/superpowers/plans/2026-05-15-frontend-overhaul.md deleted file mode 100644 index e37b52a..0000000 --- a/docs/superpowers/plans/2026-05-15-frontend-overhaul.md +++ /dev/null @@ -1,1425 +0,0 @@ -# Frontend Overhaul Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Eliminate the author-profile fetch waterfall, replace `router.refresh()` with targeted Server Action cache invalidation, and fix composition throughout the frontend. - -**Architecture:** Backend already embeds `author: UserResponse` in `ThoughtResponse` — no backend changes needed. Frontend pages ignore this and re-fetch profiles separately; we delete those calls. Mutations become Server Actions that call `revalidateTag` so only affected data refetches. `useOptimistic` (React 19) gives instant UI feedback. Large components split into focused sub-components. Near-duplicate form components unified. - -**Tech Stack:** Next.js 15 App Router, React 19, TypeScript, Tailwind CSS v4, shadcn/ui, react-hook-form + Zod, `next/cache` (`revalidateTag`), `next/headers` (`cookies`) - -**Working directory:** `thoughts-frontend/` - ---- - -## Key facts before you start - -- Auth token lives in a cookie named `auth_token`. In server components: `(await cookies()).get('auth_token')?.value`. In Server Actions: same. -- `apiFetch(endpoint, options, schema, token?)` is in `lib/api.ts`. `options` is `RequestInit`. -- `ThoughtResponse` (Zod: `ThoughtSchema`) already has `author: UserSchema` embedded — `thought.author.username`, `thought.author.avatarUrl`, `thought.author.displayName` all exist on every thought. -- `ThoughtThread` component (not the type) re-fetches the avatar via `authorDetails.get(username)` as an override — this is now unnecessary. -- Run the dev server with `npm run dev` (or `bun dev`) from `thoughts-frontend/`. -- Type-check with `npx tsc --noEmit` — run this after each task. - ---- - -## File Map - -| File | Status | Change | -|---|---|---| -| `lib/api.ts` | modify | Add `next?: NextFetchRequestConfig` to `apiFetch`; tag GET calls | -| `app/actions/thoughts.ts` | **create** | Server Actions: createThought, deleteThought | -| `app/actions/social.ts` | **create** | Server Actions: followUser, unfollowUser | -| `app/actions/profile.ts` | **create** | Server Action: updateProfile | -| `app/page.tsx` | modify | Remove getUserProfile waterfall; remove authorDetails | -| `app/users/[username]/page.tsx` | modify | Remove getUserProfile waterfall; remove authorDetails | -| `app/thoughts/[thoughtId]/page.tsx` | modify | Remove getUserProfile waterfall; remove authorDetails | -| `app/tags/[tagName]/page.tsx` | modify | Remove getUserProfile waterfall; remove authorDetails | -| `app/search/page.tsx` | modify | Remove getUserProfile waterfall; remove authorDetails | -| `components/thought-thread.tsx` | modify | Remove authorDetails prop; use thought.author directly | -| `components/thought-card.tsx` | modify | Remove author prop; use thought.author directly; call Server Action for delete | -| `components/follow-button.tsx` | modify | Replace router.refresh() with Server Action + useOptimistic | -| `components/post-thought-form.tsx` | **delete** | Replaced by ThoughtForm | -| `components/reply-form.tsx` | **delete** | Replaced by ThoughtForm | -| `components/thought-form.tsx` | **create** | Unified form accepting a Server Action | -| `components/empty-state.tsx` | **create** | Shared empty state primitive | -| `components/loading-skeleton.tsx` | **create** | Shared skeleton primitive | -| `components/edit-profile-form.tsx` | modify | Replace router.refresh() with Server Action | -| `components/remote-user-profile.tsx` | modify | Split into sub-components | -| `components/remote-user-profile/profile-card.tsx` | **create** | Avatar, bio, stats | -| `components/remote-user-profile/connections.tsx` | **create** | Followers/following lists | - ---- - -## Task 1: Add `next` tag support to `apiFetch` and tag all GET fetches - -**Files:** -- Modify: `lib/api.ts` - -- [ ] **Step 1: Update `apiFetch` signature to accept Next.js cache options** - -In `lib/api.ts`, change lines 154–188 (`apiFetch` function). Replace the function with: - -```ts -type ApiFetchOptions = Omit & { - next?: { tags?: string[]; revalidate?: number | false } -} - -async function apiFetch( - endpoint: string, - options: ApiFetchOptions = {}, - schema: z.ZodType, - token?: string | null -): Promise { - if (!API_BASE_URL) { - throw new Error("API_BASE_URL is not defined"); - } - - const { next, ...restOptions } = options; - - const headers: Record = { - "Content-Type": "application/json", - ...(restOptions.headers as Record), - }; - - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...restOptions, - headers, - ...(next ? { next } : {}), - }); - - if (!response.ok) { - throw new Error(`API request failed with status ${response.status}`); - } - - if (response.status === 204) { - return null as T; - } - - const data = await response.json(); - return schema.parse(data); -} -``` - -- [ ] **Step 2: Tag the GET fetches that pages depend on** - -Update these functions in `lib/api.ts` to pass `next.tags`: - -```ts -export const getMe = (token: string) => - apiFetch("/users/me", { next: { tags: ["me"] } }, MeSchema, token); - -export const getUserProfile = (username: string, token: string | null) => - apiFetch(`/users/${username}`, { next: { tags: [`profile:${username}`] } }, UserSchema, token); - -export const getFollowersList = (username: string, token: string | null) => - apiFetch( - `/users/${username}/followers`, - { next: { tags: [`profile:${username}`] } }, - z.object({ total: z.number(), items: z.array(UserSchema) }), - token - ); - -export const getFollowingList = (username: string, token: string | null) => - apiFetch( - `/users/${username}/following`, - { next: { tags: [`profile:${username}`] } }, - z.object({ total: z.number(), items: z.array(UserSchema) }), - token - ); - -export const getTopFriends = (username: string, token: string | null) => - apiFetch( - `/users/${username}/top-friends`, - { next: { tags: [`profile:${username}`] } }, - z.object({ topFriends: z.array(z.string()) }), - token - ); - -export const getFeed = (token: string, page: number = 1, pageSize: number = 20) => - apiFetch( - `/feed?page=${page}&per_page=${pageSize}`, - { next: { tags: ["feed"] } }, - z.object({ items: z.array(ThoughtSchema), total: z.number(), page: z.number(), per_page: z.number() }) - .transform((d) => ({ ...d, totalPages: Math.ceil(d.total / d.per_page) })), - token - ); - -export const getUserThoughts = (username: string, token: string | null) => - apiFetch( - `/users/${username}/thoughts`, - { next: { tags: [`profile:${username}`] } }, - z.object({ items: z.array(ThoughtSchema), total: z.number(), page: z.number(), per_page: z.number() }), - token - ); - -export const getThoughtById = (thoughtId: string, token: string | null) => - apiFetch( - `/thoughts/${thoughtId}`, - { next: { tags: [`thought:${thoughtId}`] } }, - ThoughtSchema, - token - ); - -export const getThoughtsByTag = (tagName: string, token: string | null) => - apiFetch( - `/tags/${tagName}`, - { next: { tags: [`tag:${tagName}`, "feed"] } }, - z.object({ tag: z.string(), items: z.array(ThoughtSchema), total: z.number(), page: z.number(), per_page: z.number() }), - token - ); - -export const search = (query: string, token: string | null) => - apiFetch( - `/search?q=${encodeURIComponent(query)}`, - { next: { tags: ["search"] } }, - SearchResultsSchema, - token - ); -``` - -- [ ] **Step 3: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. - -- [ ] **Step 4: Commit** - -```bash -git add lib/api.ts -git commit -m "feat(frontend): add next.tags support to apiFetch; tag all GET fetches" -``` - ---- - -## Task 2: Server Actions — thought mutations - -**Files:** -- Create: `app/actions/thoughts.ts` - -- [ ] **Step 1: Create the file** - -```ts -// app/actions/thoughts.ts -"use server"; - -import { revalidateTag } from "next/cache"; -import { cookies } from "next/headers"; -import { createThought as apiCreateThought, deleteThought as apiDeleteThought, CreateThoughtSchema } from "@/lib/api"; -import { z } from "zod"; - -async function getToken(): Promise { - const token = (await cookies()).get("auth_token")?.value; - if (!token) throw new Error("Not authenticated"); - return token; -} - -export async function createThought(data: z.infer) { - const token = await getToken(); - const thought = await apiCreateThought(data, token); - revalidateTag("feed"); - return thought; -} - -export async function deleteThought(thoughtId: string) { - const token = await getToken(); - await apiDeleteThought(thoughtId, token); - revalidateTag("feed"); - revalidateTag(`thought:${thoughtId}`); -} -``` - -- [ ] **Step 2: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/actions/thoughts.ts -git commit -m "feat(frontend): Server Actions for thought mutations" -``` - ---- - -## Task 3: Server Actions — social mutations - -**Files:** -- Create: `app/actions/social.ts` - -- [ ] **Step 1: Create the file** - -```ts -// app/actions/social.ts -"use server"; - -import { revalidateTag } from "next/cache"; -import { cookies } from "next/headers"; -import { - followUser as apiFollowUser, - unfollowUser as apiUnfollowUser, -} from "@/lib/api"; - -async function getToken(): Promise { - const token = (await cookies()).get("auth_token")?.value; - if (!token) throw new Error("Not authenticated"); - return token; -} - -export async function followUser(username: string) { - const token = await getToken(); - await apiFollowUser(username, token); - revalidateTag(`profile:${username}`); - revalidateTag("feed"); -} - -export async function unfollowUser(username: string) { - const token = await getToken(); - await apiUnfollowUser(username, token); - revalidateTag(`profile:${username}`); - revalidateTag("feed"); -} -``` - -- [ ] **Step 2: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/actions/social.ts -git commit -m "feat(frontend): Server Actions for social mutations" -``` - ---- - -## Task 4: Server Actions — profile mutation - -**Files:** -- Create: `app/actions/profile.ts` - -- [ ] **Step 1: Create the file** - -```ts -// app/actions/profile.ts -"use server"; - -import { revalidateTag } from "next/cache"; -import { cookies } from "next/headers"; -import { updateProfile as apiUpdateProfile, UpdateProfileSchema } from "@/lib/api"; -import { z } from "zod"; - -async function getToken(): Promise { - const token = (await cookies()).get("auth_token")?.value; - if (!token) throw new Error("Not authenticated"); - return token; -} - -export async function updateProfile( - username: string, - data: z.infer -) { - const token = await getToken(); - const updated = await apiUpdateProfile(data, token); - revalidateTag(`profile:${username}`); - revalidateTag("me"); - return updated; -} -``` - -- [ ] **Step 2: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/actions/profile.ts -git commit -m "feat(frontend): Server Action for profile mutation" -``` - ---- - -## Task 5: Remove the author-profile waterfall - -This is the biggest performance fix. Pages currently re-fetch `getUserProfile` for every unique author after the feed loads. Since `thought.author` already contains the data, we delete all of this. - -**Files:** -- Modify: `components/thought-thread.tsx` -- Modify: `components/thought-card.tsx` -- Modify: `app/page.tsx` -- Modify: `app/users/[username]/page.tsx` -- Modify: `app/thoughts/[thoughtId]/page.tsx` -- Modify: `app/tags/[tagName]/page.tsx` -- Modify: `app/search/page.tsx` - -### 5a: Update `ThoughtThread` — remove `authorDetails` prop - -- [ ] **Step 1: Replace `components/thought-thread.tsx`** - -```tsx -// components/thought-thread.tsx -import { Me, ThoughtThread as ThoughtThreadType } from "@/lib/api"; -import { ThoughtCard } from "./thought-card"; - -interface ThoughtThreadProps { - thought: ThoughtThreadType; - currentUser: Me | null; - isReply?: boolean; -} - -export function ThoughtThread({ - thought, - currentUser, - isReply = false, -}: ThoughtThreadProps) { - return ( -
- - - {thought.replies.length > 0 && ( -
- {thought.replies.map((reply) => ( - - ))} -
- )} -
- ); -} -``` - -### 5b: Update `ThoughtCard` — use `thought.author` directly, call Server Action for delete - -- [ ] **Step 2: Replace `components/thought-card.tsx`** - -```tsx -// components/thought-card.tsx -"use client"; - -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; -import { UserAvatar } from "./user-avatar"; -import { Me, Thought } from "@/lib/api"; -import { format, formatDistanceToNow } from "date-fns"; -import { useAuth } from "@/hooks/use-auth"; -import { useState } from "react"; -import { toast } from "sonner"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { CornerUpLeft, MessageSquare, MoreHorizontal, Trash2 } from "lucide-react"; -import { ReplyForm } from "@/components/reply-form"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; -import { deleteThought } from "@/app/actions/thoughts"; - -interface ThoughtCardProps { - thought: Thought; - currentUser: Me | null; - isReply?: boolean; -} - -export function ThoughtCard({ - thought, - currentUser, - isReply = false, -}: ThoughtCardProps) { - const [isAlertOpen, setIsAlertOpen] = useState(false); - const [isReplyOpen, setIsReplyOpen] = useState(false); - const { token } = useAuth(); - const timeAgo = formatDistanceToNow(new Date(thought.createdAt), { addSuffix: true }); - const { author } = thought; - const isAuthor = currentUser?.username === author.username; - - const handleDelete = async () => { - try { - await deleteThought(thought.id); - toast.success("Thought deleted successfully."); - } catch { - toast.error("Failed to delete thought."); - } finally { - setIsAlertOpen(false); - } - }; - - return ( - <> -
- {thought.replyToId && isReply && ( -
- - - Replying to{" "} - - parent thought - - -
- )} - {!thought.replyToId && thought.replyToUrl && ( -
- - - Replying to{" "} - - original post ↗ - - -
- )} -
- - - - -
- {author.displayName || author.username} - -
- - - - - - - {isAuthor && ( - setIsAlertOpen(true)}> - - Delete - - )} - - - - View - - - - -
- - {author.local ? ( -

{thought.content}

- ) : ( -
📎 Media attachment — not supported

', - }} - /> - )} - - - {token && ( - - - - )} - - {isReplyOpen && ( -
- setIsReplyOpen(false)} /> -
- )} - - - - - Are you sure? - - This action cannot be undone. This will permanently delete your thought. - - - - Cancel - Delete - - - - - ); -} -``` - -### 5c: Remove waterfall from `app/page.tsx` - -- [ ] **Step 3: Update `app/page.tsx` — delete lines 65–74 (the `getUserProfile` waterfall) and the `authorDetails` map** - -Replace the `FeedPage` function body with: - -```tsx -async function FeedPage({ - token, - searchParams, -}: { - token: string; - searchParams: { page?: string }; -}) { - const page = parseInt(searchParams.page ?? "1", 10); - - const [feedData, me] = await Promise.all([ - getFeed(token, page).catch(() => null), - getMe(token).catch(() => null) as Promise, - ]); - - if (!feedData || !me) { - redirect("/login"); - } - - const { items: allThoughts, totalPages } = feedData; - const thoughtThreads = buildThoughtThreads(allThoughts); - - const friends = (await getFriends(token)).users.map((user) => user.username); - const topFriendsData = me - ? await getTopFriends(me.username, token).catch(() => ({ topFriends: [] })) - : { topFriends: [] }; - const shouldDisplayTopFriends = topFriendsData.topFriends.length > 0; - - return ( -
-
- - -
-
-

Your Feed

-
- - -
- - {shouldDisplayTopFriends && ( - - )} - {!shouldDisplayTopFriends && token && friends.length > 0 && ( - - )} - -
- -
- {thoughtThreads.map((thought) => ( - - ))} - {thoughtThreads.length === 0 && ( -

- Your feed is empty. Follow some users to see their thoughts! -

- )} -
- `/?page=${p}`} - /> -
- - -
-
- ); -} -``` - -Also remove `getUserProfile` and `User` from the import at the top of `app/page.tsx`. - -### 5d: Remove waterfall from remaining pages - -- [ ] **Step 4: Update `app/users/[username]/page.tsx`** - -Read the full file first. Find the block that: -1. Collects unique author usernames from `thoughts` -2. Calls `getUserProfile` for each -3. Builds an `authorDetails` Map - -Delete that entire block. Remove `authorDetails` from all `ThoughtThread` usages in this file. Remove `getUserProfile` (for author fetching, not for the profile page header — keep the one that fetches the page subject's profile) and `User` from the imports if they are no longer needed. - -The `ThoughtThread` components in this file should be called as: -```tsx - -``` -(no `authorDetails` prop) - -- [ ] **Step 5: Update `app/thoughts/[thoughtId]/page.tsx`** - -Same pattern: find and delete the author waterfall, remove `authorDetails` from `ThoughtThread` calls. - -- [ ] **Step 6: Update `app/tags/[tagName]/page.tsx`** - -Same pattern. - -- [ ] **Step 7: Update `app/search/page.tsx`** - -Same pattern. - -- [ ] **Step 8: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. Fix any remaining `authorDetails` references until clean. - -- [ ] **Step 9: Verify in browser** - -Run `npm run dev`. Open the feed page. Check the Network tab — you should see **no** `getUserProfile` calls after the feed loads. Author avatars should still display correctly (from `thought.author.avatarUrl`). - -- [ ] **Step 10: Commit** - -```bash -git add components/thought-thread.tsx components/thought-card.tsx app/page.tsx \ - app/users app/thoughts app/tags app/search -git commit -m "perf(frontend): eliminate author profile waterfall — use thought.author directly" -``` - ---- - -## Task 6: Shared primitives — EmptyState and LoadingSkeleton - -**Files:** -- Create: `components/empty-state.tsx` -- Create: `components/loading-skeleton.tsx` - -- [ ] **Step 1: Create `components/empty-state.tsx`** - -```tsx -// components/empty-state.tsx -interface EmptyStateProps { - message: string - className?: string -} - -export function EmptyState({ message, className }: EmptyStateProps) { - return ( -

- {message} -

- ) -} -``` - -- [ ] **Step 2: Create `components/loading-skeleton.tsx`** - -```tsx -// components/loading-skeleton.tsx -import { Card, CardContent, CardHeader } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" - -export function ThoughtSkeleton() { - return ( - - - -
- - -
-
- - - - -
- ) -} - -export function ProfileSkeleton() { - return ( - - - -
- - -
-
-
- ) -} -``` - -- [ ] **Step 3: Replace copy-pasted empty state blocks** - -In `app/page.tsx`, replace: -```tsx -

- Your feed is empty. Follow some users to see their thoughts! -

-``` -with: -```tsx - -``` - -Add `import { EmptyState } from "@/components/empty-state"` to the file. - -Repeat for `app/users/[username]/page.tsx`, `app/tags/[tagName]/page.tsx`, `app/search/page.tsx` — find each hard-coded empty state paragraph and replace with ``. - -- [ ] **Step 4: Type-check** - -```bash -npx tsc --noEmit -``` - -Expected: 0 errors. - -- [ ] **Step 5: Commit** - -```bash -git add components/empty-state.tsx components/loading-skeleton.tsx \ - app/page.tsx app/users app/tags app/search -git commit -m "refactor(frontend): shared EmptyState and LoadingSkeleton primitives" -``` - ---- - -## Task 7: Unified ThoughtForm — replaces PostThoughtForm and ReplyForm - -**Files:** -- Create: `components/thought-form.tsx` -- Delete: `components/post-thought-form.tsx` -- Delete: `components/reply-form.tsx` -- Modify: `app/page.tsx` (update import) -- Modify: `components/thought-card.tsx` (update import) - -- [ ] **Step 1: Create `components/thought-form.tsx`** - -```tsx -// components/thought-form.tsx -"use client" - -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" -import { - Form, - FormField, - FormItem, - FormControl, - FormMessage, -} from "@/components/ui/form" -import { Textarea } from "@/components/ui/textarea" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { CreateThoughtSchema } from "@/lib/api" -import { useAuth } from "@/hooks/use-auth" -import { toast } from "sonner" -import { Globe, Lock, Users } from "lucide-react" -import { useState } from "react" -import { Confetti } from "./confetti" -import { createThought } from "@/app/actions/thoughts" - -interface ThoughtFormProps { - /** Set to the parent thought ID when composing a reply. */ - replyToId?: string - /** Called after successful submit (e.g. close the reply panel). */ - onSuccess?: () => void - /** Whether to wrap the form in a Card. Default: true for top-level, false for replies. */ - card?: boolean -} - -export function ThoughtForm({ replyToId, onSuccess, card = !replyToId }: ThoughtFormProps) { - const { token } = useAuth() - const [showConfetti, setShowConfetti] = useState(false) - - const form = useForm>({ - resolver: zodResolver(CreateThoughtSchema), - defaultValues: { - content: "", - visibility: "public", - ...(replyToId ? { inReplyToId: replyToId } : {}), - }, - }) - - async function onSubmit(values: z.infer) { - if (!token) { - toast.error("You must be logged in.") - return - } - try { - await createThought(values) - toast.success(replyToId ? "Reply posted!" : "Thought posted!") - setShowConfetti(true) - form.reset() - onSuccess?.() - } catch { - toast.error(replyToId ? "Failed to post reply." : "Failed to post thought.") - } - } - - const inner = ( -
- - ( - - -