Frontend Components Development
Overview
Guide for developing dashboard components in the ClaudeCode Sentiment Monitor project using React 19, Next.js 15, shadcn/ui, Recharts, and Tailwind CSS.
When to Use This Skill
Use this skill when:
- •Creating new dashboard components
- •Modifying existing components
- •Adding charts or visualizations
- •Implementing data fetching with SWR
- •Styling components with Tailwind CSS
- •Working with shadcn/ui components
- •Adding modals or dialogs
- •Debugging frontend issues
Tech Stack
- •Framework: Next.js 15 (App Router), React 19, TypeScript 5
- •UI Library: shadcn/ui with Tailwind CSS 4
- •Charts: Recharts 3
- •Data Fetching: SWR 2.3.6 (30-second refresh intervals)
- •Working Directory: All component development in
app/directory
Quick Start
Component Template
Use the bundled template asset: assets/component-template.tsx
This provides the standard structure:
- •"use client" directive
- •State hooks
- •SWR data fetching
- •Loading state
- •Error state
- •Empty state
- •Main render with Card layout
Copy and customize for new components.
Component Structure
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((res) => res.json());
interface ComponentNameProps {
timeRange: "7d" | "30d" | "90d";
subreddit: string;
}
export function ComponentName({ timeRange, subreddit }: ComponentNameProps) {
// 1. Hooks first
const { data, error, isLoading } = useSWR(
`/api/endpoint?range=${timeRange}&subreddit=${subreddit}`,
fetcher,
{ refreshInterval: 30000 }
);
// 2. Early returns for states
if (isLoading) return <LoadingState />;
if (error) return <ErrorState />;
if (!data) return null;
// 3. Render
return (
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>{/* Content */}</CardContent>
</Card>
);
}
Data Fetching with SWR
Standard pattern:
const { data, error, isLoading } = useSWR("/api/endpoint", fetcher, {
refreshInterval: 30000, // 30-second refresh
revalidateOnFocus: true,
});
Conditional fetching (dialogs/modals):
const { data, error, isLoading } = useSWR(
isOpen ? `/api/endpoint?param=${value}` : null,
fetcher
);
Type-safe fetching:
interface DataType {
field: string;
}
const { data } = useSWR<DataType>("/api/endpoint", fetcher);
See references/component-patterns.md for more SWR patterns.
Charts with Recharts
Line Chart (Sentiment)
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from "recharts";
import { format } from "date-fns";
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data} onClick={(e) => e && onDateClick(e.activeLabel)}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"
tickFormatter={(date) => format(new Date(date), "MMM d")}
/>
<YAxis domain={[-1, 1]} />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine y={0} stroke="#888" strokeDasharray="3 3" />
<Line
type="monotone"
dataKey="sentiment"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ r: 4 }}
/>
</LineChart>
</ResponsiveContainer>
Bar Chart (Volume)
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" tickFormatter={(date) => format(new Date(date), "MMM d")} />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="totalCount" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
Chart best practices:
- •Always wrap in
<ResponsiveContainer> - •Use Tailwind CSS variables for colors:
hsl(var(--primary)) - •Format dates with
date-fns - •Create custom tooltip components
- •Use
onClickfor drill-down functionality
See references/component-patterns.md for more chart patterns.
Sentiment Colors
Standard color scheme (used across all components):
- •Positive (> 0.2):
text-emerald-600 bg-emerald-50 - •Negative (< -0.2):
text-rose-600 bg-rose-50 - •Neutral (-0.2 to 0.2):
text-slate-600 bg-slate-50
Sentiment Badge Component:
function getSentimentColor(sentiment: number): string {
if (sentiment > 0.2) return "text-emerald-600 bg-emerald-50";
if (sentiment < -0.2) return "text-rose-600 bg-rose-50";
return "text-slate-600 bg-slate-50";
}
function SentimentBadge({ sentiment }: { sentiment: number }) {
const colorClass = getSentimentColor(sentiment);
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{sentiment > 0 ? "+" : ""}{sentiment.toFixed(2)}
</span>
);
}
Dialogs and Modals
Use shadcn/ui Dialog with conditional SWR fetching:
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
{isLoading && <p>Loading...</p>}
{error && <p className="text-destructive">Error</p>}
{data && <div className="space-y-4">{/* Content */}</div>}
</DialogContent>
</Dialog>
Best practices:
- •Use conditional fetching (only fetch when open)
- •Add
max-h-[80vh] overflow-y-autofor scrollable content - •Handle loading/error states within dialog
State Management
DashboardShell pattern (centralized state):
export function DashboardShell() {
const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("7d");
const [subreddit, setSubreddit] = useState<string>("all");
const [drillDownDate, setDrillDownDate] = useState<string | null>(null);
return (
<div>
<SentimentChart
timeRange={timeRange}
subreddit={subreddit}
onDateClick={setDrillDownDate}
/>
<DrillDownDialog
open={!!drillDownDate}
onOpenChange={(open) => !open && setDrillDownDate(null)}
date={drillDownDate}
subreddit={subreddit}
/>
</div>
);
}
Rules:
- •Keep state in nearest common ancestor (DashboardShell)
- •Pass state down as props (no context for small apps)
- •Use callback props for child-to-parent communication
- •Avoid prop drilling beyond 2-3 levels
Styling with Tailwind CSS
Common patterns:
// Spacing className="space-y-4" // Vertical spacing className="flex gap-4" // Horizontal spacing className="grid grid-cols-2 gap-4" // Grid layout // Containers className="max-w-4xl mx-auto" // Centered className="max-h-[80vh] overflow-y-auto" // Scrollable // Typography className="text-sm text-muted-foreground" // Secondary text className="text-lg font-semibold" // Heading
Best practices:
- •Use shadcn/ui CSS variables:
hsl(var(--primary)),text-muted-foreground - •Prefer
space-y-*andgap-*over margins - •Use responsive classes:
md:grid-cols-2 lg:grid-cols-3
TypeScript Best Practices
Always use explicit types:
// Component props
interface ChartProps {
timeRange: "7d" | "30d" | "90d";
subreddit: "all" | "ClaudeAI" | "ClaudeCode" | "Anthropic";
onDateClick: (date: string) => void;
}
// API responses
interface DashboardData {
dates: string[];
sentiment: number[];
}
// Type SWR responses
const { data } = useSWR<DashboardData>("/api/endpoint", fetcher);
Common UI Patterns
CSV Export Button:
import { Button } from "@/components/ui/button";
import { Download } from "lucide-react";
<Button onClick={() => window.open(`/api/export/csv?range=${timeRange}`, "_blank")} variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export CSV
</Button>
Loading Skeleton:
import { Skeleton } from "@/components/ui/skeleton";
if (isLoading) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
<CardContent>
<Skeleton className="h-[300px] w-full" />
</CardContent>
</Card>
);
}
Error Alert:
import { AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
if (error) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Failed to load data. Please try again.</AlertDescription>
</Alert>
);
}
Testing Components
cd app npm run dev # Start dev server on http://localhost:3000 # In browser DevTools: # - Check Network tab for API calls # - Verify SWR caching behavior # - Test responsive design # - Check for console errors
Component Checklist
When creating/modifying a component:
- • Explicit TypeScript interface for props
- • SWR used for data fetching (not useEffect + fetch)
- • Loading, error, and empty states handled
- • Sentiment colors follow standard scheme
- • Charts use ResponsiveContainer and custom tooltips
- • Dates formatted with date-fns
- • Tailwind classes use shadcn/ui CSS variables
- • Exported as named export (not default)
- • "use client" directive if using hooks
Common Pitfalls
Avoid these mistakes:
- •useEffect for data fetching - Use SWR instead
- •Hardcoded colors - Use Tailwind CSS variables
- •Missing loading states - Users need feedback
- •Mutating props - Components should be pure
- •Skipping TypeScript types - Explicit types prevent errors
- •Deep nesting - Extract to separate files if > 100 lines
- •Inline styles - Use Tailwind classes exclusively
Resources
- •shadcn/ui docs: https://ui.shadcn.com/
- •Recharts docs: https://recharts.org/
- •SWR docs: https://swr.vercel.app/
- •Tailwind CSS docs: https://tailwindcss.com/
- •date-fns docs: https://date-fns.org/
- •Component Patterns: See
references/component-patterns.mdin this skill
Examples
See existing implementations in app/components/dashboard/:
- •
DashboardShell.tsx- State management and layout - •
SentimentChart.tsx- Line chart with drill-down - •
VolumeChart.tsx- Bar chart with custom tooltips - •
DrillDownDialog.tsx- Modal with conditional fetching