feat: add useDecisions hook with optimistic updates
This commit is contained in:
parent
813f048e46
commit
2fdeebbae1
1 changed files with 100 additions and 0 deletions
100
frontend/src/hooks/useDecisions.ts
Normal file
100
frontend/src/hooks/useDecisions.ts
Normal file
|
|
@ -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<Map<string, DecisionType>>(new Map());
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
// Load decisions on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
fetchDecisions(user)
|
||||||
|
.then((list) => {
|
||||||
|
const map = new Map<string, DecisionType>();
|
||||||
|
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 };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue