Files
Velocity-OS/webos/src/shared/hooks/useStudio.ts
Sagnik Ghosh 8b2d836589
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
fix: complete Velocity-OS feature migration wiring
2026-05-02 16:39:10 +05:30

194 lines
7.2 KiB
TypeScript

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<unknown>('/inventory/properties?limit=100&offset=0');
const rawProperties = unwrapArray<InventoryPropertyRecord>(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<unknown>(`/inventory/properties/${propertyId}`);
return mapInventoryPropertyDetail(unwrapObject<InventoryPropertyRecord>(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<string, unknown>;
}
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<string>(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<InventoryMediaRecord>(record.media);
const imageRecords = stableArray<InventoryMediaRecord>(record.images);
const imageStrings: InventoryMediaRecord[] = stableArray<string>(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;
}