Quick save
This commit is contained in:
parent
e5d5819098
commit
2906b01734
25 changed files with 1070 additions and 237 deletions
|
|
@ -17,7 +17,9 @@ interface UseAreaSummaryResult {
|
|||
retry: () => void;
|
||||
}
|
||||
|
||||
const FORBIDDEN_FEATURES = ['% White', '% Black', '% Asian', '% Mixed', '% Other'];
|
||||
const FORBIDDEN_FEATURES = ['% White', '% Black', '% Asian', '% Mixed', '% Other',
|
||||
'Environmental risk', 'Collapsible deposits risk', 'Compressible ground risk', 'Landslide risk', 'Running sand risk', 'Shrink-swell risk', 'Soluble rocks risk'
|
||||
];
|
||||
|
||||
export function useAreaSummary({
|
||||
stats,
|
||||
|
|
|
|||
|
|
@ -100,9 +100,23 @@ export function useAuth() {
|
|||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const requestPasswordReset = useCallback(async (email: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await pb.collection('users').requestPasswordReset(email);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Password reset request failed';
|
||||
setError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { user, loading, error, login, register, loginWithOAuth, logout, clearError };
|
||||
return { user, loading, error, login, register, loginWithOAuth, logout, requestPasswordReset, clearError };
|
||||
}
|
||||
|
|
|
|||
101
frontend/src/hooks/useSavedSearches.ts
Normal file
101
frontend/src/hooks/useSavedSearches.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import pb from '../lib/pocketbase';
|
||||
import { apiUrl } from '../lib/api';
|
||||
|
||||
export interface SavedSearch {
|
||||
id: string;
|
||||
name: string;
|
||||
params: string;
|
||||
screenshotUrl: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
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 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)
|
||||
: '',
|
||||
created: r.created,
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load searches');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
const saveSearch = useCallback(
|
||||
async (name: string) => {
|
||||
if (!userId) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = window.location.search.replace(/^\?/, '');
|
||||
|
||||
// Try to capture a screenshot via the OG image endpoint
|
||||
let screenshotBlob: Blob | null = null;
|
||||
try {
|
||||
const ogUrl = apiUrl('og-image', new URLSearchParams(params));
|
||||
const res = await fetch(ogUrl);
|
||||
if (res.ok) {
|
||||
screenshotBlob = await res.blob();
|
||||
}
|
||||
} catch {
|
||||
// Screenshot is optional — save without it
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('user', userId);
|
||||
formData.append('name', name);
|
||||
formData.append('params', params);
|
||||
if (screenshotBlob) {
|
||||
formData.append('screenshot', screenshotBlob, 'screenshot.png');
|
||||
}
|
||||
|
||||
await pb.collection('saved_searches').create(formData);
|
||||
await fetchSearches();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed to save search';
|
||||
setError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[userId, fetchSearches]
|
||||
);
|
||||
|
||||
const deleteSearch = useCallback(
|
||||
async (id: string) => {
|
||||
setError(null);
|
||||
try {
|
||||
await pb.collection('saved_searches').delete(id);
|
||||
setSearches((prev) => prev.filter((s) => s.id !== id));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete search');
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return { searches, loading, saving, error, fetchSearches, saveSearch, deleteSearch };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue