1"use client";
2
3
10import { useMemo } from "react";
11import { LayoutGrid, Plus } from "lucide-react";
12import { cn } from "@/lib/utils";
13import {
14 DatabaseProperty,
15 DatabaseView,
16 RecordDto,
17 UseDatabaseRecordsResult,
18 useCreateRecord,
19 valueToText
20} from "@/hooks/use-database";
21import { cellToText } from "@/components/database/cell-renderer";
22
23export interface GalleryProps {
24 databaseId: string;
25 view: DatabaseView;
26 properties: DatabaseProperty[];
27 records: UseDatabaseRecordsResult;
28 onOpenRecord: (record: RecordDto) => void;
29 readOnly?: boolean;
30}
31
32function coverOf(record: RecordDto, properties: DatabaseProperty[]): string | null {
33 if (record.coverUrl) return record.coverUrl;
34 const fileProp = properties.find(
35 (p) => p.type === "files" && (record.properties[p.id]?.fileIds?.length ?? 0) > 0
36 );
37 if (fileProp) {
38 const id = record.properties[fileProp.id]?.fileIds?.[0];
39 if (id) return `/api/preview/file/${id}`;
40 }
41 return null;
42}
43
44export function GalleryView({ databaseId, view, properties, records, onOpenRecord, readOnly }: GalleryProps) {
45 const { data, isLoading } = records;
46 const createRecord = useCreateRecord(databaseId);
47 const titleProp = properties.find((p) => p.isTitle);
48 const perRow = view.config.cardsPerRow ?? 4;
49
50 const chips = useMemo(
51 () => properties.filter((p) => p !== titleProp && p.type !== "files" && p.type !== "relation").slice(0, 2),
52 [properties, titleProp]
53 );
54
55 const addCard = () => {
56 createRecord.mutate({ properties: titleProp ? { [titleProp.id]: { text: "New card", marks: [] } } : {} });
57 };
58
59 if (isLoading) {
60 return (
61 <div className="grid gap-3 p-4" style={{ gridTemplateColumns: `repeat(${perRow}, minmax(0, 1fr))` }}>
62 {Array.from({ length: 8 }).map((_, i) => (
63 <div key={i} className="h-40 animate-pulse rounded-lg bg-muted" />
64 ))}
65 </div>
66 );
67 }
68
69 return (
70 <div className="h-full overflow-y-auto p-4">
71 {data.records.length === 0 ? (
72 <div className="flex flex-col items-center gap-3 py-16 text-center">
73 <LayoutGrid className="size-8 text-muted-foreground" aria-hidden />
74 <p className="text-sm text-muted-foreground">No cards in this view yet.</p>
75 {!readOnly && (
76 <button type="button" className="inline-flex items-center gap-1 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90" onClick={addCard}>
77 <Plus className="size-4" /> Add card
78 </button>
79 )}
80 </div>
81 ) : (
82 <div
83 className="grid gap-3"
84 style={{ gridTemplateColumns: `repeat(${perRow}, minmax(0, 1fr))`, gridAutoRows: "auto" }}
85 >
86 {data.records.map((r) => {
87 const cover = coverOf(r, properties);
88 return (
89 <button
90 key={r.id}
91 type="button"
92 className="group overflow-hidden rounded-lg border bg-card text-left shadow-sm transition-shadow hover:shadow-md"
93 onClick={() => onOpenRecord(r)}
94 >
95 {cover && (
96 <div className="aspect-[16/9] w-full overflow-hidden bg-muted">
97 <img src={cover} alt="" className="size-full object-cover transition-transform group-hover:scale-[1.02]" loading="lazy" />
98 </div>
99 )}
100 <div className="p-2.5">
101 <p className="truncate text-sm font-medium">
102 {r.icon ? `${r.icon} ` : ""}
103 {titleProp ? valueToText(r.properties[titleProp.id], titleProp) || "Untitled" : r.id.slice(-6)}
104 </p>
105 <div className="mt-1.5 flex flex-wrap gap-1">
106 {chips.map((p) => {
107 const text = cellToText(r.properties[p.id], p);
108 if (!text) return null;
109 return (
110 <span key={p.id} className={cn("inline-flex max-w-full items-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground")}>
111 <span className="opacity-70">{p.name}:</span>
112 <span className="truncate">{text}</span>
113 </span>
114 );
115 })}
116 </div>
117 </div>
118 </button>
119 );
120 })}
121 {!readOnly && (
122 <button type="button" className="flex min-h-24 items-center justify-center gap-1 rounded-lg border border-dashed text-sm text-muted-foreground hover:bg-muted" onClick={addCard}>
123 <Plus className="size-4" /> Add card
124 </button>
125 )}
126 </div>
127 )}
128 </div>
129 );
130}
131