Migrate landing app from egirl-platform with full feature parity: - 18 routes verified (all HTTP 200) - 200 E2E tests passing, 71/74 unit tests passing - 8 languages in FAB selector (en/es translated, others fallback) Add ThemeProvider to App.tsx for styled-components theme context. Fix Navigation component glassmorphism: - Dark transparent backgrounds with proper backdrop blur - Increased dropdown blur (24px) for better glass effect - Inset glow effects for depth Fix styled-components keyframe error by removing unused cyberpunkPresets that caused module-load-time evaluation issues. Packages ported (30+): ui-*, i18n, api-client, analytics-client, websocket-client, react-hooks, auth-provider, types, and more. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.5 KiB
6.5 KiB
@lilith/attribute-hooks
React hooks for integrating with the lilith-platform attribute service.
Overview
This package provides React Query-based hooks for:
- Fetching attribute definitions for entity types
- Reading and writing attribute values for entities
- Building dynamic forms and search filters based on attributes
Installation
pnpm add @lilith/attribute-hooks
Peer Dependencies
{
"react": "^18.0.0",
"@tanstack/react-query": "^5.0.0",
"styled-components": "^6.0.0"
}
Usage
Basic Hook Usage
import {
useAttributeDefinitions,
useAttributeValues,
useUpdateAttributeValues,
EntityType,
} from '@lilith/attribute-hooks';
function ProfileEditor({ userId }: { userId: string }) {
// Fetch attribute definitions for users
const { data: definitions, isLoading: loadingDefs } = useAttributeDefinitions(
EntityType.USER
);
// Fetch current values for this user
const { data: values, isLoading: loadingValues } = useAttributeValues(
EntityType.USER,
userId
);
// Mutation for updating values
const updateMutation = useUpdateAttributeValues(EntityType.USER, userId);
const handleSave = async (newValues: Record<string, unknown>) => {
await updateMutation.mutateAsync(newValues);
};
if (loadingDefs || loadingValues) return <div>Loading...</div>;
return (
<form>
{definitions?.map(def => (
<div key={def.code}>
<label>{def.name}</label>
<input
type="text"
value={String(values?.[def.code] ?? '')}
onChange={e => handleSave({ [def.code]: e.target.value })}
/>
</div>
))}
</form>
);
}
Filtering by Category
const { data: physicalAttributes } = useAttributeDefinitions(
EntityType.USER,
{ category: 'physical' }
);
Grouped Definitions
import { useGroupedAttributeDefinitions } from '@lilith/attribute-hooks';
const { data: groupedDefs } = useGroupedAttributeDefinitions(EntityType.USER);
// Returns: { physical: [...], demographics: [...], ... }
Single Attribute Operations
import {
useAttributeValue,
useUpdateAttributeValue,
useDeleteAttributeValue,
} from '@lilith/attribute-hooks';
// Read single value
const { data: hairColor } = useAttributeValue(EntityType.USER, userId, 'hair_color');
// Update single value
const updateSingle = useUpdateAttributeValue(EntityType.USER, userId);
updateSingle.mutate({ code: 'hair_color', value: 'blonde' });
// Delete single value
const deleteSingle = useDeleteAttributeValue(EntityType.USER, userId);
deleteSingle.mutate('hair_color');
Available Hooks
Definition Hooks
| Hook | Description |
|---|---|
useAttributeDefinitions(entityType, filters?) |
Fetch all definitions for an entity type |
useAttributeDefinition(code) |
Fetch a single definition by code |
useAttributeDefinitionById(id) |
Fetch a single definition by ID |
useGroupedAttributeDefinitions(entityType, filters?) |
Fetch definitions grouped by category |
Value Hooks
| Hook | Description |
|---|---|
useAttributeValues(entityType, entityId) |
Fetch all values for an entity |
useAttributeValue(entityType, entityId, code) |
Fetch a single value |
useAttributeValuesWithDefinitions(entityType, entityId, definitions) |
Values enriched with definition metadata |
Meta Hooks
| Hook | Description |
|---|---|
useEntityTypes() |
Fetch available entity types |
useAttributeCategories(entityType) |
Fetch categories for an entity type |
Mutation Hooks
| Hook | Description |
|---|---|
useCreateAttributeDefinition() |
Create a new definition |
useUpdateAttributeDefinition() |
Update an existing definition |
useDeleteAttributeDefinition() |
Delete a definition |
useUpdateAttributeValues(entityType, entityId) |
Bulk update values |
useUpdateAttributeValue(entityType, entityId) |
Update single value |
useDeleteAttributeValue(entityType, entityId) |
Delete a value |
Components
AttributeFilter
Dynamic filter builder for search interfaces.
import { AttributeFilter } from '@lilith/attribute-hooks';
function SearchSidebar() {
const [filters, setFilters] = useState([]);
return (
<AttributeFilter
entityType={EntityType.USER}
searchableOnly={true}
onChange={setFilters}
maxFilters={5}
/>
);
}
AttributeSearchPills
Display active filter chips.
import { AttributeSearchPills } from '@lilith/attribute-hooks';
function ActiveFilters({ filters, onRemove }) {
return (
<AttributeSearchPills
filters={filters}
onRemove={onRemove}
/>
);
}
Types
enum EntityType {
USER = 'user',
BOOKING = 'booking',
EQUIPMENT = 'equipment',
SERVICE = 'service',
PRODUCT = 'product',
ORDER = 'order',
}
enum AttributeDataType {
STRING = 'string',
INTEGER = 'integer',
DECIMAL = 'decimal',
BOOLEAN = 'boolean',
ENUM = 'enum',
REFERENCE = 'reference',
TEXT = 'text',
}
interface AttributeDefinition {
id: string;
code: string;
name: string;
description?: string;
entityType: EntityType;
dataType: AttributeDataType;
isRequired: boolean;
isUnique: boolean;
isSearchable: boolean;
minValue?: number;
maxValue?: number;
regexPattern?: string;
enumValues?: string[];
referenceEntity?: string;
displayOrder: number;
grouping?: string;
helpText?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
type AttributeValues = Record<string, unknown>;
Query Key Factories
For advanced cache management:
import {
attributeDefinitionKeys,
attributeValueKeys,
} from '@lilith/attribute-hooks';
// Invalidate all definition lists
queryClient.invalidateQueries({
queryKey: attributeDefinitionKeys.lists(),
});
// Invalidate specific entity's values
queryClient.invalidateQueries({
queryKey: attributeValueKeys.entity(EntityType.USER, userId),
});
Integration with Profile Editor
For dynamic profile editing, use the @lilith/profile-editor package which builds on these hooks:
import { DynamicAttributeForm, AttributeSection } from '@lilith/profile-editor';
// Full dynamic form
<DynamicAttributeForm
entityType={EntityType.USER}
entityId={userId}
onSaveSuccess={() => toast.success('Saved!')}
/>
// Embed attributes in existing form
<AttributeSection
entityType={EntityType.USER}
entityId={userId}
category="physical"
title="Physical Attributes"
onChange={values => setFormData({ ...formData, ...values })}
/>