BMAD-METHOD/examples/dashboard-component-code.md

21 KiB

Dashboard Component Code Implementation

This file contains the complete code implementation for the dashboard component example.

Initial Implementation

import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Users, Activity, DollarSign, BarChart, ArrowUp, ArrowDown, Bell, Settings } from 'lucide-react';

interface DashboardProps {
  metrics: {
    totalUsers: number;
    activeUsers: number;
    revenue: number;
    conversionRate: number;
  };
  recentActivity: Array<{
    id: string;
    user: {
      name: string;
      avatar?: string;
    };
    action: string;
    timestamp: string;
  }>;
  userData: {
    name: string;
    email: string;
    avatar?: string;
    role: string;
  };
}

export function Dashboard({ metrics, recentActivity, userData }: DashboardProps) {
  return (
    <div className="flex flex-col gap-6">
      {/* Header */}
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
        <div>
          <h1 className="text-2xl font-bold text-slate-900">Dashboard</h1>
          <p className="text-slate-500">Welcome back, {userData.name}</p>
        </div>
        <div className="flex items-center gap-2">
          <button className="p-2 rounded-full bg-white text-slate-700 hover:bg-slate-100">
            <Bell size={20} />
          </button>
          <button className="p-2 rounded-full bg-white text-slate-700 hover:bg-slate-100">
            <Settings size={20} />
          </button>
          <Avatar className="h-10 w-10">
            <AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.name} />
            <AvatarFallback className="bg-blue-100 text-blue-600">
              {userData.name.split(' ').map(n => n[0]).join('')}
            </AvatarFallback>
          </Avatar>
        </div>
      </div>

      {/* Metrics */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
        <MetricCard 
          title="Total Users" 
          value={metrics.totalUsers.toLocaleString()} 
          icon={<Users className="h-5 w-5" />}
          trend={5.2}
          color="blue"
        />
        <MetricCard 
          title="Active Users" 
          value={metrics.activeUsers.toLocaleString()} 
          icon={<Activity className="h-5 w-5" />}
          trend={2.4}
          color="emerald"
        />
        <MetricCard 
          title="Revenue" 
          value={`$${metrics.revenue.toLocaleString()}`} 
          icon={<DollarSign className="h-5 w-5" />}
          trend={8.7}
          color="blue"
        />
        <MetricCard 
          title="Conversion Rate" 
          value={`${metrics.conversionRate}%`} 
          icon={<BarChart className="h-5 w-5" />}
          trend={-1.5}
          color="slate"
        />
      </div>

      {/* Content Area */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Recent Activity */}
        <Card className="lg:col-span-2">
          <CardHeader>
            <CardTitle>Recent Activity</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="space-y-4">
              {recentActivity.map((activity) => (
                <div key={activity.id} className="flex items-start gap-4">
                  <Avatar className="h-8 w-8">
                    <AvatarImage src={activity.user.avatar || "/placeholder.svg"} alt={activity.user.name} />
                    <AvatarFallback className="bg-slate-100 text-slate-600">
                      {activity.user.name.split(' ').map(n => n[0]).join('')}
                    </AvatarFallback>
                  </Avatar>
                  <div className="flex-1">
                    <p className="text-sm font-medium">{activity.user.name}</p>
                    <p className="text-sm text-slate-500">{activity.action}</p>
                  </div>
                  <p className="text-xs text-slate-400">{activity.timestamp}</p>
                </div>
              ))}
            </div>
          </CardContent>
        </Card>

        {/* User Profile */}
        <Card>
          <CardHeader>
            <CardTitle>Your Profile</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="flex flex-col items-center gap-4">
              <Avatar className="h-20 w-20">
                <AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.name} />
                <AvatarFallback className="bg-blue-100 text-blue-600 text-xl">
                  {userData.name.split(' ').map(n => n[0]).join('')}
                </AvatarFallback>
              </Avatar>
              <div className="text-center">
                <h3 className="font-medium text-lg">{userData.name}</h3>
                <p className="text-slate-500">{userData.email}</p>
                <p className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded-full mt-2 inline-block">
                  {userData.role}
                </p>
              </div>
              <div className="w-full border-t border-slate-200 pt-4 mt-2">
                <button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-md">
                  Edit Profile
                </button>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

interface MetricCardProps {
  title: string;
  value: string;
  icon: React.ReactNode;
  trend: number;
  color: 'blue' | 'emerald' | 'slate';
}

function MetricCard({ title, value, icon, trend, color }: MetricCardProps) {
  const colorClasses = {
    blue: {
      bg: 'bg-blue-50',
      text: 'text-blue-600',
      icon: 'bg-blue-100 text-blue-600',
    },
    emerald: {
      bg: 'bg-emerald-50',
      text: 'text-emerald-600',
      icon: 'bg-emerald-100 text-emerald-600',
    },
    slate: {
      bg: 'bg-slate-50',
      text: 'text-slate-600',
      icon: 'bg-slate-100 text-slate-600',
    },
  };

  return (
    <Card className={`${colorClasses[color].bg} border-none`}>
      <CardContent className="p-6">
        <div className="flex justify-between items-start">
          <div>
            <p className="text-sm font-medium text-slate-500">{title}</p>
            <p className={`text-2xl font-bold ${colorClasses[color].text} mt-1`}>{value}</p>
          </div>
          <div className={`p-2 rounded-full ${colorClasses[color].icon}`}>
            {icon}
          </div>
        </div>
        <div className="mt-4 flex items-center">
          {trend >= 0 ? (
            <ArrowUp className="h-4 w-4 text-emerald-500" />
          ) : (
            <ArrowDown className="h-4 w-4 text-red-500" />
          )}
          <span className={trend >= 0 ? 'text-emerald-500' : 'text-red-500'}>
            {Math.abs(trend)}%
          </span>
          <span className="text-slate-400 text-xs ml-1">vs last month</span>
        </div>
      </CardContent>
    </Card>
  );
}

Final Refined Implementation

import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Users, Activity, DollarSign, BarChart, ArrowUp, ArrowDown, Bell, Settings, Calendar } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';

interface DashboardProps {
  metrics: {
    totalUsers: number;
    activeUsers: number;
    revenue: number;
    conversionRate: number;
  };
  recentActivity: Array<{
    id: string;
    user: {
      name: string;
      avatar?: string;
    };
    action: string;
    timestamp: string;
  }>;
  userData: {
    name: string;
    email: string;
    avatar?: string;
    role: string;
  };
  isLoading?: boolean;
}

export function Dashboard({ 
  metrics, 
  recentActivity, 
  userData,
  isLoading = false
}: DashboardProps) {
  const [dateRange, setDateRange] = useState('last-30-days');

  if (isLoading) {
    return <DashboardSkeleton />;
  }

  return (
    <div className="flex flex-col gap-6">
      {/* Header */}
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
        <div>
          <h1 className="text-2xl font-bold text-slate-900">Dashboard</h1>
          <p className="text-slate-500">Welcome back, {userData.name}</p>
        </div>
        <div className="flex items-center gap-2">
          <div className="flex items-center bg-white border border-slate-200 rounded-md px-3 py-1">
            <Calendar className="h-4 w-4 text-slate-500 mr-2" />
            <Select value={dateRange} onValueChange={setDateRange}>
              <SelectTrigger className="border-0 p-0 h-auto w-[140px] text-sm">
                <SelectValue placeholder="Select range" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="today">Today</SelectItem>
                <SelectItem value="yesterday">Yesterday</SelectItem>
                <SelectItem value="last-7-days">Last 7 days</SelectItem>
                <SelectItem value="last-30-days">Last 30 days</SelectItem>
                <SelectItem value="this-month">This month</SelectItem>
                <SelectItem value="last-month">Last month</SelectItem>
              </SelectContent>
            </Select>
          </div>
          <button 
            className="p-2 rounded-full bg-white text-slate-700 hover:bg-slate-100"
            aria-label="Notifications"
          >
            <Bell size={20} />
          </button>
          <button 
            className="p-2 rounded-full bg-white text-slate-700 hover:bg-slate-100"
            aria-label="Settings"
          >
            <Settings size={20} />
          </button>
          <Avatar className="h-10 w-10">
            <AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.name} />
            <AvatarFallback className="bg-blue-100 text-blue-600">
              {userData.name.split(' ').map(n => n[0]).join('')}
            </AvatarFallback>
          </Avatar>
        </div>
      </div>

      {/* Metrics */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" role="region" aria-label="Key metrics">
        <MetricCard 
          title="Total Users" 
          value={metrics.totalUsers.toLocaleString()} 
          icon={<Users className="h-5 w-5" />}
          trend={5.2}
          color="blue"
          ariaLabel="Total users metric"
        />
        <MetricCard 
          title="Active Users" 
          value={metrics.activeUsers.toLocaleString()} 
          icon={<Activity className="h-5 w-5" />}
          trend={2.4}
          color="emerald"
          ariaLabel="Active users metric"
        />
        <MetricCard 
          title="Revenue" 
          value={`$${metrics.revenue.toLocaleString()}`} 
          icon={<DollarSign className="h-5 w-5" />}
          trend={8.7}
          color="blue"
          ariaLabel="Revenue metric"
        />
        <MetricCard 
          title="Conversion Rate" 
          value={`${metrics.conversionRate}%`} 
          icon={<BarChart className="h-5 w-5" />}
          trend={-1.5}
          color="slate"
          ariaLabel="Conversion rate metric"
        />
      </div>

      {/* Content Area */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Recent Activity */}
        <Card className="lg:col-span-2">
          <CardHeader>
            <CardTitle>Recent Activity</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="space-y-4 max-h-[400px] overflow-y-auto pr-2" role="log" aria-label="Recent activity log">
              {recentActivity.length > 0 ? (
                recentActivity.map((activity) => (
                  <div key={activity.id} className="flex items-start gap-4">
                    <Avatar className="h-8 w-8">
                      <AvatarImage src={activity.user.avatar || "/placeholder.svg"} alt={activity.user.name} />
                      <AvatarFallback className="bg-slate-100 text-slate-600">
                        {activity.user.name.split(' ').map(n => n[0]).join('')}
                      </AvatarFallback>
                    </Avatar>
                    <div className="flex-1">
                      <p className="text-sm font-medium">{activity.user.name}</p>
                      <p className="text-sm text-slate-500">{activity.action}</p>
                    </div>
                    <p className="text-xs text-slate-400">{activity.timestamp}</p>
                  </div>
                ))
              ) : (
                <p className="text-center text-slate-500 py-6">No recent activity</p>
              )}
            </div>
          </CardContent>
        </Card>

        {/* User Profile */}
        <Card>
          <CardHeader>
            <CardTitle>Your Profile</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="flex flex-col items-center gap-4">
              <Avatar className="h-20 w-20">
                <AvatarImage src={userData.avatar || "/placeholder.svg"} alt={userData.name} />
                <AvatarFallback className="bg-blue-100 text-blue-600 text-xl">
                  {userData.name.split(' ').map(n => n[0]).join('')}
                </AvatarFallback>
              </Avatar>
              <div className="text-center">
                <h3 className="font-medium text-lg">{userData.name}</h3>
                <p className="text-slate-500">{userData.email}</p>
                <p className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded-full mt-2 inline-block">
                  {userData.role}
                </p>
              </div>
              <div className="w-full border-t border-slate-200 pt-4 mt-2">
                <button 
                  className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-md focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
                  aria-label="Edit your profile"
                >
                  Edit Profile
                </button>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

interface MetricCardProps {
  title: string;
  value: string;
  icon: React.ReactNode;
  trend: number;
  color: 'blue' | 'emerald' | 'slate';
  ariaLabel: string;
}

function MetricCard({ title, value, icon, trend, color, ariaLabel }: MetricCardProps) {
  const colorClasses = {
    blue: {
      bg: 'bg-blue-50',
      text: 'text-blue-600',
      icon: 'bg-blue-100 text-blue-600',
    },
    emerald: {
      bg: 'bg-emerald-50',
      text: 'text-emerald-600',
      icon: 'bg-emerald-100 text-emerald-600',
    },
    slate: {
      bg: 'bg-slate-50',
      text: 'text-slate-600',
      icon: 'bg-slate-100 text-slate-600',
    },
  };

  const trendText = `${Math.abs(trend)}% ${trend >= 0 ? 'increase' : 'decrease'} compared to last month`;

  return (
    <Card className={`${colorClasses[color].bg} border-none`} aria-label={ariaLabel}>
      <CardContent className="p-6">
        <div className="flex justify-between items-start">
          <div>
            <p className="text-sm font-medium text-slate-500">{title}</p>
            <p className={`text-2xl font-bold ${colorClasses[color].text} mt-1`}>{value}</p>
          </div>
          <div className={`p-2 rounded-full ${colorClasses[color].icon}`} aria-hidden="true">
            {icon}
          </div>
        </div>
        <div className="mt-4 flex items-center" aria-label={trendText}>
          {trend >= 0 ? (
            <ArrowUp className="h-4 w-4 text-emerald-500" aria-hidden="true" />
          ) : (
            <ArrowDown className="h-4 w-4 text-red-500" aria-hidden="true" />
          )}
          <span className={trend >= 0 ? 'text-emerald-500' : 'text-red-500'}>
            {Math.abs(trend)}%
          </span>
          <span className="text-slate-400 text-xs ml-1">vs last month</span>
        </div>
      </CardContent>
    </Card>
  );
}

function DashboardSkeleton() {
  return (
    <div className="flex flex-col gap-6">
      {/* Header Skeleton */}
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
        <div>
          <Skeleton className="h-8 w-48" />
          <Skeleton className="h-4 w-32 mt-2" />
        </div>
        <div className="flex items-center gap-2">
          <Skeleton className="h-10 w-32 rounded-md" />
          <Skeleton className="h-10 w-10 rounded-full" />
          <Skeleton className="h-10 w-10 rounded-full" />
          <Skeleton className="h-10 w-10 rounded-full" />
        </div>
      </div>

      {/* Metrics Skeleton */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
        {[1, 2, 3, 4].map((i) => (
          <Card key={i} className="bg-slate-50 border-none">
            <CardContent className="p-6">
              <div className="flex justify-between items-start">
                <div className="w-full">
                  <Skeleton className="h-4 w-24" />
                  <Skeleton className="h-8 w-20 mt-2" />
                  <Skeleton className="h-4 w-32 mt-4" />
                </div>
                <Skeleton className="h-10 w-10 rounded-full" />
              </div>
            </CardContent>
          </Card>
        ))}
      </div>

      {/* Content Area Skeleton */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <Card className="lg:col-span-2">
          <CardHeader>
            <Skeleton className="h-6 w-32" />
          </CardHeader>
          <CardContent>
            <div className="space-y-4">
              {[1, 2, 3, 4].map((i) => (
                <div key={i} className="flex items-start gap-4">
                  <Skeleton className="h-8 w-8 rounded-full" />
                  <div className="flex-1">
                    <Skeleton className="h-4 w-24" />
                    <Skeleton className="h-3 w-48 mt-2" />
                  </div>
                  <Skeleton className="h-3 w-16" />
                </div>
              ))}
            </div>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <Skeleton className="h-6 w-24" />
          </CardHeader>
          <CardContent>
            <div className="flex flex-col items-center gap-4">
              <Skeleton className="h-20 w-20 rounded-full" />
              <div className="text-center w-full">
                <Skeleton className="h-5 w-32 mx-auto" />
                <Skeleton className="h-4 w-48 mx-auto mt-2" />
                <Skeleton className="h-6 w-16 mx-auto mt-2 rounded-full" />
              </div>
              <div className="w-full border-t border-slate-200 pt-4 mt-2">
                <Skeleton className="h-10 w-full rounded-md" />
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

Example Usage

// Example usage
import { Dashboard } from './components/Dashboard';

export default function DashboardPage() {
  const [isLoading, setIsLoading] = useState(true);
  const [dashboardData, setDashboardData] = useState({
    metrics: {
      totalUsers: 0,
      activeUsers: 0,
      revenue: 0,
      conversionRate: 0
    },
    recentActivity: [],
    userData: {
      name: '',
      email: '',
      role: ''
    }
  });

  useEffect(() => {
    // Simulate API call
    setTimeout(() => {
      setDashboardData({
        metrics: {
          totalUsers: 12487,
          activeUsers: 8761,
          revenue: 48395,
          conversionRate: 12.8
        },
        recentActivity: [
          {
            id: '1',
            user: {
              name: 'John Smith',
              avatar: '/avatars/john.jpg'
            },
            action: 'Created a new project "Q4 Marketing Campaign"',
            timestamp: '2 minutes ago'
          },
          {
            id: '2',
            user: {
              name: 'Sarah Johnson',
              avatar: '/avatars/sarah.jpg'
            },
            action: 'Updated the analytics dashboard settings',
            timestamp: '1 hour ago'
          },
          {
            id: '3',
            user: {
              name: 'Michael Brown',
              avatar: '/avatars/michael.jpg'
            },
            action: 'Invited 3 new team members',
            timestamp: '3 hours ago'
          },
          {
            id: '4',
            user: {
              name: 'Emily Davis',
              avatar: '/avatars/emily.jpg'
            },
            action: 'Completed the quarterly report',
            timestamp: 'Yesterday'
          }
        ],
        userData: {
          name: 'Alex Morgan',
          email: 'alex.morgan@example.com',
          avatar: '/avatars/alex.jpg',
          role: 'Admin'
        }
      });
      setIsLoading(false);
    }, 1500);
  }, []);

  return (
    <div className="container mx-auto px-4 py-6">
      <Dashboard 
        metrics={dashboardData.metrics}
        recentActivity={dashboardData.recentActivity}
        userData={dashboardData.userData}
        isLoading={isLoading}
      />
    </div>
  );
}