Files
app-padel/apps/web/components/dashboard/stat-card.tsx
Ivan 83fc48d7df feat(dashboard): add dashboard with statistics
- Add dashboard stats API endpoint with key metrics
- Add stat-card component for displaying metrics
- Add occupancy-chart component for court occupancy visualization
- Add recent-bookings component for today's bookings list
- Add quick-actions component for common admin actions
- Update dashboard page with full implementation

Stats include: today's bookings, revenue, occupancy rate,
active members, pending bookings, and upcoming tournaments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 07:31:23 +00:00

135 lines
3.8 KiB
TypeScript

"use client";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { ReactNode } from "react";
interface StatCardProps {
title: string;
value: string | number;
icon: ReactNode;
trend?: {
value: number;
isPositive: boolean;
};
color?: "primary" | "accent" | "green" | "blue" | "purple" | "orange";
}
const colorVariants = {
primary: {
bg: "bg-primary-50",
icon: "bg-primary-100 text-primary-600",
text: "text-primary-700",
},
accent: {
bg: "bg-accent-50",
icon: "bg-accent-100 text-accent-600",
text: "text-accent-700",
},
green: {
bg: "bg-green-50",
icon: "bg-green-100 text-green-600",
text: "text-green-700",
},
blue: {
bg: "bg-blue-50",
icon: "bg-blue-100 text-blue-600",
text: "text-blue-700",
},
purple: {
bg: "bg-purple-50",
icon: "bg-purple-100 text-purple-600",
text: "text-purple-700",
},
orange: {
bg: "bg-orange-50",
icon: "bg-orange-100 text-orange-600",
text: "text-orange-700",
},
};
export function StatCard({ title, value, icon, trend, color = "primary" }: StatCardProps) {
const colors = colorVariants[color];
return (
<Card className="hover:shadow-md transition-shadow">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-primary-500 mb-1">{title}</p>
<p className="text-3xl font-bold text-primary-800">{value}</p>
{trend && (
<div className="flex items-center mt-2">
<span
className={cn(
"text-sm font-medium flex items-center gap-1",
trend.isPositive ? "text-green-600" : "text-red-600"
)}
>
{trend.isPositive ? (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg>
) : (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
)}
{trend.isPositive ? "+" : ""}
{trend.value}%
</span>
<span className="text-xs text-primary-400 ml-1">vs ayer</span>
</div>
)}
</div>
<div
className={cn(
"flex-shrink-0 w-12 h-12 rounded-lg flex items-center justify-center",
colors.icon
)}
>
{icon}
</div>
</div>
</CardContent>
</Card>
);
}
// Loading skeleton for stat card
export function StatCardSkeleton() {
return (
<Card>
<CardContent className="p-6">
<div className="flex items-start justify-between animate-pulse">
<div className="flex-1">
<div className="h-4 bg-primary-100 rounded w-24 mb-2"></div>
<div className="h-8 bg-primary-100 rounded w-16"></div>
</div>
<div className="w-12 h-12 bg-primary-100 rounded-lg"></div>
</div>
</CardContent>
</Card>
);
}