lgtm
This commit is contained in:
parent
2efa4d9f47
commit
5e73287eaf
99 changed files with 6392 additions and 1462 deletions
82
frontend/src/hooks/useClickedListings.ts
Normal file
82
frontend/src/hooks/useClickedListings.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import pb from '../lib/pocketbase';
|
||||
|
||||
/** Per-user record of opened listings, stored in PocketBase so visited listings can be drawn
|
||||
* in a distinct colour on the map and persist across devices. One row per (user, url). */
|
||||
const CLICKED_LISTINGS_COLLECTION = 'clicked_listings';
|
||||
|
||||
function currentUserId(): string | null {
|
||||
return pb.authStore.isValid && pb.authStore.record ? pb.authStore.record.id : null;
|
||||
}
|
||||
|
||||
/** Tracks which listings the signed-in user has opened.
|
||||
*
|
||||
* Reads the user from the shared PocketBase authStore so it stays self-contained — no need to
|
||||
* thread a user id down through the map layer tree. Returns a Set for O(1) membership checks in
|
||||
* the map's colour accessor; a fresh Set instance is produced on every change so it can double
|
||||
* as a deck.gl `updateTrigger` (identity-compared).
|
||||
*
|
||||
* Writes are optimistic: `markClicked` updates local state synchronously so the pin recolours
|
||||
* the instant it is clicked, independent of the PocketBase round-trip or the next listings fetch.
|
||||
*/
|
||||
export function useClickedListings() {
|
||||
const [clickedUrls, setClickedUrls] = useState<Set<string>>(() => new Set());
|
||||
const [userId, setUserId] = useState<string | null>(currentUserId);
|
||||
|
||||
const userIdRef = useRef(userId);
|
||||
userIdRef.current = userId;
|
||||
// Mirrors the latest committed set so markClicked can decide (synchronously) whether a url is
|
||||
// new — without depending on the async state updater having run yet.
|
||||
const clickedUrlsRef = useRef(clickedUrls);
|
||||
clickedUrlsRef.current = clickedUrls;
|
||||
|
||||
// Follow login/logout via the shared authStore (also covers cross-tab auth changes).
|
||||
useEffect(() => {
|
||||
const unsubscribe = pb.authStore.onChange(() => setUserId(currentUserId()));
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
// Load the user's previously opened listings whenever the signed-in user changes. Signed-out
|
||||
// users keep an in-memory set for the current session only (nothing to persist to).
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
setClickedUrls(new Set());
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
pb.collection(CLICKED_LISTINGS_COLLECTION)
|
||||
.getFullList({ filter: `user = "${userId}"`, fields: 'url' })
|
||||
.then((records) => {
|
||||
if (cancelled) return;
|
||||
const urls = records
|
||||
.map((record) => (record as { url?: unknown }).url)
|
||||
.filter((url): url is string => typeof url === 'string');
|
||||
setClickedUrls(new Set(urls));
|
||||
})
|
||||
.catch(() => {
|
||||
// Visited history is a convenience; a failed load just starts the session empty.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
const markClicked = useCallback((url: string | undefined | null) => {
|
||||
if (!url || clickedUrlsRef.current.has(url)) return; // unknown or already visited
|
||||
|
||||
// Optimistic, synchronous local update → the map pin recolours on click.
|
||||
setClickedUrls((prev) => (prev.has(url) ? prev : new Set(prev).add(url)));
|
||||
|
||||
const uid = userIdRef.current;
|
||||
if (!uid) return; // anonymous: recolour for the session, nothing to persist
|
||||
pb.collection(CLICKED_LISTINGS_COLLECTION)
|
||||
.create({ user: uid, url })
|
||||
.catch(() => {
|
||||
// Ignore duplicate-index conflicts and transient failures — local state already
|
||||
// reflects the click, and a missed write only means it won't persist to the next visit.
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { clickedUrls, markClicked };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue