Initial commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
import { useCallback, useRef, useState, useTransition } from "react";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { setDefaultSort } from "@/app/actions/sort";
|
||||
import { SORT_OPTIONS, labelFor, type SortKey } from "@/lib/sort";
|
||||
import { useClickOutside } from "@/lib/hooks/useClickOutside";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DefaultSortSelect({ initial }: { initial: SortKey }) {
|
||||
const [value, setValue] = useState<SortKey>(initial);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [pending, start] = useTransition();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useClickOutside(ref, useCallback(() => setOpen(false), []), open);
|
||||
|
||||
const choose = (next: SortKey) => {
|
||||
setOpen(false);
|
||||
if (next === value) return;
|
||||
setValue(next);
|
||||
start(async () => {
|
||||
await setDefaultSort(next);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 1400);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Default Sort</div>
|
||||
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">
|
||||
Used on every grid page when no sort is chosen. Persisted on the server.
|
||||
</div>
|
||||
</div>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1 text-xs text-[var(--color-mint)]">
|
||||
<Check className="w-3 h-3" /> Saved
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
disabled={pending}
|
||||
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm rounded-lg glass glass-hover text-[var(--color-fg)]"
|
||||
>
|
||||
<span>{labelFor(value)}</span>
|
||||
<ChevronDown className={cn("w-3.5 h-3.5 opacity-60 transition-transform", open && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute left-0 right-0 top-full mt-2 z-30 rounded-xl shadow-2xl border border-[var(--color-glass-border-strong)] backdrop-blur-2xl overflow-hidden p-1"
|
||||
style={{ background: "color-mix(in oklch, var(--color-bg-0) 96%, transparent)" }}
|
||||
>
|
||||
{SORT_OPTIONS.map((o) => {
|
||||
const active = o.value === value;
|
||||
return (
|
||||
<button
|
||||
key={o.value}
|
||||
onClick={() => choose(o.value)}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between gap-2 px-3 py-1.5 rounded-md text-sm text-left hover:bg-[var(--color-glass)]",
|
||||
active && "text-[var(--color-cyan)]"
|
||||
)}
|
||||
>
|
||||
<span>{o.label}</span>
|
||||
{active && <Check className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user