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>
285 lines
6.5 KiB
Markdown
285 lines
6.5 KiB
Markdown
# @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
|
|
|
|
```bash
|
|
pnpm add @lilith/attribute-hooks
|
|
```
|
|
|
|
## Peer Dependencies
|
|
|
|
```json
|
|
{
|
|
"react": "^18.0.0",
|
|
"@tanstack/react-query": "^5.0.0",
|
|
"styled-components": "^6.0.0"
|
|
}
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic Hook Usage
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
const { data: physicalAttributes } = useAttributeDefinitions(
|
|
EntityType.USER,
|
|
{ category: 'physical' }
|
|
);
|
|
```
|
|
|
|
### Grouped Definitions
|
|
|
|
```tsx
|
|
import { useGroupedAttributeDefinitions } from '@lilith/attribute-hooks';
|
|
|
|
const { data: groupedDefs } = useGroupedAttributeDefinitions(EntityType.USER);
|
|
// Returns: { physical: [...], demographics: [...], ... }
|
|
```
|
|
|
|
### Single Attribute Operations
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
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.
|
|
|
|
```tsx
|
|
import { AttributeSearchPills } from '@lilith/attribute-hooks';
|
|
|
|
function ActiveFilters({ filters, onRemove }) {
|
|
return (
|
|
<AttributeSearchPills
|
|
filters={filters}
|
|
onRemove={onRemove}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Types
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```tsx
|
|
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 })}
|
|
/>
|
|
```
|