Files
tool-evaluator/artifacts/toolrate/src/hooks/use-browse-preferences.ts
T
opencode 78244c3197
Build & Push Docker Image / build (push) Successful in 2m25s
feat: user-chosen browse views with grid/list toggle and profile sync
- view modes grid | table | rows + density cozy/compact, persisted via
  localStorage and shareable ?view=?density= URL params (URL wins)
- table view: sortable columns (name, rating, reviews), new sort options
  name_asc/name_desc/recently_updated (backend enum + handler)
- live debounced search, removable filter chips, '/' focuses search
- virtualization via @tanstack/react-virtual for table and rows views
- profile sync: users.preferences jsonb + GET/PUT /api/auth/me/preferences;
  preference precedence URL > server profile > localStorage > default
- add local rollup/lightningcss/tailwindcss oxide native binaries for macos dev
2026-08-02 09:34:53 +02:00

90 lines
2.6 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { useAuth } from "@/hooks/use-auth";
import {
useGetMePreferences,
useUpdateMePreferences,
getGetMePreferencesQueryKey,
type UserPreferences,
} from "@workspace/api-client-react";
export type ViewMode = "grid" | "table" | "rows";
export type Density = "cozy" | "compact";
const LS_KEY = "toolrate:browse-preferences";
const VIEWS = new Set<string>(["grid", "table", "rows"]);
const DENSITIES = new Set<string>(["cozy", "compact"]);
export function isViewMode(v: string | null): v is ViewMode {
return v !== null && VIEWS.has(v);
}
export function isDensity(v: string | null): v is Density {
return v !== null && DENSITIES.has(v);
}
function readLocal(): Partial<UserPreferences> {
try {
const raw = window.localStorage.getItem(LS_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw) as Record<string, unknown>;
return {
...(isViewMode(typeof parsed.view === "string" ? parsed.view : null) ? { view: parsed.view as ViewMode } : {}),
...(isDensity(typeof parsed.density === "string" ? parsed.density : null) ? { density: parsed.density as Density } : {}),
};
} catch {
return {};
}
}
function writeLocal(prefs: Partial<UserPreferences>) {
try {
window.localStorage.setItem(LS_KEY, JSON.stringify(prefs));
} catch {
/* ignore */
}
}
export function useBrowsePreferences() {
const { isAuthenticated } = useAuth();
const [localPrefs, setLocalPrefs] = useState(readLocal);
const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const { data: serverPrefs } = useGetMePreferences({
query: {
queryKey: getGetMePreferencesQueryKey(),
enabled: isAuthenticated,
retry: false,
staleTime: 5 * 60 * 1000,
},
});
const { mutate: updatePrefs } = useUpdateMePreferences();
function persist(view: ViewMode, density: Density) {
const prefs: Partial<UserPreferences> = { view, density };
setLocalPrefs(prefs);
writeLocal(prefs);
if (isAuthenticated) {
if (syncTimer.current) clearTimeout(syncTimer.current);
syncTimer.current = setTimeout(() => updatePrefs({ data: prefs }), 400);
}
}
useEffect(
() => () => {
if (syncTimer.current) clearTimeout(syncTimer.current);
},
[],
);
const serverView = isViewMode(serverPrefs?.view ?? null) ? serverPrefs!.view : undefined;
const serverDensity = isDensity(serverPrefs?.density ?? null) ? serverPrefs!.density : undefined;
return {
serverView,
serverDensity,
localView: localPrefs.view,
localDensity: localPrefs.density,
persist,
};
}