Helps with React frontend development following Algorena's terminal-inspired design system and patterns.
Usage: /react-frontend <task description>
Examples:
- •
/react-frontend add leaderboard page - •
/react-frontend create tournament card component - •
/react-frontend add filter dropdown to matches
What this skill does:
Project Understanding
- •React 19 with TypeScript and Vite
- •Dark-only terminal-inspired UI (zinc palette, emerald accents)
- •TanStack Query for server state management
- •Auto-generated API client from OpenAPI spec (
bun run api:generate) - •Tailwind CSS v4 with custom utilities
- •Lucide icons for all iconography
- •i18next for internationalization
Design System - Terminal Aesthetic
Color Palette (Semantic Tokens):
The app uses semantic color tokens defined in index.css via Tailwind v4's @theme directive. Always use semantic tokens instead of hardcoded colors:
- •
Backgrounds:
- •
bg-background- Main background (zinc-950) - •
bg-surface- Card/panel background (zinc-900) - •
bg-surface-elevated- Elevated surface (zinc-800) - •
bg-surface-hover- Hover states (zinc-700) - •
bg-surface-muted- Muted backgrounds (zinc-600)
- •
- •
Borders:
- •
border-border- Standard borders (zinc-800) - •
border-border-hover- Hover borders (zinc-700)
- •
- •
Text:
- •
text-text-primary- Primary text (zinc-200) - •
text-text-bright- Bright white text (zinc-100) - use sparingly - •
text-text-secondary- Secondary text (zinc-400) - •
text-text-muted- Muted text (zinc-500) - •
text-text-inverse- Dark text on light backgrounds (zinc-900)
- •
- •
Primary/Accent:
- •
bg-primary/text-primary- Accent color (emerald-500) - •
bg-primary-hover/text-primary-hover- Hover state (emerald-600) - •
bg-primary-active/text-primary-active- Active state (emerald-700)
- •
- •
Error/Destructive:
- •
bg-error/text-error- Error color (red-600) - •
bg-error-hover/text-error-hover- Error hover (red-700)
- •
- •
Focus:
- •
ring-focus-ring- Focus ring color (emerald-500) - •
ring-offset-focus-ring-offset- Focus ring offset (zinc-950)
- •
- •
Other Status Colors (keep hardcoded for semantic meaning):
- •Success:
text-emerald-500/bg-emerald-500 - •Warning:
text-yellow-500/bg-yellow-500 - •Info:
text-blue-500/bg-blue-500
- •Success:
Typography:
- •Always use
font-monofor terminal feel - •Headers: Terminal-style commands (e.g.,
$ ./bots --list) - •Subheaders: Commented descriptions (e.g.,
# Manage your battle bots) - •Uppercase text for status labels (e.g.,
ACTIVE,LIVE,FINISHED)
Terminal UI Components:
- •
TerminalTable - Primary data display component
tsx<TerminalTable title="bots.db" // File name in terminal header headers={['status', 'name', 'game', 'actions']} > <TerminalTableRow onClick={() => {}}> {/* Optional click handler */} <TerminalTableCell>Content</TerminalTableCell> </TerminalTableRow> </TerminalTable>- •Features macOS-style traffic lights (red/yellow/green dots)
- •Terminal header bar with file name
- •Hover effect on clickable rows:
hover:bg-zinc-800/50
- •
Page Headers - Terminal command style
tsx<h1 className="font-mono text-2xl font-bold text-primary"> $ ./bots --list </h1> <p className="mt-1 font-mono text-sm text-text-muted"> # Manage your battle bots </p>
- •
Empty States - Command-line themed
tsx<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"> <Bot className="h-12 w-12 text-surface-muted" /> <h3 className="mt-4 font-mono text-lg font-semibold">No bots found</h3> <p className="mt-1 font-mono text-sm text-text-muted"> $ create your first bot to start competing </p> <Button onClick={handleCreate} className="mt-6 gap-2 font-mono"> <Plus className="h-4 w-4" /> new bot </Button> </div> - •
Status Indicators
tsx{/* Circle indicator with color */} <Circle className="h-2 w-2 fill-emerald-500 text-emerald-500" /> {/* With label */} <div className="flex items-center gap-2"> <Circle className={cn('h-2 w-2', active ? 'fill-emerald-500 text-emerald-500' : 'fill-zinc-600 text-zinc-600')} /> <span className={cn('text-xs', active ? 'text-emerald-500' : 'text-zinc-600')}> {active ? 'ACTIVE' : 'IDLE'} </span> </div> - •
Badges/Tags - Rounded with dark background
tsx<span className="rounded bg-surface-elevated px-2 py-0.5 text-xs text-text-secondary"> CHESS </span>
Component Patterns
File Structure:
frontend/src/
├── routes/ # Page components (HomePage.tsx, BotsPage.tsx)
├── features/ # Feature modules with colocation
│ └── {feature}/
│ ├── use{Feature}.ts # TanStack Query hooks
│ ├── {Feature}Dialog.tsx # Dialogs for CRUD
│ └── components/ # Feature-specific components
├── components/
│ └── ui/ # Reusable UI components (shadcn-style)
└── api/
└── generated/ # Auto-generated API client (DO NOT edit)
Page Component Pattern:
export function BotsPage() {
const { isAuthenticated, login } = useAuth();
const { data: botsPage, isLoading, error } = useBots();
const [createDialogOpen, setCreateDialogOpen] = useState(false);
// Early return for unauthenticated
if (!isAuthenticated) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<Bot className="h-16 w-16 text-zinc-600" />
<h2 className="mt-4 text-xl font-semibold">{t('errors.unauthorized')}</h2>
<Button onClick={login}>{t('nav.login')}</Button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header with terminal command */}
<div className="flex items-center justify-between">
<div>
<h1 className="font-mono text-2xl font-bold text-primary">
$ ./bots --list
</h1>
<p className="mt-1 font-mono text-sm text-text-muted">
# Manage your battle bots
</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)} className="gap-2 font-mono">
<Plus className="h-4 w-4" />
new bot
</Button>
</div>
{/* Loading/Error/Empty/Data states */}
{isLoading ? <LoadingState /> : error ? <ErrorState /> : data.length === 0 ? <EmptyState /> : <DataTable />}
</div>
);
}
TanStack Query Hooks Pattern:
// Query key factory
export const botKeys = {
all: ['bots'] as const,
lists: () => [...botKeys.all, 'list'] as const,
list: (filters: Record<string, unknown>) => [...botKeys.lists(), filters] as const,
details: () => [...botKeys.all, 'detail'] as const,
detail: (id: number) => [...botKeys.details(), id] as const,
stats: (id: number) => [...botKeys.all, 'stats', id] as const,
};
// Query hook
export function useBots() {
return useQuery({
queryKey: botKeys.lists(),
queryFn: async () => {
const response = await getBots(); // Auto-generated API client
if (response.error) {
throw new Error(response.error.message || 'Failed to fetch bots');
}
return response.data;
},
});
}
// Mutation hook
export function useCreateBot() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreateBotRequest) => {
const response = await createBot({ body: data });
if (response.error) {
throw new Error(response.error.message || 'Failed to create bot');
}
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: botKeys.lists() });
},
});
}
Dialog Pattern:
export function CreateBotDialog({ open, onOpenChange }: DialogProps) {
const [name, setName] = useState('');
const createBot = useCreateBot();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createBot.mutateAsync({ name /* ... */ });
// Reset form
setName('');
onOpenChange(false);
} catch (error) {
console.error('Failed to create:', error);
}
};
const handleClose = () => {
// Reset form state
setName('');
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Bot</DialogTitle>
<DialogDescription>Description here</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<DialogFooter className="pt-4">
<Button type="button" variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button type="submit" disabled={createBot.isPending}>
{createBot.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
UI Components (Shadcn-style)
All UI components use:
- •Class Variance Authority (CVA) for variants
- •
cn()utility for className merging - •TypeScript with proper prop types
- •
forwardReffor ref forwarding
Button:
<Button variant="default">Default (emerald)</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="destructive">Destructive (red)</Button> <Button size="sm" className="gap-2 font-mono"> <Plus className="h-4 w-4" /> new bot </Button>
Form Components:
{/* Always wrap in div with space-y-2 */}
<div className="space-y-2">
<Label htmlFor="field">Field Name</Label>
<Input
id="field"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Placeholder"
required
/>
<p className="text-xs text-text-muted">Helper text</p>
</div>
{/* Select */}
<Select value={game} onChange={(e) => setGame(e.target.value)}>
<option value="CHESS">Chess</option>
<option value="CONNECT_FOUR">Connect 4</option>
</Select>
{/* Textarea */}
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
maxLength={500}
/>
Icons (Lucide)
Common icons and their usage:
- •
Plus- Create actions - •
Edit2- Edit actions - •
Trash2- Delete actions - •
Eye- View details - •
Loader2- Loading spinner (withanimate-spin) - •
Circle- Status indicators (usefill-andtext-for color) - •
Bot- Bot-related features - •
Swords- Matches/battles - •
Filter- Filter controls - •
Clock,PlayCircle,CheckCircle,XCircle- Match statuses
Size classes: h-4 w-4 (default), h-3.5 w-3.5 (small), h-12 w-12 (large)
Common Utilities
Date Formatting:
// Relative time for recent items
const formatDate = (dateString?: string) => {
if (!dateString) return '—';
const date = new Date(dateString);
const now = new Date();
const diffMins = Math.floor((now.getTime() - date.getTime()) / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
};
// Standard date format
const formatDate = (dateString?: string) => {
if (!dateString) return '—';
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: '2-digit'
});
};
Filtering Pattern:
const [statusFilter, setStatusFilter] = useState<'all' | 'active'>('all');
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useMemo(() => {
if (!data) return [];
return data.filter(item => {
if (statusFilter === 'active' && !item.active) return false;
if (searchQuery && !item.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
return true;
});
}, [data, statusFilter, searchQuery]);
API Client Integration
Auto-generated client:
- •NEVER edit files in
api/generated/ - •Run
bun run api:generateafter backend OpenAPI spec changes - •Import types and functions from
@/api/generated
Usage:
import { getBots, createBot } from '@/api/generated';
import type { BotDto, CreateBotRequest } from '@/api/generated';
// In TanStack Query hook
const response = await getBots();
if (response.error) {
throw new Error(response.error.message || 'Failed to fetch');
}
return response.data;
// With path parameters
const response = await getBotStats({ path: { botId } });
// With body
const response = await createBot({ body: data });
Styling Guidelines
Spacing:
- •Page wrapper:
<div className="space-y-6"> - •Form fields:
<div className="space-y-2"> - •Button groups:
gap-2,gap-4
Text Styling:
- •Always use
font-monofor terminal aesthetic - •Headers:
text-2xl font-bold text-primary - •Subtext:
text-sm text-text-muted - •Labels:
text-xs uppercase tracking-wider text-text-muted - •Body text:
text-text-primary
Interactive Elements:
- •Hover effects:
hover:bg-surface-elevated,hover:bg-surface-elevated/50(with opacity) - •Transitions:
transition-colors - •Disabled:
disabled:opacity-50 disabled:pointer-events-none
Borders & Backgrounds:
- •Cards:
rounded-lg border border-border bg-background - •Surface cards:
rounded-lg border border-border bg-surface - •Subtle dividers:
border-border/50,divide-border/50 - •Focus rings:
focus-visible:ring-1 focus-visible:ring-ring-default
Color Usage Rules:
- •Always use semantic tokens for UI chrome (backgrounds, borders, text)
- •Only use hardcoded colors for semantic status (yellow for warning, blue for info, etc.)
- •Game-specific colors (Connect4 pieces, chess highlights) can be hardcoded as they represent game state
- •Never use
zinc-*directly - use the semantic tokens instead
Commands
Frontend commands (from frontend/ directory):
- •
bun install- Install dependencies - •
bun run dev- Start dev server (port 5173) - •
bun run build- Production build - •
bun run lint- Run ESLint - •
bun run api:generate- Regenerate API client from OpenAPI spec
What to Ask
Before generating code:
- •Is this a new page or component? Where should it live?
- •Does it need data fetching? What API endpoints?
- •Are there filters, search, or pagination requirements?
- •Should it be accessible without authentication?
- •Does it need i18n keys added?
Important Notes
- •Use Bun, not npm/yarn/pnpm
- •Dark mode only - no light mode variants
- •Always use semantic color tokens (bg-background, text-primary, etc.) instead of hardcoded Tailwind colors
- •All text should use
font-monounless there's a specific reason not to - •Status/state text should be UPPERCASE
- •Empty states should feel like terminal prompts
- •Dialogs should have proper form reset on close
- •Always handle loading, error, and empty states
- •Use
cn()for conditional className logic - •API client is auto-generated - regenerate after backend changes
Tech Stack
- •React 19 with TypeScript
- •Vite for build tooling
- •Bun for package management
- •TanStack Query v5 for server state
- •Tailwind CSS v4 for styling
- •Lucide React for icons
- •i18next for internationalization
- •@hey-api/openapi-ts for API client generation