From 2fdeebbae12541bd86fae9696e12f77d26d97876 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 21 Feb 2026 13:51:50 +0000 Subject: [PATCH] feat: add useDecisions hook with optimistic updates --- frontend/src/hooks/useDecisions.ts | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 frontend/src/hooks/useDecisions.ts diff --git a/frontend/src/hooks/useDecisions.ts b/frontend/src/hooks/useDecisions.ts new file mode 100644 index 0000000..aa20a88 --- /dev/null +++ b/frontend/src/hooks/useDecisions.ts @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import type { AuthUser } from '@/auth/types'; +import type { DecisionType } from '@/types'; +import { fetchDecisions, setDecision as apiSetDecision, clearDecision as apiClearDecision } from '@/services'; + +function decisionKey(listingId: number, listingType: string): string { + return `${listingId}-${listingType}`; +} + +export function useDecisions(user: AuthUser | null) { + const [decisions, setDecisions] = useState>(new Map()); + const [isLoaded, setIsLoaded] = useState(false); + + // Load decisions on mount + useEffect(() => { + if (!user) return; + fetchDecisions(user) + .then((list) => { + const map = new Map(); + for (const d of list) { + map.set(decisionKey(d.listing_id, d.listing_type), d.decision); + } + setDecisions(map); + setIsLoaded(true); + }) + .catch(() => setIsLoaded(true)); + }, [user]); + + const decide = useCallback( + async (listingId: number, decision: DecisionType, listingType: 'RENT' | 'BUY' = 'RENT') => { + if (!user) return; + const key = decisionKey(listingId, listingType); + + // Optimistic update + setDecisions((prev) => { + const next = new Map(prev); + next.set(key, decision); + return next; + }); + + try { + await apiSetDecision(user, listingId, decision, listingType); + } catch { + // Revert on failure + setDecisions((prev) => { + const next = new Map(prev); + next.delete(key); + return next; + }); + } + }, + [user], + ); + + const clear = useCallback( + async (listingId: number, listingType: 'RENT' | 'BUY' = 'RENT') => { + if (!user) return; + const key = decisionKey(listingId, listingType); + const previous = decisions.get(key); + + setDecisions((prev) => { + const next = new Map(prev); + next.delete(key); + return next; + }); + + try { + await apiClearDecision(user, listingId, listingType); + } catch { + if (previous) { + setDecisions((prev) => { + const next = new Map(prev); + next.set(key, previous); + return next; + }); + } + } + }, + [user, decisions], + ); + + const getDecision = useCallback( + (listingId: number, listingType: string = 'RENT'): DecisionType | undefined => { + return decisions.get(decisionKey(listingId, listingType)); + }, + [decisions], + ); + + const likedCount = useMemo( + () => Array.from(decisions.values()).filter((d) => d === 'liked').length, + [decisions], + ); + + const dislikedCount = useMemo( + () => Array.from(decisions.values()).filter((d) => d === 'disliked').length, + [decisions], + ); + + return { decisions, isLoaded, decide, clear, getDecision, likedCount, dislikedCount }; +}