feat(20-01): extend UIStore with FAB/catalog state, add useTags hook, update useGlobalItems
- Add fabMenuOpen, openFabMenu, closeFabMenu to UIStore - Add catalogSearchOpen, catalogSearchMode, openCatalogSearch, closeCatalogSearch - openCatalogSearch also closes FAB menu (natural flow) - Create useTags hook with 5-min staleTime cache - Add optional tags parameter to useGlobalItems for tag filtering
This commit is contained in:
@@ -23,13 +23,16 @@ interface ItemGlobalLink {
|
|||||||
globalItemId: number;
|
globalItemId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGlobalItems(query?: string) {
|
export function useGlobalItems(query?: string, tags?: string[]) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (query) params.set("q", query);
|
||||||
|
if (tags && tags.length > 0) params.set("tags", tags.join(","));
|
||||||
|
const qs = params.toString();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["global-items", query ?? ""],
|
queryKey: ["global-items", query ?? "", tags ?? []],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
apiGet<GlobalItem[]>(
|
apiGet<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
|
||||||
`/api/global-items${query ? `?q=${encodeURIComponent(query)}` : ""}`,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/client/hooks/useTags.ts
Normal file
15
src/client/hooks/useTags.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { apiGet } from "../lib/api";
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTags() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tags"],
|
||||||
|
queryFn: () => apiGet<Tag[]>("/api/tags"),
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -56,6 +56,17 @@ interface UIState {
|
|||||||
// Setup impact preview
|
// Setup impact preview
|
||||||
selectedSetupId: number | null;
|
selectedSetupId: number | null;
|
||||||
setSelectedSetupId: (id: number | null) => void;
|
setSelectedSetupId: (id: number | null) => void;
|
||||||
|
|
||||||
|
// FAB menu
|
||||||
|
fabMenuOpen: boolean;
|
||||||
|
openFabMenu: () => void;
|
||||||
|
closeFabMenu: () => void;
|
||||||
|
|
||||||
|
// Catalog search overlay
|
||||||
|
catalogSearchOpen: boolean;
|
||||||
|
catalogSearchMode: "collection" | "thread" | null;
|
||||||
|
openCatalogSearch: (mode: "collection" | "thread") => void;
|
||||||
|
closeCatalogSearch: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUIStore = create<UIState>((set) => ({
|
export const useUIStore = create<UIState>((set) => ({
|
||||||
@@ -119,4 +130,21 @@ export const useUIStore = create<UIState>((set) => ({
|
|||||||
// Setup impact preview
|
// Setup impact preview
|
||||||
selectedSetupId: null,
|
selectedSetupId: null,
|
||||||
setSelectedSetupId: (id) => set({ selectedSetupId: id }),
|
setSelectedSetupId: (id) => set({ selectedSetupId: id }),
|
||||||
|
|
||||||
|
// FAB menu
|
||||||
|
fabMenuOpen: false,
|
||||||
|
openFabMenu: () => set({ fabMenuOpen: true }),
|
||||||
|
closeFabMenu: () => set({ fabMenuOpen: false }),
|
||||||
|
|
||||||
|
// Catalog search overlay
|
||||||
|
catalogSearchOpen: false,
|
||||||
|
catalogSearchMode: null,
|
||||||
|
openCatalogSearch: (mode) =>
|
||||||
|
set({
|
||||||
|
catalogSearchOpen: true,
|
||||||
|
catalogSearchMode: mode,
|
||||||
|
fabMenuOpen: false,
|
||||||
|
}),
|
||||||
|
closeCatalogSearch: () =>
|
||||||
|
set({ catalogSearchOpen: false, catalogSearchMode: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import { mcpRoutes } from "./mcp/index.ts";
|
|||||||
import { requireAuth } from "./middleware/auth.ts";
|
import { requireAuth } from "./middleware/auth.ts";
|
||||||
import { authRoutes } from "./routes/auth.ts";
|
import { authRoutes } from "./routes/auth.ts";
|
||||||
import { categoryRoutes } from "./routes/categories.ts";
|
import { categoryRoutes } from "./routes/categories.ts";
|
||||||
|
import { globalItemRoutes } from "./routes/global-items.ts";
|
||||||
import { imageRoutes } from "./routes/images.ts";
|
import { imageRoutes } from "./routes/images.ts";
|
||||||
import { itemRoutes } from "./routes/items.ts";
|
import { itemRoutes } from "./routes/items.ts";
|
||||||
import { oauthRoutes, wellKnownRoute } from "./routes/oauth.ts";
|
import { oauthRoutes, wellKnownRoute } from "./routes/oauth.ts";
|
||||||
import { globalItemRoutes } from "./routes/global-items.ts";
|
|
||||||
import { profileRoutes } from "./routes/profiles.ts";
|
import { profileRoutes } from "./routes/profiles.ts";
|
||||||
import { tagRoutes } from "./routes/tags.ts";
|
|
||||||
import { settingsRoutes } from "./routes/settings.ts";
|
import { settingsRoutes } from "./routes/settings.ts";
|
||||||
import { setupRoutes } from "./routes/setups.ts";
|
import { setupRoutes } from "./routes/setups.ts";
|
||||||
|
import { tagRoutes } from "./routes/tags.ts";
|
||||||
import { threadRoutes } from "./routes/threads.ts";
|
import { threadRoutes } from "./routes/threads.ts";
|
||||||
import { totalRoutes } from "./routes/totals.ts";
|
import { totalRoutes } from "./routes/totals.ts";
|
||||||
|
|
||||||
@@ -53,10 +53,7 @@ if (process.env.NODE_ENV !== "production") {
|
|||||||
if (setCookies.length > 0) {
|
if (setCookies.length > 0) {
|
||||||
c.res.headers.delete("Set-Cookie");
|
c.res.headers.delete("Set-Cookie");
|
||||||
for (const cookie of setCookies) {
|
for (const cookie of setCookies) {
|
||||||
c.res.headers.append(
|
c.res.headers.append("Set-Cookie", cookie.replace(/;\s*Secure/gi, ""));
|
||||||
"Set-Cookie",
|
|
||||||
cookie.replace(/;\s*Secure/gi, ""),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,10 +36,9 @@ describe("Tag Routes", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns 200 with tag objects after seeding", async () => {
|
it("returns 200 with tag objects after seeding", async () => {
|
||||||
await db.insert(tags).values([
|
await db
|
||||||
{ name: "bikepacking" },
|
.insert(tags)
|
||||||
{ name: "ultralight" },
|
.values([{ name: "bikepacking" }, { name: "ultralight" }]);
|
||||||
]);
|
|
||||||
|
|
||||||
const res = await app.request("/api/tags");
|
const res = await app.request("/api/tags");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ describe("Tag Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns all tags as { id, name } ordered alphabetically", async () => {
|
it("returns all tags as { id, name } ordered alphabetically", async () => {
|
||||||
await db.insert(tags).values([
|
await db
|
||||||
{ name: "bikepacking" },
|
.insert(tags)
|
||||||
{ name: "ultralight" },
|
.values([
|
||||||
{ name: "accessories" },
|
{ name: "bikepacking" },
|
||||||
]);
|
{ name: "ultralight" },
|
||||||
|
{ name: "accessories" },
|
||||||
|
]);
|
||||||
|
|
||||||
const result = await getAllTags(db);
|
const result = await getAllTags(db);
|
||||||
expect(result).toHaveLength(3);
|
expect(result).toHaveLength(3);
|
||||||
|
|||||||
Reference in New Issue
Block a user