oneie
Version:
Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.
247 lines (212 loc) • 7.64 kB
Markdown
---
title: Component Template
dimension: knowledge
category: patterns
tags: backend, frontend
related_dimensions: groups, things
scope: global
created: 2025-11-03
updated: 2025-11-03
version: 1.0.0
ai_context: |
This document is part of the knowledge dimension in the patterns category.
Location: one/knowledge/patterns/frontend/component-template.md
Purpose: Documents pattern: react component template
Related dimensions: groups, things
For AI agents: Read this to understand component template.
---
**Category:** Frontend
**Context:** When creating React components with shadcn/ui for interactive features
**Problem:** Need consistent component structure that uses DataProvider pattern, handles loading/error states, and follows accessibility guidelines
Use shadcn/ui components, Effect.ts services with DataProvider pattern for backend-agnostic data, proper loading/error states, and TypeScript for type safety.
```tsx
// src/components/features/{EntityName}List.tsx
import { useEffectRunner } from "@/hooks/useEffectRunner";
import { ThingClientService } from "@/services/ThingClientService";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Plus } from "lucide-react";
import { useEffect, useState } from "react";
import { Effect } from "effect";
interface {EntityName}ListProps {
type: string;
groupId?: string;
status?: "draft" | "active" | "archived";
limit?: number;
}
export function {EntityName}List({ type, groupId, status, limit }: {EntityName}ListProps) {
const { run, loading, error } = useEffectRunner();
const [items, setItems] = useState<any[]>([]);
// Fetch data on mount via Effect.ts service
useEffect(() => {
const program = Effect.gen(function* () {
const service = yield* ThingClientService;
return yield* service.list(type as any, groupId);
});
run(program, {
onSuccess: (results) => setItems(results || [])
});
}, [type, groupId]);
// Loading state
if (loading && items.length === 0) {
return (
<div className="space-y-4">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
);
}
// Error state
if (error) {
return (
<Alert variant="destructive">
<AlertDescription>Failed to load items: {error}</AlertDescription>
</Alert>
);
}
// Empty state
if (items.length === 0) {
return (
<Alert>
<AlertDescription>
No {type}s found. Create your first {type} to get started.
</AlertDescription>
<Button asChild className="mt-4">
<a href={`/${type}s/new`}>
<Plus className="mr-2 h-4 w-4" />
Create {type}
</a>
</Button>
</Alert>
);
}
// Success state
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">{type}s</h2>
<Button asChild>
<a href={`/${type}s/new`}>
<Plus className="mr-2 h-4 w-4" />
New {type}
</a>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<Card key={item._id}>
<CardHeader>
<CardTitle>{item.name || "Untitled"}</CardTitle>
<CardDescription>
{item.properties?.description || ""}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
Status: {item.status || "unknown"}
</span>
<Button asChild variant="outline" size="sm">
<a href={`/${type}s/${item._id}`}>View</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
```
- `{EntityName}` - PascalCase entity name (e.g., "Course", "Lesson")
- `{entity}` - Lowercase entity name (e.g., "course", "lesson")
- `{entities}` - Plural lowercase (e.g., "courses", "lessons")
## Usage
1. Copy template to `src/components/features/{EntityName}List.tsx`
2. Replace all `{EntityName}`, `{entity}`, `{entities}`
3. Import shadcn/ui components as needed
4. Customize card content for entity-specific fields
5. Add filtering/sorting UI if needed
```tsx
// src/components/features/CourseList.tsx
import { useEffectRunner } from "@/hooks/useEffectRunner";
import { ThingClientService } from "@/services/ThingClientService";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import { Effect } from "effect";
interface CourseListProps {
groupId?: string;
}
export function CourseList({ groupId }: CourseListProps) {
const { run, loading } = useEffectRunner();
const [courses, setCourses] = useState<any[]>([]);
useEffect(() => {
const program = Effect.gen(function* () {
const service = yield* ThingClientService;
return yield* service.list("course" as any, groupId);
});
run(program, {
onSuccess: (results) => setCourses(results || [])
});
}, [groupId]);
if (loading && courses.length === 0) {
return <div className="grid gap-4 md:grid-cols-3">
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-48" />)}
</div>;
}
return (
<div className="grid gap-4 md:grid-cols-3">
{courses.map((course) => (
<Card key={course._id}>
<CardHeader>
<div className="flex justify-between">
<CardTitle>{course.name}</CardTitle>
<Badge>{course.properties?.level}</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm">{course.properties?.description}</p>
<div className="mt-4 flex justify-between">
<span className="font-bold">${course.properties?.price}</span>
<Button asChild size="sm">
<a href={`/courses/${course._id}`}>View</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
);
}
```
Always handle three states:
1. **Loading** (`data === undefined`) → Show `Skeleton`
2. **Empty** (`data.length === 0`) → Show empty state with CTA
3. **Success** (`data.length > 0`) → Show data
- **Mistake:** Not handling loading state
- **Fix:** Check if `data === undefined` and show skeleton
- **Mistake:** Not handling empty state
- **Fix:** Show helpful message and CTA when no data
- **Mistake:** Not using shadcn/ui components
- **Fix:** Use Card, Button, etc. for consistency
- **Mistake:** Hardcoding href paths
- **Fix:** Use template literals with entity IDs
- **Mistake:** Assuming properties exist
- **Fix:** Use optional chaining (`properties?.field`)
## Related Patterns
- **form-template.md** - Forms for creating/editing
- **page-template.md** - Pages that use these components
- **shadcn/ui docs** - Component API reference