feat: implement GradientStatCard component and apply Figtree font across the dashboard with responsive grid layout updates.

This commit is contained in:
Yashwin 2026-05-07 17:39:34 +05:30
parent 950cfe9f83
commit d8d7e542d0
9 changed files with 136 additions and 168 deletions

View File

@ -0,0 +1,53 @@
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface GradientStatCardProps {
icon: LucideIcon;
value: string | number;
label: string;
badge?: {
text: string;
variant: 'success' | 'warning' | 'info' | 'error' | 'green' | 'gray';
};
}
export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon, value, label, badge }) => {
return (
<div
className="rounded-[8px] p-[1px] h-full"
style={{
background: 'var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))',
}}
>
<div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-[#F8FAFC]">
<div className="flex items-start justify-between w-full">
<div className="text-[#94A3B8]">
<Icon className="w-5 h-5 stroke-[1.8]" />
</div>
{badge && (
<div className={cn(
"px-2.5 py-1 rounded-full text-[10px] font-bold tracking-tight whitespace-nowrap",
(badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" :
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
badge.variant === 'error' ? "bg-[#fdf5f4] text-[#e0352a]" :
"bg-[#f3f4f6] text-[#6b7280]" // default / gray
)}>
{badge.text}
</div>
)}
</div>
<div>
<h4 className="text-[28px] leading-none font-bold text-[#1E293B] tracking-[-0.02em]">
{value}
</h4>
<p className="text-[11px] font-semibold text-[#64748B] uppercase tracking-wider mt-1">
{label}
</p>
</div>
</div>
</div>
);
};

View File

@ -43,3 +43,4 @@ export { ActiveOnlyToggle } from './ActiveOnlyToggle';
export { SearchBox } from './SearchBox'; export { SearchBox } from './SearchBox';
export { FormTagInput } from './FormTagInput'; export { FormTagInput } from './FormTagInput';
export { MarkdownViewer } from './MarkdownViewer'; export { MarkdownViewer } from './MarkdownViewer';
export { GradientStatCard } from './GradientStatCard';

View File

@ -2,12 +2,10 @@ import { useState, useEffect } from "react";
import { import {
Building2, Building2,
CheckCircle2, CheckCircle2,
// Users,
// TrendingUp,
Package, Package,
Heart, Heart,
} from "lucide-react"; } from "lucide-react";
import { StatCard } from "./StatCard"; import { GradientStatCard } from "@/components/shared";
import type { StatCardData } from "@/types/dashboard"; import type { StatCardData } from "@/types/dashboard";
import { dashboardService } from "@/services/dashboard-service"; import { dashboardService } from "@/services/dashboard-service";
@ -29,7 +27,7 @@ export const StatsGrid = () => {
icon: Building2, icon: Building2,
value: data.totalTenants, value: data.totalTenants,
label: "Total Tenants", label: "Total Tenants",
badge: { text: `${data.activeTenants} active`, variant: "green" }, badge: { text: `${data.activeTenants} active`, variant: "success" },
}, },
{ {
icon: CheckCircle2, icon: CheckCircle2,
@ -40,26 +38,14 @@ export const StatsGrid = () => {
data.totalTenants > 0 data.totalTenants > 0
? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% Rate` ? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% Rate`
: "0% Rate", : "0% Rate",
variant: "green", variant: "success",
}, },
}, },
// {
// icon: Users,
// value: data.totalUsers,
// label: "Total Users",
// badge: { text: "All users", variant: "gray" },
// },
// {
// icon: TrendingUp,
// value: data.activeSessions,
// label: "Active Sessions",
// badge: { text: "Live now", variant: "gray" },
// },
{ {
icon: Package, icon: Package,
value: data.registeredModules, value: data.registeredModules,
label: "Registered Modules", label: "Registered Modules",
badge: { text: "Total", variant: "gray" }, badge: { text: "Total", variant: "info" },
}, },
{ {
icon: Heart, icon: Heart,
@ -73,8 +59,8 @@ export const StatsGrid = () => {
variant: variant:
data.healthyModules === data.registeredModules && data.healthyModules === data.registeredModules &&
data.registeredModules > 0 data.registeredModules > 0
? "green" ? "success"
: "gray", : "info",
}, },
}, },
]; ];
@ -93,15 +79,17 @@ export const StatsGrid = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 mb-4 md:mb-6 auto-rows-fr"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 mb-4 md:mb-6 auto-rows-fr">
{Array.from({ length: 6 }).map((_, index) => ( {Array.from({ length: 4 }).map((_, index) => (
<div <div
key={index} key={index}
className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-[17px] flex flex-col gap-3 h-[107px] animate-pulse" className="rounded-[8px] p-[1px] h-[108px] bg-gray-100 animate-pulse"
> >
<div className="h-4 bg-gray-200 rounded w-1/4"></div> <div className="w-full h-full rounded-[7px] bg-white p-4 space-y-3">
<div className="h-8 bg-gray-200 rounded w-1/2"></div> <div className="h-4 bg-gray-50 rounded w-1/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/3"></div> <div className="h-8 bg-gray-50 rounded w-1/2"></div>
<div className="h-3 bg-gray-50 rounded w-1/3"></div>
</div>
</div> </div>
))} ))}
</div> </div>
@ -117,9 +105,9 @@ export const StatsGrid = () => {
} }
return ( return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 mb-4 md:mb-6 auto-rows-fr"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 mb-4 md:mb-6 auto-rows-fr">
{statsData.map((stat, index) => ( {statsData.map((stat, index) => (
<StatCard key={index} data={stat} /> <GradientStatCard key={index} {...stat} />
))} ))}
</div> </div>
); );

View File

@ -1,9 +1,10 @@
@import url('https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap');
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--font-sans: "Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
@ -115,9 +116,12 @@
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; border-color: var(--color-border);
outline-color: var(--color-ring);
} }
body { body {
@apply bg-background text-foreground; background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
} }
} }

View File

@ -1,8 +1,8 @@
import type { ReactElement } from 'react'; import type { ReactElement } from "react";
import { Layout } from '@/components/layout/Layout'; import { Layout } from "@/components/layout/Layout";
import { StatsGrid } from '@/features/dashboard/components/StatsGrid'; import { StatsGrid } from "@/features/dashboard/components/StatsGrid";
import { RecentActivity } from '@/features/dashboard/components/RecentActivity'; import { RecentActivity } from "@/features/dashboard/components/RecentActivity";
import { QuickActions } from '@/features/dashboard/components/QuickActions'; import { QuickActions } from "@/features/dashboard/components/QuickActions";
// import { SystemHealth } from '@/features/dashboard/components/SystemHealth'; // import { SystemHealth } from '@/features/dashboard/components/SystemHealth';
const Dashboard = (): ReactElement => { const Dashboard = (): ReactElement => {
@ -10,19 +10,22 @@ const Dashboard = (): ReactElement => {
<Layout <Layout
currentPage="Dashboard Overview" currentPage="Dashboard Overview"
pageHeader={{ pageHeader={{
title: 'Platform Overview', title: "Platform Overview",
description: 'Monitor system health, tenant activity, and user metrics in real-time.', description:
"Monitor system health, tenant activity, and user metrics in real-time.",
}} }}
> >
{/* Stats Grid */}
<StatsGrid /> <StatsGrid />
{/* Bottom Section */} <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6 pb-8">
<div className="flex flex-col lg:flex-row gap-4 md:gap-6 items-start mt-4 md:mt-6"> {/* Main Content Area (Left) */}
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
<RecentActivity /> <RecentActivity />
<div className="flex flex-col gap-4 md:gap-6 w-full lg:w-[300px] lg:shrink-0"> </div>
{/* Sidebar (Right) */}
<div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-6">
<QuickActions /> <QuickActions />
{/* <SystemHealth /> */}
</div> </div>
</div> </div>
</Layout> </Layout>

View File

@ -16,9 +16,11 @@ import { workflowService } from "@/services/workflow-service";
import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service"; import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service";
import type { WorkflowTask } from "@/types/workflow"; import type { WorkflowTask } from "@/types/workflow";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { GradientStatCard } from "@/components/shared";
import type { LucideIcon } from "lucide-react";
interface StatCardProps { interface StatCardProps {
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; icon: LucideIcon;
value: string | number; value: string | number;
label: string; label: string;
badge?: { badge?: {
@ -27,52 +29,6 @@ interface StatCardProps {
}; };
} }
const StatCard = ({
icon: Icon,
value,
label,
badge
}: StatCardProps): ReactElement => {
const { primaryColor } = useAppTheme();
return (
<div className="relative group h-full">
<div className="bg-white border border-[#e5e7eb] rounded-xl p-4.5 flex flex-col gap-4 shadow-sm hover:shadow-md transition-all h-full relative overflow-hidden">
{/* Interaction Gradient */}
<div
className="absolute top-0 left-0 w-full h-[2px] opacity-0 group-hover:opacity-100 transition-opacity"
style={{ background: `linear-gradient(to right, ${primaryColor}, #75c044, #fed314)` }}
/>
<div className="flex items-start justify-between">
<div className="w-10 h-10 rounded-md bg-gray-50 flex items-center justify-center border border-gray-100">
<Icon className="w-5 h-5 text-[#374151]" strokeWidth={1.5} />
</div>
{badge && (
<div className={cn(
"px-2.5 py-1 rounded-full text-[10px] font-bold tracking-tight whitespace-nowrap",
badge.variant === 'success' ? "bg-[#f1fffb] text-[#16c784]" :
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
"bg-[#fdf5f4] text-[#e0352a]"
)}>
{badge.text}
</div>
)}
</div>
<div className="flex flex-col gap-0.5">
<div className="text-[28px] font-bold tracking-tight text-[#111827] leading-none">
{value}
</div>
<div className="text-[11px] font-semibold text-[#6b7280] uppercase tracking-wider mt-1">
{label}
</div>
</div>
</div>
</div>
);
};
const TaskCard = ({ task }: { task: WorkflowTask }) => { const TaskCard = ({ task }: { task: WorkflowTask }) => {
// const { primaryColor } = useAppTheme(); // const { primaryColor } = useAppTheme();
const navigate = useNavigate(); const navigate = useNavigate();
@ -309,12 +265,12 @@ const Dashboard = (): ReactElement => {
{/* Main Content Area (Left) */} {/* Main Content Area (Left) */}
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6"> <div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4">
{loading ? ( {loading ? (
<div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div> <div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div>
) : statCards.length > 0 ? ( ) : statCards.length > 0 ? (
statCards.map((card, index) => ( statCards.map((card, index) => (
<StatCard key={index} {...card} /> <GradientStatCard key={index} {...card} />
)) ))
) : ( ) : (
<div className="col-span-full text-center py-8 text-gray-400 text-sm">No statistics available</div> <div className="col-span-full text-center py-8 text-gray-400 text-sm">No statistics available</div>

View File

@ -1,6 +1,11 @@
import { useEffect, useState, type ReactElement } from "react"; import { useEffect, useState, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { DataTable, type Column, FilterDropdown } from "@/components/shared"; import {
DataTable,
type Column,
FilterDropdown,
GradientStatCard,
} from "@/components/shared";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import type { AICostSummary } from "@/types/ai"; import type { AICostSummary } from "@/types/ai";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -184,7 +189,11 @@ export const TenantAIDashboard = (): ReactElement => {
<input <input
type="text" type="text"
readOnly readOnly
value={startDate && endDate ? `${formatDate(startDate)} - ${formatDate(endDate)}` : "All time"} value={
startDate && endDate
? `${formatDate(startDate)} - ${formatDate(endDate)}`
: "All time"
}
className="w-full sm:w-[220px] h-11 px-4 pr-10 rounded-[8px] border border-[#D1D5DB] bg-white text-[14px] font-medium text-[#334155] outline-none cursor-pointer" className="w-full sm:w-[220px] h-11 px-4 pr-10 rounded-[8px] border border-[#D1D5DB] bg-white text-[14px] font-medium text-[#334155] outline-none cursor-pointer"
/> />
<Calendar className="w-4 h-4 text-[#475569] absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none" /> <Calendar className="w-4 h-4 text-[#475569] absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none" />
@ -209,78 +218,30 @@ export const TenantAIDashboard = (): ReactElement => {
</div> </div>
{/* Stats Row */} {/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 gap-4 select-none"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 select-none">
{/* Card 1 */} <GradientStatCard
<div className="rounded-[8px] bg-gradient-to-r from-[#3B82F6] via-[#22C55E] to-[#EAB308] p-[1px]"> icon={MessageSquare}
<div className="flex flex-col items-start gap-3 px-4 py-3 min-h-[108px] rounded-[7px] bg-[#F8FAFC]"> value={costs?.summary?.total_completions || 0}
<div className="text-[#94A3B8]"> label="Total Completions"
<MessageSquare className="w-4 h-4 stroke-[1.8]" /> />
</div>
<div> <GradientStatCard
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]"> icon={Coins}
{costs?.summary?.total_completions || 0} value={formatTokens(costs?.summary?.total_tokens || 0)}
</h4> label="Total Tokens"
<p className="text-[14px] leading-[20px] font-normal text-[#64748B]"> />
Total Completions
</p>
</div>
</div>
</div>
{/* Card 2 */} <GradientStatCard
<div className="rounded-[8px] bg-gradient-to-r from-[#3B82F6] via-[#22C55E] to-[#EAB308] p-[1px]"> icon={DollarSign}
<div className="flex flex-col items-start gap-3 px-4 py-3 min-h-[108px] rounded-[7px] bg-[#F8FAFC]"> value={`$${(costs?.summary?.total_cost || 0).toFixed(2)}`}
<div className="text-[#94A3B8]"> label="Total Cost (USD)"
<Coins className="w-4 h-4 stroke-[1.8]" /> />
</div>
<div> <GradientStatCard
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]"> icon={Timer}
{formatTokens(costs?.summary?.total_tokens || 0)} value={formatLatency(costs?.summary?.avg_latency_ms || 0)}
</h4> label="Avg Latency (ms)"
<p className="text-[14px] leading-[20px] font-normal text-[#64748B]"> />
Total Tokens
</p>
</div>
</div>
</div>
{/* Card 3 */}
<div className="rounded-[8px] bg-gradient-to-r from-[#3B82F6] via-[#22C55E] to-[#EAB308] p-[1px]">
<div className="flex flex-col items-start gap-3 px-4 py-3 min-h-[108px] rounded-[7px] bg-[#F8FAFC]">
<div className="text-[#94A3B8]">
<DollarSign className="w-4 h-4 stroke-[1.8]" />
</div>
<div>
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]">
${(costs?.summary?.total_cost || 0).toFixed(2)}
</h4>
<p className="text-[14px] leading-[20px] font-normal text-[#64748B]">
Total Cost (USD)
</p>
</div>
</div>
</div>
{/* Card 4 */}
<div className="rounded-[8px] bg-gradient-to-r from-[#3B82F6] via-[#22C55E] to-[#EAB308] p-[1px]">
<div className="flex flex-col items-start gap-3 px-4 py-3 min-h-[108px] rounded-[7px] bg-[#F8FAFC]">
<div className="text-[#94A3B8]">
<Timer className="w-4 h-4 stroke-[1.8]" />
</div>
<div>
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]">
{formatLatency(costs?.summary?.avg_latency_ms || 0)}
</h4>
<p className="text-[14px] leading-[20px] font-normal text-[#64748B]">
Avg Latency (ms)
</p>
</div>
</div>
</div>
</div> </div>
{/* Charts and Tables */} {/* Charts and Tables */}

View File

@ -1,10 +1,12 @@
import type { LucideIcon } from "lucide-react";
export interface StatCardData { export interface StatCardData {
icon: React.ComponentType<{ className?: string; strokeWidth?: number; color?: string; style?: React.CSSProperties }>; icon: LucideIcon;
value: string | number; value: string | number;
label: string; label: string;
badge?: { badge?: {
text: string; text: string;
variant: 'green' | 'gray'; variant: 'success' | 'warning' | 'info' | 'error' | 'green' | 'gray';
}; };
} }
@ -17,7 +19,7 @@ export interface ActivityLog {
} }
export interface QuickAction { export interface QuickAction {
icon: React.ComponentType<{ className?: string; strokeWidth?: number; color?: string; style?: React.CSSProperties }>; icon: LucideIcon;
label: string; label: string;
onClick: () => void; onClick: () => void;
} }