perfect-postcode/frontend/src/hooks/useSavedProperties.ts
2026-03-15 17:38:26 +00:00

148 lines
4.3 KiB
TypeScript

import { useState, useCallback, useMemo } from 'react';
import pb from '../lib/pocketbase';
import { trackEvent } from '../lib/analytics';
import type { Property } from '../types';
import { getNum } from '../lib/property-fields';
export interface SavedPropertyData {
propertyType?: string;
propertySubType?: string;
builtForm?: string;
duration?: string;
energyRating?: string;
price?: number;
estimatedPrice?: number;
askingPrice?: number;
askingRent?: number;
bedrooms?: number;
floorArea?: number;
listingUrl?: string;
}
export interface SavedProperty {
id: string;
address: string;
postcode: string;
data: SavedPropertyData;
created: string;
}
export function useSavedProperties(userId: string | null) {
const [properties, setProperties] = useState<SavedProperty[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchProperties = useCallback(async () => {
if (!userId) return;
setLoading(true);
setError(null);
try {
const records = await pb.collection('saved_properties').getFullList({
sort: '-created',
filter: `user = "${userId}"`,
});
setProperties(
records.map((r) => {
const raw = r as Record<string, unknown>;
let data: SavedPropertyData = {};
try {
data =
typeof raw.data === 'string'
? JSON.parse(raw.data)
: (raw.data as SavedPropertyData) || {};
} catch {
// Invalid JSON — use empty data
}
return {
id: r.id,
address: raw.address as string,
postcode: raw.postcode as string,
data,
created: r.created,
};
})
);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load saved properties');
} finally {
setLoading(false);
}
}, [userId]);
const saveProperty = useCallback(
async (property: Property) => {
if (!userId) return;
setError(null);
try {
const data: SavedPropertyData = {
propertyType: property.property_type,
propertySubType: property.property_sub_type,
builtForm: property.built_form,
duration: property.duration,
energyRating: property.current_energy_rating,
price: getNum(property, 'Last known price'),
estimatedPrice: getNum(property, 'Estimated current price'),
askingPrice: getNum(property, 'Asking price'),
askingRent: getNum(property, 'Asking rent (monthly)'),
bedrooms: getNum(property, 'Bedrooms'),
floorArea: getNum(property, 'Total floor area (sqm)'),
listingUrl: property.listing_url || undefined,
};
await pb.collection('saved_properties').create({
user: userId,
address: property.address || 'Unknown',
postcode: property.postcode || '',
data: JSON.stringify(data),
});
trackEvent('Property Save');
await fetchProperties();
} catch (err) {
const msg = err instanceof Error ? err.message : 'Failed to save property';
setError(msg);
}
},
[userId, fetchProperties]
);
const deleteProperty = useCallback(async (id: string) => {
setError(null);
try {
await pb.collection('saved_properties').delete(id);
trackEvent('Property Delete');
setProperties((prev) => prev.filter((p) => p.id !== id));
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete property');
}
}, []);
const savedPropertyKeys = useMemo(
() => new Set(properties.map((p) => `${p.address}|${p.postcode}`)),
[properties]
);
const isPropertySaved = useCallback(
(address?: string, postcode?: string) =>
savedPropertyKeys.has(`${address || ''}|${postcode || ''}`),
[savedPropertyKeys]
);
const getSavedPropertyId = useCallback(
(address?: string, postcode?: string) => {
const key = `${address || ''}|${postcode || ''}`;
return properties.find((p) => `${p.address}|${p.postcode}` === key)?.id;
},
[properties]
);
return {
properties,
loading,
error,
fetchProperties,
saveProperty,
deleteProperty,
isPropertySaved,
getSavedPropertyId,
};
}