feat(02-02): add thread hooks, UI store, tab navigation, and thread list

- Create useThreads/useCandidates TanStack Query hooks
- Extend uiStore with candidate panel and resolve dialog state
- Add ThreadTabs component for gear/planning tab switching
- Add ThreadCard component with candidate count and price range chips
- Refactor index.tsx to tabbed HomePage with PlanningView
- Create placeholder thread detail route for navigation target
This commit is contained in:
2026-03-15 11:44:17 +01:00
parent 53d6fa445d
commit a9d624dc83
7 changed files with 472 additions and 14 deletions

View File

@@ -0,0 +1,61 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiPost, apiPut, apiDelete } from "../lib/api";
import type { CreateCandidate, UpdateCandidate } from "../../shared/types";
interface CandidateResponse {
id: number;
threadId: number;
name: string;
weightGrams: number | null;
priceCents: number | null;
categoryId: number;
notes: string | null;
productUrl: string | null;
imageFilename: string | null;
createdAt: string;
updatedAt: string;
}
export function useCreateCandidate(threadId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateCandidate & { imageFilename?: string }) =>
apiPost<CandidateResponse>(`/api/threads/${threadId}/candidates`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["threads", threadId] });
queryClient.invalidateQueries({ queryKey: ["threads"] });
},
});
}
export function useUpdateCandidate(threadId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
candidateId,
...data
}: UpdateCandidate & { candidateId: number; imageFilename?: string }) =>
apiPut<CandidateResponse>(
`/api/threads/${threadId}/candidates/${candidateId}`,
data,
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["threads", threadId] });
queryClient.invalidateQueries({ queryKey: ["threads"] });
},
});
}
export function useDeleteCandidate(threadId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (candidateId: number) =>
apiDelete<{ success: boolean }>(
`/api/threads/${threadId}/candidates/${candidateId}`,
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["threads", threadId] });
queryClient.invalidateQueries({ queryKey: ["threads"] });
},
});
}