import React, { createContext, useContext, useState, useRef, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient, } from "@tanstack/react-query"; import { createTweet, readTweet, destroyTweet, updateTweet, buildCSRFHeaders, } from "./ash_rpc"; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10_000 } }, }); // ── Types ────────────────────────────────────────────────────────────────────── type Tweet = { id: string; content: string; userId: string; state: string }; // ── Auth context ─────────────────────────────────────────────────────────────── const AuthCtx = createContext({ email: "", userId: "" }); // ── Helpers ──────────────────────────────────────────────────────────────────── function timeAgo(): string { return "just now"; } // ── Components ───────────────────────────────────────────────────────────────── function Spinner() { return (
); } function ErrorBanner({ message }: { message: string }) { return (
{message}
); } function CharCount({ current, max }: { current: number; max: number }) { const remaining = max - current; const pct = current / max; const color = pct > 0.9 ? "#ef4444" : pct > 0.75 ? "#f59e0b" : "var(--mx-muted)"; return ( {remaining} ); } function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) { const [text, setText] = useState(""); const [error, setError] = useState(null); const textareaRef = useRef(null); const qc = useQueryClient(); const MAX = 280; const mutation = useMutation({ mutationFn: async (content: string) => { const res = await createTweet({ input: { content }, fields: ["id", "content", "userId", "state"], headers: buildCSRFHeaders(), }); if (!res.success) throw new Error(res.errors?.[0]?.message ?? "Failed"); return res.data; }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["tweets"] }); setText(""); setError(null); onSuccess?.(); }, onError: (e: Error) => setError(e.message), }); function handleKeyDown(e: React.KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); submit(); } } function submit() { const trimmed = text.trim(); if (!trimmed) return; if (trimmed.length > MAX) { setError(`Max ${MAX} characters`); return; } setError(null); mutation.mutate(trimmed); } // Auto-resize textarea useEffect(() => { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }, [text]); return (
M