Lots of frontend changes
This commit is contained in:
parent
ec29631c44
commit
555ba7cf53
38 changed files with 1508 additions and 648 deletions
108
frontend/src/hooks/useAuth.ts
Normal file
108
frontend/src/hooks/useAuth.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import pb from '../lib/pocketbase';
|
||||
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
// PocketBase RecordModel stores user fields as dynamic properties
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function recordToUser(record: any): AuthUser {
|
||||
return {
|
||||
id: record.id || '',
|
||||
email: record.email || '',
|
||||
name: record.name || '',
|
||||
avatar: record.avatar || '',
|
||||
verified: record.verified || false,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const [user, setUser] = useState<AuthUser | null>(() => {
|
||||
if (pb.authStore.isValid && pb.authStore.record) {
|
||||
return recordToUser(pb.authStore.record);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Sync with authStore changes (cross-tab, external updates)
|
||||
useEffect(() => {
|
||||
const unsubscribe = pb.authStore.onChange(() => {
|
||||
if (pb.authStore.isValid && pb.authStore.record) {
|
||||
setUser(recordToUser(pb.authStore.record));
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (email: string, password: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await pb.collection('users').authWithPassword(email, password);
|
||||
setUser(recordToUser(result.record));
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Login failed';
|
||||
setError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (email: string, password: string, name?: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await pb.collection('users').create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
name: name || '',
|
||||
});
|
||||
// Auto-login after registration
|
||||
const result = await pb.collection('users').authWithPassword(email, password);
|
||||
setUser(recordToUser(result.record));
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Registration failed';
|
||||
setError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loginWithOAuth = useCallback(async (provider: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await pb.collection('users').authWithOAuth2({ provider });
|
||||
setUser(recordToUser(result.record));
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'OAuth login failed';
|
||||
setError(msg);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
pb.authStore.clear();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { user, loading, error, login, register, loginWithOAuth, logout, clearError };
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
HexagonStatsResponse,
|
||||
NumericFeatureStats,
|
||||
} from '../types';
|
||||
import { buildFilterString, apiUrl, logNonAbortError } from '../lib/api';
|
||||
import { buildFilterString, apiUrl, logNonAbortError, authHeaders } from '../lib/api';
|
||||
|
||||
interface SelectedHexagon {
|
||||
id: string;
|
||||
|
|
@ -50,7 +50,7 @@ export function useHexagonSelection({
|
|||
if (fields) {
|
||||
params.set('fields', fields.join(','));
|
||||
}
|
||||
const response = await fetch(apiUrl('hexagon-stats', params), { signal });
|
||||
const response = await fetch(apiUrl('hexagon-stats', params), authHeaders({ signal }));
|
||||
return (await response.json()) as HexagonStatsResponse;
|
||||
},
|
||||
[filters, features]
|
||||
|
|
@ -96,7 +96,7 @@ export function useHexagonSelection({
|
|||
const filterStr = buildFilterString(filters, features);
|
||||
if (filterStr) params.append('filters', filterStr);
|
||||
|
||||
const response = await fetch(apiUrl('hexagon-properties', params));
|
||||
const response = await fetch(apiUrl('hexagon-properties', params), authHeaders());
|
||||
const data: HexagonPropertiesResponse = await response.json();
|
||||
|
||||
if (offset === 0) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
ViewChangeParams,
|
||||
ApiResponse,
|
||||
} from '../types';
|
||||
import { buildFilterString, apiUrl, logNonAbortError } from '../lib/api';
|
||||
import { buildFilterString, apiUrl, logNonAbortError, authHeaders } from '../lib/api';
|
||||
import { POSTCODE_ZOOM_THRESHOLD } from '../lib/map-utils';
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
|
@ -76,9 +76,12 @@ export function useMapData({
|
|||
const params = new URLSearchParams({ bounds: boundsStr });
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', viewFeature || '');
|
||||
const res = await fetch(apiUrl('postcodes', params), {
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const res = await fetch(
|
||||
apiUrl('postcodes', params),
|
||||
authHeaders({
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
);
|
||||
const json: { features: PostcodeFeature[] } = await res.json();
|
||||
setPostcodeData(json.features || []);
|
||||
setRawData([]);
|
||||
|
|
@ -89,9 +92,12 @@ export function useMapData({
|
|||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', viewFeature || '');
|
||||
const res = await fetch(apiUrl('hexagons', params), {
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const res = await fetch(
|
||||
apiUrl('hexagons', params),
|
||||
authHeaders({
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
);
|
||||
const json: ApiResponse = await res.json();
|
||||
setRawData(json.features || []);
|
||||
setPostcodeData([]);
|
||||
|
|
@ -162,7 +168,13 @@ export function useMapData({
|
|||
}, [viewFeature, features, dataRange, activeFeature, dragValue]);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
({ resolution: newRes, bounds: newBounds, zoom: newZoom, latitude, longitude }: ViewChangeParams) => {
|
||||
({
|
||||
resolution: newRes,
|
||||
bounds: newBounds,
|
||||
zoom: newZoom,
|
||||
latitude,
|
||||
longitude,
|
||||
}: ViewChangeParams) => {
|
||||
const boundsKey = `${newBounds.south},${newBounds.west},${newBounds.north},${newBounds.east},${newRes}`;
|
||||
if (boundsKey !== prevBoundsRef.current) {
|
||||
prevBoundsRef.current = boundsKey;
|
||||
|
|
@ -175,10 +187,13 @@ export function useMapData({
|
|||
[]
|
||||
);
|
||||
|
||||
const setInitialView = useCallback((view: { latitude: number; longitude: number; zoom: number }) => {
|
||||
setCurrentView(view);
|
||||
setZoom(view.zoom);
|
||||
}, []);
|
||||
const setInitialView = useCallback(
|
||||
(view: { latitude: number; longitude: number; zoom: number }) => {
|
||||
setCurrentView(view);
|
||||
setZoom(view.zoom);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { Bounds, POI, POIResponse } from '../types';
|
||||
import { apiUrl, logNonAbortError } from '../lib/api';
|
||||
import { apiUrl, logNonAbortError, authHeaders } from '../lib/api';
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
||||
|
|
@ -32,9 +32,12 @@ export function usePOIData(bounds: Bounds | null, selectedCategories: Set<string
|
|||
categories: categoriesStr,
|
||||
bounds: boundsStr,
|
||||
});
|
||||
const res = await fetch(apiUrl('pois', params), {
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const res = await fetch(
|
||||
apiUrl('pois', params),
|
||||
authHeaders({
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
);
|
||||
const json: POIResponse = await res.json();
|
||||
setPois(json.pois || []);
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue