SPB Git
4.4 KB · 51 lines tsx
1"use client";2import { useEffect, useRef, useState } from "react";3import Link from "next/link";4import * as DialogP from "@radix-ui/react-dialog";5import { useQuery } from "@tanstack/react-query";6import { X, Maximize2, Trash2 } from "lucide-react";7import { Spinner } from "@/components/ui/primitives";8import { Editor } from "@/components/editor/Editor";9import { api } from "@/lib/client/api";10import { debounce } from "@/lib/utils";11import { toast } from "@/components/ui/toast";12import { useDb } from "./hooks";13import { Cell } from "./cells/Cell";14import { TypeIcon } from "./meta";1516/** Side peek of a row: title, all properties, and the row page body. */17export function RowPeek({ rowId, onClose }: { rowId: string | null; onClose: () => void }) {18  const ctx = useDb(); const row = ctx.rows.find((r) => r.id === rowId); const tp = ctx.props.find((p) => p.type === "title");19  const { data: page } = useQuery<any>({ queryKey: ["page", row?.pageId], enabled: !!row, queryFn: () => api(`/api/pages/${row!.pageId}`) });20  const [editing, setEditing] = useState<string | null>(null); const [title, setTitle] = useState(""); const saveTitle = useRef(debounce((v: string) => { if (row && tp) ctx.setCell(row.id, { [tp.id]: v }); }, 500)).current;21  const saveBody = useRef(debounce(async (pageId: string, content: unknown) => { try { await api(`/api/pages/${pageId}`, { method: "PATCH", json: { content } }); } catch (e: any) { toast("Save failed", { description: e?.message, tone: "danger" }); } }, 800)).current;22  useEffect(() => { if (row) setTitle(row.title === "Untitled" ? "" : row.title); }, [row?.id]); // eslint-disable-line react-hooks/exhaustive-deps23  return (24    <DialogP.Root open={!!rowId} onOpenChange={(o) => { if (!o) onClose(); }}>25      <DialogP.Portal>26        <DialogP.Overlay className="fixed inset-0 z-40 bg-black/30 backdrop-blur-[1px]" />27        <DialogP.Content className="fixed inset-y-0 right-0 z-50 flex w-full flex-col border-l border-line bg-bg shadow-pop outline-none sm:w-[min(100vw,38rem)]" onOpenAutoFocus={(e) => e.preventDefault()}>28          <DialogP.Title className="sr-only">Row</DialogP.Title><DialogP.Description className="sr-only">Row details</DialogP.Description>29          <div className="flex h-12 shrink-0 items-center gap-1 border-b border-line px-3">30            <DialogP.Close className="grid size-9 place-items-center rounded-md text-fg-muted hover:bg-bg-sunk"><X className="size-4" /></DialogP.Close>31            {row && <Link href={`/p/${row.pageId}`} className="flex h-8 items-center gap-1 rounded-md px-2 text-[12px] text-fg-muted hover:bg-bg-sunk hover:text-fg"><Maximize2 className="size-3.5" /> Open as page</Link>}32            {row && <button type="button" onClick={async () => { if (!confirm("Delete this row?")) return; await ctx.removeRows([row.id]); onClose(); }} className="ml-auto flex h-8 items-center gap-1 rounded-md px-2 text-[12px] text-fg-muted hover:bg-bg-sunk hover:text-danger"><Trash2 className="size-3.5" /> Delete</button>}33          </div>34          {!row ? <div className="grid flex-1 place-items-center"><Spinner /></div> : (35            <div className="flex-1 overflow-y-auto px-5 py-4 sm:px-7">36              <textarea value={title} onChange={(e) => { setTitle(e.target.value); saveTitle(e.target.value); }} placeholder="Untitled" rows={1} className="w-full resize-none bg-transparent text-2xl font-semibold tracking-tight outline-none placeholder:text-fg-faint" onKeyDown={(e) => { if (e.key === "Enter") e.preventDefault(); }} />37              <div className="mt-3 divide-y divide-line border-y border-line">38                {ctx.props.filter((p) => p.type !== "title").map((p) => (39                  <div key={p.id} className="grid grid-cols-[7.5rem_1fr] items-start gap-2 py-0.5 sm:grid-cols-[9rem_1fr]">40                    <div className="flex h-10 items-center gap-1.5 truncate text-[12px] text-fg-muted"><TypeIcon type={p.type} /><span className="truncate">{p.name}</span></div>41                    <div className="min-w-0"><Cell p={p} row={row} mode="form" editing={editing === p.id} onEdit={(o) => setEditing(o ? p.id : null)} /></div>42                  </div>))}43              </div>44              <div className="mt-4">{page ? <Editor key={page.id} content={page.content} pageId={page.id} editable placeholder="Notes for this row…" onChange={(doc) => saveBody(page.id, doc)} /> : <Spinner />}</div>45            </div>)}46        </DialogP.Content>47      </DialogP.Portal>48    </DialogP.Root>49  );50}51
Files · main
Menu

Theme