feat: Multi-role platform expansion, mobile nav redesign, and UI polish
- Add Owner, Contractor, Vendor, Subcontractor dashboards and role-based routing - Owner now has superuser access to all Admin pages (dashboard, schedule, leaderboard) - Redesign landing page mobile menu as slide-in sidebar (replaces broken Framer Motion overlay) - Add body scroll lock to app sidebar for mobile consistency - Fix Team Schedule to match Admin Dashboard design language (ambient glows, gradient header, SpotlightCard, zinc palette) - Fix login page tab overflow — use abbreviated labels and grid layout for 6 role tabs - Fix Recharts ResponsiveContainer warnings — replace height="100%" with fixed pixel heights - Fix lightbox image viewer — unified nav bar, touch swipe support, no more overlapping text - Add AI Assistant page, People/Vendor/Document management for Owner role - Expand mock data store with contractor, vendor, and subcontractor data
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { SpotlightCard } from './SpotlightCard';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
export const StatCard = ({ label, value, suffix = '', icon: Icon, trend, trendLabel, color = 'blue', delay = 0 }) => {
|
||||
const counterRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!counterRef.current) return;
|
||||
|
||||
// Clean value string if it contains non-numeric chars (e.g., "$5,000") to get raw number
|
||||
// But for GSAP to animate "5,000" to "10,000" gracefully, best to animate a proxy object
|
||||
// and format on update.
|
||||
|
||||
let targetValue = 0;
|
||||
let prefix = '';
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// Check for prefix like $
|
||||
if (value.startsWith('$')) prefix = '$';
|
||||
// Parse number
|
||||
targetValue = parseFloat(value.replace(/[^0-9.-]+/g, ""));
|
||||
} else {
|
||||
targetValue = value;
|
||||
}
|
||||
|
||||
const counter = { val: 0 };
|
||||
const el = counterRef.current;
|
||||
|
||||
// Initial state
|
||||
el.innerText = prefix + '0' + suffix;
|
||||
|
||||
gsap.to(counter, {
|
||||
val: targetValue,
|
||||
duration: 2,
|
||||
ease: "power2.out",
|
||||
delay: delay,
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top 90%",
|
||||
toggleActions: "play none none none"
|
||||
},
|
||||
onUpdate: () => {
|
||||
// Determine if we need integers or decimals based on target
|
||||
const isFloat = targetValue % 1 !== 0;
|
||||
const current = isFloat ? counter.val.toFixed(2) : Math.round(counter.val);
|
||||
el.innerText = prefix + current.toLocaleString('en-US') + suffix;
|
||||
}
|
||||
});
|
||||
|
||||
}, [value, suffix, delay]);
|
||||
|
||||
// Color maps
|
||||
const colors = {
|
||||
blue: 'text-blue-500 bg-blue-500/10',
|
||||
emerald: 'text-emerald-500 bg-emerald-500/10',
|
||||
amber: 'text-amber-500 bg-amber-500/10',
|
||||
red: 'text-red-500 bg-red-500/10',
|
||||
purple: 'text-purple-500 bg-purple-500/10',
|
||||
};
|
||||
|
||||
// Safely get color class or default
|
||||
const colorClass = colors[color] || colors.blue;
|
||||
const textColor = colorClass.split(' ')[0];
|
||||
const bgColor = colorClass.split(' ')[1];
|
||||
|
||||
return (
|
||||
<SpotlightCard className="h-full flex flex-col justify-between p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">{label}</h3>
|
||||
<div className="mt-2 flex items-baseline gap-1">
|
||||
<span
|
||||
ref={counterRef}
|
||||
className={`text-3xl font-mono font-bold ${textColor} tracking-tight`}
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{Icon && (
|
||||
<div className={`p-3 rounded-xl ${bgColor} ${textColor}`}>
|
||||
<Icon size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{trend && (
|
||||
<div className="flex items-center text-xs font-medium">
|
||||
<span className={`${trend > 0 ? 'text-emerald-500' : 'text-red-500'} flex items-center`}>
|
||||
{trend > 0 ? '+' : ''}{trend}%
|
||||
</span>
|
||||
<span className="text-zinc-400 ml-2">{trendLabel || 'vs last month'}</span>
|
||||
</div>
|
||||
)}
|
||||
</SpotlightCard>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user