diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts new file mode 100644 index 0000000..0164581 --- /dev/null +++ b/src/app/api/posts/route.ts @@ -0,0 +1,26 @@ +import { NextResponse, type NextRequest } from "next/server"; +import { SESSION_COOKIE } from "@/lib/cookie"; +import { createTextPost } from "@/lib/post-create"; + +// POST /api/posts { content } +// Creates a public text draft on the feed, then publishes it. Returns { id }. +// The create call can report status "draft" for a post already on the home +// feed, so the browser reloads the first page and confirms the id there. +// 401 { error: "sign_in" } without a session. + +export const runtime = "nodejs"; + +export async function POST(req: NextRequest) { + const sessionId = req.cookies.get(SESSION_COOKIE)?.value; + let content: unknown = null; + try { + content = ((await req.json()) as { content?: unknown }).content; + } catch { + content = null; + } + const result = await createTextPost(sessionId, content); + if (!result.ok) { + return NextResponse.json({ error: result.error }, { status: result.status }); + } + return NextResponse.json({ id: result.id }); +} diff --git a/src/app/globals.css b/src/app/globals.css index 8897492..2c90508 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -42,3 +42,8 @@ button:disabled { opacity: .5; cursor: not-allowed; } .like-btn:disabled:not(.liked) { color: var(--grey); } .like-error { color: #a8141b; font-size: 13px; align-self: center; } .post-stats { align-items: center; } +.composer { background: #fff; border: 1px solid var(--line); border-radius: 0; padding: 16px; display: flex; flex-direction: column; gap: 8px; } +.composer-label { margin: 0; } +.composer textarea { font: inherit; width: 100%; padding: 12px; border: 1px solid #999; border-radius: 0; resize: vertical; background: #fff; color: var(--ink); } +.composer textarea:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; } +.composer .composer-post { align-self: flex-end; margin: 0; padding: 10px 24px; border-radius: 0; } diff --git a/src/components/Composer.tsx b/src/components/Composer.tsx new file mode 100644 index 0000000..02b367b --- /dev/null +++ b/src/components/Composer.tsx @@ -0,0 +1,36 @@ +import type { FormEvent } from "react"; + +export interface ComposerProps { + value: string; + busy: boolean; + error: boolean; + onChange: (value: string) => void; + onSubmit: () => void; +} + +export function Composer({ value, busy, error, onChange, onSubmit }: ComposerProps) { + const empty = value.trim().length === 0; + function submit(e: FormEvent) { + e.preventDefault(); + if (!empty && !busy) onSubmit(); + } + return ( +
+ ); +} diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index 1e0409a..8ed3f5d 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchFeedPage, FeedLoadError } from "@/lib/feed-client"; import { applyLikeOutcome, LikeError, sendLike } from "@/lib/like-client"; import { mergePosts, toCardPosts, type CardPost } from "@/lib/feed-model"; +import { PostError, publishAndConfirm } from "@/lib/post-client"; import { FeedView, type FeedStatus } from "./FeedView"; const POLL_MS = 45_000; @@ -17,6 +18,12 @@ export function Feed({ onSignedOut }: { onSignedOut: () => void }) { const [pending, setPending] = useState