7694788387
- Complete ProCanvas redesign with retro sports game aesthetic - Card-game style photo frame with tilt, shimmer, corner star accents - Daily Missions card with weekly challenge + individual quest progress bars - Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting) - Each log action opens themed modal with relevant input fields - Challenges modal with 6 active challenges and progress tracking - Achievements modal with all badges, unlock status, descriptions - Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals - Rewards & Checkpoints with named stages (Daily Grind to Legend Run) - Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings) - Hit The Map button with smooth pulsing glow animation - Grid pattern overlay in Leaderboard card - Light mode support via dark: Tailwind variants throughout - Top 3 badges displayed on profile card - Fixed dropdown option visibility in dark mode
1060 lines
72 KiB
React
1060 lines
72 KiB
React
import React, { useState, useEffect } from 'react';
|
|
import profilePic from '../assets/images/Profile_Pic_1_Agent.png';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
Flame, Target, Crosshair, Trophy, Shield, Zap, Star, Medal, Map, Activity, Home,
|
|
Users, TrendingUp, CheckCircle, Lock, Award, Calendar, Camera, ChevronRight, Swords,
|
|
X, ClipboardList, Handshake, DoorOpen, UserPlus
|
|
} from 'lucide-react';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { useMockStore } from '../data/mockStore';
|
|
import { useGamification as useOldGamification } from '../hooks/useGamification';
|
|
import { GamificationProvider, useGamification } from '../context/GamificationContext';
|
|
import FloatingXPManager from '../components/ProCanvas/gamification/FloatingXPManager';
|
|
import LevelUpModal from '../components/ProCanvas/gamification/LevelUpModal';
|
|
import { AnimatedCounter } from '../components/AnimatedCounter';
|
|
import { SpotlightCard } from '../components/SpotlightCard';
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// DESIGN TOKENS
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const C = {
|
|
gold: '#fda913', neon: '#AAFF00', fire: '#ff4500', blue: '#3b82f6',
|
|
cyan: '#06b6d4', green: '#22c55e', pink: '#ec4899', purple: '#a855f7',
|
|
};
|
|
|
|
// ── Pixel-art SVGs ──
|
|
const PixelTrophy = ({ size = 20, color = C.gold, className = '' }) => (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" className={className}>
|
|
<path d="M7 4h10v2h2v4h-2v2h-2v2h-2v2h2v2H9v-2h2v-2H9v-2H7V6H5V4h2z" fill={color} opacity="0.9" />
|
|
<path d="M9 18h6v2H9z" fill={color} opacity="0.6" />
|
|
</svg>
|
|
);
|
|
const PixelFlame = ({ size = 20, color = C.fire }) => (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
<path d="M11 2h2v2h2v2h2v4h-2v2h-2v2h-2v-2H9v-2H7V6h2V4h2V2z" fill={color} opacity="0.9" />
|
|
<path d="M11 14h2v2h2v4H9v-4h2v-2z" fill={C.gold} opacity="0.7" />
|
|
</svg>
|
|
);
|
|
const PixelBolt = ({ size = 18, color = C.neon }) => (
|
|
<svg width={size} height={size} viewBox="0 0 18 18" fill="none">
|
|
<path d="M8 0h4v2h-2v2h-2v2h4v2h-2v2h-2v2h-2v2H4v-2h2v-2h2V8H4V6h2V4h2V0z" fill={color} opacity="0.9" />
|
|
</svg>
|
|
);
|
|
const PixelStar = ({ size = 16, color = C.gold }) => (
|
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
<path d="M7 0h2v4h4v2h-4v4H7V6H3V4h4V0z" fill={color} opacity="0.85" />
|
|
</svg>
|
|
);
|
|
|
|
// ── Corner brackets decoration (like old design) ──
|
|
const CornerBrackets = ({ color = C.gold, thickness = 2 }) => (
|
|
<>
|
|
<div className="absolute top-0 left-0 w-5 h-5 pointer-events-none z-20" style={{
|
|
borderTop: `${thickness}px solid ${color}55`, borderLeft: `${thickness}px solid ${color}55`, borderTopLeftRadius: '6px',
|
|
}} />
|
|
<div className="absolute top-0 right-0 w-5 h-5 pointer-events-none z-20" style={{
|
|
borderTop: `${thickness}px solid ${color}55`, borderRight: `${thickness}px solid ${color}55`, borderTopRightRadius: '6px',
|
|
}} />
|
|
<div className="absolute bottom-0 left-0 w-5 h-5 pointer-events-none z-20" style={{
|
|
borderBottom: `${thickness}px solid ${color}55`, borderLeft: `${thickness}px solid ${color}55`, borderBottomLeftRadius: '6px',
|
|
}} />
|
|
<div className="absolute bottom-0 right-0 w-5 h-5 pointer-events-none z-20" style={{
|
|
borderBottom: `${thickness}px solid ${color}55`, borderRight: `${thickness}px solid ${color}55`, borderBottomRightRadius: '6px',
|
|
}} />
|
|
</>
|
|
);
|
|
|
|
// ── Animated floating dots ──
|
|
const FloatingDots = ({ color = C.gold, count = 5 }) => (
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none z-0 opacity-15">
|
|
{Array.from({ length: count }).map((_, i) => (
|
|
<motion.div key={i} className="absolute rounded-full"
|
|
style={{ width: 3 + (i % 3) * 2, height: 3 + (i % 3) * 2, background: color, left: `${10 + i * 16}%`, top: `${15 + (i * 19) % 60}%` }}
|
|
animate={{ y: [0, -10, 0], opacity: [0.2, 0.7, 0.2] }}
|
|
transition={{ repeat: Infinity, duration: 2.5 + i * 0.4, delay: i * 0.3, ease: 'easeInOut' }}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
// Pulse ring — smoother timing
|
|
const PulseRing = ({ color = C.fire, size = 56 }) => (
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
{[0, 1, 2].map(i => (
|
|
<motion.div key={i} className="absolute rounded-full"
|
|
style={{ width: size, height: size, border: `1.5px solid ${color}` }}
|
|
animate={{ scale: [1, 2.6], opacity: [0.4, 0] }}
|
|
transition={{ duration: 3, delay: i * 0.9, repeat: Infinity, ease: 'easeInOut' }}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
// Grid pattern overlay
|
|
const GridPattern = ({ color = C.gold, opacity = 0.06 }) => (
|
|
<div className="absolute inset-0 pointer-events-none z-0 rounded-[inherit]"
|
|
style={{
|
|
opacity,
|
|
backgroundImage: `linear-gradient(${color} 1px, transparent 1px), linear-gradient(90deg, ${color} 1px, transparent 1px)`,
|
|
backgroundSize: '24px 24px',
|
|
}}
|
|
/>
|
|
);
|
|
|
|
// CRT scanlines
|
|
const ScanlineOverlay = () => (
|
|
<div className="absolute inset-0 pointer-events-none z-30 rounded-[inherit]"
|
|
style={{ background: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.04) 2px, rgba(0,0,0,0.04) 4px)' }}
|
|
/>
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// GAME CARD — the main card primitive (chunky, with corner brackets)
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const GameCard = ({ children, className = '', accent = C.gold, scanlines = false, brackets = true, spotlightColor, glow = false, ...props }) => (
|
|
<SpotlightCard
|
|
className={`relative overflow-hidden group ${className}`}
|
|
spotlightColor={spotlightColor || accent + '30'}
|
|
{...props}
|
|
>
|
|
{/* Top accent line */}
|
|
<div className="absolute top-0 inset-x-0 h-[2px] z-20" style={{ background: `linear-gradient(90deg, transparent 10%, ${accent}, transparent 90%)` }} />
|
|
{/* Bottom accent line */}
|
|
<div className="absolute bottom-0 inset-x-0 h-[2px] z-20" style={{ background: `linear-gradient(90deg, ${C.neon}00, ${C.cyan}44, ${C.neon}44, ${C.gold}44, ${C.fire}00)` }} />
|
|
{brackets && <CornerBrackets color={accent} />}
|
|
{scanlines && <ScanlineOverlay />}
|
|
<div className="relative z-10 p-6 md:p-7 lg:p-8 flex flex-col h-full">{children}</div>
|
|
</SpotlightCard>
|
|
);
|
|
|
|
// ── Section header — big, bold, like the old design ──
|
|
const SectionHeader = ({ icon: Icon, pixelIcon: PIcon, title, color = C.gold, extra = null, size = 'lg' }) => (
|
|
<div className="flex items-center justify-between mb-5">
|
|
<div className="flex items-center gap-3">
|
|
{PIcon ? <PIcon size={size === 'lg' ? 28 : 22} color={color} /> : (
|
|
<div className={`${size === 'lg' ? 'w-10 h-10' : 'w-8 h-8'} rounded-xl flex items-center justify-center`} style={{ background: color + '18' }}>
|
|
<Icon size={size === 'lg' ? 22 : 16} style={{ color }} />
|
|
</div>
|
|
)}
|
|
<h2 className={`font-['Barlow_Condensed'] font-black ${size === 'lg' ? 'text-xl md:text-2xl' : 'text-base md:text-lg'} tracking-wider uppercase text-zinc-900 dark:text-white`}>
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
{extra}
|
|
</div>
|
|
);
|
|
|
|
// ── Progress bar — thick and glowing ──
|
|
const ProgressBar = ({ progress, goal, color = C.gold, height = 'h-4', showLabel = true, label = '' }) => {
|
|
const pct = Math.min((progress / goal) * 100, 100);
|
|
return (
|
|
<div className="w-full">
|
|
{showLabel && (
|
|
<div className="flex items-center justify-between mb-2">
|
|
{label && <span className="text-xs font-['Barlow_Condensed'] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">{label}</span>}
|
|
<span className="text-sm font-mono font-bold text-zinc-900 dark:text-white ml-auto"><AnimatedCounter to={progress} /> / {goal}</span>
|
|
</div>
|
|
)}
|
|
<div className={`${height} w-full rounded-full relative overflow-hidden bg-zinc-200 dark:bg-white/[0.06]`}>
|
|
<motion.div initial={{ width: 0 }} animate={{ width: `${pct}%` }}
|
|
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
|
className="absolute top-0 left-0 h-full rounded-full"
|
|
style={{ background: `linear-gradient(90deg, ${color}88, ${color})`, boxShadow: `0 0 16px ${color}66, 0 0 6px ${color}44` }}
|
|
/>
|
|
<motion.div initial={{ left: '0%' }} animate={{ left: `${Math.max(pct - 1, 0)}%` }}
|
|
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
|
|
className="absolute top-0 w-3 h-full rounded-full"
|
|
style={{ background: 'white', boxShadow: `0 0 10px ${color}`, opacity: pct > 3 ? 0.9 : 0 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ── Stagger entrance ──
|
|
const containerV = { hidden: {}, show: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } } };
|
|
const itemV = { hidden: { opacity: 0, y: 28 }, show: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 240, damping: 22 } } };
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// HOOKS
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const useMonthResetCountdown = () => {
|
|
const [t, setT] = useState('');
|
|
useEffect(() => {
|
|
const calc = () => {
|
|
const now = new Date(), end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
const diff = end - now; if (diff <= 0) { setT('Resetting...'); return; }
|
|
setT(`${Math.floor(diff / 86400000)}d ${String(Math.floor((diff % 86400000) / 3600000)).padStart(2, '0')}h ${String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0')}m`);
|
|
};
|
|
calc(); const id = setInterval(calc, 60000); return () => clearInterval(id);
|
|
}, []); return t;
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// CONFIG
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const RANK_TIERS = {
|
|
'Rookie': { color: '#CD7F32' }, 'Field Runner': { color: '#A8A9AD' },
|
|
'Roof Warrior': { color: '#fda913' }, 'Territory Titan': { color: '#A855F7' },
|
|
'Legendary Closer': { color: '#AAFF00' },
|
|
};
|
|
const RANK_ORDER = ['Rookie', 'Field Runner', 'Roof Warrior', 'Territory Titan', 'Legendary Closer'];
|
|
const nextRank = (c) => { const i = RANK_ORDER.indexOf(c); return i >= 0 && i < RANK_ORDER.length - 1 ? RANK_ORDER[i + 1] : 'MAX RANK'; };
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 1. OPERATOR PROFILE — big chunky player card
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const OperatorProfile = ({ gamestate, user, badges = [], className = '' }) => {
|
|
const tier = RANK_TIERS[gamestate.currentTitle] || RANK_TIERS['Field Runner'];
|
|
const initials = user.name.split(' ').map(n => n[0]).join('').slice(0, 2);
|
|
const BADGE_ICONS_MAP = { Hunter: Crosshair, Storm: Zap, Star: Star, Master: Map };
|
|
const BADGE_COLORS_MAP = { Hunter: C.fire, Storm: C.gold, Star: C.pink, Master: C.green };
|
|
const topBadges = badges.filter(b => b.status === 'unlocked').slice(0, 3);
|
|
|
|
return (
|
|
<GameCard accent={tier.color} scanlines className={className} spotlightColor={tier.color + '22'}>
|
|
<FloatingDots color={tier.color} count={6} />
|
|
<div className="flex flex-col items-center text-center gap-5 relative flex-1">
|
|
{/* Card-game style photo frame */}
|
|
<div className="relative mt-2">
|
|
{/* Outer decorative frame */}
|
|
<motion.div className="relative" style={{ transform: 'rotate(-2deg)' }}
|
|
whileHover={{ rotate: 0, scale: 1.05 }} transition={{ type: 'spring', stiffness: 300, damping: 20 }}>
|
|
{/* Frame background with gradient border */}
|
|
<div className="absolute -inset-2 rounded-2xl z-0"
|
|
style={{ background: `linear-gradient(135deg, ${tier.color}, ${C.gold}88, ${tier.color}44)`, opacity: 0.7 }} />
|
|
<div className="absolute -inset-1.5 rounded-xl bg-zinc-950 dark:bg-zinc-950 z-0" />
|
|
{/* Inner card with photo */}
|
|
<div className="relative z-10 w-32 h-36 md:w-36 md:h-40 rounded-xl overflow-hidden"
|
|
style={{ border: `2px solid ${tier.color}88`, boxShadow: `0 0 24px ${tier.color}33, inset 0 0 20px rgba(0,0,0,0.3)` }}>
|
|
{user.profilePhoto ? (
|
|
<img src={user.profilePhoto} alt={user.name} className="w-full h-full object-cover object-top" />
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center bg-zinc-900">
|
|
<span className="font-['Barlow_Condensed'] font-bold text-5xl" style={{ color: tier.color }}>{initials}</span>
|
|
</div>
|
|
)}
|
|
{/* Holographic shimmer overlay */}
|
|
<motion.div className="absolute inset-0 pointer-events-none"
|
|
style={{ background: `linear-gradient(115deg, transparent 30%, ${tier.color}15 45%, transparent 55%)` }}
|
|
animate={{ x: ['-100%', '200%'] }}
|
|
transition={{ repeat: Infinity, duration: 3.5, ease: 'linear', repeatDelay: 2 }} />
|
|
</div>
|
|
{/* Corner star accents on frame */}
|
|
<PixelStar size={12} color={tier.color} className="absolute -top-1 -left-1 z-20" />
|
|
<PixelStar size={10} color={tier.color} className="absolute -bottom-1 -right-1 z-20 opacity-60" />
|
|
</motion.div>
|
|
{/* Streak badge */}
|
|
{gamestate.streak > 0 && (
|
|
<motion.div className="absolute -top-4 -right-5 flex items-center gap-1 rounded-full px-2.5 py-1 z-30"
|
|
style={{ background: '#18181b', border: `2px solid ${C.fire}`, boxShadow: `0 0 14px ${C.fire}55` }}
|
|
animate={{ scale: [1, 1.08, 1] }} transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}>
|
|
<PixelFlame size={16} />
|
|
<span className="font-['Barlow_Condensed'] font-black text-sm" style={{ color: C.fire }}>{gamestate.streak}</span>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Name & Rank */}
|
|
<div className="w-full">
|
|
<h2 className="font-['Barlow_Condensed'] font-black text-2xl uppercase leading-none tracking-tight text-zinc-900 dark:text-white">{user.name.toUpperCase()}</h2>
|
|
<p className="font-['Barlow_Condensed'] font-bold text-sm uppercase tracking-widest mt-1.5" style={{ color: tier.color }}>
|
|
LVL {gamestate.level} — {gamestate.currentTitle}
|
|
</p>
|
|
</div>
|
|
|
|
{/* XP Bar */}
|
|
<div className="w-full">
|
|
<ProgressBar progress={gamestate.xp} goal={gamestate.nextLevelXP || 1} color={C.gold} height="h-4" label="XP" />
|
|
<p className="text-[10px] text-zinc-500 text-right mt-1 font-mono">→ {nextRank(gamestate.currentTitle)}</p>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="w-full grid grid-cols-3 gap-3 mt-1">
|
|
{[
|
|
{ icon: Home, val: gamestate.dailyQuests?.knocks?.current || 18, lbl: 'Knocked', color: C.neon },
|
|
{ icon: Users, val: gamestate.dailyQuests?.leads?.current || 6, lbl: 'Leads', color: C.gold },
|
|
{ icon: Calendar, val: gamestate.dailyQuests?.inspections?.current || 3, lbl: 'Appts', color: C.cyan },
|
|
].map(({ icon: I, val, lbl, color }) => (
|
|
<div key={lbl} className="rounded-xl py-3 text-center bg-zinc-100 dark:bg-white/[0.04] border border-zinc-200 dark:border-white/[0.06]">
|
|
<I size={16} className="mx-auto mb-1.5" style={{ color }} />
|
|
<p className="font-['Barlow_Condensed'] font-black text-xl leading-none text-zinc-900 dark:text-white"><AnimatedCounter to={val} /></p>
|
|
<p className="font-['Barlow_Condensed'] text-[10px] uppercase tracking-wider text-zinc-500 mt-1">{lbl}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Top 3 Badges */}
|
|
{topBadges.length > 0 && (
|
|
<div className="w-full pt-4 border-t border-zinc-200 dark:border-white/[0.06]">
|
|
<p className="font-['Barlow_Condensed'] text-[10px] uppercase tracking-widest text-zinc-500 mb-3">Top Badges</p>
|
|
<div className="flex justify-center gap-3">
|
|
{topBadges.map((b, i) => {
|
|
const key = Object.keys(BADGE_ICONS_MAP).find(k => b.id?.includes(k));
|
|
const Icon = key ? BADGE_ICONS_MAP[key] : Shield;
|
|
const bColor = key ? BADGE_COLORS_MAP[key] : C.cyan;
|
|
return (
|
|
<motion.div key={i} whileHover={{ scale: 1.15, y: -3 }}
|
|
className="w-12 h-12 rounded-full flex items-center justify-center"
|
|
style={{ background: bColor + '18', border: `2px solid ${bColor}55`, boxShadow: `0 0 10px ${bColor}22` }}>
|
|
<Icon size={18} style={{ color: bColor }} />
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 2. DAILY MISSIONS — like the old design, chunky quest list
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const DailyMissions = ({ gamestate, role, className = '' }) => {
|
|
const navigate = useNavigate();
|
|
const weeklyKnocks = gamestate.weeklyChallenge?.knocks || { current: 140, target: 150 };
|
|
const dailyQuests = [
|
|
{ icon: Home, label: 'Door Knocks', current: gamestate.dailyQuests?.knocks?.current || 18, target: gamestate.dailyQuests?.knocks?.target || 20, color: C.blue },
|
|
{ icon: Users, label: 'New Leads', current: gamestate.dailyQuests?.leads?.current || 4, target: gamestate.dailyQuests?.leads?.target || 5, color: C.neon },
|
|
{ icon: Calendar, label: 'Inspections', current: gamestate.dailyQuests?.inspections?.current || 0, target: gamestate.dailyQuests?.inspections?.target || 1, color: C.gold },
|
|
];
|
|
|
|
return (
|
|
<GameCard accent={C.neon} className={className} spotlightColor="rgba(170,255,0,0.12)">
|
|
<FloatingDots color={C.neon} count={4} />
|
|
<SectionHeader pixelIcon={PixelBolt} title="Daily Missions" color={C.neon} />
|
|
|
|
{/* Weekly Challenge */}
|
|
<div className="rounded-xl p-5 mb-5 relative overflow-hidden bg-zinc-100 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/[0.06]">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Target size={16} style={{ color: C.gold }} />
|
|
<span className="font-['Barlow_Condensed'] font-black text-sm uppercase tracking-wider" style={{ color: C.gold }}>Weekly Challenge</span>
|
|
</div>
|
|
<ProgressBar progress={weeklyKnocks.current} goal={weeklyKnocks.target} color={C.gold} height="h-4" label="Total Knocks" />
|
|
</div>
|
|
|
|
{/* Daily Quests */}
|
|
<div className="rounded-xl p-5 relative overflow-hidden bg-zinc-100 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/[0.06]">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<PixelBolt size={16} color={C.neon} />
|
|
<span className="font-['Barlow_Condensed'] font-black text-sm uppercase tracking-wider" style={{ color: C.neon }}>Daily Quests</span>
|
|
</div>
|
|
<div className="space-y-5">
|
|
{dailyQuests.map(({ icon: I, label, current, target, color }) => (
|
|
<div key={label}>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<I size={16} style={{ color }} />
|
|
<span className="font-['Barlow_Condensed'] font-bold text-sm text-zinc-700 dark:text-white/80">{label}</span>
|
|
<span className="ml-auto font-mono font-bold text-sm text-zinc-900 dark:text-white"><AnimatedCounter to={current} /> / {target}</span>
|
|
</div>
|
|
<ProgressBar progress={current} goal={target} color={color} height="h-3" showLabel={false} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA Button — smooth pulsing glow */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.04 }} whileTap={{ scale: 0.96 }}
|
|
animate={{ boxShadow: [`0 0 15px ${C.neon}33`, `0 0 35px ${C.neon}66`, `0 0 15px ${C.neon}33`] }}
|
|
transition={{ boxShadow: { repeat: Infinity, duration: 2, ease: 'easeInOut' } }}
|
|
onClick={() => navigate('/emp/fa/maps')}
|
|
className="w-full mt-6 py-4 rounded-xl font-['Barlow_Condensed'] font-black text-lg uppercase tracking-wider flex items-center justify-center gap-3 cursor-pointer"
|
|
style={{ background: C.neon, color: '#09090b', border: `2px solid ${C.neon}88` }}>
|
|
Hit The Map <ChevronRight size={20} />
|
|
</motion.button>
|
|
</GameCard>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 3. SHOWDOWN & STREAK — side by side stat cards
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const ShowdownCard = ({ gamestate }) => (
|
|
<GameCard accent={C.gold} scanlines className="h-full">
|
|
<SectionHeader pixelIcon={PixelTrophy} title="Showdown" color={C.gold} size="sm"
|
|
extra={<span className="text-[10px] font-mono px-2.5 py-1 rounded-full bg-white/5 text-zinc-400">Neighborhood</span>} />
|
|
<div className="flex-1 flex flex-col justify-center items-center text-center py-4 relative">
|
|
<div className="relative mb-3">
|
|
<PixelTrophy size={48} color={C.gold} />
|
|
<motion.div className="absolute -top-2 -right-2" animate={{ rotate: [0, 20, -20, 0] }}
|
|
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}>
|
|
<PixelStar size={18} color={C.gold} />
|
|
</motion.div>
|
|
</div>
|
|
<p className="font-['Barlow_Condensed'] font-black text-5xl leading-none text-white">1<sup className="text-xl text-zinc-400">ST</sup></p>
|
|
<p className="font-['Barlow_Condensed'] text-base text-zinc-400 mt-2"><AnimatedCounter to={gamestate.dailyQuests?.knocks?.current || 18} /> Leads Today</p>
|
|
</div>
|
|
<div className="flex items-center justify-between mt-auto pt-4 border-t border-white/[0.06]">
|
|
<div className="flex gap-1">{[1, 2, 3, 4, 5].map(s => <Star key={s} size={16} className={s <= 4 ? 'fill-current' : ''} style={{ color: s <= 4 ? C.gold : 'rgb(63 63 70)' }} />)}</div>
|
|
<span className="text-xs font-['Barlow_Condensed'] font-bold px-3 py-1 rounded-full" style={{ background: C.green + '18', color: C.green }}>+250 XP</span>
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
const StreakCard = ({ gamestate }) => (
|
|
<GameCard accent={C.fire} spotlightColor="rgba(255,69,0,0.12)" className="h-full">
|
|
<SectionHeader pixelIcon={PixelFlame} title="Hot Streak" color={C.fire} size="sm" />
|
|
<div className="flex-1 flex flex-col justify-center items-center text-center py-4">
|
|
<div className="relative mb-2">
|
|
<PulseRing color={C.fire} size={72} />
|
|
<motion.div animate={{ y: [0, -8, 0], scale: [1, 1.06, 1] }}
|
|
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}>
|
|
<PixelFlame size={52} />
|
|
</motion.div>
|
|
</div>
|
|
<p className="font-['Barlow_Condensed'] font-bold text-sm text-zinc-500 dark:text-zinc-400 mt-3">Current Streak</p>
|
|
<p className="font-['Barlow_Condensed'] font-black text-4xl" style={{ color: C.gold }}>
|
|
<AnimatedCounter to={gamestate.streak || 5} /> Days
|
|
</p>
|
|
<p className="text-[10px] font-mono text-zinc-500 dark:text-zinc-600 mt-1">Personal Best: 14 Days</p>
|
|
</div>
|
|
<div className="flex items-center justify-between mt-auto pt-4 border-t border-zinc-200 dark:border-white/[0.06]">
|
|
<div className="flex gap-1">{[1, 2, 3, 4, 5].map(s => <PixelStar key={s} size={14} color={s <= (gamestate.streak || 5) ? C.gold : 'rgb(63 63 70)'} />)}</div>
|
|
<span className="text-xs font-['Barlow_Condensed'] font-bold px-3 py-1 rounded-full" style={{ background: C.green + '18', color: C.green }}>+50 XP</span>
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 4. REWARDS & CHECKPOINTS — named stages with reward labels
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const STAGE_NAMES = ['Daily Grind', 'Door Warrior', 'Lead Machine', 'Closer Mode', 'Legend Run'];
|
|
const STAGE_REWARDS = ['+500 XP Boost', '$25 Gift Card', '$50 Cash Bonus', '???', '???'];
|
|
|
|
const RewardsCheckpoints = ({ stages, className = '' }) => {
|
|
const completedCount = stages.filter(s => s.status === 'completed').length;
|
|
const totalPct = stages.length > 0 ? (completedCount / stages.length) * 100 : 0;
|
|
const activeStage = stages.find(s => s.status === 'in-progress') || stages[completedCount] || stages[0];
|
|
|
|
return (
|
|
<GameCard accent={C.gold} className={className} spotlightColor="rgba(253,169,19,0.10)">
|
|
<SectionHeader icon={Award} title="Rewards & Checkpoints" color={C.gold}
|
|
extra={<span className="text-sm font-mono" style={{ color: C.gold }}>{Math.round(totalPct)}%</span>} />
|
|
|
|
{/* Stage timeline */}
|
|
<div className="flex items-start justify-between relative mb-6">
|
|
{/* Connecting line through all stages */}
|
|
<div className="absolute top-5 left-[10%] right-[10%] h-[3px] rounded-full" style={{ background: 'rgba(255,255,255,0.06)' }} />
|
|
<div className="absolute top-5 left-[10%] h-[3px] rounded-full transition-all duration-1000" style={{
|
|
width: `${Math.min(completedCount / Math.max(stages.length - 1, 1) * 80, 80)}%`,
|
|
background: `linear-gradient(90deg, ${C.gold}, ${C.neon})`,
|
|
boxShadow: `0 0 10px ${C.gold}66`,
|
|
}} />
|
|
|
|
{stages.map((stage, idx) => {
|
|
const done = stage.status === 'completed', active = stage.status === 'in-progress';
|
|
const stageColor = done ? C.gold : active ? C.neon : 'rgb(63 63 70)';
|
|
const stageName = STAGE_NAMES[idx] || `Stage ${idx + 1}`;
|
|
const stageReward = STAGE_REWARDS[idx] || '???';
|
|
|
|
return (
|
|
<div key={stage.id} className="flex flex-col items-center text-center relative z-10" style={{ width: `${100 / stages.length}%` }}>
|
|
<motion.div whileHover={{ scale: 1.15 }}
|
|
className="w-10 h-10 rounded-full flex items-center justify-center mb-2"
|
|
style={{
|
|
background: done ? C.gold : active ? 'rgba(170,255,0,0.15)' : '#18181b',
|
|
border: `2.5px solid ${done ? C.gold : active ? C.neon : 'rgba(255,255,255,0.1)'}`,
|
|
color: done ? '#09090b' : active ? C.neon : 'rgb(82 82 91)',
|
|
boxShadow: done ? `0 0 12px ${C.gold}44` : active ? `0 0 12px ${C.neon}33` : 'none',
|
|
}}>
|
|
{done ? <CheckCircle size={18} /> : !done && !active ? <Lock size={14} /> :
|
|
<span className="font-['Barlow_Condensed'] font-bold text-sm">{idx + 1}</span>}
|
|
</motion.div>
|
|
<p className="font-['Barlow_Condensed'] font-bold text-[10px] uppercase tracking-wider" style={{ color: stageColor }}>{stageName}</p>
|
|
{done && <p className="text-[8px] font-mono text-zinc-500 mt-0.5">
|
|
{stage.progress?.current || 0}/{stage.progress?.target || 0} ✓
|
|
</p>}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Reward tiles */}
|
|
<div className="grid grid-cols-5 gap-3">
|
|
{stages.map((stage, idx) => {
|
|
const done = stage.status === 'completed', active = stage.status === 'in-progress';
|
|
const stageReward = stage.reward?.name || STAGE_REWARDS[idx] || '???';
|
|
return (
|
|
<div key={`reward-${stage.id}`} className="rounded-xl py-3 px-2 text-center"
|
|
style={{
|
|
background: done ? C.gold + '12' : active ? C.neon + '10' : 'rgba(255,255,255,0.02)',
|
|
border: `1px solid ${done ? C.gold + '44' : active ? C.neon + '33' : 'rgba(255,255,255,0.05)'}`,
|
|
}}>
|
|
<Award size={14} className="mx-auto mb-1" style={{ color: done ? C.gold : active ? C.neon : 'rgb(63 63 70)' }} />
|
|
<p className="font-['Barlow_Condensed'] font-bold text-[9px] leading-tight" style={{ color: done ? C.gold : active ? C.neon : 'rgb(82 82 91)' }}>
|
|
{stageReward}
|
|
</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 5. BADGES & ACHIEVEMENTS — tall badge cards with descriptions
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const BADGE_ICONS = { Hunter: Crosshair, Storm: Zap, Star: Star, Master: Map };
|
|
const BADGE_COLORS = { Hunter: C.fire, Storm: C.gold, Star: C.pink, Master: C.green };
|
|
|
|
const BadgesAchievements = ({ badges, className = '' }) => (
|
|
<GameCard accent={C.gold} className={className}>
|
|
<SectionHeader icon={Medal} title="Badges & Achievements" color={C.gold} />
|
|
<div className="grid grid-cols-5 gap-4">
|
|
{badges.map((badge, idx) => {
|
|
const unlocked = badge.status === 'unlocked';
|
|
const key = Object.keys(BADGE_ICONS).find(k => badge.id?.includes(k)) || null;
|
|
const Icon = key ? BADGE_ICONS[key] : Shield;
|
|
const color = key ? BADGE_COLORS[key] : C.cyan;
|
|
return (
|
|
<motion.div key={idx} whileHover={unlocked ? { scale: 1.05, y: -4 } : {}}
|
|
className="rounded-xl p-4 flex flex-col items-center text-center cursor-default min-h-[160px] relative overflow-hidden"
|
|
style={{
|
|
background: unlocked ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.02)',
|
|
border: `1.5px solid ${unlocked ? color + '55' : 'rgba(255,255,255,0.06)'}`,
|
|
opacity: unlocked ? 1 : 0.35,
|
|
}}>
|
|
{unlocked && (
|
|
<motion.div className="absolute top-2 right-2 z-10"
|
|
animate={{ scale: [1, 1.2, 1] }} transition={{ repeat: Infinity, duration: 2 }}>
|
|
<CheckCircle size={12} style={{ color }} />
|
|
</motion.div>
|
|
)}
|
|
<div className="w-14 h-14 rounded-full flex items-center justify-center mb-3 relative"
|
|
style={{
|
|
background: unlocked ? color + '18' : 'transparent',
|
|
boxShadow: unlocked ? `0 0 20px ${color}22` : 'none',
|
|
}}>
|
|
{unlocked && <div className="absolute inset-0 rounded-full" style={{ boxShadow: `0 0 20px ${color}33`, animation: 'pulse 2s ease-in-out infinite' }} />}
|
|
<Icon size={24} style={{ color: unlocked ? color : 'rgb(63 63 70)' }} />
|
|
</div>
|
|
<span className="font-['Barlow_Condensed'] font-black text-xs leading-tight text-white mb-1">{badge.title}</span>
|
|
<span className="text-[9px] font-mono leading-tight mt-auto" style={{ color: unlocked ? color : 'rgb(82 82 91)' }}>
|
|
{unlocked ? (badge.progressText || 'UNLOCKED') : badge.criteria}
|
|
</span>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 6. LEADERBOARD — full-width, chunky rows
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const Leaderboard = ({ className = '' }) => {
|
|
const { leaderboard } = useGamification();
|
|
const countdown = useMonthResetCountdown();
|
|
|
|
return (
|
|
<GameCard accent={C.gold} className={className} spotlightColor="rgba(253,169,19,0.08)">
|
|
<GridPattern color={C.gold} opacity={0.05} />
|
|
<SectionHeader pixelIcon={PixelTrophy} title="Leaderboard" color={C.gold}
|
|
extra={<div className="flex flex-col items-end">
|
|
<span className="text-[9px] font-mono uppercase tracking-widest text-zinc-500 dark:text-zinc-600">Resets in</span>
|
|
<span className="text-sm font-mono font-bold" style={{ color: C.gold }}>{countdown}</span>
|
|
</div>} />
|
|
<motion.ul layout className="flex flex-col gap-2">
|
|
<AnimatePresence>
|
|
{leaderboard.map((agent, i) => {
|
|
const isUser = agent.isUser, rank = i + 1, isTop3 = rank <= 3;
|
|
const medalColor = rank === 1 ? C.gold : rank === 2 ? '#A8A9AD' : rank === 3 ? '#CD7F32' : null;
|
|
return (
|
|
<motion.li key={agent.id} layout
|
|
initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 30, delay: i * 0.05 }}
|
|
className="flex items-center justify-between px-5 py-3.5 rounded-xl relative overflow-hidden"
|
|
style={{ background: isUser ? C.blue + '15' : isTop3 ? 'rgba(255,255,255,0.03)' : 'transparent', border: isUser ? `1px solid ${C.blue}33` : '1px solid transparent' }}>
|
|
{isTop3 && (
|
|
<motion.div className="absolute inset-0 pointer-events-none"
|
|
style={{ background: `linear-gradient(90deg, transparent, ${medalColor}0C, transparent)` }}
|
|
animate={{ x: ['-100%', '200%'] }} transition={{ repeat: Infinity, duration: 4.5, ease: 'linear', delay: i * 1.2 }} />
|
|
)}
|
|
<div className="flex items-center gap-3 flex-1 min-w-0 relative z-10">
|
|
{isTop3 ? (
|
|
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{ background: medalColor + '22' }}>
|
|
<PixelTrophy size={16} color={medalColor} />
|
|
</div>
|
|
) : <span className="w-8 text-center font-mono font-bold text-sm text-zinc-500 dark:text-zinc-600">{rank}.</span>}
|
|
<div className="w-9 h-9 rounded-full flex items-center justify-center text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10"
|
|
style={{ color: isUser ? C.blue : 'rgb(161 161 170)' }}>
|
|
{agent.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
|
|
</div>
|
|
<span className="font-['Barlow_Condensed'] font-bold text-base truncate" style={{ color: isUser ? C.blue : undefined }}>
|
|
<span className={isUser ? '' : 'text-zinc-900 dark:text-white'}>{agent.name}</span>
|
|
</span>
|
|
</div>
|
|
<span className="font-mono font-bold text-base flex-shrink-0 relative z-10" style={{ color: isUser ? C.blue : undefined }}>
|
|
<span className={isUser ? '' : 'text-zinc-900 dark:text-white'}><AnimatedCounter to={agent.points} /></span>
|
|
</span>
|
|
</motion.li>
|
|
);
|
|
})}
|
|
</AnimatePresence>
|
|
</motion.ul>
|
|
</GameCard>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 7. ADMIN / OWNER WIDGETS
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const AdminCommandCenter = ({ className = '' }) => (
|
|
<GameCard accent={C.cyan} className={className} scanlines>
|
|
<SectionHeader icon={Activity} title="Command Center" color={C.cyan}
|
|
extra={<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-wider">Season Ops</span>} />
|
|
<div className="grid grid-cols-3 gap-5">
|
|
{[
|
|
{ label: 'Active Squads', value: 12, sub: 'All Online', color: C.cyan },
|
|
{ label: 'Doors Today', value: 1432, sub: '+14% vs Yesterday', color: C.gold },
|
|
{ label: 'Approval Queue', value: 8, sub: 'Needs Review', color: C.fire },
|
|
].map(({ label, value, sub, color }) => (
|
|
<div key={label} className="rounded-xl p-5 text-center bg-zinc-100 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/[0.06]">
|
|
<p className="text-[10px] font-mono uppercase tracking-wider text-zinc-600 mb-2">{label}</p>
|
|
<p className="font-['Barlow_Condensed'] font-black text-4xl" style={{ color }}><AnimatedCounter to={value} /></p>
|
|
<p className="text-[10px] font-mono mt-1" style={{ color: C.green }}>{sub}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
const OwnerROIDashboard = ({ className = '' }) => (
|
|
<GameCard accent={C.gold} className={className}>
|
|
<SectionHeader icon={TrendingUp} title="Executive ROI" color={C.gold}
|
|
extra={<span className="text-sm font-mono" style={{ color: C.fire }}>Cost YTD: $<AnimatedCounter to={12450} /></span>} />
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{[
|
|
{ label: 'Participation', value: '92%', color: C.green },
|
|
{ label: 'Prize Fund', value: '68%', color: C.gold },
|
|
{ label: 'Conversion Lift', value: '+24%', color: C.green },
|
|
{ label: 'Revenue', value: '$1.2M', color: C.gold },
|
|
].map(({ label, value, color }) => (
|
|
<div key={label} className="rounded-xl p-5 text-center bg-white/[0.03] border border-white/[0.06]">
|
|
<p className="text-[10px] font-mono uppercase tracking-wider text-zinc-600 mb-2">{label}</p>
|
|
<p className="font-['Barlow_Condensed'] font-black text-3xl" style={{ color }}>{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 8. LOG ACTION CARD + MODAL
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const LOG_ACTIONS = [
|
|
{
|
|
key: 'knock', label: 'Door Knocked', icon: DoorOpen, color: C.cyan,
|
|
fields: [
|
|
{ name: 'address', label: 'Address', type: 'text', placeholder: '123 Main St' },
|
|
{ name: 'outcome', label: 'Outcome', type: 'select', options: ['Interested', 'Not Home', 'Not Interested', 'Follow Up'] },
|
|
{ name: 'notes', label: 'Notes', type: 'textarea', placeholder: 'Any details...' },
|
|
]
|
|
},
|
|
{
|
|
key: 'lead', label: 'Lead Gained', icon: UserPlus, color: C.neon,
|
|
fields: [
|
|
{ name: 'name', label: 'Contact Name', type: 'text', placeholder: 'John Doe' },
|
|
{ name: 'phone', label: 'Phone', type: 'text', placeholder: '(555) 123-4567' },
|
|
{ name: 'email', label: 'Email', type: 'text', placeholder: 'john@example.com' },
|
|
{ name: 'address', label: 'Property Address', type: 'text', placeholder: '456 Oak Ave' },
|
|
{ name: 'notes', label: 'Notes', type: 'textarea', placeholder: 'Lead source & details...' },
|
|
]
|
|
},
|
|
{
|
|
key: 'appt', label: 'Appointment Set', icon: Calendar, color: C.gold,
|
|
fields: [
|
|
{ name: 'clientName', label: 'Client Name', type: 'text', placeholder: 'Jane Smith' },
|
|
{ name: 'date', label: 'Date & Time', type: 'datetime-local' },
|
|
{ name: 'address', label: 'Address', type: 'text', placeholder: '789 Pine Rd' },
|
|
{ name: 'type', label: 'Type', type: 'select', options: ['Roof Inspection', 'Estimate', 'Follow Up', 'Other'] },
|
|
{ name: 'notes', label: 'Notes', type: 'textarea', placeholder: 'Meeting details...' },
|
|
]
|
|
},
|
|
{
|
|
key: 'meeting', label: 'Client Meeting', icon: Handshake, color: C.purple,
|
|
fields: [
|
|
{ name: 'clientName', label: 'Client Name', type: 'text', placeholder: 'Client name' },
|
|
{ name: 'outcome', label: 'Outcome', type: 'select', options: ['Deal Closed', 'Follow Up Needed', 'Proposal Sent', 'No Deal'] },
|
|
{ name: 'followUp', label: 'Follow-up Date', type: 'date' },
|
|
{ name: 'notes', label: 'Notes', type: 'textarea', placeholder: 'Meeting summary...' },
|
|
]
|
|
},
|
|
];
|
|
|
|
const LogActionCard = ({ className = '', onAction }) => (
|
|
<GameCard accent={C.purple} className={className} spotlightColor="rgba(168,85,247,0.10)">
|
|
<SectionHeader icon={ClipboardList} title="Log Action" color={C.purple} />
|
|
<div className="grid grid-cols-2 gap-3 flex-1">
|
|
{LOG_ACTIONS.map(({ key, label, icon: I, color }) => (
|
|
<motion.button key={key} whileHover={{ scale: 1.05, y: -2 }} whileTap={{ scale: 0.93 }}
|
|
onClick={() => onAction(key)}
|
|
className="rounded-xl p-4 flex flex-col items-center justify-center gap-2.5 cursor-pointer bg-zinc-100 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/[0.06] hover:border-white/15 transition-colors">
|
|
<div className="w-11 h-11 rounded-xl flex items-center justify-center" style={{ background: color + '15', boxShadow: `0 0 12px ${color}11` }}>
|
|
<I size={22} style={{ color }} />
|
|
</div>
|
|
<span className="font-['Barlow_Condensed'] font-bold text-xs uppercase tracking-wider text-zinc-500 dark:text-zinc-400">{label}</span>
|
|
</motion.button>
|
|
))}
|
|
</div>
|
|
</GameCard>
|
|
);
|
|
|
|
const LogActionModal = ({ actionKey, onClose }) => {
|
|
const config = LOG_ACTIONS.find(a => a.key === actionKey);
|
|
const [form, setForm] = useState({});
|
|
if (!config) return null;
|
|
const Icon = config.icon;
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
// In production: dispatch to API
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
onClick={onClose}>
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
<motion.div initial={{ scale: 0.9, opacity: 0, y: 30 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 30 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
|
onClick={e => e.stopPropagation()}
|
|
className="relative w-full max-w-lg rounded-2xl overflow-hidden bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-white/10 shadow-2xl">
|
|
{/* Header */}
|
|
<div className="p-6 pb-4 flex items-center justify-between" style={{ borderBottom: `2px solid ${config.color}33` }}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: config.color + '18' }}>
|
|
<Icon size={22} style={{ color: config.color }} />
|
|
</div>
|
|
<h3 className="font-['Barlow_Condensed'] font-black text-xl uppercase tracking-wider text-zinc-900 dark:text-white">{config.label}</h3>
|
|
</div>
|
|
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={onClose}
|
|
className="w-8 h-8 rounded-lg flex items-center justify-center bg-white/5 hover:bg-white/10 transition-colors">
|
|
<X size={18} className="text-zinc-500" />
|
|
</motion.button>
|
|
</div>
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
{config.fields.map(field => (
|
|
<div key={field.name}>
|
|
<label className="block font-['Barlow_Condensed'] font-bold text-xs uppercase tracking-wider text-zinc-500 dark:text-zinc-400 mb-1.5">{field.label}</label>
|
|
{field.type === 'select' ? (
|
|
<select value={form[field.name] || ''} onChange={e => setForm(p => ({ ...p, [field.name]: e.target.value }))}
|
|
className="w-full rounded-xl px-4 py-3 bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-white/[0.08] text-zinc-900 dark:text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[var(--ring)] [&>option]:bg-white [&>option]:dark:bg-zinc-900 [&>option]:text-zinc-900 [&>option]:dark:text-white"
|
|
style={{ '--ring': config.color + '55' }}>
|
|
<option value="" className="bg-white dark:bg-zinc-900 text-zinc-500 dark:text-zinc-400">Select...</option>
|
|
{field.options.map(o => <option key={o} value={o} className="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white">{o}</option>)}
|
|
</select>
|
|
) : field.type === 'textarea' ? (
|
|
<textarea rows={3} value={form[field.name] || ''} onChange={e => setForm(p => ({ ...p, [field.name]: e.target.value }))}
|
|
placeholder={field.placeholder}
|
|
className="w-full rounded-xl px-4 py-3 bg-zinc-100 dark:bg-white/[0.04] border border-zinc-200 dark:border-white/[0.08] text-zinc-900 dark:text-white font-mono text-sm placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2 resize-none"
|
|
style={{ '--ring': config.color + '55', focusRingColor: config.color + '55' }} />
|
|
) : (
|
|
<input type={field.type} value={form[field.name] || ''} onChange={e => setForm(p => ({ ...p, [field.name]: e.target.value }))}
|
|
placeholder={field.placeholder}
|
|
className="w-full rounded-xl px-4 py-3 bg-zinc-100 dark:bg-white/[0.04] border border-zinc-200 dark:border-white/[0.08] text-zinc-900 dark:text-white font-mono text-sm placeholder:text-zinc-400 dark:placeholder:text-zinc-600 focus:outline-none focus:ring-2"
|
|
style={{ '--ring': config.color + '55' }} />
|
|
)}
|
|
</div>
|
|
))}
|
|
<motion.button type="submit" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.97 }}
|
|
className="w-full mt-2 py-3.5 rounded-xl font-['Barlow_Condensed'] font-black text-base uppercase tracking-wider cursor-pointer"
|
|
style={{ background: config.color, color: '#09090b', boxShadow: `0 0 16px ${config.color}44` }}>
|
|
Log {config.label}
|
|
</motion.button>
|
|
</form>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 9. CHALLENGES MODAL
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const ChallengesModal = ({ onClose, gamestate }) => {
|
|
const challenges = [
|
|
{ title: 'Weekly Knock Target', desc: 'Hit 150 doors this week', current: gamestate.weeklyChallenge?.knocks?.current || 140, target: 150, color: C.gold, reward: '+500 XP', icon: Home },
|
|
{ title: 'Daily Lead Master', desc: 'Generate 5 leads today', current: gamestate.dailyQuests?.leads?.current || 4, target: 5, color: C.neon, reward: '+100 XP', icon: UserPlus },
|
|
{ title: 'Inspection Sprint', desc: 'Complete 1 inspection today', current: gamestate.dailyQuests?.inspections?.current || 0, target: 1, color: C.cyan, reward: '+75 XP', icon: Calendar },
|
|
{ title: 'Door Warrior', desc: 'Knock 20 doors in a single day', current: gamestate.dailyQuests?.knocks?.current || 18, target: 20, color: C.blue, reward: '+200 XP', icon: DoorOpen },
|
|
{ title: 'Streak Keeper', desc: 'Maintain your streak for 7 days', current: gamestate.streak || 5, target: 7, color: C.fire, reward: '+300 XP', icon: Flame },
|
|
{ title: 'Photo Hunter', desc: 'Upload 5 roof photos today', current: 3, target: 5, color: C.green, reward: '+150 XP', icon: Camera },
|
|
];
|
|
|
|
return (
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
onClick={onClose}>
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
<motion.div initial={{ scale: 0.9, opacity: 0, y: 30 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 30 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
|
onClick={e => e.stopPropagation()}
|
|
className="relative w-full max-w-2xl max-h-[80vh] rounded-2xl overflow-hidden bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-white/10 shadow-2xl flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-6 pb-4 flex items-center justify-between flex-shrink-0" style={{ borderBottom: `2px solid ${C.neon}33` }}>
|
|
<div className="flex items-center gap-3">
|
|
<PixelBolt size={28} color={C.neon} />
|
|
<h3 className="font-['Barlow_Condensed'] font-black text-2xl uppercase tracking-wider text-zinc-900 dark:text-white">Active Challenges</h3>
|
|
</div>
|
|
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={onClose}
|
|
className="w-8 h-8 rounded-lg flex items-center justify-center bg-white/5 hover:bg-white/10 transition-colors">
|
|
<X size={18} className="text-zinc-500" />
|
|
</motion.button>
|
|
</div>
|
|
{/* Content */}
|
|
<div className="p-6 overflow-y-auto space-y-4 flex-1">
|
|
{challenges.map((ch, idx) => {
|
|
const pct = Math.min((ch.current / ch.target) * 100, 100);
|
|
const done = pct >= 100;
|
|
const Icon = ch.icon;
|
|
return (
|
|
<motion.div key={idx} initial={{ opacity: 0, x: -15 }} animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: idx * 0.06 }}
|
|
className="rounded-xl p-5 relative overflow-hidden bg-zinc-100 dark:bg-white/[0.03] border border-zinc-200 dark:border-white/[0.06]">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center" style={{ background: ch.color + '15' }}>
|
|
<Icon size={18} style={{ color: ch.color }} />
|
|
</div>
|
|
<div>
|
|
<p className="font-['Barlow_Condensed'] font-black text-sm uppercase tracking-wider text-zinc-900 dark:text-white">{ch.title}</p>
|
|
<p className="text-[11px] font-mono text-zinc-500">{ch.desc}</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs font-['Barlow_Condensed'] font-bold px-2.5 py-1 rounded-full flex-shrink-0"
|
|
style={{ background: done ? C.green + '18' : ch.color + '12', color: done ? C.green : ch.color }}>
|
|
{done ? '✓ DONE' : ch.reward}
|
|
</span>
|
|
</div>
|
|
<ProgressBar progress={ch.current} goal={ch.target} color={ch.color} height="h-3" showLabel={false} />
|
|
<div className="flex justify-between mt-1.5">
|
|
<span className="text-[10px] font-mono text-zinc-500">{ch.current} / {ch.target}</span>
|
|
<span className="text-[10px] font-mono font-bold" style={{ color: ch.color }}>{Math.round(pct)}%</span>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 10. ACHIEVEMENTS MODAL
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
const BADGE_ICONS_GLOBAL = { Hunter: Crosshair, Storm: Zap, Star: Star, Master: Map };
|
|
const BADGE_COLORS_GLOBAL = { Hunter: C.fire, Storm: C.gold, Star: C.pink, Master: C.green };
|
|
|
|
const AchievementsModal = ({ onClose, badges }) => (
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
onClick={onClose}>
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
<motion.div initial={{ scale: 0.9, opacity: 0, y: 30 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 30 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
|
|
onClick={e => e.stopPropagation()}
|
|
className="relative w-full max-w-2xl max-h-[80vh] rounded-2xl overflow-hidden bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-white/10 shadow-2xl flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-6 pb-4 flex items-center justify-between flex-shrink-0" style={{ borderBottom: `2px solid ${C.gold}33` }}>
|
|
<div className="flex items-center gap-3">
|
|
<PixelTrophy size={28} color={C.gold} />
|
|
<h3 className="font-['Barlow_Condensed'] font-black text-2xl uppercase tracking-wider text-zinc-900 dark:text-white">All Achievements</h3>
|
|
<span className="text-xs font-mono px-2 py-0.5 rounded-full bg-zinc-200 dark:bg-white/5 text-zinc-600 dark:text-zinc-400">
|
|
{badges.filter(b => b.status === 'unlocked').length}/{badges.length} Unlocked
|
|
</span>
|
|
</div>
|
|
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={onClose}
|
|
className="w-8 h-8 rounded-lg flex items-center justify-center bg-white/5 hover:bg-white/10 transition-colors">
|
|
<X size={18} className="text-zinc-500" />
|
|
</motion.button>
|
|
</div>
|
|
{/* Content */}
|
|
<div className="p-6 overflow-y-auto space-y-3 flex-1">
|
|
{badges.map((badge, idx) => {
|
|
const unlocked = badge.status === 'unlocked';
|
|
const key = Object.keys(BADGE_ICONS_GLOBAL).find(k => badge.id?.includes(k));
|
|
const Icon = key ? BADGE_ICONS_GLOBAL[key] : Shield;
|
|
const color = key ? BADGE_COLORS_GLOBAL[key] : C.cyan;
|
|
return (
|
|
<motion.div key={idx} initial={{ opacity: 0, x: -15 }} animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: idx * 0.05 }}
|
|
className="rounded-xl p-5 flex items-center gap-4 relative overflow-hidden"
|
|
style={{
|
|
background: unlocked ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.02)',
|
|
border: `1.5px solid ${unlocked ? color + '44' : 'rgba(255,255,255,0.06)'}`,
|
|
opacity: unlocked ? 1 : 0.5,
|
|
}}>
|
|
<div className="w-14 h-14 rounded-full flex items-center justify-center flex-shrink-0"
|
|
style={{ background: unlocked ? color + '18' : 'rgba(255,255,255,0.03)', boxShadow: unlocked ? `0 0 16px ${color}22` : 'none' }}>
|
|
<Icon size={26} style={{ color: unlocked ? color : 'rgb(82 82 91)' }} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-['Barlow_Condensed'] font-black text-sm uppercase tracking-wider text-zinc-900 dark:text-white">{badge.title}</p>
|
|
{unlocked && <CheckCircle size={14} style={{ color }} />}
|
|
</div>
|
|
<p className="text-xs font-mono text-zinc-500 mt-0.5">{badge.criteria || 'Complete the challenge to unlock'}</p>
|
|
{badge.progressText && <p className="text-[10px] font-mono mt-0.5" style={{ color }}>{badge.progressText}</p>}
|
|
</div>
|
|
<span className="text-xs font-['Barlow_Condensed'] font-bold px-3 py-1 rounded-full flex-shrink-0"
|
|
style={{ background: unlocked ? color + '15' : 'rgba(255,255,255,0.03)', color: unlocked ? color : 'rgb(82 82 91)' }}>
|
|
{unlocked ? 'UNLOCKED' : 'LOCKED'}
|
|
</span>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// MAIN LAYOUT — two-column bento, no bottom ribbon
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
function ProCanvasContent() {
|
|
const { user } = useAuth();
|
|
const { users } = useMockStore();
|
|
const role = user?.role || 'FIELD_AGENT';
|
|
|
|
const mockDbUser = {
|
|
...(users.find(u => u.email === user?.email) || users.find(u => u.roles?.includes('FIELD_AGENT')) || users.find(u => u.type === 'employee') || {
|
|
name: user?.name || 'Agent Unknown', empId: 'OP-00', xp: 12450, doorsKnocked: 342, leadsGained: 45,
|
|
appointmentsSet: 18, streakDays: 5, achievements: ['Hot Spot Hunter', 'Storm Chaser'],
|
|
}),
|
|
profilePhoto: profilePic,
|
|
};
|
|
|
|
const oldGamestate = useOldGamification(mockDbUser);
|
|
const gs = useGamification();
|
|
|
|
// Modal state
|
|
const [logAction, setLogAction] = useState(null); // 'knock' | 'lead' | 'appt' | 'meeting' | null
|
|
const [showChallenges, setShowChallenges] = useState(false);
|
|
const [showAchievements, setShowAchievements] = useState(false);
|
|
|
|
if (!oldGamestate || !gs) return (
|
|
<div className="min-h-screen flex items-center justify-center bg-[#09090b]">
|
|
<motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1.5, ease: 'linear' }}>
|
|
<PixelBolt size={44} color={C.gold} />
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-full bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-white selection:bg-[#fda913]/30 relative pb-8">
|
|
{/* Ambient glows */}
|
|
<div className="fixed top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
|
|
<div className="absolute top-[-15%] left-[-10%] w-[55%] h-[55%] bg-purple-200/30 dark:bg-purple-900/10 rounded-full blur-[140px]" />
|
|
<div className="absolute bottom-[-15%] right-[-10%] w-[55%] h-[55%] bg-blue-200/30 dark:bg-blue-900/10 rounded-full blur-[140px]" />
|
|
<motion.div className="absolute top-[20%] right-[15%] w-[30%] h-[30%] rounded-full blur-[120px]"
|
|
style={{ background: `${C.gold}06` }}
|
|
animate={{ opacity: [0.3, 0.7, 0.3] }} transition={{ repeat: Infinity, duration: 5, ease: 'easeInOut' }} />
|
|
</div>
|
|
|
|
<FloatingXPManager />
|
|
<LevelUpModal />
|
|
|
|
{/* ── Modals ── */}
|
|
<AnimatePresence>
|
|
{logAction && <LogActionModal actionKey={logAction} onClose={() => setLogAction(null)} />}
|
|
{showChallenges && <ChallengesModal onClose={() => setShowChallenges(false)} gamestate={gs} />}
|
|
{showAchievements && <AchievementsModal onClose={() => setShowAchievements(false)} badges={oldGamestate.badges} />}
|
|
</AnimatePresence>
|
|
|
|
<div className="relative z-10 p-4 md:p-6 lg:p-8 max-w-[1650px] mx-auto">
|
|
<motion.div variants={containerV} initial="hidden" animate="show" className="space-y-6">
|
|
|
|
{/* ══ HEADER ══ */}
|
|
<motion.header variants={itemV} className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 pb-4 border-b border-zinc-200 dark:border-white/5">
|
|
<div className="flex items-center gap-3">
|
|
<PixelBolt size={32} color={C.gold} />
|
|
<h1 className="font-['Barlow_Condensed'] font-black text-3xl tracking-tight text-zinc-900 dark:text-white">
|
|
LynkedUp <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#fda913] to-[#AAFF00]">Pro Canvas</span>
|
|
</h1>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{[{ label: 'Leaderboard', color: C.gold, onClick: () => { const el = document.getElementById('procanvas-leaderboard'); el?.scrollIntoView({ behavior: 'smooth' }); } },
|
|
{ label: 'Challenges', color: C.neon, onClick: () => setShowChallenges(true) },
|
|
{ label: 'Achievements', color: C.gold, onClick: () => setShowAchievements(true) },
|
|
].map((tab, i) => (
|
|
<motion.button key={tab.label} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}
|
|
onClick={tab.onClick}
|
|
className="font-['Barlow_Condensed'] font-bold text-xs uppercase tracking-wider px-4 py-2 rounded-lg transition-all cursor-pointer"
|
|
style={{ background: tab.color + '18', color: tab.color, border: `1px solid ${tab.color}33` }}>
|
|
{tab.label}
|
|
</motion.button>
|
|
))}
|
|
<div className="flex items-center gap-2.5 ml-3 pl-4 border-l border-zinc-200 dark:border-white/10">
|
|
<div className="w-9 h-9 rounded-full overflow-hidden" style={{ border: `2px solid ${C.gold}` }}>
|
|
<img src={mockDbUser.profilePhoto} alt="" className="w-full h-full object-cover" />
|
|
</div>
|
|
<div>
|
|
<p className="font-['Barlow_Condensed'] font-bold text-sm leading-none text-zinc-900 dark:text-white/90">{mockDbUser.name?.split(' ')[0]}</p>
|
|
<p className="font-mono font-bold text-xs" style={{ color: C.gold }}><AnimatedCounter to={gs.xp} /> XP</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.header>
|
|
|
|
{/* ══ TWO-COLUMN LAYOUT ══ */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
|
|
{/* Left: Player Card + Daily Missions (sticky sidebar, 4 cols) */}
|
|
<motion.div variants={itemV} className="lg:col-span-4 flex flex-col gap-6 lg:sticky lg:top-6 lg:self-start">
|
|
<OperatorProfile gamestate={gs} user={mockDbUser} badges={oldGamestate.badges} />
|
|
<LogActionCard onAction={setLogAction} />
|
|
<DailyMissions gamestate={gs} role={role} />
|
|
</motion.div>
|
|
|
|
{/* Right: Main content (8 cols) */}
|
|
<div className="lg:col-span-8 flex flex-col gap-6">
|
|
|
|
{/* Showdown + Streak (side by side) */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<motion.div variants={itemV}><ShowdownCard gamestate={gs} /></motion.div>
|
|
<motion.div variants={itemV}><StreakCard gamestate={gs} /></motion.div>
|
|
</div>
|
|
|
|
{/* Rewards & Checkpoints (full width) */}
|
|
<motion.div variants={itemV}>
|
|
<RewardsCheckpoints stages={oldGamestate.stages} />
|
|
</motion.div>
|
|
|
|
{/* Badges & Achievements (full width, clickable) */}
|
|
<motion.div variants={itemV} onClick={() => setShowAchievements(true)} className="cursor-pointer">
|
|
<BadgesAchievements badges={oldGamestate.badges} />
|
|
</motion.div>
|
|
|
|
{/* Leaderboard (full width) */}
|
|
<motion.div variants={itemV} id="procanvas-leaderboard">
|
|
<Leaderboard />
|
|
</motion.div>
|
|
|
|
{/* Admin/Owner */}
|
|
{role === 'OWNER' && <motion.div variants={itemV}><OwnerROIDashboard /></motion.div>}
|
|
{(role === 'ADMIN' || role === 'OWNER') && <motion.div variants={itemV}><AdminCommandCenter /></motion.div>}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ProCanvas() {
|
|
return (
|
|
<GamificationProvider>
|
|
<ProCanvasContent />
|
|
</GamificationProvider>
|
|
);
|
|
}
|