Morning improvements
This commit is contained in:
parent
3e9fba5303
commit
53fff3efaa
41 changed files with 2438 additions and 637 deletions
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -771,9 +771,12 @@ export function useDeckLayers({
|
|||
onHexagonHoverRef.current(null);
|
||||
}, []);
|
||||
|
||||
const clearPopupInfo = useCallback(() => setPopupInfo(null), []);
|
||||
|
||||
return {
|
||||
layers,
|
||||
popupInfo,
|
||||
clearPopupInfo,
|
||||
hoverPosition,
|
||||
countRange,
|
||||
postcodeCountRange,
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue