feat: implement GradientStatCard component and apply Figtree font across the dashboard with responsive grid layout updates.
This commit is contained in:
parent
950cfe9f83
commit
d8d7e542d0
53
src/components/shared/GradientStatCard.tsx
Normal file
53
src/components/shared/GradientStatCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -43,3 +43,4 @@ export { ActiveOnlyToggle } from './ActiveOnlyToggle';
|
||||
export { SearchBox } from './SearchBox';
|
||||
export { FormTagInput } from './FormTagInput';
|
||||
export { MarkdownViewer } from './MarkdownViewer';
|
||||
export { GradientStatCard } from './GradientStatCard';
|
||||
|
||||
@ -2,12 +2,10 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
// Users,
|
||||
// TrendingUp,
|
||||
Package,
|
||||
Heart,
|
||||
} from "lucide-react";
|
||||
import { StatCard } from "./StatCard";
|
||||
import { GradientStatCard } from "@/components/shared";
|
||||
import type { StatCardData } from "@/types/dashboard";
|
||||
import { dashboardService } from "@/services/dashboard-service";
|
||||
|
||||
@ -29,7 +27,7 @@ export const StatsGrid = () => {
|
||||
icon: Building2,
|
||||
value: data.totalTenants,
|
||||
label: "Total Tenants",
|
||||
badge: { text: `${data.activeTenants} active`, variant: "green" },
|
||||
badge: { text: `${data.activeTenants} active`, variant: "success" },
|
||||
},
|
||||
{
|
||||
icon: CheckCircle2,
|
||||
@ -40,26 +38,14 @@ export const StatsGrid = () => {
|
||||
data.totalTenants > 0
|
||||
? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% 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,
|
||||
value: data.registeredModules,
|
||||
label: "Registered Modules",
|
||||
badge: { text: "Total", variant: "gray" },
|
||||
badge: { text: "Total", variant: "info" },
|
||||
},
|
||||
{
|
||||
icon: Heart,
|
||||
@ -73,8 +59,8 @@ export const StatsGrid = () => {
|
||||
variant:
|
||||
data.healthyModules === data.registeredModules &&
|
||||
data.registeredModules > 0
|
||||
? "green"
|
||||
: "gray",
|
||||
? "success"
|
||||
: "info",
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -93,15 +79,17 @@ export const StatsGrid = () => {
|
||||
|
||||
if (isLoading) {
|
||||
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">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<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: 4 }).map((_, index) => (
|
||||
<div
|
||||
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="h-8 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/3"></div>
|
||||
<div className="w-full h-full rounded-[7px] bg-white p-4 space-y-3">
|
||||
<div className="h-4 bg-gray-50 rounded w-1/4"></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>
|
||||
@ -117,9 +105,9 @@ export const StatsGrid = () => {
|
||||
}
|
||||
|
||||
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) => (
|
||||
<StatCard key={index} data={stat} />
|
||||
<GradientStatCard key={index} {...stat} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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 "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@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-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
@ -115,9 +116,12 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
border-color: var(--color-border);
|
||||
outline-color: var(--color-ring);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { Layout } from '@/components/layout/Layout';
|
||||
import { StatsGrid } from '@/features/dashboard/components/StatsGrid';
|
||||
import { RecentActivity } from '@/features/dashboard/components/RecentActivity';
|
||||
import { QuickActions } from '@/features/dashboard/components/QuickActions';
|
||||
import type { ReactElement } from "react";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import { StatsGrid } from "@/features/dashboard/components/StatsGrid";
|
||||
import { RecentActivity } from "@/features/dashboard/components/RecentActivity";
|
||||
import { QuickActions } from "@/features/dashboard/components/QuickActions";
|
||||
// import { SystemHealth } from '@/features/dashboard/components/SystemHealth';
|
||||
|
||||
const Dashboard = (): ReactElement => {
|
||||
@ -10,19 +10,22 @@ const Dashboard = (): ReactElement => {
|
||||
<Layout
|
||||
currentPage="Dashboard Overview"
|
||||
pageHeader={{
|
||||
title: 'Platform Overview',
|
||||
description: 'Monitor system health, tenant activity, and user metrics in real-time.',
|
||||
title: "Platform Overview",
|
||||
description:
|
||||
"Monitor system health, tenant activity, and user metrics in real-time.",
|
||||
}}
|
||||
>
|
||||
{/* Stats Grid */}
|
||||
<StatsGrid />
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-6 items-start mt-4 md:mt-6">
|
||||
<RecentActivity />
|
||||
<div className="flex flex-col gap-4 md:gap-6 w-full lg:w-[300px] lg:shrink-0">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6 pb-8">
|
||||
{/* Main Content Area (Left) */}
|
||||
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
|
||||
<RecentActivity />
|
||||
</div>
|
||||
|
||||
{/* Sidebar (Right) */}
|
||||
<div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-6">
|
||||
<QuickActions />
|
||||
{/* <SystemHealth /> */}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@ -16,9 +16,11 @@ import { workflowService } from "@/services/workflow-service";
|
||||
import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service";
|
||||
import type { WorkflowTask } from "@/types/workflow";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GradientStatCard } from "@/components/shared";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
||||
icon: LucideIcon;
|
||||
value: string | number;
|
||||
label: string;
|
||||
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 { primaryColor } = useAppTheme();
|
||||
const navigate = useNavigate();
|
||||
@ -309,12 +265,12 @@ const Dashboard = (): ReactElement => {
|
||||
{/* Main Content Area (Left) */}
|
||||
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
|
||||
{/* 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 ? (
|
||||
<div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div>
|
||||
) : statCards.length > 0 ? (
|
||||
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>
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { useEffect, useState, type ReactElement } from "react";
|
||||
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 type { AICostSummary } from "@/types/ai";
|
||||
import { showToast } from "@/utils/toast";
|
||||
@ -184,7 +189,11 @@ export const TenantAIDashboard = (): ReactElement => {
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 gap-4 select-none">
|
||||
{/* Card 1 */}
|
||||
<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]">
|
||||
<MessageSquare className="w-4 h-4 stroke-[1.8]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 select-none">
|
||||
<GradientStatCard
|
||||
icon={MessageSquare}
|
||||
value={costs?.summary?.total_completions || 0}
|
||||
label="Total Completions"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]">
|
||||
{costs?.summary?.total_completions || 0}
|
||||
</h4>
|
||||
<p className="text-[14px] leading-[20px] font-normal text-[#64748B]">
|
||||
Total Completions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GradientStatCard
|
||||
icon={Coins}
|
||||
value={formatTokens(costs?.summary?.total_tokens || 0)}
|
||||
label="Total Tokens"
|
||||
/>
|
||||
|
||||
{/* Card 2 */}
|
||||
<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]">
|
||||
<Coins className="w-4 h-4 stroke-[1.8]" />
|
||||
</div>
|
||||
<GradientStatCard
|
||||
icon={DollarSign}
|
||||
value={`$${(costs?.summary?.total_cost || 0).toFixed(2)}`}
|
||||
label="Total Cost (USD)"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h4 className="text-[18px] leading-[24px] font-semibold text-[#1E293B] tracking-[-0.02em]">
|
||||
{formatTokens(costs?.summary?.total_tokens || 0)}
|
||||
</h4>
|
||||
<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>
|
||||
<GradientStatCard
|
||||
icon={Timer}
|
||||
value={formatLatency(costs?.summary?.avg_latency_ms || 0)}
|
||||
label="Avg Latency (ms)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts and Tables */}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export interface StatCardData {
|
||||
icon: React.ComponentType<{ className?: string; strokeWidth?: number; color?: string; style?: React.CSSProperties }>;
|
||||
icon: LucideIcon;
|
||||
value: string | number;
|
||||
label: string;
|
||||
badge?: {
|
||||
text: string;
|
||||
variant: 'green' | 'gray';
|
||||
variant: 'success' | 'warning' | 'info' | 'error' | 'green' | 'gray';
|
||||
};
|
||||
}
|
||||
|
||||
@ -17,7 +19,7 @@ export interface ActivityLog {
|
||||
}
|
||||
|
||||
export interface QuickAction {
|
||||
icon: React.ComponentType<{ className?: string; strokeWidth?: number; color?: string; style?: React.CSSProperties }>;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import path from "path";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(),react()],
|
||||
plugins: [tailwindcss(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user