1"use client";
2
3
8import { ArrowDown, ArrowUp, Plus, X } from "lucide-react";
9import { cn } from "@/lib/utils";
10import { DatabaseProperty, SortSpec } from "@/hooks/use-database";
11
12const inputCls =
13 "rounded border border-input bg-background px-1.5 py-0.5 text-xs outline-none focus-visible:ring-1 focus-visible:ring-ring";
14
15export interface SortBuilderProps {
16 value: SortSpec[] | null;
17 onChange: (sort: SortSpec[] | null) => void;
18 properties: DatabaseProperty[];
19 className?: string;
20}
21
22export function SortBuilder({ value, onChange, properties, className }: SortBuilderProps) {
23 const sorts = value ?? [];
24 const usedProps = new Set(sorts.map((s) => s.propertyId));
25 const available = properties.filter((p) => !usedProps.has(p.id) || p.isTitle);
26
27 if (properties.length === 0) {
28 return <p className="px-1 py-2 text-xs text-muted-foreground">Add properties before sorting.</p>;
29 }
30
31 return (
32 <div className={className}>
33 {sorts.map((s, i) => (
34 <div key={i} className="flex items-center gap-1.5 py-1">
35 <select
36 className={cn(inputCls, "w-36")}
37 value={s.propertyId}
38 onChange={(e) => {
39 const next = sorts.map((x, j) => (j === i ? { ...x, propertyId: e.target.value } : x));
40 onChange(next);
41 }}
42 aria-label={`Sort property ${i + 1}`}
43 >
44 {properties.map((p) => (
45 <option key={p.id} value={p.id}>
46 {p.name}
47 </option>
48 ))}
49 </select>
50 <select
51 className={cn(inputCls, "w-24")}
52 value={s.direction}
53 onChange={(e) => {
54 const next = sorts.map((x, j) => (j === i ? { ...x, direction: e.target.value as "asc" | "desc" } : x));
55 onChange(next);
56 }}
57 aria-label={`Direction ${i + 1}`}
58 >
59 <option value="asc">Ascending</option>
60 <option value="desc">Descending</option>
61 </select>
62 <button
63 type="button"
64 className="rounded p-0.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
65 disabled={i === 0}
66 onClick={() => {
67 const next = [...sorts];
68 [next[i - 1], next[i]] = [next[i], next[i - 1]];
69 onChange(next);
70 }}
71 aria-label="Move sort up"
72 >
73 <ArrowUp className="size-3.5" />
74 </button>
75 <button
76 type="button"
77 className="rounded p-0.5 text-muted-foreground hover:bg-muted disabled:opacity-30"
78 disabled={i === sorts.length - 1}
79 onClick={() => {
80 const next = [...sorts];
81 [next[i + 1], next[i]] = [next[i], next[i + 1]];
82 onChange(next);
83 }}
84 aria-label="Move sort down"
85 >
86 <ArrowDown className="size-3.5" />
87 </button>
88 <button
89 type="button"
90 className="rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-destructive"
91 onClick={() => onChange(sorts.filter((_, j) => j !== i))}
92 aria-label="Remove sort"
93 >
94 <X className="size-3.5" />
95 </button>
96 </div>
97 ))}
98 {sorts.length === 0 && <p className="py-1 text-xs text-muted-foreground">No sorts — records stay in position order.</p>}
99 <div className="flex items-center gap-2 pt-1">
100 <button
101 type="button"
102 disabled={sorts.length >= 8 || available.length === 0}
103 className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-primary hover:bg-muted disabled:opacity-40"
104 onClick={() => {
105 const p = available[0];
106 onChange([...sorts, { propertyId: p.id, direction: "asc" }]);
107 }}
108 >
109 <Plus className="size-3" /> Add sort
110 </button>
111 {sorts.length > 0 && (
112 <button type="button" className="rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-destructive" onClick={() => onChange(null)}>
113 Clear all
114 </button>
115 )}
116 </div>
117 </div>
118 );
119}
120