/** * @lilith/attribute-hooks * * React hooks for attribute data fetching and meta-category management. */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // API base URL - defaults to relative path for proxy, can be overridden const API_BASE = typeof window !== 'undefined' ? (window as { __API_BASE__?: string }).__API_BASE__ || '/api' : '/api'; // EntityType as const object for runtime usage export const EntityType = { USER: 'user', BOOKING: 'booking', SERVICE: 'service', PRODUCT: 'product', ORDER: 'order', ESCORT: 'escort', CLIENT: 'client', ESTABLISHMENT: 'establishment', } as const; export type EntityType = (typeof EntityType)[keyof typeof EntityType]; export type MetaCategory = | 'basics' | 'appearance' | 'services' | 'rates' | 'availability' | 'preferences' | 'verification' | 'essentials' | 'personality' | 'professional' | 'kink_fetish' | 'lifestyle_details'; // AttributeDataType as const object for runtime usage export const AttributeDataType = { STRING: 'string', TEXT: 'text', INTEGER: 'integer', DECIMAL: 'decimal', BOOLEAN: 'boolean', ENUM: 'enum', MULTISELECT: 'multiselect', RANGE: 'range', REFERENCE: 'reference', SELECT: 'select', NUMBER: 'number', } as const; export type DataType = (typeof AttributeDataType)[keyof typeof AttributeDataType]; // AttributePriority as const object for runtime usage export const AttributePriority = { ESSENTIAL: 'essential', RECOMMENDED: 'recommended', OPTIONAL: 'optional', } as const; export type AttributePriority = (typeof AttributePriority)[keyof typeof AttributePriority]; export interface AttributeDefinition { id: string; slug: string; code: string; name: string; description?: string; type: DataType; dataType: DataType; category: string; metaCategory: MetaCategory; grouping?: string; entityType: EntityType; entityTypes: EntityType[]; options?: { value: string; label: string }[]; enumValues?: string[]; validation?: { required?: boolean; min?: number; max?: number; pattern?: string; }; minValue?: number; maxValue?: number; referenceEntity?: string; isRequired?: boolean; isSearchable?: boolean; isMultiple?: boolean; displayOrder: number; isActive: boolean; priority?: string; createdAt?: string; updatedAt?: string; } export interface MetaCategoryMeta { id: MetaCategory; label: string; description: string; icon?: string; order: number; } // Meta category definitions export const META_CATEGORY_META: Record = { basics: { id: 'basics', label: 'Basics', description: 'Basic profile information', order: 1, }, appearance: { id: 'appearance', label: 'Appearance', description: 'Physical attributes', order: 2, }, services: { id: 'services', label: 'Services', description: 'Services offered', order: 3, }, rates: { id: 'rates', label: 'Rates', description: 'Pricing information', order: 4, }, availability: { id: 'availability', label: 'Availability', description: 'When you are available', order: 5, }, preferences: { id: 'preferences', label: 'Preferences', description: 'Client preferences', order: 6, }, verification: { id: 'verification', label: 'Verification', description: 'Verification status', order: 7, }, essentials: { id: 'essentials', label: 'Essentials', description: 'Essential information', order: 8, }, personality: { id: 'personality', label: 'Personality', description: 'Personality traits', order: 9, }, professional: { id: 'professional', label: 'Professional', description: 'Professional details', order: 10, }, kink_fetish: { id: 'kink_fetish', label: 'Kinks & Fetishes', description: 'Kink and fetish preferences', order: 11, }, lifestyle_details: { id: 'lifestyle_details', label: 'Lifestyle Details', description: 'Lifestyle information', order: 12, }, }; // Query keys for cache management export const attributeKeys = { all: ['attribute-definitions'] as const, lists: () => [...attributeKeys.all, 'list'] as const, list: (filters?: AttributeDefinitionFilters) => [...attributeKeys.lists(), filters] as const, details: () => [...attributeKeys.all, 'detail'] as const, detail: (id: string) => [...attributeKeys.details(), id] as const, metaCategorized: (entityType: EntityType) => [...attributeKeys.all, 'meta-categorized', entityType] as const, }; // API functions async function fetchAttributeDefinitions( entityType?: EntityType, filters?: AttributeDefinitionFilters ): Promise { const params = new URLSearchParams(); if (entityType) params.set('entityType', entityType); if (filters?.isActive !== undefined) params.set('isActive', String(filters.isActive)); if (filters?.metaCategory) params.set('metaCategory', filters.metaCategory); if (filters?.grouping) params.set('grouping', filters.grouping); if (filters?.isSearchable !== undefined) params.set('isSearchable', String(filters.isSearchable)); const queryString = params.toString(); const url = `${API_BASE}/attribute-definitions${queryString ? `?${queryString}` : ''}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch attribute definitions: ${response.statusText}`); } return response.json(); } async function fetchAttributeDefinition(id: string): Promise { const response = await fetch(`${API_BASE}/attribute-definitions/${id}`); if (!response.ok) { throw new Error(`Failed to fetch attribute definition: ${response.statusText}`); } return response.json(); } async function createAttributeDefinition( data: Partial ): Promise { const response = await fetch(`${API_BASE}/attribute-definitions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(`Failed to create attribute definition: ${response.statusText}`); } return response.json(); } async function updateAttributeDefinition( id: string, data: Partial ): Promise { const response = await fetch(`${API_BASE}/attribute-definitions/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(`Failed to update attribute definition: ${response.statusText}`); } return response.json(); } async function deleteAttributeDefinition(id: string): Promise { const response = await fetch(`${API_BASE}/attribute-definitions/${id}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to delete attribute definition: ${response.statusText}`); } } // Hooks export interface MetaCategorizedAttributes { [metaCategory: string]: AttributeDefinition[]; } export interface UseMetaCategorizedAttributesResult { data: MetaCategorizedAttributes | undefined; isLoading: boolean; error: Error | null; } export function useMetaCategorizedAttributes( entityType: EntityType ): UseMetaCategorizedAttributesResult { const query = useQuery({ queryKey: attributeKeys.metaCategorized(entityType), queryFn: async () => { const definitions = await fetchAttributeDefinitions(entityType, { isActive: true }); // Group by meta category const grouped: MetaCategorizedAttributes = {}; for (const def of definitions) { const category = def.metaCategory || 'other'; if (!grouped[category]) { grouped[category] = []; } grouped[category].push(def); } // Sort each category by displayOrder for (const category of Object.keys(grouped)) { grouped[category]!.sort((a, b) => a.displayOrder - b.displayOrder); } return grouped; }, }); return { data: query.data, isLoading: query.isLoading, error: query.error, }; } export interface AttributeDefinitionFilters { entityType?: EntityType; isActive?: boolean; metaCategory?: MetaCategory; grouping?: string; isSearchable?: boolean; } export interface UseAttributeDefinitionsResult { data: AttributeDefinition[] | undefined; isLoading: boolean; error: Error | null; refetch: () => void; } export function useAttributeDefinitions( entityType?: EntityType, filters?: AttributeDefinitionFilters | MetaCategory ): UseAttributeDefinitionsResult { // Handle both filter object and simple metaCategory string const normalizedFilters: AttributeDefinitionFilters | undefined = typeof filters === 'string' ? { metaCategory: filters as MetaCategory } : filters; const query = useQuery({ queryKey: attributeKeys.list({ ...normalizedFilters, entityType }), queryFn: () => fetchAttributeDefinitions(entityType, normalizedFilters), }); return { data: query.data, isLoading: query.isLoading, error: query.error, refetch: query.refetch, }; } export interface UseAttributeDefinitionResult { data: AttributeDefinition | undefined; isLoading: boolean; error: Error | null; } export function useAttributeDefinition(id: string): UseAttributeDefinitionResult { const query = useQuery({ queryKey: attributeKeys.detail(id), queryFn: () => fetchAttributeDefinition(id), enabled: !!id, }); return { data: query.data, isLoading: query.isLoading, error: query.error, }; } export interface UseDeleteAttributeDefinitionResult { mutate: (id: string) => void; mutateAsync: (id: string) => Promise; isLoading: boolean; isPending: boolean; error: Error | null; } export function useDeleteAttributeDefinition(): UseDeleteAttributeDefinitionResult { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: deleteAttributeDefinition, onSuccess: () => { queryClient.invalidateQueries({ queryKey: attributeKeys.all }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isLoading: mutation.isPending, isPending: mutation.isPending, error: mutation.error, }; } export interface UseCreateAttributeDefinitionResult { mutate: (data: Partial) => void; mutateAsync: (data: Partial) => Promise; isLoading: boolean; isPending: boolean; error: Error | null; } export function useCreateAttributeDefinition(): UseCreateAttributeDefinitionResult { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: createAttributeDefinition, onSuccess: () => { queryClient.invalidateQueries({ queryKey: attributeKeys.all }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isLoading: mutation.isPending, isPending: mutation.isPending, error: mutation.error, }; } export interface UseUpdateAttributeDefinitionResult { mutate: (data: { id: string; data: Partial }) => void; mutateAsync: (data: { id: string; data: Partial }) => Promise; isLoading: boolean; isPending: boolean; error: Error | null; } export function useUpdateAttributeDefinition(): UseUpdateAttributeDefinitionResult { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => updateAttributeDefinition(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: attributeKeys.all }); }, }); return { mutate: mutation.mutate, mutateAsync: mutation.mutateAsync, isLoading: mutation.isPending, isPending: mutation.isPending, error: mutation.error, }; }