1"use client";
2
3
22import { useEffect, useState } from "react";
23import { useQuery } from "@tanstack/react-query";
24import { Loader2 } from "lucide-react";
25import { apiFetch } from "@/lib/api";
26import {
27 DatabaseDto,
28 DatabaseProperty,
29 RecordDto,
30 useDatabase
31} from "@/hooks/use-database";
32import { CellRenderer } from "@/components/database/cell-renderer";
33import { CellEditor } from "@/components/database/cell-editor";
34
35export interface RecordHeaderProps {
36
37 recordId?: string;
38
39 databaseId?: string;
40
41 record?: RecordDto;
42
43 properties?: DatabaseProperty[];
44 readOnly?: boolean;
45 className?: string;
46}
47
48function unwrap<T>(body: unknown): T {
49 const b = body as { data?: T } | null;
50 if (b && typeof b === "object" && "data" in (b as Record<string, unknown>)) {
51 return (b as { data: T }).data;
52 }
53 return body as T;
54}
55
56const SKIP_TYPES = new Set(["button"]);
57
58export function RecordHeader({
59 recordId,
60 databaseId: dbIdProp,
61 record: recordProp,
62 properties: propsProp,
63 readOnly,
64 className
65}: RecordHeaderProps) {
66
67 const { data: fetched, isLoading } = useQuery({
68 queryKey: ["record-fresh", recordId ?? recordProp?.id],
69 enabled: !!(recordId ?? recordProp?.id),
70 queryFn: async () => unwrap<RecordDto>(await apiFetch(`/records/${recordId ?? recordProp!.id}`))
71 });
72
73
74 const effectiveRecord: RecordDto | undefined = fetched ?? recordProp;
75 const databaseId = dbIdProp ?? effectiveRecord?.databaseId;
76
77 const { data: database } = useDatabase(databaseId);
78
79 const properties =
80 propsProp ??
81 (database?.properties.filter((p) => !SKIP_TYPES.has(p.type)) as DatabaseProperty[]);
82
83 if (!effectiveRecord) {
84 return (
85 <div className={className}>
86 <div className="mb-2 inline-flex items-center gap-2 text-sm text-muted-foreground">
87 <Loader2 className="size-4 animate-spin" aria-hidden />
88 {isLoading ? "Loading record…" : "Record unavailable"}
89 </div>
90 </div>
91 );
92 }
93
94 const titleProp = properties?.find((p) => p.isTitle) ?? properties?.find((p) => p.type === "title");
95
96 const header = (
97 <div className="flex items-start gap-3">
98 <div className="text-4xl leading-none" aria-hidden>
99 {effectiveRecord.icon ?? "📄"}
100 </div>
101 <div className="min-w-0 flex-1 pt-1">
102 {titleProp ? (
103 <CellEditor
104 key={`title-${effectiveRecord.id}`}
105 record={effectiveRecord}
106 property={titleProp}
107 onDone={() => undefined}
108 className="text-3xl font-bold"
109 />
110 ) : (
111 <h1 className="text-3xl font-bold">Record</h1>
112 )}
113 </div>
114 </div>
115 );
116
117 if (!properties || properties.length === 0) return <div className={className}>{header}</div>;
118
119 const fields = properties.filter((p) => p !== titleProp);
120
121 return (
122 <div className={className}>
123 {header}
124 {fields.length > 0 && (
125 <div className="mt-4 divide-y divide-border/60 border-t border-border">
126 {fields.map((p) => (
127 <div key={p.id} className="flex items-start gap-4 py-2">
128 <div className="w-40 shrink-0 truncate pt-0.5 text-sm text-muted-foreground" title={p.name}>
129 {p.name}
130 </div>
131 <div className="min-w-0 flex-1">
132 {readOnly ? (
133 <CellRenderer record={effectiveRecord} property={p} value={effectiveRecord.properties[p.id]} readOnly />
134 ) : (
135 <CellEditor
136 key={`${effectiveRecord.id}-${p.id}`}
137 record={effectiveRecord}
138 property={p}
139 onDone={() => undefined}
140 />
141 )}
142 </div>
143 </div>
144 ))}
145 </div>
146 )}
147 </div>
148 );
149}
150