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:
@@ -46,8 +46,8 @@ export const GoldenLeadsScatter = ({ properties }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full min-h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<ScatterChart
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 0 }}
|
||||
>
|
||||
|
||||
@@ -61,6 +61,19 @@ const IntelligenceSidePanel = ({ data, onClose, variant = 'default' }) => {
|
||||
return () => document.removeEventListener('keydown', handleLightboxKeys);
|
||||
}, [lightboxIndex, photos.length]);
|
||||
|
||||
// Lightbox Touch Swipe Navigation
|
||||
const touchStartX = useRef(null);
|
||||
const handleTouchStart = (e) => { touchStartX.current = e.touches[0].clientX; };
|
||||
const handleTouchEnd = (e) => {
|
||||
if (touchStartX.current === null) return;
|
||||
const diff = touchStartX.current - e.changedTouches[0].clientX;
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (diff > 0) setLightboxIndex((prev) => (prev + 1) % photos.length);
|
||||
else setLightboxIndex((prev) => (prev - 1 + photos.length) % photos.length);
|
||||
}
|
||||
touchStartX.current = null;
|
||||
};
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
@@ -303,6 +316,9 @@ const IntelligenceSidePanel = ({ data, onClose, variant = 'default' }) => {
|
||||
ref={lightboxRef}
|
||||
role="dialog"
|
||||
aria-label="Photo Lightbox"
|
||||
data-lenis-prevent
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
@@ -313,41 +329,34 @@ const IntelligenceSidePanel = ({ data, onClose, variant = 'default' }) => {
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
{/* Navigation Left */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightboxIndex((prev) => (prev - 1 + photos.length) % photos.length); }}
|
||||
className="absolute left-4 text-cyan-400/70 hover:text-cyan-400 p-3 rounded-full hover:bg-white/10 transition-colors z-50 focus:ring-2 focus:ring-cyan-400 hidden md:block"
|
||||
aria-label="Previous photo"
|
||||
>
|
||||
<ChevronLeft size={32} />
|
||||
</button>
|
||||
|
||||
{/* Main Image */}
|
||||
<div className="relative max-w-5xl max-h-[85vh] w-full h-full flex flex-col items-center justify-center">
|
||||
<div className="relative max-w-5xl max-h-[75vh] w-full h-full flex flex-col items-center justify-center">
|
||||
<img
|
||||
src={photos[lightboxIndex].url}
|
||||
alt={photos[lightboxIndex].caption || "Full screen view"}
|
||||
className="max-w-full max-h-full object-contain rounded-lg shadow-2xl border border-white/10"
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-[-40px] text-cyan-400/50 text-xs uppercase tracking-widest font-mono">
|
||||
IMG_SEQ: {lightboxIndex + 1} / {photos.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Right */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightboxIndex((prev) => (prev + 1) % photos.length); }}
|
||||
className="absolute right-4 text-cyan-400/70 hover:text-cyan-400 p-3 rounded-full hover:bg-white/10 transition-colors z-50 focus:ring-2 focus:ring-cyan-400 hidden md:block"
|
||||
aria-label="Next photo"
|
||||
>
|
||||
<ChevronRight size={32} />
|
||||
</button>
|
||||
|
||||
{/* Mobile Navigation Hints */}
|
||||
<div className="md:hidden absolute bottom-8 flex space-x-8 text-cyan-400/30 text-xs pointer-events-none font-mono uppercase tracking-widest">
|
||||
<span>< PREV</span>
|
||||
<span>NEXT ></span>
|
||||
{/* Navigation Bar — ← IMG_SEQ: 2/3 → */}
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-3 z-50 bg-black/60 rounded-full px-2 py-1.5 backdrop-blur-sm border border-white/5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightboxIndex((prev) => (prev - 1 + photos.length) % photos.length); }}
|
||||
className="text-cyan-400/70 hover:text-cyan-400 p-2.5 rounded-full hover:bg-white/10 transition-colors active:bg-white/20"
|
||||
aria-label="Previous photo"
|
||||
>
|
||||
<ChevronLeft size={22} />
|
||||
</button>
|
||||
<span className="text-cyan-400/50 text-xs uppercase tracking-widest font-mono select-none whitespace-nowrap px-1">
|
||||
IMG_SEQ: {lightboxIndex + 1}/{photos.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setLightboxIndex((prev) => (prev + 1) % photos.length); }}
|
||||
className="text-cyan-400/70 hover:text-cyan-400 p-2.5 rounded-full hover:bg-white/10 transition-colors active:bg-white/20"
|
||||
aria-label="Next photo"
|
||||
>
|
||||
<ChevronRight size={22} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -43,8 +43,8 @@ export const OwnerIntentFunnel = ({ properties }) => {
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">Conversion pipeline</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full min-h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart
|
||||
layout="vertical"
|
||||
data={data}
|
||||
|
||||
@@ -46,8 +46,8 @@ export const RevenueByNeighborhood = ({ properties }) => {
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400">By Neighborhood Rating</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full min-h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<div className="w-full">
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{ top: 10, right: 10, left: -20, bottom: 0 }}
|
||||
|
||||
@@ -63,8 +63,8 @@ export const RoofConditionChart = ({ properties }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full min-h-[250px] relative">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<div className="w-full relative" style={{ height: 250 }}>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
|
||||
@@ -56,7 +56,7 @@ export const WeatherRiskGauge = ({ properties, weather }) => {
|
||||
|
||||
<div className="relative w-full h-[140px] flex items-end justify-center">
|
||||
{/* Speedometer Chart */}
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ResponsiveContainer width="100%" height={140}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
|
||||
Reference in New Issue
Block a user