import { useQuery } from '@tanstack/react-query'; import { api } from '@/shared/lib/apiClient'; import { stableArray, unwrapArray, unwrapObject } from '@/shared/lib/apiShape'; /** * useStudioProperties — Studio Pillar property listing */ export function useStudioProperties() { const query = useQuery({ queryKey: ['studio-properties'], queryFn: async () => { const response = await api.get('/inventory/properties?limit=100&offset=0'); const rawProperties = unwrapArray(response, ['properties']); return rawProperties.map(mapInventoryProperty); }, staleTime: 120_000, refetchOnMount: 'always', }); return { properties: query.data ?? [], isLoading: query.isLoading }; } /** * useProperty — single property entity with full details */ export function useProperty(propertyId: string) { const query = useQuery({ queryKey: ['property', propertyId], queryFn: async () => { const payload = await api.get(`/inventory/properties/${propertyId}`); return mapInventoryPropertyDetail(unwrapObject(payload) ?? {}); }, staleTime: 120_000, enabled: !!propertyId, }); return { property: query.data, isLoading: query.isLoading }; } // ── Types ──────────────────────────────────────────────────── export interface StudioProperty { id: string; name: string; location: string; priceRange?: string; thumbnailUrl?: string; availableUnits?: number; } export interface PropertyDetail { id: string; name: string; config: string; area: string; price: string; description?: string; thumbnailUrl?: string; interiorImageUrl?: string; modelUrl?: string; // GLB/GLTF for R3F images?: string[]; amenities?: string[]; } interface InventoryPropertyRecord { property_id?: string; id?: string; project_name?: string; name?: string; developer_name?: string; property_type?: string; location?: { city?: string; district?: string; area?: string; address?: string; }; price_bands?: unknown; unit_mix?: unknown; amenities?: string[]; media?: unknown; images?: unknown; thumbnailUrl?: string; availableUnits?: number; } interface InventoryMediaRecord { url?: string; thumbnail_url?: string; thumbnailUrl?: string; media_type?: string; type?: string; kind?: string; label?: string; metadata?: Record; } function mapLocation(location: InventoryPropertyRecord['location']): string { if (!location) return 'Location pending'; return [location.district, location.area, location.city, location.address].filter(Boolean).join(', ') || 'Location pending'; } function mapPriceRange(priceBands: InventoryPropertyRecord['price_bands']): string | undefined { const bands = stableArray<{ label?: string; min?: number; max?: number; currency?: string }>(priceBands); if (bands.length === 0) return undefined; const mins = bands.map((band) => Number(band.min)).filter(Number.isFinite); const maxes = bands.map((band) => Number(band.max)).filter(Number.isFinite); if (!mins.length && !maxes.length) return bands[0]?.label; const currency = bands[0]?.currency ?? 'INR'; const min = mins.length ? Math.min(...mins) : undefined; const max = maxes.length ? Math.max(...maxes) : undefined; if (min !== undefined && max !== undefined) return `${currency} ${min} - ${max}`; if (min !== undefined) return `From ${currency} ${min}`; return `Up to ${currency} ${max}`; } function mapInventoryProperty(record: InventoryPropertyRecord): StudioProperty { const media = normalizeMedia(record); const unitMix = stableArray<{ configuration?: string; count?: number; available?: number; area?: string; price?: string }>(record.unit_mix); const mediaThumb = media.find((item) => item.thumbnail_url || item.thumbnailUrl || item.url); const availableUnits = unitMix.length ? unitMix.reduce((sum, unit) => sum + Number(unit.available ?? unit.count ?? 0), 0) : record.availableUnits; const stableId = record.property_id ?? record.id ?? record.project_name ?? record.name ?? 'unknown-property'; return { id: String(stableId), name: record.project_name ?? record.name ?? 'Unnamed property', location: mapLocation(record.location), priceRange: mapPriceRange(record.price_bands), thumbnailUrl: record.thumbnailUrl ?? mediaThumb?.thumbnail_url ?? mediaThumb?.thumbnailUrl ?? mediaThumb?.url, availableUnits, }; } function mapInventoryPropertyDetail(record: InventoryPropertyRecord): PropertyDetail { const base = mapInventoryProperty(record); const unitMix = stableArray<{ configuration?: string; area?: string; price?: string }>(record.unit_mix); const media = normalizeMedia(record); const primaryUnit = unitMix[0]; const imageUrls = stableArray(record.images); const mediaImages = media .filter((item) => isImageMedia(item)) .map((item) => item.url ?? item.thumbnail_url ?? item.thumbnailUrl) .filter((url): url is string => Boolean(url)); const images = imageUrls.length ? imageUrls : mediaImages; const modelUrl = media.find((item) => isModelMedia(item))?.url; const interiorImageUrl = findInteriorImage(media) ?? images[0] ?? base.thumbnailUrl; return { ...base, config: primaryUnit?.configuration ?? record.property_type ?? 'Mixed configuration', area: primaryUnit?.area ?? 'Area pending', price: primaryUnit?.price ?? base.priceRange ?? 'Price pending', description: `${record.developer_name ?? 'Developer'} property in ${base.location}`, interiorImageUrl, modelUrl, images, amenities: Array.isArray(record.amenities) ? record.amenities : [], }; } function normalizeMedia(record: InventoryPropertyRecord): InventoryMediaRecord[] { const media = stableArray(record.media); const imageRecords = stableArray(record.images); const imageStrings: InventoryMediaRecord[] = stableArray(record.images).map((url) => ({ url, media_type: 'image', })); return [...media, ...imageRecords, ...imageStrings].filter((item) => Boolean(item.url ?? item.thumbnail_url ?? item.thumbnailUrl)); } function mediaKind(item: InventoryMediaRecord): string { return [item.media_type, item.type, item.kind, item.label] .filter(Boolean) .join(' ') .toLowerCase(); } function isImageMedia(item: InventoryMediaRecord): boolean { const url = (item.url ?? item.thumbnail_url ?? item.thumbnailUrl ?? '').toLowerCase(); const kind = mediaKind(item); return kind.includes('image') || kind.includes('photo') || /\.(png|jpe?g|webp|gif|avif)(\?|$)/.test(url); } function isModelMedia(item: InventoryMediaRecord): boolean { const url = (item.url ?? '').toLowerCase(); const kind = mediaKind(item); return kind.includes('model') || kind.includes('3d') || kind.includes('vr') || /\.(glb|gltf)(\?|$)/.test(url); } function findInteriorImage(media: InventoryMediaRecord[]): string | undefined { const item = media.find((entry) => { const kind = mediaKind(entry); return isImageMedia(entry) && (kind.includes('interior') || kind.includes('room') || kind.includes('staging')); }); return item?.url ?? item?.thumbnail_url ?? item?.thumbnailUrl; }