feat: ProCanvas refinements, gamification hooks, a11y updates, index.css polish, gitignore cleanup, and profile asset addition
This commit is contained in:
@@ -28,4 +28,5 @@ dist-ssr
|
||||
*.md
|
||||
!README.md
|
||||
!data_schema.txt
|
||||
!docs/**/*.md
|
||||
.env
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -4,7 +4,7 @@ import { useAuth } from './AuthContext';
|
||||
const GamificationContext = createContext();
|
||||
|
||||
const INITIAL_STATE = {
|
||||
xp: 3550,
|
||||
xp: 3360,
|
||||
level: 12,
|
||||
streak: 4,
|
||||
todayPoints: 150,
|
||||
|
||||
@@ -65,27 +65,43 @@ export const useGamification = (user, role = 'FIELD_AGENT') => {
|
||||
STAGES = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Stage 1: Groundwork",
|
||||
requirement: { type: "doorsKnocked", amount: 20 },
|
||||
reward: { type: "coupon", name: "$25 Uber Eats Card", icon: "uber" },
|
||||
progress: Math.min((user.doorsKnocked || 0), 20),
|
||||
goal: 20
|
||||
title: "Daily Grind",
|
||||
requirement: { type: "dailyQuests", amount: 5 },
|
||||
reward: { type: "xp", name: "+500 XP Boost", icon: "zap" },
|
||||
progress: 5,
|
||||
goal: 5
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Stage 2: Connections",
|
||||
requirement: { type: "appointmentsSet", amount: 10 },
|
||||
reward: { type: "giftcard", name: "$50 Amazon Card", icon: "amazon" },
|
||||
progress: Math.min((user.appointmentsSet || 0), 10),
|
||||
goal: 10
|
||||
title: "Door Warrior",
|
||||
requirement: { type: "doorsKnocked", amount: 40 },
|
||||
reward: { type: "coupon", name: "$25 Gift Card", icon: "gift" },
|
||||
progress: 40,
|
||||
goal: 40
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Stage 3: Rainmaker",
|
||||
requirement: { type: "leadsGained", amount: 25 },
|
||||
reward: { type: "cash", name: "$150 Performance Bonus", icon: "cash" },
|
||||
progress: Math.min((user.leadsGained || 0), 25),
|
||||
goal: 25
|
||||
title: "Lead Machine",
|
||||
requirement: { type: "leadsGained", amount: 10 },
|
||||
reward: { type: "cash", name: "$50 Cash Bonus", icon: "cash" },
|
||||
progress: 4,
|
||||
goal: 10
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Closer Mode",
|
||||
requirement: { type: "appointmentsSet", amount: 5 },
|
||||
reward: { type: "giftcard", name: "$100 Amazon", icon: "amazon" },
|
||||
progress: 0,
|
||||
goal: 5
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Legend Run",
|
||||
requirement: { type: "deals", amount: 3 },
|
||||
reward: { type: "cash", name: "$250 Bonus", icon: "cash" },
|
||||
progress: 0,
|
||||
goal: 3
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
/* Inter: "The Engineer" — body text, UI, navigation (already system-available, loaded for consistency) */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap');
|
||||
/* Barlow Condensed: "The Arcade Font" — section headers, action labels, sports display text */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,700;0,900;1,700;1,900&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
+570
-175
@@ -1,4 +1,5 @@
|
||||
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 {
|
||||
@@ -15,12 +16,12 @@ import LevelUpModal from '../components/ProCanvas/gamification/LevelUpModal';
|
||||
import { SpotlightCard } from '../components/SpotlightCard';
|
||||
import { AnimatedCounter } from '../components/AnimatedCounter';
|
||||
|
||||
// --- NEOMORPHIC STYLING CONSTANTS ---
|
||||
const NEO_PANEL_CLASS = "bg-zinc-900/40 backdrop-blur-3xl border border-white/5 shadow-[8px_8px_16px_rgba(0,0,0,0.4),-8px_-8px_16px_rgba(255,255,255,0.02)] rounded-2xl relative overflow-hidden transition-all duration-300";
|
||||
const NEON_GREEN = "text-[#39ff14] drop-shadow-[0_0_8px_rgba(57,255,20,0.4)]";
|
||||
const NEON_GOLD = "text-[#fda913] drop-shadow-[0_0_8px_rgba(253,169,19,0.4)]";
|
||||
const NEON_ORANGE = "text-[#ff4500] drop-shadow-[0_0_8px_rgba(255,69,0,0.4)]";
|
||||
const NEON_BLUE = "text-[#00f0ff] drop-shadow-[0_0_8px_rgba(0,240,255,0.4)]";
|
||||
// --- ARCADE SPORTS STYLING CONSTANTS ---
|
||||
const NEO_PANEL_CLASS = "bg-[#0a0d1a]/90 backdrop-blur-xl border-2 border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.7),inset_0_1px_0_rgba(255,255,255,0.07)] rounded-xl relative overflow-hidden transition-all duration-300";
|
||||
const NEON_GREEN = "text-[#AAFF00] drop-shadow-[0_0_10px_rgba(170,255,0,0.6)]";
|
||||
const NEON_GOLD = "text-[#fda913] drop-shadow-[0_0_12px_rgba(253,169,19,0.7)]";
|
||||
const NEON_ORANGE = "text-[#ff4500] drop-shadow-[0_0_12px_rgba(255,69,0,0.7)]";
|
||||
const NEON_BLUE = "text-[#00f0ff] drop-shadow-[0_0_12px_rgba(0,240,255,0.7)]";
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// SHARED UI COMPONENTS
|
||||
@@ -28,16 +29,21 @@ const NEON_BLUE = "text-[#00f0ff] drop-shadow-[0_0_8px_rgba(0,240,255,0.4)]";
|
||||
|
||||
const NeoCard = ({ children, className = "", innerClassName = "", spotlightColor = "rgba(255, 255, 255, 0.05)" }) => (
|
||||
<SpotlightCard className={`${NEO_PANEL_CLASS} h-full w-full ${className}`} spotlightColor={spotlightColor}>
|
||||
{/* Arcade corner bracket accents */}
|
||||
<div className="absolute top-0 left-0 w-6 h-6 border-t-2 border-l-2 border-[#fda913]/50 z-20 pointer-events-none" />
|
||||
<div className="absolute top-0 right-0 w-6 h-6 border-t-2 border-r-2 border-[#fda913]/50 z-20 pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 w-6 h-6 border-b-2 border-l-2 border-[#fda913]/50 z-20 pointer-events-none" />
|
||||
<div className="absolute bottom-0 right-0 w-6 h-6 border-b-2 border-r-2 border-[#fda913]/50 z-20 pointer-events-none" />
|
||||
<div className={`relative z-10 w-full h-full p-4 md:p-6 flex flex-col ${innerClassName}`}>
|
||||
{children}
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
);
|
||||
|
||||
const ProgressBar = ({ progress, goal, colorClass = "bg-[#39ff14]", shadowClass = "shadow-[0_0_10px_#39ff14]" }) => {
|
||||
const ProgressBar = ({ progress, goal, colorClass = "bg-[#AAFF00]", shadowClass = "shadow-[0_0_10px_#AAFF00]" }) => {
|
||||
const pct = Math.min((progress / goal) * 100, 100);
|
||||
return (
|
||||
<div className="h-2 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.6)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||
<div className="h-3 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.8)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
@@ -48,66 +54,289 @@ const ProgressBar = ({ progress, goal, colorClass = "bg-[#39ff14]", shadowClass
|
||||
);
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// HOOKS
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
const useMonthResetCountdown = () => {
|
||||
const [timeLeft, setTimeLeft] = useState('');
|
||||
useEffect(() => {
|
||||
const calc = () => {
|
||||
const now = new Date();
|
||||
const end = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
||||
const diff = end.getTime() - now.getTime();
|
||||
if (diff <= 0) { setTimeLeft('Resetting...'); return; }
|
||||
const d = Math.floor(diff / 86400000);
|
||||
const h = Math.floor((diff % 86400000) / 3600000);
|
||||
const m = Math.floor((diff % 3600000) / 60000);
|
||||
const s = Math.floor((diff % 60000) / 1000);
|
||||
setTimeLeft(`${d}d ${String(h).padStart(2, '0')}h ${String(m).padStart(2, '0')}m ${String(s).padStart(2, '0')}s`);
|
||||
};
|
||||
calc();
|
||||
const id = setInterval(calc, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ROLE-AGNOSTIC WIDGETS
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
const OperatorProfile = ({ gamestate, user, className = "" }) => {
|
||||
const RANK_TIERS = {
|
||||
'Rookie': { color: '#CD7F32', glow: 'rgba(205,127,50,0.55)', label: 'BRONZE' },
|
||||
'Field Runner': { color: '#A8A9AD', glow: 'rgba(168,169,173,0.55)', label: 'SILVER' },
|
||||
'Roof Warrior': { color: '#fda913', glow: 'rgba(253,169,19,0.65)', label: 'GOLD' },
|
||||
'Territory Titan': { color: '#A855F7', glow: 'rgba(168,85,247,0.65)', label: 'EPIC' },
|
||||
'Legendary Closer': { color: '#AAFF00', glow: 'rgba(170,255,0,0.65)', label: 'LEGENDARY' },
|
||||
};
|
||||
const RANK_PROGRESSION = ['Rookie', 'Field Runner', 'Roof Warrior', 'Territory Titan', 'Legendary Closer'];
|
||||
const getNextRankTitle = (currentTitle) => {
|
||||
const idx = RANK_PROGRESSION.indexOf(currentTitle);
|
||||
return idx >= 0 && idx < RANK_PROGRESSION.length - 1 ? RANK_PROGRESSION[idx + 1] : 'MAX RANK';
|
||||
};
|
||||
|
||||
const BADGE_CONFIG = {
|
||||
default: { color: '#00f0ff', glow: 'rgba(0,240,255,0.65)' },
|
||||
Hunter: { color: '#ff4500', glow: 'rgba(255,69,0,0.65)' },
|
||||
Storm: { color: '#fda913', glow: 'rgba(253,169,19,0.65)' },
|
||||
Star: { color: '#FF1493', glow: 'rgba(255,20,147,0.65)' },
|
||||
Master: { color: '#AAFF00', glow: 'rgba(170,255,0,0.65)' },
|
||||
};
|
||||
|
||||
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 displayBadges = badges.slice(0, 3);
|
||||
const PIN_TILTS = [-7, 0, 7];
|
||||
|
||||
const getBadgeIcon = (badge) => {
|
||||
if (badge.id?.includes("Hunter")) return Crosshair;
|
||||
if (badge.id?.includes("Storm")) return Zap;
|
||||
if (badge.id?.includes("Star")) return Star;
|
||||
if (badge.id?.includes("Master")) return Map;
|
||||
return Shield;
|
||||
};
|
||||
|
||||
return (
|
||||
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" className={className} innerClassName="justify-center">
|
||||
<div className="flex flex-col md:flex-row items-center gap-6 md:gap-8">
|
||||
<div className="relative w-28 h-28 md:w-32 md:h-32 flex-shrink-0">
|
||||
<motion.svg
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute inset-0 w-full h-full text-[#fda913]/30"
|
||||
viewBox="0 0 100 100"
|
||||
<div className="flex flex-col md:flex-row items-center gap-6 md:gap-10">
|
||||
|
||||
{/* ── PLAYER CARD PORTRAIT ── */}
|
||||
<div className="relative flex-shrink-0">
|
||||
{/* Trading card frame */}
|
||||
<div
|
||||
className="relative overflow-hidden rounded-xl"
|
||||
style={{
|
||||
width: '130px',
|
||||
aspectRatio: '3/4',
|
||||
border: `3px solid ${tier.color}`,
|
||||
boxShadow: `0 0 22px ${tier.glow}, 0 0 60px ${tier.glow.replace(/[\d.]+\)$/, '0.18)')}, 0 8px 24px rgba(0,0,0,0.6)`,
|
||||
}}
|
||||
>
|
||||
<circle cx="50" cy="50" r="48" fill="none" stroke="currentColor" strokeWidth="1" strokeDasharray="4 4" />
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="20 10" />
|
||||
</motion.svg>
|
||||
<div className="absolute inset-3 rounded-full bg-zinc-900 border border-white/10 shadow-[inset_4px_4px_10px_rgba(0,0,0,0.8),inset_-4px_-4px_10px_rgba(255,255,255,0.05)] flex items-center justify-center overflow-hidden">
|
||||
<span className="text-3xl md:text-4xl font-black text-transparent bg-clip-text bg-gradient-to-br from-white to-zinc-500 md:translate-y-0.5">
|
||||
{user.name.split(' ').map(n => n[0]).join('')}
|
||||
</span>
|
||||
{/* Rank tier ribbon at top */}
|
||||
<div className="absolute top-0 inset-x-0 flex justify-center z-10 pt-1">
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-bold text-[10px] tracking-[0.2em] uppercase px-3 py-0.5"
|
||||
style={{
|
||||
color: tier.color,
|
||||
background: 'rgba(0,0,0,0.75)',
|
||||
borderBottom: `1px solid ${tier.color}40`,
|
||||
}}
|
||||
>{tier.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Photo or initials */}
|
||||
{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 relative" style={{ background: '#070A18' }}>
|
||||
{/* Diagonal stripe pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.12]"
|
||||
style={{
|
||||
backgroundImage: `repeating-linear-gradient(45deg, ${tier.color} 0px, ${tier.color} 1.5px, transparent 1.5px, transparent 14px)`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-bold relative z-10 leading-none"
|
||||
style={{
|
||||
fontSize: '64px',
|
||||
color: tier.color,
|
||||
textShadow: `0 0 30px ${tier.glow}, 0 0 60px ${tier.glow.replace(/[\d.]+\)$/, '0.3)')}`,
|
||||
}}
|
||||
>{initials}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gloss overlay — top catch light + bottom vignette */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-white/[0.09] via-transparent to-black/50 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* ── BADGE PINS — overlap portrait bottom edge ── */}
|
||||
{displayBadges.length > 0 && (
|
||||
<div className="absolute -bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-20">
|
||||
{displayBadges.map((badge, i) => {
|
||||
const BadgeIcon = getBadgeIcon(badge);
|
||||
const isUnlocked = badge.status === 'unlocked';
|
||||
return (
|
||||
<div
|
||||
key={badge.id || i}
|
||||
title={badge.title}
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center flex-shrink-0 cursor-default"
|
||||
style={{
|
||||
transform: `rotate(${PIN_TILTS[i]}deg)`,
|
||||
background: isUnlocked
|
||||
? `radial-gradient(circle at 35% 30%, #232640, #0a0d1a)`
|
||||
: 'radial-gradient(circle at 35% 30%, #1a1a1a, #0a0a0a)',
|
||||
border: `2.5px solid ${isUnlocked ? tier.color : '#333'}`,
|
||||
boxShadow: isUnlocked
|
||||
? `0 0 10px ${tier.glow}, inset 0 1px 0 rgba(255,255,255,0.18)`
|
||||
: 'none',
|
||||
opacity: isUnlocked ? 1 : 0.35,
|
||||
filter: isUnlocked ? 'none' : 'grayscale(1)',
|
||||
}}
|
||||
>
|
||||
<BadgeIcon size={14} style={{ color: isUnlocked ? tier.color : '#555' }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-center md:text-left z-10 w-full flex flex-col justify-center">
|
||||
<div className="flex justify-between items-start mb-4 gap-4 md:gap-8 w-full">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-[10px] font-mono text-zinc-500 tracking-[0.2em] uppercase truncate">Op ID: {user.empId || 'UNK-99'}</p>
|
||||
<div className="flex items-center gap-1 bg-orange-500/10 text-orange-500 px-2 py-0.5 rounded-full border border-orange-500/20">
|
||||
<motion.div animate={{ scale: [1, 1.2, 1] }} transition={{ repeat: Infinity, duration: 2 }}>
|
||||
<Flame size={12} className="fill-current drop-shadow-[0_0_5px_rgba(249,115,22,0.8)]" />
|
||||
</motion.div>
|
||||
<span className="text-[10px] font-bold font-mono">{gamestate.streak} Day Streak</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-black text-white tracking-wide truncate">{user.name}</h2>
|
||||
</div>
|
||||
<div className="flex flex-col items-end flex-shrink-0">
|
||||
<span className={`text-xl font-black ${NEON_GOLD}`}>LVL <AnimatedCounter to={gamestate.level} duration={1} /></span>
|
||||
<span className="text-xs font-mono text-zinc-400 tracking-widest uppercase whitespace-nowrap">{gamestate.currentTitle}</span>
|
||||
{/* ── INFO COLUMN ── */}
|
||||
<div className="flex-1 w-full flex flex-col gap-2.5 text-center md:text-left mt-2 md:mt-0">
|
||||
|
||||
{/* Row 1: Player ID + Level badge */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-mono text-zinc-500 tracking-[0.2em] uppercase">
|
||||
Player ID: {user.empId || 'P-99'}
|
||||
</p>
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-bold text-xl leading-none"
|
||||
style={{ color: '#fda913', textShadow: '0 0 14px rgba(253,169,19,0.75)' }}
|
||||
>
|
||||
LVL <AnimatedCounter to={gamestate.level} duration={1} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Player name */}
|
||||
<h2
|
||||
className="font-['Barlow_Condensed'] font-bold italic text-white leading-none truncate"
|
||||
style={{ fontSize: 'clamp(1.6rem, 3.5vw, 2.4rem)' }}
|
||||
>
|
||||
{user.name.toUpperCase()}
|
||||
</h2>
|
||||
|
||||
{/* Row 3: RANK TITLE — primary status identity */}
|
||||
<div className="flex flex-col gap-0.5 justify-center md:justify-start">
|
||||
<h3
|
||||
className="font-['Barlow_Condensed'] font-black uppercase leading-none tracking-tight"
|
||||
style={{
|
||||
fontSize: 'clamp(1.35rem, 3vw, 2rem)',
|
||||
color: tier.color,
|
||||
textShadow: `0 0 24px ${tier.glow}, 0 0 60px ${tier.glow.replace(/[\d.]+\)$/, '0.28)')}, 0 2px 0 rgba(0,0,0,0.8)`,
|
||||
WebkitTextStroke: `0.5px ${tier.color}88`,
|
||||
}}
|
||||
>
|
||||
{gamestate.currentTitle}
|
||||
</h3>
|
||||
<p
|
||||
className="font-mono text-[9px] uppercase tracking-[0.28em] leading-none"
|
||||
style={{ color: tier.color, opacity: 0.55 }}
|
||||
>
|
||||
◈ {tier.label} TIER
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Day Streak combo strip */}
|
||||
<div className="flex items-center gap-3 bg-black/50 border border-[#ff4500]/25 rounded-lg px-4 py-2.5 shadow-[0_0_16px_rgba(255,69,0,0.08),inset_0_1px_0_rgba(255,255,255,0.04)]">
|
||||
<motion.div
|
||||
animate={{ scale: [1, 1.18, 1], rotate: [-8, 8, -8] }}
|
||||
transition={{ repeat: Infinity, duration: 1.6, ease: "easeInOut" }}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Flame
|
||||
size={36}
|
||||
className="fill-current"
|
||||
style={{
|
||||
color: '#ff4500',
|
||||
filter: 'drop-shadow(0 0 12px rgba(255,69,0,0.9)) drop-shadow(0 0 24px rgba(255,120,0,0.5))',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.span
|
||||
className="font-['Barlow_Condensed'] font-bold text-white leading-none flex-shrink-0"
|
||||
style={{ fontSize: '3.2rem', textShadow: '0 0 24px rgba(255,69,0,0.7)' }}
|
||||
initial={{ scale: 0.7 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 350, damping: 18 }}
|
||||
>
|
||||
<AnimatedCounter to={gamestate.streak} />
|
||||
</motion.span>
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="font-['Barlow_Condensed'] font-bold text-sm text-zinc-300 uppercase tracking-wide">Day</span>
|
||||
<span className="font-['Barlow_Condensed'] font-bold text-sm text-zinc-300 uppercase tracking-wide">Streak</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full mt-2">
|
||||
<div className="flex justify-between text-xs font-mono mb-2">
|
||||
<span className="text-zinc-400">XP <span className="text-white font-bold"><AnimatedCounter to={gamestate.xp} /></span></span>
|
||||
<span className="text-zinc-500">NEXT <span className="text-zinc-300">{gamestate.nextLevelXP.toLocaleString()}</span></span>
|
||||
{/* Row 5: XP section — big stats + bar */}
|
||||
<div className="w-full">
|
||||
{/* Stat callouts above bar */}
|
||||
<div className="flex justify-between items-end mb-2">
|
||||
{/* Total XP — left */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-mono text-[9px] uppercase tracking-widest text-zinc-500">Total XP</span>
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-black leading-none"
|
||||
style={{ fontSize: 'clamp(1.4rem, 2.8vw, 1.9rem)', color: '#ffffff', textShadow: '0 0 16px rgba(255,255,255,0.2)' }}
|
||||
>
|
||||
<AnimatedCounter to={gamestate.xp} /><span className="text-xs text-zinc-400 ml-1 font-bold">XP</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* XP to go — right */}
|
||||
<div className="flex flex-col gap-0.5 items-end">
|
||||
<span className="font-mono text-[9px] uppercase tracking-widest text-zinc-500">To Level Up</span>
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-black leading-none"
|
||||
style={{
|
||||
fontSize: 'clamp(1.4rem, 2.8vw, 1.9rem)',
|
||||
color: '#ff4500',
|
||||
textShadow: '0 0 18px rgba(255,69,0,0.7), 0 0 40px rgba(255,69,0,0.25)',
|
||||
}}
|
||||
>
|
||||
{Math.max(0, (gamestate.nextLevelXP || 0) - gamestate.xp).toLocaleString()}<span className="text-xs ml-1 font-bold" style={{ color: '#ff4500' }}>XP</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Using context's exact progressPercent for precision inside the progress bar */}
|
||||
<div className="h-2 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.6)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-3.5 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.8)] border border-white/5 relative overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${gamestate.progressPercent}%` }}
|
||||
transition={{ type: "spring", stiffness: 50, damping: 15 }}
|
||||
className={`absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-amber-600 to-[#fda913] shadow-[0_0_15px_#fda913]`}
|
||||
className="absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-amber-600 to-[#fda913] shadow-[0_0_20px_#fda913]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Below bar: cap XP + next rank */}
|
||||
<div className="flex justify-between items-center mt-1.5">
|
||||
<span className="font-mono text-[9px] text-zinc-700">/ {gamestate.nextLevelXP?.toLocaleString()} XP cap</span>
|
||||
<span
|
||||
className="font-['Barlow_Condensed'] font-bold text-xs uppercase tracking-wider"
|
||||
style={{ color: tier.color, opacity: 0.8 }}
|
||||
>
|
||||
→ {getNextRankTitle(gamestate.currentTitle)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</NeoCard>
|
||||
@@ -147,12 +376,12 @@ const TacticalMissions = ({ role, gamestate, className = "" }) => {
|
||||
<AnimatedCounter to={current} /> / {target}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.6)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||
<div className="h-2.5 w-full bg-black/80 rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.8)] border border-white/5 relative overflow-hidden flex-shrink-0">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 1.5, type: "spring", stiffness: 50, damping: 15 }}
|
||||
className={`absolute top-0 left-0 h-full rounded-full ${isComplete ? 'bg-[#39ff14] shadow-[0_0_10px_#39ff14]' : `bg-gradient-to-r ${colorClass}`}`}
|
||||
className={`absolute top-0 left-0 h-full rounded-full ${isComplete ? 'bg-[#AAFF00] shadow-[0_0_10px_#AAFF00]' : `bg-gradient-to-r ${colorClass}`}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,11 +389,11 @@ const TacticalMissions = ({ role, gamestate, className = "" }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" className={className} innerClassName="justify-between h-full">
|
||||
<NeoCard spotlightColor="rgba(170,255,0,0.08)" className={className} innerClassName="justify-between h-full">
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="flex items-center gap-3 mb-6 flex-shrink-0">
|
||||
<Crosshair className={NEON_GREEN} size={24} />
|
||||
<h3 className="text-sm md:text-base font-mono font-bold text-white tracking-widest uppercase pb-1">Tactical Missions</h3>
|
||||
<Zap className={NEON_GREEN} size={22} />
|
||||
<h3 className="text-base md:text-lg font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">Daily Missions</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 flex flex-col justify-center min-h-0">
|
||||
@@ -195,19 +424,19 @@ const TacticalMissions = ({ role, gamestate, className = "" }) => {
|
||||
{role === 'ADMIN' ? (
|
||||
<>
|
||||
<MissionBar current={gamestate.dailyQuests?.knocks?.current || 0} target={gamestate.dailyQuests?.knocks?.target || 1} colorClass="from-blue-600 to-[#00f0ff]" label="Active Recruits" icon={Users} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#39ff14]" label="Territories Mapped" icon={Map} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#AAFF00]" label="Territories Mapped" icon={Map} />
|
||||
<MissionBar current={gamestate.dailyQuests?.inspections?.current || 0} target={gamestate.dailyQuests?.inspections?.target || 1} colorClass="from-purple-600 to-pink-500" label="Squad Reviews" icon={ClipboardCheck} />
|
||||
</>
|
||||
) : role === 'OWNER' ? (
|
||||
<>
|
||||
<MissionBar current={gamestate.dailyQuests?.knocks?.current || 0} target={gamestate.dailyQuests?.knocks?.target || 1} colorClass="from-amber-600 to-[#fda913]" label="Capital Deployed" icon={TrendingUp} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#39ff14]" label="New Franchises" icon={Map} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#AAFF00]" label="New Franchises" icon={Map} />
|
||||
<MissionBar current={gamestate.dailyQuests?.inspections?.current || 0} target={gamestate.dailyQuests?.inspections?.target || 1} colorClass="from-purple-600 to-pink-500" label="Exec Meetings" icon={Users} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MissionBar current={gamestate.dailyQuests?.knocks?.current || 0} target={gamestate.dailyQuests?.knocks?.target || 1} colorClass="from-blue-600 to-[#00f0ff]" label="Door Knocks" icon={Home} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#39ff14]" label="New Leads" icon={Users} />
|
||||
<MissionBar current={gamestate.dailyQuests?.leads?.current || 0} target={gamestate.dailyQuests?.leads?.target || 1} colorClass="from-emerald-600 to-[#AAFF00]" label="New Leads" icon={Users} />
|
||||
<MissionBar current={gamestate.dailyQuests?.inspections?.current || 0} target={gamestate.dailyQuests?.inspections?.target || 1} colorClass="from-purple-600 to-pink-500" label="Inspections" icon={ClipboardCheck} />
|
||||
</>
|
||||
)}
|
||||
@@ -217,13 +446,12 @@ const TacticalMissions = ({ role, gamestate, className = "" }) => {
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/emp/fa/maps')}
|
||||
className="w-full group mt-6 relative overflow-hidden rounded-xl bg-zinc-900 border border-[#39ff14]/30 p-4 shadow-[4px_4px_15px_rgba(0,0,0,0.5),-4px_-4px_15px_rgba(255,255,255,0.02)] transition-all hover:-translate-y-1 hover:shadow-[#39ff14]/20 flex-shrink-0"
|
||||
className="w-full group mt-6 relative overflow-hidden rounded-xl bg-gradient-to-r from-[#AAFF00]/15 to-[#AAFF00]/8 border-2 border-[#AAFF00]/50 p-4 shadow-[0_0_20px_rgba(170,255,0,0.12),inset_0_1px_0_rgba(170,255,0,0.2)] transition-all hover:-translate-y-1 hover:shadow-[0_0_30px_rgba(170,255,0,0.28)] hover:border-[#AAFF00]/70 flex-shrink-0"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#39ff14]/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<span className={`relative z-10 font-mono font-bold tracking-[0.2em] text-sm md:text-base uppercase flex justify-center w-full ${NEON_GREEN}`}>
|
||||
[ ENGAGE MAP ]
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#AAFF00]/0 via-[#AAFF00]/12 to-[#AAFF00]/0 -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
|
||||
<span className={`relative z-10 font-['Barlow_Condensed'] font-bold tracking-[0.25em] text-base md:text-lg uppercase flex justify-center items-center gap-2 w-full ${NEON_GREEN}`}>
|
||||
HIT THE MAP <span className="text-sm opacity-70">→</span>
|
||||
</span>
|
||||
<span className="absolute inset-0 border border-[#39ff14] rounded-xl animate-ping opacity-10" />
|
||||
</button>
|
||||
</div>
|
||||
</NeoCard>
|
||||
@@ -235,86 +463,229 @@ const CheckpointsSystem = ({ stages, role, className = "" }) => {
|
||||
if (role === 'ADMIN') headerTitle = "Squad Milestone Tracking";
|
||||
if (role === 'OWNER') headerTitle = "Program Milestones";
|
||||
|
||||
const completedCount = stages.filter(s => s.status === 'completed').length;
|
||||
const fillPct = stages.length > 1 ? (completedCount / (stages.length - 1)) * 100 : 0;
|
||||
|
||||
return (
|
||||
<NeoCard spotlightColor="rgba(0, 240, 255, 0.05)" className={className}>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<FastForward className={NEON_BLUE} size={24} />
|
||||
<h3 className="text-lg font-mono font-bold text-white tracking-widest uppercase">{headerTitle}</h3>
|
||||
<h3 className="text-xl font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">{headerTitle}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 relative w-full flex-1">
|
||||
{/* Connecting Line for Desktop */}
|
||||
<div className="hidden md:block absolute top-[28px] left-[15%] right-[15%] h-0.5 bg-zinc-800 rounded z-0" />
|
||||
{/* Level Map */}
|
||||
<div className="relative w-full">
|
||||
{/* Animated connecting path — desktop only */}
|
||||
<div className="hidden md:block absolute top-[27px] left-[10%] right-[10%] h-1 bg-white/[0.06] rounded-full z-0 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${fillPct}%` }}
|
||||
transition={{ duration: 1.4, ease: 'easeOut', delay: 0.3 }}
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #fda913 0%, #AAFF00 100%)',
|
||||
boxShadow: '0 0 10px rgba(170,255,0,0.45)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{stages.map((stage, idx) => {
|
||||
const isLocked = stage.status === 'locked';
|
||||
const isCompleted = stage.status === 'completed';
|
||||
const isInProgress = stage.status === 'in-progress';
|
||||
{/* Stage nodes */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-3 relative z-10">
|
||||
{stages.map((stage, idx) => {
|
||||
const isLocked = stage.status === 'locked';
|
||||
const isCompleted = stage.status === 'completed';
|
||||
const isActive = stage.status === 'in-progress';
|
||||
const pct = Math.min((stage.progress / stage.goal) * 100, 100);
|
||||
|
||||
return (
|
||||
<div key={stage.id} className="relative z-10 flex flex-col items-center md:items-start text-center md:text-left bg-black/40 md:bg-transparent p-4 md:p-0 rounded-xl md:rounded-none border border-white/5 md:border-none shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] md:shadow-none h-full">
|
||||
{/* Icon Node */}
|
||||
<div className={`w-14 h-14 flex-shrink-0 rounded-xl flex items-center justify-center mb-4 transition-all duration-500 mx-auto md:mx-0
|
||||
${isCompleted ? 'bg-[#39ff14]/20 text-[#39ff14] border-2 border-[#39ff14] shadow-[0_0_15px_#39ff14]' :
|
||||
isInProgress ? 'bg-amber-500/20 text-amber-500 border-2 border-amber-500 shadow-[0_0_15px_#fda913] animate-pulse' :
|
||||
'bg-zinc-900 text-zinc-600 border border-zinc-700'}
|
||||
`}>
|
||||
{isCompleted ? <CheckCircle size={24} /> : isLocked ? <Lock size={24} /> : <Target size={24} />}
|
||||
const nodeStyle = isCompleted
|
||||
? { background: 'rgba(253,169,19,0.18)', border: '2.5px solid #fda913', boxShadow: '0 0 20px rgba(253,169,19,0.55), 0 0 50px rgba(253,169,19,0.12)' }
|
||||
: isActive
|
||||
? { background: 'rgba(170,255,0,0.12)', border: '2.5px solid #AAFF00', boxShadow: '0 0 20px rgba(170,255,0,0.55), 0 0 50px rgba(170,255,0,0.12)' }
|
||||
: { background: '#0d0e18', border: '1.5px solid #1e1e2c', boxShadow: 'none' };
|
||||
|
||||
return (
|
||||
<div key={stage.id} className="flex flex-col items-center gap-2 text-center">
|
||||
{/* Circular node */}
|
||||
<motion.div
|
||||
className="relative w-14 h-14 rounded-full flex items-center justify-center flex-shrink-0"
|
||||
style={nodeStyle}
|
||||
animate={isActive ? { scale: [1, 1.05, 1] } : {}}
|
||||
transition={isActive ? { repeat: Infinity, duration: 2.2, ease: 'easeInOut' } : {}}
|
||||
>
|
||||
{/* Pulse ring for active stage */}
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full pointer-events-none"
|
||||
style={{ border: '2px solid rgba(170,255,0,0.45)' }}
|
||||
animate={{ scale: [1, 1.65], opacity: [0.7, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.8, ease: 'easeOut' }}
|
||||
/>
|
||||
)}
|
||||
{isCompleted ? (
|
||||
<CheckCircle size={22} style={{ color: '#fda913', filter: 'drop-shadow(0 0 7px rgba(253,169,19,0.9))' }} />
|
||||
) : isActive ? (
|
||||
<span className="font-['Barlow_Condensed'] font-black text-xl leading-none" style={{ color: '#AAFF00', textShadow: '0 0 10px rgba(170,255,0,0.8)' }}>{idx + 1}</span>
|
||||
) : (
|
||||
<Lock size={15} className="text-zinc-700" />
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Stage name */}
|
||||
<p className={`font-['Barlow_Condensed'] font-bold text-xs uppercase tracking-wide leading-tight ${isLocked ? 'text-zinc-700' : 'text-white'}`}>
|
||||
{stage.title}
|
||||
</p>
|
||||
|
||||
{/* Active: live progress bar */}
|
||||
{isActive && (
|
||||
<div className="w-full space-y-0.5">
|
||||
<div className="flex justify-between text-[9px] font-mono">
|
||||
<span className="text-zinc-600">{stage.progress}/{stage.goal}</span>
|
||||
<span style={{ color: '#AAFF00' }}>{Math.round(pct)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-black/60 rounded-full border border-white/5 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${pct}%` }}
|
||||
transition={{ duration: 1.4, ease: 'easeOut' }}
|
||||
style={{ background: 'linear-gradient(90deg, #fda913, #AAFF00)', boxShadow: '0 0 8px rgba(170,255,0,0.5)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCompleted && (
|
||||
<p className="text-[9px] font-mono text-zinc-600">{stage.progress}/{stage.goal} ✓</p>
|
||||
)}
|
||||
|
||||
{/* Reward chip */}
|
||||
<div
|
||||
className="mt-auto w-full flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-[9px] font-mono font-bold"
|
||||
style={{
|
||||
background: isCompleted ? 'rgba(253,169,19,0.1)' : isActive ? 'rgba(170,255,0,0.07)' : 'rgba(0,0,0,0.4)',
|
||||
border: `1px solid ${isCompleted ? 'rgba(253,169,19,0.3)' : isActive ? 'rgba(170,255,0,0.2)' : 'rgba(255,255,255,0.04)'}`,
|
||||
color: isCompleted ? '#fda913' : isActive ? 'rgba(170,255,0,0.75)' : '#2a2a38',
|
||||
}}
|
||||
>
|
||||
<Award size={10} className="flex-shrink-0" />
|
||||
<span className="truncate">{isLocked ? '???' : stage.reward.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="font-bold text-white mb-1">{stage.title}</h4>
|
||||
<p className="text-xs font-mono text-zinc-400 mb-3">{stage.progress} / {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||
|
||||
{/* Reward Box */}
|
||||
<div className={`px-3 py-2 mt-auto rounded flex items-center gap-2 text-[10px] font-mono w-full justify-center md:justify-start
|
||||
${isCompleted ? 'bg-[#39ff14]/10 border border-[#39ff14]/30 text-[#39ff14]' : 'bg-black/50 border border-white/5 text-zinc-500'}
|
||||
`}>
|
||||
<Award size={14} className="flex-shrink-0" />
|
||||
<span className="truncate">{stage.reward.name} {isLocked ? '(LOCKED)' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</NeoCard>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailedBadges = ({ badges, className = "" }) => {
|
||||
const getBadgeConfig = (badge) => {
|
||||
if (badge.id?.includes("Hunter")) return BADGE_CONFIG.Hunter;
|
||||
if (badge.id?.includes("Storm")) return BADGE_CONFIG.Storm;
|
||||
if (badge.id?.includes("Star")) return BADGE_CONFIG.Star;
|
||||
if (badge.id?.includes("Master")) return BADGE_CONFIG.Master;
|
||||
return BADGE_CONFIG.default;
|
||||
};
|
||||
const getBadgeIcon = (badge) => {
|
||||
if (badge.id?.includes("Hunter")) return Crosshair;
|
||||
if (badge.id?.includes("Storm")) return Zap;
|
||||
if (badge.id?.includes("Star")) return Star;
|
||||
if (badge.id?.includes("Master")) return Map;
|
||||
return Shield;
|
||||
};
|
||||
|
||||
return (
|
||||
<NeoCard className={className}>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Medal className={NEON_GOLD} size={24} />
|
||||
<h3 className="text-lg font-mono font-bold text-white tracking-widest uppercase">Commendations & Badges</h3>
|
||||
<h3 className="text-xl font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">Badges & Achievements</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-5 gap-4">
|
||||
{badges.map((badge, idx) => {
|
||||
const isUnlocked = badge.status === 'unlocked';
|
||||
|
||||
let Icon = Shield;
|
||||
if (badge.id.includes("Hunter")) Icon = Crosshair;
|
||||
if (badge.id.includes("Storm")) Icon = Zap;
|
||||
if (badge.id.includes("Star")) Icon = Star;
|
||||
if (badge.id.includes("Master")) Icon = Map;
|
||||
const config = getBadgeConfig(badge);
|
||||
const Icon = getBadgeIcon(badge);
|
||||
|
||||
return (
|
||||
<div key={idx} className={`p-4 rounded-xl flex gap-4 transition-all duration-300 h-full
|
||||
${isUnlocked ? 'bg-zinc-800/50 border border-amber-500/30 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)]' : 'bg-black/40 border border-white/5 opacity-60 grayscale'}`
|
||||
}>
|
||||
<div className={`w-12 h-12 rounded-lg flex-shrink-0 flex items-center justify-center
|
||||
${isUnlocked ? 'bg-black border border-amber-500/50 shadow-[0_0_10px_#fda913]' : 'bg-zinc-900 border border-zinc-700'}`
|
||||
}>
|
||||
<Icon size={20} className={isUnlocked ? NEON_GOLD : 'text-zinc-600'} />
|
||||
<motion.div
|
||||
key={idx}
|
||||
whileHover={isUnlocked ? { scale: 1.05, y: -6 } : { scale: 1.01 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 22 }}
|
||||
className="relative rounded-xl overflow-hidden flex flex-col cursor-default"
|
||||
style={{
|
||||
background: isUnlocked
|
||||
? `linear-gradient(160deg, #0f1430 0%, #0a0d1a 100%)`
|
||||
: '#0a0a12',
|
||||
border: `2px solid ${isUnlocked ? config.color + '55' : '#1e1e2a'}`,
|
||||
boxShadow: isUnlocked
|
||||
? `0 0 24px ${config.glow.replace('0.65', '0.1')}, inset 0 1px 0 rgba(255,255,255,0.06)`
|
||||
: 'none',
|
||||
opacity: isUnlocked ? 1 : 0.42,
|
||||
filter: isUnlocked ? 'none' : 'grayscale(0.85)',
|
||||
}}
|
||||
>
|
||||
{/* Top radial glow bleed for unlocked */}
|
||||
{isUnlocked && (
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
background: `radial-gradient(ellipse at 50% -10%, ${config.glow.replace('0.65', '0.22')} 0%, transparent 65%)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Status chip — top right */}
|
||||
<div className="absolute top-2.5 right-2.5 z-10">
|
||||
{isUnlocked
|
||||
? <CheckCircle size={13} style={{ color: config.color, filter: `drop-shadow(0 0 5px ${config.glow})` }} />
|
||||
: <Lock size={12} className="text-zinc-700" />
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-bold text-sm text-white truncate">{badge.title}</h4>
|
||||
{!isUnlocked && <Lock size={12} className="text-zinc-600 flex-shrink-0" />}
|
||||
|
||||
{/* Medallion */}
|
||||
<div className="flex justify-center pt-6 pb-2 relative z-10">
|
||||
<div
|
||||
className="relative w-16 h-16 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: isUnlocked
|
||||
? `radial-gradient(circle at 38% 32%, ${config.color}28 0%, #07091a 65%)`
|
||||
: 'radial-gradient(circle, #141418, #0a0a0f)',
|
||||
border: `2.5px solid ${isUnlocked ? config.color + '70' : '#222'}`,
|
||||
boxShadow: isUnlocked
|
||||
? `0 0 18px ${config.glow.replace('0.65', '0.45')}, 0 0 50px ${config.glow.replace('0.65', '0.12')}, inset 0 1px 0 rgba(255,255,255,0.14)`
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{isUnlocked && (
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-b from-white/[0.11] to-transparent pointer-events-none" />
|
||||
)}
|
||||
<Icon
|
||||
size={28}
|
||||
style={{
|
||||
color: isUnlocked ? config.color : '#2a2a30',
|
||||
filter: isUnlocked ? `drop-shadow(0 0 8px ${config.glow})` : 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] md:text-xs text-zinc-400 font-mono mb-2 leading-tight line-clamp-2">{badge.desc}</p>
|
||||
<p className="text-[9px] text-[#39ff14] font-mono tracking-widest uppercase opacity-80 truncate">CRITERIA: {badge.criteria}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badge text */}
|
||||
<div className="px-3 pb-5 pt-1 flex flex-col gap-1 text-center relative z-10">
|
||||
<h4
|
||||
className="font-['Barlow_Condensed'] font-bold text-base leading-tight"
|
||||
style={{
|
||||
color: isUnlocked ? 'white' : '#3a3a45',
|
||||
textShadow: isUnlocked ? `0 0 14px ${config.glow.replace('0.65', '0.35')}` : 'none',
|
||||
}}
|
||||
>{badge.title}</h4>
|
||||
<p className="text-[10px] text-zinc-600 font-mono leading-snug line-clamp-2">{badge.desc}</p>
|
||||
<span
|
||||
className="text-[9px] font-mono tracking-widest uppercase mt-0.5 line-clamp-1"
|
||||
style={{ color: isUnlocked ? config.color + 'bb' : '#2a2a30' }}
|
||||
>{badge.criteria}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -324,13 +695,18 @@ const DetailedBadges = ({ badges, className = "" }) => {
|
||||
|
||||
const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className = "" }) => {
|
||||
const { leaderboard } = useGamification();
|
||||
const countdown = useMonthResetCountdown();
|
||||
|
||||
return (
|
||||
<NeoCard className={className} innerClassName="flex flex-col h-full">
|
||||
<div className="flex flex-shrink-0 items-center justify-between mb-6">
|
||||
<h3 className="text-sm md:text-base font-mono text-zinc-400 font-bold tracking-widest uppercase flex items-center gap-2">
|
||||
<Icon size={18} className={NEON_BLUE} /> {title}
|
||||
<div className="flex flex-shrink-0 items-center justify-between mb-5">
|
||||
<h3 className="text-base md:text-lg font-['Barlow_Condensed'] text-white font-bold tracking-wide uppercase flex items-center gap-2">
|
||||
<Icon size={28} className={NEON_BLUE} /> {title}
|
||||
</h3>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<span className="text-[8px] font-mono text-zinc-700 uppercase tracking-widest">Resets in</span>
|
||||
<span className="text-[11px] font-mono font-bold" style={{ color: '#fda913', textShadow: '0 0 8px rgba(253,169,19,0.45)' }}>{countdown}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden relative min-h-[300px]">
|
||||
@@ -352,9 +728,13 @@ const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className
|
||||
className={`group relative flex items-center justify-between p-3 md:p-4 rounded-xl transition-all duration-300
|
||||
${isUser
|
||||
? 'bg-blue-600/20 border border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.2)]'
|
||||
: isTop3
|
||||
? 'bg-black/40 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] border border-white/5'
|
||||
: 'bg-transparent border border-transparent hover:bg-white/5 border-b-white/5'}`}
|
||||
: rank === 1
|
||||
? 'bg-[#fda913]/[0.07] border border-[#fda913]/25'
|
||||
: rank === 2
|
||||
? 'bg-[#A8A9AD]/[0.04] border border-[#A8A9AD]/10'
|
||||
: rank === 3
|
||||
? 'bg-[#CD7F32]/[0.06] border border-[#CD7F32]/15'
|
||||
: 'bg-transparent border border-transparent hover:bg-white/5 border-b-white/5'}`}
|
||||
>
|
||||
{isUser && (
|
||||
<motion.div
|
||||
@@ -364,13 +744,19 @@ const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-3 md:gap-4 flex-1 min-w-0 z-10 relative">
|
||||
<span className={`font-mono font-bold w-6 text-lg md:text-xl flex-shrink-0 ${rank === 1 ? NEON_GOLD : rank === 2 ? 'text-zinc-300' : rank === 3 ? 'text-amber-700' : 'text-zinc-600'}`}>
|
||||
0{rank}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<span className={`font-bold text-sm md:text-base block truncate ${isUser ? 'text-blue-400' : 'text-white'}`}>{agent.name}</span>
|
||||
<span className="text-[9px] md:text-[10px] text-zinc-500 uppercase font-mono tracking-widest block truncate">{agent.role}</span>
|
||||
</div>
|
||||
{isTop3 ? (
|
||||
<Trophy
|
||||
size={rank === 1 ? 24 : rank === 2 ? 20 : 17}
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
color: rank === 1 ? '#fda913' : rank === 2 ? '#A8A9AD' : '#CD7F32',
|
||||
filter: `drop-shadow(0 0 ${rank === 1 ? 8 : 5}px ${rank === 1 ? 'rgba(253,169,19,0.8)' : rank === 2 ? 'rgba(168,169,173,0.6)' : 'rgba(205,127,50,0.6)'})`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-mono font-bold w-6 text-sm flex-shrink-0 text-zinc-600">{String(rank).padStart(2, '0')}</span>
|
||||
)}
|
||||
<span className={`font-bold text-sm md:text-base truncate ${isUser ? 'text-blue-400' : 'text-white'}`}>{agent.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 font-mono text-xs md:text-sm flex-shrink-0 z-10 relative">
|
||||
<span className={`font-bold ${isUser ? 'text-white' : 'text-zinc-300'}`}><AnimatedCounter to={agent.points} /> XP</span>
|
||||
@@ -395,8 +781,8 @@ const AdminCommandCenter = ({ className = "" }) => {
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Activity className={NEON_BLUE} size={28} />
|
||||
<div>
|
||||
<h2 className="text-xl md:text-2xl font-black text-white tracking-widest uppercase">Global Control</h2>
|
||||
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.2em]">Administrative Overseer Panel</p>
|
||||
<h2 className="text-2xl md:text-3xl font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">Command Center</h2>
|
||||
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.15em]">Season Operations Overview</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -405,7 +791,7 @@ const AdminCommandCenter = ({ className = "" }) => {
|
||||
<p className="text-[10px] md:text-xs font-mono text-zinc-400 tracking-widest mb-2">ACTIVE SQUADS</p>
|
||||
<div className="flex items-end gap-3">
|
||||
<span className={`text-3xl md:text-4xl font-black ${NEON_BLUE}`}><AnimatedCounter to={12} /></span>
|
||||
<span className="text-[10px] md:text-sm font-mono text-[#39ff14] pb-1">All Online</span>
|
||||
<span className="text-[10px] md:text-sm font-mono text-[#AAFF00] pb-1">All Online</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -413,7 +799,7 @@ const AdminCommandCenter = ({ className = "" }) => {
|
||||
<p className="text-[10px] md:text-xs font-mono text-zinc-400 tracking-widest mb-2">DOORS KNOCKED TODAY</p>
|
||||
<div className="flex items-end gap-3">
|
||||
<span className={`text-3xl md:text-4xl font-black ${NEON_GOLD}`}><AnimatedCounter to={1432} /></span>
|
||||
<span className="text-[10px] md:text-sm font-mono text-[#39ff14] pb-1">+14% vs Yday</span>
|
||||
<span className="text-[10px] md:text-sm font-mono text-[#AAFF00] pb-1">+14% vs Yday</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -438,8 +824,8 @@ const OwnerROIDashboard = ({ className = "" }) => {
|
||||
<div className="flex items-center gap-3">
|
||||
<TrendingUp className={NEON_GOLD} size={28} />
|
||||
<div>
|
||||
<h2 className="text-xl md:text-2xl font-black text-white tracking-widest uppercase">Executive ROI</h2>
|
||||
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.2em]">Gamification Investment Returns</p>
|
||||
<h2 className="text-2xl md:text-3xl font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">Executive ROI</h2>
|
||||
<p className="font-mono text-[10px] md:text-xs text-zinc-500 tracking-[0.15em]">Program Performance</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-3 bg-black/50 border border-amber-500/20 rounded-lg flex items-center gap-4 shadow-[inset_0_2px_10px_rgba(0,0,0,0.5)] w-full md:w-auto overflow-hidden">
|
||||
@@ -453,7 +839,7 @@ const OwnerROIDashboard = ({ className = "" }) => {
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between text-[10px] md:text-xs font-mono mb-2">
|
||||
<span className="text-zinc-400">TEAM PARTICIPATION</span>
|
||||
<span className="text-[#39ff14]"><AnimatedCounter to={92} />%</span>
|
||||
<span className="text-[#AAFF00]"><AnimatedCounter to={92} />%</span>
|
||||
</div>
|
||||
<ProgressBar progress={92} goal={100} />
|
||||
</div>
|
||||
@@ -491,10 +877,10 @@ const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-
|
||||
const { logKnock, logLead, logInspection } = useGamification();
|
||||
|
||||
return (
|
||||
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" className={className} innerClassName="flex flex-col h-full">
|
||||
<NeoCard spotlightColor="rgba(170,255,0,0.08)" className={className} innerClassName="flex flex-col h-full">
|
||||
<div className="flex items-center gap-3 mb-4 flex-shrink-0">
|
||||
<Zap className={NEON_GREEN} size={20} />
|
||||
<h3 className="text-sm md:text-base font-mono font-bold text-white tracking-widest uppercase">Quick Operations Log</h3>
|
||||
<h3 className="text-base md:text-lg font-['Barlow_Condensed'] font-bold text-white tracking-wide uppercase">Quick Actions</h3>
|
||||
</div>
|
||||
<div className={`w-full ${gridClassName}`}>
|
||||
<button
|
||||
@@ -503,8 +889,8 @@ const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-
|
||||
const isHighValue = e.shiftKey || Math.random() > 0.75;
|
||||
logKnock(isHighValue, { x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-[#39ff14]/50 hover:shadow-[0_0_15px_rgba(57,255,20,0.2)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||
<Map size={18} className="text-zinc-500 group-hover:text-[#39ff14] transition-colors flex-shrink-0" />
|
||||
className="h-full p-1.5 md:p-2 rounded-xl bg-black/40 border border-white/5 hover:bg-zinc-800 hover:border-[#AAFF00]/50 hover:shadow-[0_0_15px_rgba(170,255,0,0.25)] transition-all flex flex-col items-center justify-center gap-1 md:gap-2 group cursor-crosshair min-h-0">
|
||||
<Map size={18} className="text-zinc-500 group-hover:text-[#AAFF00] transition-colors flex-shrink-0" />
|
||||
<span className="text-[9px] md:text-[10px] text-center font-mono text-zinc-400 group-hover:text-white transition-colors truncate w-full px-1">Door Knock</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -531,11 +917,11 @@ const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-
|
||||
const ScanUpload = ({ className = "" }) => {
|
||||
return (
|
||||
<NeoCard className={className} innerClassName="flex flex-col items-center justify-center text-center h-full group cursor-pointer w-full">
|
||||
<div className="w-16 h-16 rounded-full bg-zinc-900 shadow-[inset_4px_4px_8px_rgba(0,0,0,0.5),inset_-4px_-4px_8px_rgba(255,255,255,0.05)] flex items-center justify-center mb-4 group-hover:shadow-[0_0_15px_#39ff14] transition-all duration-300 transform group-hover:-translate-y-2">
|
||||
<UploadCloud size={28} className="text-zinc-500 group-hover:text-[#39ff14] transition-colors" />
|
||||
<div className="w-16 h-16 rounded-full bg-zinc-900 shadow-[inset_4px_4px_8px_rgba(0,0,0,0.5),inset_-4px_-4px_8px_rgba(255,255,255,0.05)] flex items-center justify-center mb-4 group-hover:shadow-[0_0_15px_#AAFF00] transition-all duration-300 transform group-hover:-translate-y-2">
|
||||
<UploadCloud size={28} className="text-zinc-500 group-hover:text-[#AAFF00] transition-colors" />
|
||||
</div>
|
||||
<h4 className="font-mono text-sm tracking-widest text-white mb-2">SCAN UPLOAD</h4>
|
||||
<p className="text-[10px] text-zinc-500 font-mono mb-0">DRAG RECON INTEL HERE (+500XP)</p>
|
||||
<p className="text-[10px] text-zinc-500 font-mono mb-0">DROP FILES TO EARN +500 XP</p>
|
||||
</NeoCard>
|
||||
);
|
||||
};
|
||||
@@ -550,68 +936,77 @@ function ProCanvasContent() {
|
||||
|
||||
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']
|
||||
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 newGamestate = useGamification();
|
||||
|
||||
if (!oldGamestate || !newGamestate) return <div className="p-8 text-white font-mono flex items-center justify-center h-screen bg-[#050505]">INITIALIZING HUD...</div>;
|
||||
if (!oldGamestate || !newGamestate) return <div className="p-8 text-white font-mono flex items-center justify-center h-screen bg-[#060812]">LOADING PLAYER DATA...</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#09090b] p-4 md:p-8 overflow-y-auto selection:bg-[#fda913] selection:text-black relative">
|
||||
<div className="min-h-full bg-[#060812] p-4 md:p-8 overflow-y-auto selection:bg-[#fda913] selection:text-black relative">
|
||||
|
||||
{/* New Gamification Overlay Components */}
|
||||
<FloatingXPManager />
|
||||
<LevelUpModal />
|
||||
|
||||
{/* Hex Grid Background & Scanlines */}
|
||||
{/* Retro Arcade Background */}
|
||||
<div className="fixed inset-0 pointer-events-none z-0">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<svg className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="hexagons" width="50" height="43.4" patternUnits="userSpaceOnUse" patternTransform="scale(2)">
|
||||
<path d="M25 0 L50 14.4 L50 43.3 L25 57.7 L0 43.3 L0 14.4 Z" fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="1" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#hexagons)" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* CSS Scanline Overlay */}
|
||||
<div className="absolute inset-0 opacity-[0.03] bg-[linear-gradient(to_bottom,transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]" />
|
||||
{/* Halftone dot grid */}
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.055) 1px, transparent 1px)',
|
||||
backgroundSize: '28px 28px'
|
||||
}} />
|
||||
{/* Warm orange-red bloom from bottom — fire/energy */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_105%,rgba(255,75,0,0.07)_0%,transparent_55%)]" />
|
||||
{/* Cool cyan bloom from top — arena lights */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_-10%,rgba(0,240,255,0.05)_0%,transparent_50%)]" />
|
||||
{/* Subtle vignette */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_50%,transparent_50%,rgba(0,0,0,0.4)_100%)]" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto space-y-6 md:space-y-8 relative z-10 w-full">
|
||||
|
||||
<header className="mb-6 md:mb-10 flex flex-col md:flex-row justify-between items-start md:items-end gap-4 border-b border-white/10 pb-6 w-full">
|
||||
<header className="mb-6 md:mb-10 flex flex-col md:flex-row justify-between items-start md:items-end gap-4 border-b-2 border-white/[0.08] pb-6 w-full">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-zinc-300 to-zinc-600 tracking-tighter drop-shadow-xl">
|
||||
PRO<span className="text-[#fda913]">CANVAS</span>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white to-zinc-400 tracking-tighter leading-none">
|
||||
PRO<span className="text-[#fda913] drop-shadow-[0_0_24px_rgba(253,169,19,0.5)]">CANVAS</span>
|
||||
</h1>
|
||||
<p className="font-mono text-zinc-500 text-xs md:text-sm tracking-widest uppercase mt-2">
|
||||
{role === 'FIELD_AGENT' ? 'Tactical HUD Controller' : role === 'ADMIN' ? 'Command Center Dashboard' : 'Executive ROI Overview'}
|
||||
<p className="font-['Barlow_Condensed'] font-bold text-zinc-400 text-sm md:text-base tracking-[0.15em] uppercase mt-2">
|
||||
{role === 'FIELD_AGENT' ? 'Player Season Dashboard' : role === 'ADMIN' ? 'Command Center' : 'Executive Overview'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="font-mono text-[9px] md:text-[10px] text-[#39ff14] flex flex-col md:items-end opacity-70 bg-black/40 p-2 md:p-3 rounded border border-[#39ff14]/20 shadow-[0_0_10px_rgba(57,255,20,0.1)] w-full md:w-auto">
|
||||
<span className="flex justify-between md:justify-end gap-8 w-full">SYS_STATUS: <span>ONLINE</span></span>
|
||||
<span className="flex justify-between md:justify-end gap-8 w-full">NET_SECURE: <span>TRUE</span></span>
|
||||
<span className="flex justify-between md:justify-end gap-8 w-full text-zinc-500">AUTH_LVL: <span>{role}</span></span>
|
||||
{/* Season Badge — sports broadcast style */}
|
||||
<div className="flex items-center gap-4 bg-[#0a0d1a] border-2 border-[#fda913]/30 rounded-xl px-4 py-3 shadow-[0_0_24px_rgba(253,169,19,0.08),inset_0_1px_0_rgba(255,255,255,0.06)] w-full md:w-auto">
|
||||
<Trophy size={22} className="text-[#fda913] drop-shadow-[0_0_10px_rgba(253,169,19,0.7)] flex-shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-['Barlow_Condensed'] font-bold text-white text-sm md:text-base tracking-wider uppercase leading-tight">Season 2026</span>
|
||||
<span className="font-mono text-[10px] text-[#AAFF00] drop-shadow-[0_0_6px_rgba(170,255,0,0.5)]">● LIVE · Week 8</span>
|
||||
</div>
|
||||
<div className="ml-auto md:ml-6 flex flex-col items-end border-l border-white/10 pl-4">
|
||||
<span className="font-mono text-[9px] text-zinc-500 uppercase tracking-widest">Status</span>
|
||||
<span className="font-bold text-xs text-[#AAFF00] drop-shadow-[0_0_6px_rgba(170,255,0,0.5)]">ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8 w-full items-stretch">
|
||||
|
||||
{/* Unified Top Row */}
|
||||
<OperatorProfile gamestate={newGamestate} user={mockDbUser} className="lg:col-span-2" />
|
||||
<OperatorProfile gamestate={newGamestate} user={mockDbUser} badges={oldGamestate.badges} className="lg:col-span-2" />
|
||||
<TacticalMissions role={role} gamestate={newGamestate} className="lg:col-span-1" />
|
||||
|
||||
{/* Checkpoints & Stages */}
|
||||
|
||||
Reference in New Issue
Block a user