Morning improvements

This commit is contained in:
Andras Schmelczer 2026-03-17 13:29:03 +00:00
parent 3e9fba5303
commit 53fff3efaa
41 changed files with 2438 additions and 637 deletions

View file

@ -19,7 +19,7 @@ export interface AiFiltersResult {
summary: string;
}
export type AiFilterErrorType = 'auth' | 'verification' | 'limit' | 'error';
export type AiFilterErrorType = 'auth' | 'limit' | 'error';
/** Context of currently active filters, sent for conversational refinement. */
export interface AiFiltersContext {
@ -102,9 +102,6 @@ export function useAiFilters(): UseAiFiltersResult {
if (response.status === 401) {
setErrorType('auth');
setError(text || 'Login required');
} else if (response.status === 403) {
setErrorType('verification');
setError(text || 'Email verification required');
} else if (response.status === 429) {
setErrorType('limit');
setError(text || 'Weekly usage limit reached');

View file

@ -5,7 +5,6 @@ import { trackEvent } from '../lib/analytics';
export interface AuthUser {
id: string;
email: string;
verified: boolean;
isAdmin: boolean;
subscription: string;
newsletter: boolean;
@ -18,7 +17,6 @@ function recordToUser(record: { id: string; [key: string]: unknown }): AuthUser
return {
id: record.id,
email: record.email,
verified: typeof record.verified === 'boolean' ? record.verified : false,
isAdmin: typeof record.is_admin === 'boolean' ? record.is_admin : false,
subscription: typeof record.subscription === 'string' ? record.subscription : 'free',
newsletter: typeof record.newsletter === 'boolean' ? record.newsletter : false,
@ -136,20 +134,6 @@ export function useAuth() {
}
}, []);
const requestVerification = useCallback(async (email: string) => {
setLoading(true);
setError(null);
try {
await pb.collection('users').requestVerification(email);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Verification request failed';
setError(msg);
throw err;
} finally {
setLoading(false);
}
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
@ -163,7 +147,6 @@ export function useAuth() {
loginWithOAuth,
logout,
requestPasswordReset,
requestVerification,
refreshAuth,
clearError,
};

View file

@ -771,9 +771,12 @@ export function useDeckLayers({
onHexagonHoverRef.current(null);
}, []);
const clearPopupInfo = useCallback(() => setPopupInfo(null), []);
return {
layers,
popupInfo,
clearPopupInfo,
hoverPosition,
countRange,
postcodeCountRange,

View file

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef, useEffect } from 'react';
import pb from '../lib/pocketbase';
import { apiUrl, authHeaders } from '../lib/api';
import { trackEvent } from '../lib/analytics';
@ -12,39 +12,94 @@ export interface SavedSearch {
created: string;
}
const POLL_INTERVAL_MS = 2000;
const MAX_POLL_ATTEMPTS = 15;
export function useSavedSearches(userId: string | null) {
const [searches, setSearches] = useState<SavedSearch[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollAttemptsRef = useRef(0);
const userIdRef = useRef(userId);
userIdRef.current = userId;
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
pollAttemptsRef.current = 0;
}, []);
// Clean up polling on unmount or userId change
useEffect(() => stopPolling, [userId, stopPolling]);
const fetchRecords = useCallback(async (uid: string): Promise<SavedSearch[]> => {
const records = await pb.collection('saved_searches').getFullList({
sort: '-created',
filter: `user = "${uid}"`,
});
return records.map((r) => ({
id: r.id,
name: (r as Record<string, unknown>).name as string,
params: (r as Record<string, unknown>).params as string,
screenshotUrl: (r as Record<string, unknown>).screenshot
? pb.files.getURL(r, (r as Record<string, unknown>).screenshot as string)
: '',
notes: ((r as Record<string, unknown>).notes as string) || '',
created: r.created,
}));
}, []);
const startPolling = useCallback(() => {
if (pollTimerRef.current) return;
pollAttemptsRef.current = 0;
pollTimerRef.current = setInterval(async () => {
const uid = userIdRef.current;
if (!uid) {
stopPolling();
return;
}
pollAttemptsRef.current++;
if (pollAttemptsRef.current >= MAX_POLL_ATTEMPTS) {
stopPolling();
return;
}
try {
const mapped = await fetchRecords(uid);
setSearches(mapped);
if (!mapped.some((s) => !s.screenshotUrl)) {
stopPolling();
}
} catch {
// Silent — background poll errors don't surface to UI
}
}, POLL_INTERVAL_MS);
}, [stopPolling, fetchRecords]);
const fetchSearches = useCallback(async () => {
if (!userId) return;
setLoading(true);
setError(null);
try {
const records = await pb.collection('saved_searches').getFullList({
sort: '-created',
filter: `user = "${userId}"`,
});
setSearches(
records.map((r) => ({
id: r.id,
name: (r as Record<string, unknown>).name as string,
params: (r as Record<string, unknown>).params as string,
screenshotUrl: (r as Record<string, unknown>).screenshot
? pb.files.getURL(r, (r as Record<string, unknown>).screenshot as string)
: '',
notes: ((r as Record<string, unknown>).notes as string) || '',
created: r.created,
}))
);
const mapped = await fetchRecords(userId);
setSearches(mapped);
// Poll for missing screenshots so they appear without a page refresh
if (mapped.some((s) => !s.screenshotUrl)) {
startPolling();
} else {
stopPolling();
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load searches');
} finally {
setLoading(false);
}
}, [userId]);
}, [userId, fetchRecords, startPolling, stopPolling]);
const saveSearch = useCallback(
async (name: string) => {