Initial commit
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { ArrowDownAZ, ArrowDownUp, ArrowUpAZ, Check, ChevronDown, Clock, Hash } from "lucide-react";
|
||||
import { useClickOutside } from "@/lib/hooks/useClickOutside";
|
||||
import { SORT_OPTIONS, labelFor, type SortKey } from "@/lib/sort";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ICONS: Record<SortKey, React.ComponentType<{ className?: string }>> = {
|
||||
newest: Clock,
|
||||
oldest: Clock,
|
||||
az: ArrowDownAZ,
|
||||
za: ArrowUpAZ,
|
||||
"code-az": Hash,
|
||||
"code-za": Hash,
|
||||
};
|
||||
|
||||
export function SortMenu({ activeSort }: { activeSort: SortKey }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useClickOutside(ref, useCallback(() => setOpen(false), []), open);
|
||||
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
|
||||
const hrefFor = useMemo(() => (next: SortKey) => {
|
||||
const sp = new URLSearchParams(params);
|
||||
sp.set("sort", next);
|
||||
return `${pathname}?${sp.toString()}`;
|
||||
}, [pathname, params]);
|
||||
|
||||
const Icon = ICONS[activeSort] ?? ArrowDownUp;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full border text-sm transition-colors glass glass-hover text-[var(--color-fg-dim)]"
|
||||
title={`Sort: ${labelFor(activeSort)}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">{labelFor(activeSort)}</span>
|
||||
<ChevronDown className={cn("w-3 h-3 opacity-60 transition-transform", open && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 z-30 min-w-[200px] rounded-xl shadow-2xl border border-[var(--color-glass-border-strong)] backdrop-blur-2xl overflow-hidden"
|
||||
style={{ background: "color-mix(in oklch, var(--color-bg-0) 96%, transparent)" }}
|
||||
>
|
||||
<div className="p-1">
|
||||
{SORT_OPTIONS.map((o, i) => {
|
||||
const OptIcon = ICONS[o.value];
|
||||
const active = o.value === activeSort;
|
||||
const prev = SORT_OPTIONS[i - 1];
|
||||
// Group divider whenever the underlying sort dimension
|
||||
// changes (date → title → code). newest/oldest share the
|
||||
// date dimension; az/za and code-az/code-za each share theirs.
|
||||
const groupOf = (v: string) =>
|
||||
v === "newest" || v === "oldest"
|
||||
? "date"
|
||||
: v.replace(/-?(az|za)$/, "") || "title";
|
||||
const showDivider = prev && groupOf(prev.value) !== groupOf(o.value);
|
||||
return (
|
||||
<div key={o.value}>
|
||||
{showDivider && (
|
||||
<div className="my-1 mx-2 border-t border-[var(--color-glass-border)]" />
|
||||
)}
|
||||
<Link
|
||||
href={hrefFor(o.value)}
|
||||
onClick={() => setOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm hover:bg-[var(--color-glass)]",
|
||||
active && "text-[var(--color-cyan)]"
|
||||
)}
|
||||
>
|
||||
<OptIcon className="w-3.5 h-3.5" />
|
||||
<span className="flex-1">{o.label}</span>
|
||||
{active && <Check className="w-3.5 h-3.5" />}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user