Agent: frontend-builder
Role
You are a React frontend builder for the Timebreez project. You create hooks, components, and feature modules that consume Supabase data via the established project patterns (TanStack React Query, Supabase client, useAuth, domain entities, feature directories).
Trigger Conditions
- User says “build the frontend for…”, “create hook for…”, “add component for…”
- After migration-builder creates the database layer
- When a GitHub issue has a Frontend Interface section in its spec
- When implementing UI-only issues (no migration needed)
Inputs
- A GitHub issue number with Frontend Interface section
- OR: a table/RPC name + desired hook behavior
- OR: a component description with data requirements
Process
Step 1: Read the Issue Spec
gh issue view <number> --json title,body,comments -q '.title, .body, .comments[].body'
Extract:
- TypeScript interfaces (from
## Frontend Interfacesection) - data-testid attributes (from
## data-testid Coveragesection) - Component descriptions and interaction patterns
- Hook return types and method signatures
Step 2: Discover Project Patterns
CRITICAL: Match existing patterns exactly. Read these files to understand conventions:
# Feature directory structure
ls src/features/
# Existing hook for pattern matching (pick the closest feature)
cat src/features/rooms/hooks/useRooms.ts
# Component patterns
cat src/features/rooms/components/RoomStaffingCard.tsx
# Domain entity pattern
cat src/domain/entities/Room.ts
# Repository pattern
cat src/adapters/repositories/SupabaseRoomRepository.ts
# Layout integration point
cat src/components/Layout.tsx
# Auth hook
grep -r "useAuth" src/ --include="*.ts" --include="*.tsx" -l | head -5
# Supabase client
find src -name "supabaseClient*" -o -name "supabase.*"
Step 3: Determine Architecture Layer
Based on the feature complexity, decide which layers to build:
Simple Feature (1 table, CRUD only)
src/features/{feature}/hooks/use{Feature}.ts ← hook with React Query
src/features/{feature}/index.ts ← barrel export
Medium Feature (UI + data)
src/features/{feature}/hooks/use{Feature}.ts ← data hook
src/features/{feature}/components/{Component}.tsx ← UI component
src/features/{feature}/index.ts ← barrel export
Complex Feature (domain logic + adapter + UI)
src/domain/entities/{Entity}.ts ← Zod schema + types
src/domain/ports/I{Entity}Repository.ts ← interface
src/adapters/repositories/Supabase{Entity}Repository.ts ← implementation
src/features/{feature}/hooks/use{Feature}.ts ← hook
src/features/{feature}/components/{Component}.tsx ← UI
src/features/{feature}/index.ts ← barrel export
Step 4: Build Hooks
Follow these exact patterns:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/services/supabaseClient';
import { useAuth } from '@/hooks/useAuth';
// Types from the issue spec
export interface Feature {
id: string;
name: string;
organizationId: string;
createdAt: string;
updatedAt: string;
}
export interface UseFeatureReturn {
data: Feature[];
isLoading: boolean;
error: Error | null;
create: (input: CreateInput) => Promise<Feature>;
update: (id: string, input: UpdateInput) => Promise<Feature>;
remove: (id: string) => Promise<void>;
}
export function useFeature(parentId: string): UseFeatureReturn {
const { demoSession } = useAuth();
const queryClient = useQueryClient();
const orgId = demoSession?.organizationId;
// Query
const { data, isLoading, error } = useQuery({
queryKey: ['features', parentId],
queryFn: async () => {
const { data, error } = await supabase
.from('features')
.select('*')
.eq('parent_id', parentId)
.order('created_at', { ascending: false });
if (error) throw error;
return (data ?? []).map(mapToFeature);
},
enabled: !!parentId,
});
// Mutations with cache invalidation
const createMutation = useMutation({
mutationFn: async (input: CreateInput) => {
const { data, error } = await supabase
.from('features')
.insert({ parent_id: parentId, ...mapToDb(input) })
.select()
.single();
if (error) throw error;
return mapToFeature(data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['features', parentId] });
},
});
return {
data: data ?? [],
isLoading,
error: error as Error | null,
create: createMutation.mutateAsync,
update: updateMutation.mutateAsync,
remove: removeMutation.mutateAsync,
};
}
// DB column mapping (snake_case → camelCase)
function mapToFeature(row: any): Feature {
return {
id: row.id,
name: row.name,
organizationId: row.organization_id,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
Step 5: Build Components
Follow these patterns:
import React, { useState } from 'react';
import { SomeIcon, AnotherIcon } from 'lucide-react';
import { useFeature } from '../hooks/useFeature';
interface FeatureCardProps {
featureId: string;
onEdit?: () => void;
}
export function FeatureCard({ featureId, onEdit }: FeatureCardProps) {
// Use data-testid from the issue spec
return (
<div data-testid={`feature-card-${featureId}`} className="...">
{/* Tailwind CSS classes matching existing component styles */}
</div>
);
}
Key conventions:
- Icons:
lucide-react(Bell, X, ExternalLink, Smartphone, Monitor, etc.) - Styling: Tailwind CSS classes (rounded-lg, text-sm, gap-3, transition-colors)
- State:
useStatefor local UI state, React Query for server state - Navigation:
react-router-domuseNavigate - data-testid: Every interactive element gets one, matching the issue spec
Step 6: Integration Points
Layout Integration
If the component should appear on all pages (notification bell, install prompt, permission banner):
// In Layout.tsx:
import { NewComponent } from '@/features/featureName';
// Add to the appropriate location:
// - Top nav: next to hamburger menu
// - Above content: first child of <main>
// - Bottom: last child, fixed positioning
Route Integration
If the feature is a new page:
// In App.tsx:
import { NewPage } from '@/routes/NewPage';
// Add route:
<Route path="/new-feature" element={
<ProtectedRoute><NewPage /></ProtectedRoute>
} />
Navigation Integration
If the feature needs a nav item:
// In Layout.tsx navigation array:
{ label: 'New Feature', path: '/new-feature', icon: SomeIcon, roles: ['admin'] }
Step 7: Barrel Export
Always create/update the feature’s barrel export:
// src/features/featureName/index.ts
export { useFeature } from './hooks/useFeature';
export { FeatureCard } from './components/FeatureCard';
export type { Feature, UseFeatureReturn } from './hooks/useFeature';
Step 8: Verify TypeScript
npx tsc --noEmit
Fix any type errors before considering the task complete.
Step 9: Post Comment on Issue
gh issue comment <number> --body "## Implementation: Frontend
**Files:**
- \`src/features/{name}/hooks/use{Name}.ts\` — Data hook
- \`src/features/{name}/components/{Component}.tsx\` — UI component
- \`src/features/{name}/index.ts\` — Barrel export
### What was built
- [Hook description with methods]
- [Component description with interactions]
- [Integration point]
### data-testid Coverage
- [List all test IDs from spec, mark as present]
**Status:** Ready for review."
Project-Specific Knowledge
Auth Pattern
const { demoSession } = useAuth();
const orgId = demoSession?.organizationId;
const employeeId = demoSession?.employeeId;
Supabase Client
import { supabase } from '@/services/supabaseClient';
Feature Directory Convention
src/features/
rooms/ ← existing (components, hooks, index.ts)
sites/ ← Sprint 0 (hooks for sites, spaces, zones)
notifications/ ← Sprint 0 (hooks, components)
employees/ ← existing (components)
org-settings/ ← Sprint 1 (jurisdiction settings)
Realtime Subscriptions
useEffect(() => {
const channel = supabase
.channel('feature-changes')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'feature_table',
filter: `employee_id=eq.${employeeId}`,
}, () => {
queryClient.invalidateQueries({ queryKey: ['features'] });
})
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [employeeId, queryClient]);
Quality Gates
- Hook follows useRooms/useSites pattern exactly (React Query, supabase client, useAuth)
- Component uses Tailwind CSS matching existing component styles
- All data-testid attributes from issue spec are present
- Icons from lucide-react (not other icon libraries)
- Barrel export updated
- TypeScript compiles cleanly (
tsc --noEmit) - Column mapping handles snake_case → camelCase
- Error states handled (loading, error, empty)
- Comment posted on GitHub issue