// This skill should be used when creating or modifying frontend dashboard components in the ClaudeCode Sentiment Monitor project. Specifically trigger this skill for tasks involving React 19 components, Next.js 15 App Router pages, shadcn/ui components, Recharts visualizations, SWR data fetching, or Tailwind CSS styling.
| name | frontend-components |
| description | This skill should be used when creating or modifying frontend dashboard components in the ClaudeCode Sentiment Monitor project. Specifically trigger this skill for tasks involving React 19 components, Next.js 15 App Router pages, shadcn/ui components, Recharts visualizations, SWR data fetching, or Tailwind CSS styling. |
Guide for developing dashboard components in the ClaudeCode Sentiment Monitor project using React 19, Next.js 15, shadcn/ui, Recharts, and Tailwind CSS.
Use this skill when:
app/ directoryUse the bundled template asset: assets/component-template.tsx
This provides the standard structure:
Copy and customize for new components.
"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>
);
}
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.
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>
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:
<ResponsiveContainer>hsl(var(--primary))date-fnsonClick for drill-down functionalitySee references/component-patterns.md for more chart patterns.
Standard color scheme (used across all components):
text-emerald-600 bg-emerald-50text-rose-600 bg-rose-50text-slate-600 bg-slate-50Sentiment 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>
);
}
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:
max-h-[80vh] overflow-y-auto for scrollable contentDashboardShell 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:
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:
hsl(var(--primary)), text-muted-foregroundspace-y-* and gap-* over marginsmd:grid-cols-2 lg:grid-cols-3Always 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);
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>
);
}
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
When creating/modifying a component:
Avoid these mistakes:
references/component-patterns.md in this skillSee existing implementations in app/components/dashboard/:
DashboardShell.tsx - State management and layoutSentimentChart.tsx - Line chart with drill-downVolumeChart.tsx - Bar chart with custom tooltipsDrillDownDialog.tsx - Modal with conditional fetching