feat(ui): Implement AuraUI Bento Grid Dashboard
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Added BentoGrid and BentoCard components for modular widget layout
- Refactored DashboardPage to use the new command center grid
- Improved visual hierarchy with large KPI cards and operational activity feeds
- Maintained responsive layout for mobile and desktop
This commit is contained in:
fullsizemalt 2025-12-19 18:24:52 -08:00
parent d51c5c162b
commit e1ea974b18
2 changed files with 216 additions and 152 deletions

View file

@ -0,0 +1,129 @@
import { ReactNode } from "react";
import { ArrowRight, LucideIcon } from "lucide-react";
import { Link } from "react-router-dom";
import { cn } from "../../lib/utils";
/**
* Bento Grid Components - Feature 14 from AuraUI
*
* A responsive, modular grid system for dashboard widgets.
*/
// --- Grid Container ---
export const BentoGrid = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"grid w-full auto-rows-[22rem] grid-cols-1 md:grid-cols-3 gap-4",
className
)}
>
{children}
</div>
);
};
// --- Grid Item (Card) ---
interface BentoCardProps {
name: string;
className: string;
background?: ReactNode;
Icon: LucideIcon;
description: string;
href: string;
cta?: string;
children?: ReactNode; // For custom content inside the card
value?: string | number; // For metric displays
}
export const BentoCard = ({
name,
className,
background,
Icon,
description,
href,
cta = "View Details",
children,
value
}: BentoCardProps) => {
return (
<div
key={name}
className={cn(
"group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl",
// Light mode styles
"bg-white border border-slate-200", // shadow-sm
// Dark mode styles
"dark:bg-slate-950 dark:border-slate-800",
className
)}
>
{/* Background Graphic (optional) */}
<div className="absolute inset-0 z-0 opacity-20 group-hover:opacity-40 transition-opacity duration-500">
{background}
</div>
{/* Content Area */}
<div className="relative z-10 p-6 flex flex-col h-full pointer-events-none">
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<div className="p-2 rounded-lg bg-slate-100 dark:bg-slate-900 group-hover:bg-cyan-50 dark:group-hover:bg-cyan-950/30 transition-colors duration-300">
<Icon className="h-5 w-5 text-slate-600 dark:text-slate-400 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors" />
</div>
<h3 className="font-semibold text-slate-900 dark:text-slate-50 text-lg">
{name}
</h3>
</div>
{/* Metric Value (if provided) - Large display */}
{value && (
<div className="mt-4 mb-2">
<span className="text-4xl font-bold text-slate-900 dark:text-white tracking-tight">
{value}
</span>
</div>
)}
{/* Description / Subtext */}
<p className="text-slate-500 dark:text-slate-400 text-sm leading-relaxed max-w-[90%]">
{description}
</p>
{/* Custom Children (Charts, Lists, etc) */}
{children && (
<div className="flex-1 mt-4 min-h-0 overflow-hidden">
{children}
</div>
)}
</div>
{/* Interactive Footer / CTA */}
<div
className={cn(
"pointer-events-auto",
"absolute bottom-0 flex w-full translate-y-10 flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100",
"bg-gradient-to-t from-white via-white to-transparent dark:from-slate-950 dark:via-slate-950"
)}
>
<Link
to={href}
className="flex items-center gap-2 text-sm font-medium text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300"
>
{cta}
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</div>
{/* Hover Effect Details */}
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
);
};

View file

@ -1,14 +1,13 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../context/ToastContext';
import { Plus, Leaf, ClipboardCheck, AlertTriangle, Activity, Droplet, Salad, Bug, Eye, ArrowRight } from 'lucide-react';
import { Leaf, ClipboardCheck, Activity, Droplet, Salad, Bug, Eye, CalendarDays, Zap } from 'lucide-react';
import TouchPointModal from '../components/touchpoints/TouchPointModal';
import TasksDueTodayWidget from '../components/tasks/TasksDueTodayWidget';
import { SmartAlerts } from '../components/dashboard/SmartAlerts';
import { touchPointsApi } from '../lib/touchPointsApi';
import { analyticsApi, AnalyticsOverview } from '../lib/analyticsApi';
import { PullToRefresh } from '../components/ui/PullToRefresh';
import { BentoGrid, BentoCard } from '../components/aura/BentoGrid';
export default function DashboardPage() {
const { user } = useAuth();
@ -17,7 +16,6 @@ export default function DashboardPage() {
const [recentActivity, setRecentActivity] = useState<any[]>([]);
const [overview, setOverview] = useState<AnalyticsOverview | null>(null);
const [loading, setLoading] = useState(true);
const [activityLoading, setActivityLoading] = useState(true);
useEffect(() => {
loadData();
@ -25,7 +23,6 @@ export default function DashboardPage() {
const loadData = async () => {
setLoading(true);
setActivityLoading(true);
try {
const [overviewData, activityData] = await Promise.all([
analyticsApi.getOverview(),
@ -38,7 +35,6 @@ export default function DashboardPage() {
addToast('Failed to load dashboard data', 'error');
} finally {
setLoading(false);
setActivityLoading(false);
}
};
@ -47,6 +43,10 @@ export default function DashboardPage() {
addToast('Activity logged successfully', 'success');
};
const handlePullRefresh = async () => {
await loadData();
};
const getActivityIcon = (type: string) => {
const iconClass = "transition-transform duration-fast";
switch (type) {
@ -58,176 +58,111 @@ export default function DashboardPage() {
}
};
const metrics = overview ? [
{ label: 'Active Batches', value: overview.activeBatches.toString(), icon: Leaf, accent: 'accent', link: '/batches' },
{ label: 'Total Plants', value: overview.totalPlants.toString(), icon: Leaf, accent: 'success', link: '/batches' },
{ label: 'Tasks Completed', value: overview.tasksCompletedThisWeek.toString(), icon: ClipboardCheck, accent: 'accent', link: '/tasks' },
{ label: 'Tasks Pending', value: overview.tasksPending.toString(), icon: AlertTriangle, accent: 'warning', link: '/tasks' },
{ label: 'Touch Points', value: overview.touchPointsToday.toString(), icon: Activity, accent: 'accent', link: '/touch-points' },
{ label: 'Rooms', value: overview.totalRooms.toString(), icon: Leaf, accent: 'neutral', link: '/rooms' },
] : [];
const handlePullRefresh = async () => {
await loadData();
};
const getAccentClasses = (accent: string) => {
switch (accent) {
case 'success': return 'bg-success-muted text-success';
case 'warning': return 'bg-warning-muted text-warning';
case 'destructive': return 'bg-destructive-muted text-destructive';
case 'accent': return 'bg-accent-muted text-accent';
default: return 'bg-tertiary text-secondary';
}
};
return (
<PullToRefresh onRefresh={handlePullRefresh} disabled={loading}>
<div className="space-y-6 pb-20 animate-in">
<div className="space-y-8 pb-20 animate-in fade-in duration-500">
{/* Header */}
<header className="flex justify-between items-start">
<header className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-2xl font-semibold text-primary tracking-tight">
Welcome back, {user?.name || user?.email?.split('@')[0]}
<h1 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">
Command Center
</h1>
<p className="text-secondary text-sm mt-1">
<p className="text-slate-500 dark:text-slate-400 mt-1">
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</p>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 bg-success-muted text-success rounded-md text-xs font-medium">
<div className="w-1.5 h-1.5 bg-success rounded-full animate-pulse" />
Online
<div className="flex items-center gap-3">
<button
onClick={() => setIsTouchModalOpen(true)}
className="bg-cyan-600 hover:bg-cyan-700 text-white px-4 py-2 rounded-lg font-medium shadow-lg shadow-cyan-500/20 transition-all flex items-center gap-2"
>
<Zap size={18} />
Log Action
</button>
</div>
</header>
{/* Smart Alerts */}
<SmartAlerts />
{/* Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{loading ? (
Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card p-4 space-y-3">
<div className="skeleton w-8 h-8 rounded-md" />
<div className="skeleton w-12 h-6" />
<div className="skeleton w-20 h-3" />
</div>
))
) : (
metrics.map((m, i) => (
<Link
key={i}
to={m.link}
className="card card-interactive p-4 group"
style={{ animationDelay: `${i * 50}ms` }}
>
<div className="flex items-start justify-between mb-3">
<div className={`w-8 h-8 rounded-md flex items-center justify-center ${getAccentClasses(m.accent)}`}>
<m.icon size={16} />
</div>
<ArrowRight size={12} className="text-tertiary opacity-0 group-hover:opacity-100 transition-opacity duration-fast" />
</div>
<p className="text-2xl font-semibold text-primary tracking-tight">
{m.value}
</p>
<p className="text-xs text-tertiary mt-1">
{m.label}
</p>
</Link>
))
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Tasks Widget */}
<TasksDueTodayWidget userId={user?.id || ''} />
{/* Recent Activity */}
<div className="card p-5">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-medium text-primary">Recent Activity</h3>
<button
onClick={() => setIsTouchModalOpen(true)}
className="btn btn-ghost text-xs h-8 px-3 gap-1 text-accent hover:text-accent"
>
<Plus size={14} />
Log
</button>
<BentoGrid>
{/* 1. Smart Alerts & Overview (Top Left - Large) */}
<BentoCard
name="System Status"
className="md:col-span-2"
Icon={Activity}
description="Real-time alerts and facility health monitoring."
href="/alerts"
cta="View All Alerts"
background={<div className="absolute right-0 top-0 h-full w-1/2 bg-gradient-to-l from-red-50 to-transparent dark:from-red-900/10 pointer-events-none" />}
>
<div className="mt-2">
<SmartAlerts />
</div>
</BentoCard>
{activityLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex gap-3 p-3 rounded-md">
<div className="skeleton w-8 h-8 rounded-full" />
<div className="flex-1 space-y-2">
<div className="skeleton w-3/4 h-4" />
<div className="skeleton w-1/2 h-3" />
</div>
</div>
))}
</div>
) : recentActivity.length === 0 ? (
<div className="text-center py-8">
<Activity size={32} className="mx-auto text-quaternary mb-3" />
<p className="text-tertiary text-sm">No recent activity</p>
<button
onClick={() => setIsTouchModalOpen(true)}
className="btn btn-secondary mt-4 h-9"
>
Log First Activity
<ArrowRight size={14} />
</button>
</div>
) : (
<div className="space-y-1">
{recentActivity.map((activity: any) => (
{/* 2. Active Batches (Top Right - Small) */}
<BentoCard
name="Active Batches"
className="md:col-span-1"
Icon={Leaf}
value={overview?.activeBatches.toString() || "0"}
description={`${overview?.totalPlants || 0} total plants in production.`}
href="/batches"
cta="Manage Batches"
background={<Leaf className="absolute -right-4 -top-4 text-emerald-100 dark:text-emerald-900/20 w-32 h-32 rotate-12" />}
/>
{/* 3. Tasks (Bottom Left - Small) */}
<BentoCard
name="Tasks Due"
className="md:col-span-1"
Icon={ClipboardCheck}
value={overview?.tasksPending.toString() || "0"}
description={`${overview?.tasksCompletedThisWeek || 0} completed this week.`}
href="/tasks"
cta="View Tasks"
background={<CalendarDays className="absolute -right-4 -bottom-4 text-blue-100 dark:text-blue-900/20 w-32 h-32 -rotate-12" />}
/>
{/* 4. Recent Activity (Bottom Right - Large) */}
<BentoCard
name="Recent Activity"
className="md:col-span-2"
Icon={Activity}
description="Latest operational touchpoints and logs."
href="/touch-points"
cta="View Full Log"
>
<div className="space-y-2 mt-2">
{loading ? (
<div className="space-y-4">
<div className="h-12 bg-slate-100 dark:bg-slate-800 rounded-lg animate-pulse" />
<div className="h-12 bg-slate-100 dark:bg-slate-800 rounded-lg animate-pulse" />
</div>
) : recentActivity.length === 0 ? (
<div className="text-center py-6 text-slate-400 text-sm">No recent activity logged.</div>
) : (
recentActivity.map((activity) => (
<div
key={activity.id}
className="flex items-start gap-3 p-3 rounded-md hover:bg-tertiary transition-colors duration-fast group"
className="flex items-center gap-3 p-3 rounded-xl bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800/50 group hover:border-cyan-200 dark:hover:border-cyan-800 transition-colors"
>
<div className={`
w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0
${activity.type === 'WATER' ? 'bg-blue-500/10' :
activity.type === 'FEED' ? 'bg-success-muted' :
activity.type === 'IPM' ? 'bg-destructive-muted' :
activity.type === 'INSPECT' ? 'bg-accent-muted' :
'bg-tertiary'
}
`}>
<div className="p-2 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
{getActivityIcon(activity.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-primary truncate">
<span className="font-medium">{activity.type}</span>
<span className="text-tertiary"> </span>
<p className="text-sm font-medium text-slate-900 dark:text-slate-200 truncate">
{activity.batch?.name || 'Unknown Batch'}
<span className="text-slate-400 font-normal"> {activity.type}</span>
</p>
<p className="text-xs text-tertiary mt-0.5">
{activity.user?.name || 'User'} · {new Date(activity.createdAt).toLocaleString()}
<p className="text-xs text-slate-500 truncate">
{activity.user?.name} · {new Date(activity.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
{activity.notes && (
<p className="text-xs text-secondary mt-1 truncate">
"{activity.notes}"
</p>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Floating Action Button (Mobile) */}
<button
onClick={() => setIsTouchModalOpen(true)}
className="md:hidden fixed bottom-20 right-4 w-12 h-12 btn-primary rounded-full shadow-lg flex items-center justify-center z-30"
style={{ backgroundColor: 'var(--color-accent)' }}
aria-label="Log Activity"
>
<Plus size={24} />
</button>
))
)}
</div>
</BentoCard>
</BentoGrid>
<TouchPointModal
isOpen={isTouchModalOpen}