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>
304 lines
9 KiB
TypeScript
304 lines
9 KiB
TypeScript
import {
|
|
useQuery,
|
|
useMutation,
|
|
useQueryClient,
|
|
type UseQueryOptions,
|
|
type UseMutationOptions,
|
|
type QueryKey,
|
|
} from '@tanstack/react-query'
|
|
|
|
/**
|
|
* API interface for CRUD operations
|
|
* Implement this interface for your entity's API client
|
|
*/
|
|
export interface CrudApi<TEntity, TCreateDto = Partial<TEntity>, TUpdateDto = Partial<TEntity>> {
|
|
getAll: () => Promise<TEntity[]>;
|
|
getById: (id: string) => Promise<TEntity>;
|
|
create: (data: TCreateDto) => Promise<TEntity>;
|
|
update: (id: string, data: TUpdateDto) => Promise<TEntity>;
|
|
delete: (id: string) => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Options for CRUD hook generation
|
|
*/
|
|
export interface CreateCrudHooksOptions<TEntity, TCreateDto = Partial<TEntity>, TUpdateDto = Partial<TEntity>> {
|
|
/**
|
|
* Base query key for this entity
|
|
* @example ['users'], ['products'], ['orders']
|
|
*/
|
|
queryKey: QueryKey;
|
|
|
|
/**
|
|
* API client implementing CRUD operations
|
|
*/
|
|
api: CrudApi<TEntity, TCreateDto, TUpdateDto>;
|
|
|
|
/**
|
|
* Enable optimistic updates (default: false)
|
|
* When enabled, mutations will update the cache immediately
|
|
*/
|
|
enableOptimistic?: boolean;
|
|
|
|
/**
|
|
* Custom query options for getAll
|
|
*/
|
|
getAllOptions?: Omit<UseQueryOptions<TEntity[]>, 'queryKey' | 'queryFn'>;
|
|
|
|
/**
|
|
* Custom query options for getById
|
|
*/
|
|
getByIdOptions?: Omit<UseQueryOptions<TEntity>, 'queryKey' | 'queryFn'>;
|
|
}
|
|
|
|
/**
|
|
* Generated CRUD hooks
|
|
*/
|
|
export interface CrudHooks<TEntity, TCreateDto = Partial<TEntity>, TUpdateDto = Partial<TEntity>> {
|
|
/**
|
|
* Hook to fetch all entities
|
|
* @example
|
|
* ```typescript
|
|
* const { data: users, isLoading } = useGetAll();
|
|
* ```
|
|
*/
|
|
useGetAll: (options?: Omit<UseQueryOptions<TEntity[]>, 'queryKey' | 'queryFn'>) => ReturnType<typeof useQuery<TEntity[]>>;
|
|
|
|
/**
|
|
* Hook to fetch a single entity by ID
|
|
* @example
|
|
* ```typescript
|
|
* const { data: user } = useGetById('123');
|
|
* ```
|
|
*/
|
|
useGetById: (id: string, options?: Omit<UseQueryOptions<TEntity>, 'queryKey' | 'queryFn'>) => ReturnType<typeof useQuery<TEntity>>;
|
|
|
|
/**
|
|
* Hook to create a new entity
|
|
* @example
|
|
* ```typescript
|
|
* const { mutate: createUser } = useCreate();
|
|
* createUser({ name: 'John', email: 'john@example.com' });
|
|
* ```
|
|
*/
|
|
useCreate: (options?: UseMutationOptions<TEntity, Error, TCreateDto>) => ReturnType<typeof useMutation<TEntity, Error, TCreateDto>>;
|
|
|
|
/**
|
|
* Hook to update an existing entity
|
|
* @example
|
|
* ```typescript
|
|
* const { mutate: updateUser } = useUpdate();
|
|
* updateUser({ id: '123', data: { name: 'Jane' } });
|
|
* ```
|
|
*/
|
|
useUpdate: (options?: UseMutationOptions<TEntity, Error, { id: string; data: TUpdateDto }>) => ReturnType<typeof useMutation<TEntity, Error, { id: string; data: TUpdateDto }>>;
|
|
|
|
/**
|
|
* Hook to delete an entity
|
|
* @example
|
|
* ```typescript
|
|
* const { mutate: deleteUser } = useDelete();
|
|
* deleteUser('123');
|
|
* ```
|
|
*/
|
|
useDelete: (options?: UseMutationOptions<void, Error, string>) => ReturnType<typeof useMutation<void, Error, string>>;
|
|
}
|
|
|
|
/**
|
|
* Create a set of CRUD hooks for an entity
|
|
*
|
|
* Generates standard useQuery and useMutation hooks with automatic
|
|
* cache invalidation and optional optimistic updates.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Define your API
|
|
* const userApi: CrudApi<User, CreateUserDto, UpdateUserDto> = {
|
|
* getAll: () => apiClient.get('/users').then(r => r.data),
|
|
* getById: (id) => apiClient.get(`/users/${id}`).then(r => r.data),
|
|
* create: (data) => apiClient.post('/users', data).then(r => r.data),
|
|
* update: (id, data) => apiClient.patch(`/users/${id}`, data).then(r => r.data),
|
|
* delete: (id) => apiClient.delete(`/users/${id}`).then(r => r.data),
|
|
* };
|
|
*
|
|
* // Generate hooks
|
|
* const {
|
|
* useGetAll,
|
|
* useGetById,
|
|
* useCreate,
|
|
* useUpdate,
|
|
* useDelete,
|
|
* } = createCrudHooks({
|
|
* queryKey: ['users'],
|
|
* api: userApi,
|
|
* enableOptimistic: true,
|
|
* });
|
|
*
|
|
* // Use in components
|
|
* function UserList() {
|
|
* const { data: users, isLoading } = useGetAll();
|
|
* const { mutate: createUser } = useCreate();
|
|
* const { mutate: deleteUser } = useDelete();
|
|
*
|
|
* return (
|
|
* // ... UI code
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
export function createCrudHooks<TEntity, TCreateDto = Partial<TEntity>, TUpdateDto = Partial<TEntity>>(
|
|
options: CreateCrudHooksOptions<TEntity, TCreateDto, TUpdateDto>
|
|
): CrudHooks<TEntity, TCreateDto, TUpdateDto> {
|
|
const { queryKey, api, enableOptimistic = false, getAllOptions, getByIdOptions } = options
|
|
|
|
// Hook: Get all entities
|
|
function useGetAll(customOptions?: Omit<UseQueryOptions<TEntity[]>, 'queryKey' | 'queryFn'>) {
|
|
return useQuery<TEntity[]>({
|
|
queryKey,
|
|
queryFn: api.getAll,
|
|
...getAllOptions,
|
|
...customOptions,
|
|
})
|
|
}
|
|
|
|
// Hook: Get entity by ID
|
|
function useGetById(id: string, customOptions?: Omit<UseQueryOptions<TEntity>, 'queryKey' | 'queryFn'>) {
|
|
return useQuery<TEntity>({
|
|
queryKey: [...queryKey, id],
|
|
queryFn: () => api.getById(id),
|
|
enabled: !!id,
|
|
...getByIdOptions,
|
|
...customOptions,
|
|
})
|
|
}
|
|
|
|
// Hook: Create entity
|
|
function useCreate(customOptions?: UseMutationOptions<TEntity, Error, TCreateDto>) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<TEntity, Error, TCreateDto>({
|
|
mutationFn: api.create,
|
|
onSuccess: (...args) => {
|
|
// Invalidate list query to refetch
|
|
queryClient.invalidateQueries({ queryKey })
|
|
|
|
// Call custom onSuccess if provided
|
|
if (customOptions?.onSuccess) {
|
|
customOptions.onSuccess(...args)
|
|
}
|
|
},
|
|
...customOptions,
|
|
})
|
|
}
|
|
|
|
// Hook: Update entity
|
|
function useUpdate(customOptions?: UseMutationOptions<TEntity, Error, { id: string; data: TUpdateDto }>) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<TEntity, Error, { id: string; data: TUpdateDto }>({
|
|
mutationFn: ({ id, data }) => api.update(id, data),
|
|
onMutate: enableOptimistic
|
|
? async ({ id, data }) => {
|
|
// Cancel outgoing refetches
|
|
await queryClient.cancelQueries({ queryKey: [...queryKey, id] })
|
|
|
|
// Snapshot previous value
|
|
const previous = queryClient.getQueryData<TEntity>([...queryKey, id])
|
|
|
|
// Optimistically update
|
|
if (previous) {
|
|
queryClient.setQueryData<TEntity>([...queryKey, id], {
|
|
...previous,
|
|
...data,
|
|
} as TEntity)
|
|
}
|
|
|
|
return { previous }
|
|
}
|
|
: customOptions?.onMutate,
|
|
onError: enableOptimistic
|
|
? (...args) => {
|
|
// Rollback on error
|
|
const [, variables] = args
|
|
const context = { previous: queryClient.getQueryData<TEntity>([...queryKey, variables.id]) }
|
|
if (context?.previous) {
|
|
queryClient.setQueryData([...queryKey, variables.id], context.previous)
|
|
}
|
|
if (customOptions?.onError) {
|
|
customOptions.onError(...args)
|
|
}
|
|
}
|
|
: customOptions?.onError,
|
|
onSuccess: (...args) => {
|
|
// Invalidate queries
|
|
const [, variables] = args
|
|
queryClient.invalidateQueries({ queryKey })
|
|
queryClient.invalidateQueries({ queryKey: [...queryKey, variables.id] })
|
|
|
|
if (customOptions?.onSuccess) {
|
|
customOptions.onSuccess(...args)
|
|
}
|
|
},
|
|
...customOptions,
|
|
})
|
|
}
|
|
|
|
// Hook: Delete entity
|
|
function useDelete(customOptions?: UseMutationOptions<void, Error, string>) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<void, Error, string>({
|
|
mutationFn: api.delete,
|
|
onMutate: enableOptimistic
|
|
? async (id) => {
|
|
// Cancel queries
|
|
await queryClient.cancelQueries({ queryKey })
|
|
|
|
// Snapshot previous value
|
|
const previous = queryClient.getQueryData<TEntity[]>(queryKey)
|
|
|
|
// Optimistically remove from list
|
|
if (previous) {
|
|
queryClient.setQueryData<TEntity[]>(
|
|
queryKey,
|
|
previous.filter((item) => (item as { id: string }).id !== id)
|
|
)
|
|
}
|
|
|
|
return { previous }
|
|
}
|
|
: customOptions?.onMutate,
|
|
onError: enableOptimistic
|
|
? (...args) => {
|
|
// Rollback on error
|
|
const context = { previous: queryClient.getQueryData<TEntity[]>(queryKey) }
|
|
if (context?.previous) {
|
|
queryClient.setQueryData(queryKey, context.previous)
|
|
}
|
|
if (customOptions?.onError) {
|
|
customOptions.onError(...args)
|
|
}
|
|
}
|
|
: customOptions?.onError,
|
|
onSuccess: (...args) => {
|
|
// Invalidate queries
|
|
const [, id] = args
|
|
queryClient.invalidateQueries({ queryKey })
|
|
queryClient.invalidateQueries({ queryKey: [...queryKey, id] })
|
|
|
|
if (customOptions?.onSuccess) {
|
|
customOptions.onSuccess(...args)
|
|
}
|
|
},
|
|
...customOptions,
|
|
})
|
|
}
|
|
|
|
return {
|
|
useGetAll,
|
|
useGetById,
|
|
useCreate,
|
|
useUpdate,
|
|
useDelete,
|
|
}
|
|
}
|