From b8a20144d3041186ae9d50834000ba6d07d3ce3f Mon Sep 17 00:00:00 2001 From: Satyam <95536056+Satyam-Rastogi@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:24:26 +0530 Subject: [PATCH] feat(ui): refactor gamification UI for role awareness and remove floaters --- .../gamification/FloatingXPManager.jsx | 77 +++++ .../ProCanvas/gamification/LevelUpModal.jsx | 90 ++++++ src/context/GamificationContext.jsx | 247 +++++++++++++++ src/hooks/useGamification.js | 69 +++-- src/pages/ProCanvas.jsx | 281 +++++++++++++----- 5 files changed, 668 insertions(+), 96 deletions(-) create mode 100644 src/components/ProCanvas/gamification/FloatingXPManager.jsx create mode 100644 src/components/ProCanvas/gamification/LevelUpModal.jsx create mode 100644 src/context/GamificationContext.jsx diff --git a/src/components/ProCanvas/gamification/FloatingXPManager.jsx b/src/components/ProCanvas/gamification/FloatingXPManager.jsx new file mode 100644 index 0000000..940f9d2 --- /dev/null +++ b/src/components/ProCanvas/gamification/FloatingXPManager.jsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useGamification } from '../../../context/GamificationContext'; + +const FloatingXPManager = () => { + const { recentAction, setRecentAction, missionComplete } = useGamification(); + const [popups, setPopups] = useState([]); + + useEffect(() => { + if (recentAction) { + setPopups(prev => [...prev, recentAction]); + // The context requires us to clear the recentAction immediately so same actions can fire again + setRecentAction(null); + } + }, [recentAction, setRecentAction]); + + useEffect(() => { + if (popups.length > 0) { + const timer = setTimeout(() => { + setPopups(prev => prev.slice(1)); + }, 1500); // Remove after animation finishes + return () => clearTimeout(timer); + } + }, [popups]); + + return ( + <> +
+ + {popups.map(popup => ( + + {popup.text} +
+ {popup.actionLabel} +
+
+ ))} +
+
+ + {/* Mission Complete Banner Banner */} + + {missionComplete && ( +
+ +
+ Mission Complete +
+
+ {missionComplete.message} +
+
+
+ )} +
+ + ); +}; + +export default FloatingXPManager; diff --git a/src/components/ProCanvas/gamification/LevelUpModal.jsx b/src/components/ProCanvas/gamification/LevelUpModal.jsx new file mode 100644 index 0000000..a8417fa --- /dev/null +++ b/src/components/ProCanvas/gamification/LevelUpModal.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Star, Trophy, Zap } from 'lucide-react'; +import { useGamification } from '../../../context/GamificationContext'; + +const LevelUpModal = () => { + const { levelUpData } = useGamification(); + + return ( + + {levelUpData && ( +
+ {/* Backdrop glow */} + + + + {/* Celebration Ring */} + + +
+ + {/* Rotating background ray effect */} + + +
+ + + + +

+ Level Up! +

+ +
+ + Level {levelUpData.newLevel} Reached + +
+ + {levelUpData.titleChanged && ( + +
New Rank Unlocked
+
+ + {levelUpData.newTitle} +
+
+ )} +
+
+
+
+ )} +
+ ); +}; + +export default LevelUpModal; diff --git a/src/context/GamificationContext.jsx b/src/context/GamificationContext.jsx new file mode 100644 index 0000000..84337df --- /dev/null +++ b/src/context/GamificationContext.jsx @@ -0,0 +1,247 @@ +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { useAuth } from './AuthContext'; + +const GamificationContext = createContext(); + +const INITIAL_STATE = { + xp: 3550, + level: 12, + streak: 4, + todayPoints: 150, + weeklyPoints: 850, + dailyQuests: { + knocks: { current: 18, target: 20 }, + leads: { current: 4, target: 5 }, + inspections: { current: 0, target: 1 } + }, + weeklyChallenge: { + knocks: { current: 140, target: 150 } + } +}; + +const LEADERBOARD_MOCK = [ + { id: '1', name: 'Alex M.', points: 1200, avatar: 'AM', isUser: false }, + { id: '2', name: 'Jessica R.', points: 1050, avatar: 'JR', isUser: false }, + { id: '3', name: 'David L.', points: 980, avatar: 'DL', isUser: false }, + { id: '4', name: 'Sarah M.', points: 900, avatar: 'SM', isUser: false }, + { id: 'user', name: 'You', points: 850, avatar: 'You', isUser: true }, + { id: '6', name: 'Mike T.', points: 700, avatar: 'MT', isUser: false }, + { id: '7', name: 'Emily C.', points: 650, avatar: 'EC', isUser: false }, +]; + +export const GamificationProvider = ({ children }) => { + const { user } = useAuth(); + const role = user?.role || 'FIELD_AGENT'; + + const [gameState, setGameState] = useState(INITIAL_STATE); + const [leaderboard, setLeaderboard] = useState(() => { + return LEADERBOARD_MOCK.map((u, i) => { + if (u.isUser) { + return { ...u, name: user?.name || 'You', role: role === 'FIELD_AGENT' ? 'Field Agent' : role === 'ADMIN' ? 'Admin' : 'Owner' }; + } + // Assign realistic mock roles to others + const mockRole = i < 2 ? 'Owner' : (i < 4 ? 'Admin' : 'Field Agent'); + return { ...u, role: mockRole }; + }); + }); + + // Ephemeral states for animations + const [recentAction, setRecentAction] = useState(null); // { id, x, y, amount, text, type } + const [levelUpData, setLevelUpData] = useState(null); // { newLevel, newTitle } + const [missionComplete, setMissionComplete] = useState(null); // { type, message } + const [rankToast, setRankToast] = useState(null); // { newRank, oldRank, message } + + const getRankTitle = (level) => { + if (level <= 5) return 'Rookie'; + if (level <= 15) return 'Field Runner'; + if (level <= 30) return 'Roof Warrior'; + if (level <= 45) return 'Territory Titan'; + return 'Legendary Closer'; + }; + + const getNextLevelXP = (level) => { + // Demo formula: Level 13 is 3600. (Since demo starts at L12, 3550 XP) + // Formula: next level needs `1000 + (level * 200)` + return 1000 + (level * 200); + }; + + const addXP = useCallback((amount, coordinates, actionName, isCrit = false) => { + setGameState(prev => { + let newXp = prev.xp + amount; + const threshold = getNextLevelXP(prev.level); + + let newLevel = prev.level; + let titleChanged = false; + + // Handle Level Up + if (newXp >= threshold) { + newLevel = prev.level + 1; + // Check if title changed + if (getRankTitle(newLevel) !== getRankTitle(prev.level)) { + titleChanged = true; + } + + // Trigger celebration + setLevelUpData({ + newLevel, + newTitle: getRankTitle(newLevel), + titleChanged + }); + + // Auto clear level up modal after 5s + setTimeout(() => setLevelUpData(null), 5000); + } + + // Add points + const newWeeklyPoints = prev.weeklyPoints + amount; + + // Setup float text + setRecentAction({ + id: Date.now(), + x: coordinates?.x || window.innerWidth / 2, + y: coordinates?.y || window.innerHeight / 2, + amount, + text: isCrit ? `CRIT! +${amount} XP` : `+${amount} XP`, + type: isCrit ? 'crit' : 'normal', + actionLabel: actionName + }); + + return { + ...prev, + xp: newXp, + level: newLevel, + todayPoints: prev.todayPoints + amount, + weeklyPoints: newWeeklyPoints + }; + }); + }, []); + + // Sync leaderboard when weeklyPoints change + useEffect(() => { + setLeaderboard(prev => { + // Find old rank + const sortedPrev = [...prev].sort((a, b) => b.points - a.points); + const oldRankIndex = sortedPrev.findIndex(u => u.isUser); + + const newBoard = prev.map(user => { + if (user.isUser) { + return { ...user, points: gameState.weeklyPoints }; + } + return user; + }); + + // Sort descending + const sortedNew = newBoard.sort((a, b) => b.points - a.points); + const newRankIndex = sortedNew.findIndex(u => u.isUser); + + // Trigger Toast if rank went up + if (oldRankIndex > newRankIndex && newRankIndex !== -1) { + const surpassedUser = sortedPrev[newRankIndex]; + setRankToast({ + oldRank: oldRankIndex + 1, + newRank: newRankIndex + 1, + message: `You passed ${surpassedUser.name}! (Rank #${newRankIndex + 1})` + }); + setTimeout(() => setRankToast(null), 4000); + } + + return sortedNew; + }); + }, [gameState.weeklyPoints]); + + const updateQuest = useCallback((type, amount = 1) => { + setGameState(prev => { + const quest = prev.dailyQuests[type]; + if (!quest) return prev; + + const newCurrent = Math.min(quest.current + amount, quest.target); + + // Check if just completed + if (quest.current < quest.target && newCurrent >= quest.target) { + setMissionComplete({ + type: 'daily', + message: `Daily ${type} mission complete!` + }); + setTimeout(() => setMissionComplete(null), 3500); + } + + const newState = { + ...prev, + dailyQuests: { + ...prev.dailyQuests, + [type]: { ...quest, current: newCurrent } + } + }; + + // Also update weekly if it's a knock + if (type === 'knocks') { + const wQuest = prev.weeklyChallenge.knocks; + const newWCurrent = Math.min(wQuest.current + amount, wQuest.target); + if (wQuest.current < wQuest.target && newWCurrent >= wQuest.target) { + setTimeout(() => { + setMissionComplete({ + type: 'weekly', + message: `Weekly Challenge Complete!` + }); + setTimeout(() => setMissionComplete(null), 3500); + }, 1000); // offset from daily complete + } + newState.weeklyChallenge.knocks.current = newWCurrent; + } + + return newState; + }); + }, []); + + // Actions + const logKnock = useCallback((isHighValue, coords) => { + const baseXP = 10; + const multi = isHighValue ? 1.5 : 1; + const finalXP = Math.round(baseXP * multi); + + addXP(finalXP, coords, 'Knock', isHighValue); + updateQuest('knocks', 1); + }, [addXP, updateQuest]); + + const logLead = useCallback((coords) => { + addXP(50, coords, 'New Lead'); + updateQuest('leads', 1); + }, [addXP, updateQuest]); + + const logInspection = useCallback((coords) => { + addXP(150, coords, 'Inspection Set'); + updateQuest('inspections', 1); + }, [addXP, updateQuest]); + + // Expose rank title logic + const currentTitle = getRankTitle(gameState.level); + const nextLevelXP = getNextLevelXP(gameState.level); + // Calculate prior level XP to know the base for the progress bar + const prevLevelXP = gameState.level === 1 ? 0 : getNextLevelXP(gameState.level - 1); + const progressPercent = Math.max(0, Math.min(100, ((gameState.xp - prevLevelXP) / (nextLevelXP - prevLevelXP)) * 100)); + + const value = { + ...gameState, + leaderboard, + recentAction, + levelUpData, + missionComplete, + rankToast, + currentTitle, + nextLevelXP, + prevLevelXP, + progressPercent, + logKnock, + logLead, + logInspection, + setRecentAction // For clearing after animation + }; + + return ( + + {children} + + ); +}; + +export const useGamification = () => useContext(GamificationContext); diff --git a/src/hooks/useGamification.js b/src/hooks/useGamification.js index 9b12b64..4604351 100644 --- a/src/hooks/useGamification.js +++ b/src/hooks/useGamification.js @@ -13,7 +13,7 @@ const LEVEL_THRESHOLDS = [ { level: 10, xp: 100000, title: 'Legend' } ]; -export const useGamification = (user) => { +export const useGamification = (user, role = 'FIELD_AGENT') => { return useMemo(() => { if (!user) return null; @@ -48,32 +48,47 @@ export const useGamification = (user) => { } // --- PHASE 3: CHECKPOINTS & STAGES --- - const 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 - }, - { - 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 - }, - { - 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 - } - ]; + let STAGES = []; + if (role === 'OWNER') { + STAGES = [ + { id: 1, title: "Q1 Launch", requirement: { type: "onboarding", amount: 3 }, reward: { type: "coupon", name: "Territory Expansion", icon: "map" }, progress: 1, goal: 3 }, + { id: 2, title: "Market Domination", requirement: { type: "revenue", amount: 100 }, reward: { type: "giftcard", name: "Executive Suite Unlock", icon: "star" }, progress: 85, goal: 100 }, + { id: 3, title: "Empire Builder", requirement: { type: "deals", amount: 50 }, reward: { type: "cash", name: "Profit Share Tier 2", icon: "cash" }, progress: 20, goal: 50 } + ]; + } else if (role === 'ADMIN') { + STAGES = [ + { id: 1, title: "Squad Setup", requirement: { type: "recruits", amount: 5 }, reward: { type: "coupon", name: "Team Bonus Unlock", icon: "users" }, progress: 3, goal: 5 }, + { id: 2, title: "Territory Control", requirement: { type: "regions", amount: 4 }, reward: { type: "giftcard", name: "Admin Power-Up", icon: "zap" }, progress: 4, goal: 4 }, + { id: 3, title: "Regional Manager", requirement: { type: "sales", amount: 20 }, reward: { type: "cash", name: "Override Commission", icon: "cash" }, progress: 12, goal: 20 } + ]; + } else { + 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 + }, + { + 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 + }, + { + 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 + } + ]; + } // Determine lock states const stages = STAGES.map((s, idx) => { diff --git a/src/pages/ProCanvas.jsx b/src/pages/ProCanvas.jsx index a3bc296..a77e87a 100644 --- a/src/pages/ProCanvas.jsx +++ b/src/pages/ProCanvas.jsx @@ -1,14 +1,17 @@ -import React from 'react'; -import { motion } from 'framer-motion'; +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { useNavigate } from 'react-router-dom'; import { Flame, Target, Crosshair, Trophy, UploadCloud, - Shield, Zap, Star, Medal, Map, Activity, - Users, TrendingUp, CheckCircle, Lock, FastForward, Award, Calendar + Shield, Zap, Star, Medal, Map, Activity, Home, + Users, TrendingUp, CheckCircle, Lock, FastForward, Award, Calendar, ClipboardCheck } from 'lucide-react'; import { useAuth } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; -import { useGamification } from '../hooks/useGamification'; +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 { SpotlightCard } from '../components/SpotlightCard'; import { AnimatedCounter } from '../components/AnimatedCounter'; @@ -73,21 +76,37 @@ const OperatorProfile = ({ gamestate, user, className = "" }) => {
-

Op ID: {user.empId || 'UNK-99'}

+
+

Op ID: {user.empId || 'UNK-99'}

+
+ + + + {gamestate.streak} Day Streak +
+

{user.name}

LVL - {gamestate.title} + {gamestate.currentTitle}
- XP - NEXT {gamestate.nextLevelXp.toLocaleString()} + XP + NEXT {gamestate.nextLevelXP.toLocaleString()} +
+ {/* Using context's exact progressPercent for precision inside the progress bar */} +
+
-
@@ -95,40 +114,114 @@ const OperatorProfile = ({ gamestate, user, className = "" }) => { ); }; -const LiveMissionTracker = ({ stage, className = "" }) => { +const TacticalMissions = ({ role, gamestate, className = "" }) => { const navigate = useNavigate(); + // Role-based content adaptations + let headerTitle = "Daily Quests"; + let weeklyTitle = "Weekly Challenge"; + let weeklyTargetLabel = "Total Knocks"; + + if (role === 'ADMIN') { + headerTitle = "Squad Directives"; + weeklyTitle = "Global Target"; + weeklyTargetLabel = "Global Knocks"; + } else if (role === 'OWNER') { + headerTitle = "Key Objectives"; + weeklyTitle = "Revenue Drivers"; + weeklyTargetLabel = "Required Activity"; + } + + const MissionBar = ({ current, target, colorClass, label, icon: Icon }) => { + const pct = Math.min((current / target) * 100, 100); + const isComplete = current >= target; + + return ( +
+
+
+ + {label} +
+
+ / {target} +
+
+
+ +
+
+ ); + }; + return (
-

Live Mission Tracker

+

Tactical Missions

-
-

ACTIVE DIRECTIVE

-

{stage.title}

-

- Target: {stage.goal} {stage.requirement.type.replace(/([A-Z])/g, ' $1').trim()} -

-
- -
-
- PROGRESS - / {stage.goal} +
+ {/* Weekly Challenge Component */} +
+
+ + {weeklyTitle} + +
+ +
+ + {/* Daily Quests */} +
+
+ + {headerTitle} + +
+
+ {role === 'ADMIN' ? ( + <> + + + + + ) : role === 'OWNER' ? ( + <> + + + + + ) : ( + <> + + + + + )} +
-
@@ -137,12 +230,16 @@ const LiveMissionTracker = ({ stage, className = "" }) => { ); }; -const CheckpointsSystem = ({ stages, className = "" }) => { +const CheckpointsSystem = ({ stages, role, className = "" }) => { + let headerTitle = "Rewards & Checkpoints"; + if (role === 'ADMIN') headerTitle = "Squad Milestone Tracking"; + if (role === 'OWNER') headerTitle = "Program Milestones"; + return (
-

Rewards & Checkpoints

+

{headerTitle}

@@ -226,12 +323,7 @@ const DetailedBadges = ({ badges, className = "" }) => { }; const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className = "" }) => { - const leaders = [ - { rank: 1, name: "Felicity Fast", xp: 15600, delta: "+450", role: "Agent" }, - { rank: 2, name: "Frank Agent", xp: 12450, delta: "+310", role: "Agent" }, - { rank: 3, name: "Fiona Field", xp: 8200, delta: "+120", role: "Agent" }, - { rank: 4, name: "Felix Fixer", xp: 5400, delta: "+50", role: "Agent" }, - ]; + const { leaderboard } = useGamification(); return ( @@ -241,28 +333,53 @@ const Leaderboard = ({ title = "Live Intel Feed", icon: Icon = Target, className
-
- {leaders.map((agent, i) => ( -
-
- - 0{agent.rank} - -
- {agent.name} - {agent.role} -
-
-
- XP - {agent.delta} Today -
-
- ))} +
+ + + {leaderboard.map((agent, i) => { + const isUser = agent.isUser; + const isTop3 = i < 3; + const rank = i + 1; + + return ( + + {isUser && ( + + )} +
+ + 0{rank} + +
+ {agent.name} + {agent.role} +
+
+
+ XP +
+
+ ); + })} +
+
); @@ -371,6 +488,8 @@ const OwnerROIDashboard = ({ className = "" }) => { // ----------------------------------------------------------------- const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-3 flex-1" }) => { + const { logKnock, logLead, logInspection } = useGamification(); + return (
@@ -378,15 +497,25 @@ const QuickLogAction = ({ className = "", gridClassName = "grid grid-cols-2 gap-

Quick Operations Log

- - - @@ -415,7 +544,7 @@ const ScanUpload = ({ className = "" }) => { // MAIN COMPONENT EXPORT // ----------------------------------------------------------------- -export default function ProCanvas() { +function ProCanvasContent() { const { user } = useAuth(); const { users } = useMockStore(); @@ -433,12 +562,18 @@ export default function ProCanvas() { achievements: ['Hot Spot Hunter', 'Storm Chaser'] }; - const gamestate = useGamification(mockDbUser); + const oldGamestate = useOldGamification(mockDbUser); + const newGamestate = useGamification(); - if (!gamestate) return
INITIALIZING HUD...
; + if (!oldGamestate || !newGamestate) return
INITIALIZING HUD...
; return ( -
+
+ + {/* New Gamification Overlay Components */} + + + {/* Hex Grid Background & Scanlines */}
@@ -476,15 +611,15 @@ export default function ProCanvas() {
{/* Unified Top Row */} - - + + {/* Checkpoints & Stages */} - + {/* Field Agent Only Features */} {role === 'FIELD_AGENT' && ( - + )} {/* Owner / Admin Tops */} @@ -497,7 +632,7 @@ export default function ProCanvas() { {/* Shared Bottom Row */} @@ -518,3 +653,11 @@ export default function ProCanvas() {
); } + +export default function ProCanvas() { + return ( + + + + ); +}