481 lines
17 KiB
TypeScript
481 lines
17 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
|
import { getSendNotificationUseCase } from '@/lib/di-container';
|
|
import type { NotificationUrgency } from '@gridpilot/notifications/application';
|
|
import {
|
|
Bell,
|
|
AlertTriangle,
|
|
Vote,
|
|
Shield,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Wrench,
|
|
X,
|
|
MessageSquare,
|
|
AlertCircle,
|
|
BellRing,
|
|
User,
|
|
Building2,
|
|
LogOut,
|
|
LogIn,
|
|
} from 'lucide-react';
|
|
|
|
type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required';
|
|
type DemoUrgency = 'silent' | 'toast' | 'modal';
|
|
|
|
interface NotificationOption {
|
|
type: DemoNotificationType;
|
|
label: string;
|
|
description: string;
|
|
icon: typeof Bell;
|
|
color: string;
|
|
}
|
|
|
|
interface UrgencyOption {
|
|
urgency: DemoUrgency;
|
|
label: string;
|
|
description: string;
|
|
icon: typeof Bell;
|
|
}
|
|
|
|
const notificationOptions: NotificationOption[] = [
|
|
{
|
|
type: 'protest_filed',
|
|
label: 'Protest Against You',
|
|
description: 'A protest was filed against you',
|
|
icon: AlertTriangle,
|
|
color: 'text-red-400',
|
|
},
|
|
{
|
|
type: 'defense_requested',
|
|
label: 'Defense Requested',
|
|
description: 'A steward requests your defense',
|
|
icon: Shield,
|
|
color: 'text-warning-amber',
|
|
},
|
|
{
|
|
type: 'vote_required',
|
|
label: 'Vote Required',
|
|
description: 'You need to vote on a protest',
|
|
icon: Vote,
|
|
color: 'text-primary-blue',
|
|
},
|
|
];
|
|
|
|
const urgencyOptions: UrgencyOption[] = [
|
|
{
|
|
urgency: 'silent',
|
|
label: 'Silent',
|
|
description: 'Only shows in notification center',
|
|
icon: Bell,
|
|
},
|
|
{
|
|
urgency: 'toast',
|
|
label: 'Toast',
|
|
description: 'Shows a temporary popup',
|
|
icon: BellRing,
|
|
},
|
|
{
|
|
urgency: 'modal',
|
|
label: 'Modal',
|
|
description: 'Shows blocking modal (must respond)',
|
|
icon: AlertCircle,
|
|
},
|
|
];
|
|
|
|
type LoginMode = 'none' | 'driver' | 'sponsor';
|
|
|
|
export default function DevToolbar() {
|
|
const router = useRouter();
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [isMinimized, setIsMinimized] = useState(false);
|
|
const [selectedType, setSelectedType] = useState<DemoNotificationType>('protest_filed');
|
|
const [selectedUrgency, setSelectedUrgency] = useState<DemoUrgency>('toast');
|
|
const [sending, setSending] = useState(false);
|
|
const [lastSent, setLastSent] = useState<string | null>(null);
|
|
const [loginMode, setLoginMode] = useState<LoginMode>('none');
|
|
const [loggingIn, setLoggingIn] = useState(false);
|
|
|
|
const currentDriverId = useEffectiveDriverId();
|
|
|
|
// Sync login mode with actual cookie state on mount
|
|
useEffect(() => {
|
|
if (typeof document !== 'undefined') {
|
|
const cookies = document.cookie.split(';');
|
|
const demoModeCookie = cookies.find(c => c.trim().startsWith('gridpilot_demo_mode='));
|
|
if (demoModeCookie) {
|
|
const value = demoModeCookie.split('=')[1]?.trim();
|
|
if (value === 'sponsor') {
|
|
setLoginMode('sponsor');
|
|
} else if (value === 'driver') {
|
|
setLoginMode('driver');
|
|
} else {
|
|
setLoginMode('none');
|
|
}
|
|
} else {
|
|
// Default to driver mode if no cookie (for demo purposes)
|
|
setLoginMode('driver');
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const handleLoginAsDriver = async () => {
|
|
setLoggingIn(true);
|
|
try {
|
|
// Demo: Set cookie to indicate driver mode
|
|
document.cookie = 'gridpilot_demo_mode=driver; path=/; max-age=86400';
|
|
setLoginMode('driver');
|
|
// Refresh to update all components that depend on demo mode
|
|
window.location.reload();
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
};
|
|
|
|
const handleLoginAsSponsor = async () => {
|
|
setLoggingIn(true);
|
|
try {
|
|
// Demo: Set cookie to indicate sponsor mode
|
|
document.cookie = 'gridpilot_demo_mode=sponsor; path=/; max-age=86400';
|
|
setLoginMode('sponsor');
|
|
// Navigate to sponsor dashboard
|
|
window.location.href = '/sponsor/dashboard';
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setLoggingIn(true);
|
|
try {
|
|
// Demo: Clear demo mode cookie
|
|
document.cookie = 'gridpilot_demo_mode=; path=/; max-age=0';
|
|
setLoginMode('none');
|
|
// Refresh to update all components
|
|
window.location.href = '/';
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
};
|
|
|
|
// Only show in development
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return null;
|
|
}
|
|
|
|
const handleSendNotification = async () => {
|
|
setSending(true);
|
|
try {
|
|
const sendNotification = getSendNotificationUseCase();
|
|
|
|
let title: string;
|
|
let body: string;
|
|
let notificationType: 'protest_filed' | 'protest_defense_requested' | 'protest_vote_required';
|
|
let actionUrl: string;
|
|
|
|
switch (selectedType) {
|
|
case 'protest_filed':
|
|
title = '🚨 Protest Filed Against You';
|
|
body = 'Max Verstappen has filed a protest against you for unsafe rejoining at Turn 3, Lap 12 during the Spa-Francorchamps race.';
|
|
notificationType = 'protest_filed';
|
|
actionUrl = '/races/race-1/stewarding';
|
|
break;
|
|
case 'defense_requested':
|
|
title = '⚖️ Defense Requested';
|
|
body = 'A steward has requested your defense regarding the incident at Turn 1 in the Monza race. Please provide your side of the story within 48 hours.';
|
|
notificationType = 'protest_defense_requested';
|
|
actionUrl = '/races/race-2/stewarding';
|
|
break;
|
|
case 'vote_required':
|
|
title = '🗳️ Your Vote Required';
|
|
body = 'As a league steward, you are required to vote on the protest: Driver A vs Driver B - Causing a collision at Eau Rouge.';
|
|
notificationType = 'protest_vote_required';
|
|
actionUrl = '/leagues/league-1/stewarding';
|
|
break;
|
|
}
|
|
|
|
// For modal urgency, add actions
|
|
const actions = selectedUrgency === 'modal' ? [
|
|
{ label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
|
|
{ label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
|
|
] : undefined;
|
|
|
|
await sendNotification.execute({
|
|
recipientId: currentDriverId,
|
|
type: notificationType,
|
|
title,
|
|
body,
|
|
actionUrl,
|
|
urgency: selectedUrgency as NotificationUrgency,
|
|
requiresResponse: selectedUrgency === 'modal',
|
|
actions,
|
|
data: {
|
|
protestId: `demo-protest-${Date.now()}`,
|
|
raceId: 'race-1',
|
|
leagueId: 'league-1',
|
|
deadline: selectedUrgency === 'modal' ? new Date(Date.now() + 48 * 60 * 60 * 1000) : undefined,
|
|
},
|
|
});
|
|
|
|
setLastSent(`${selectedType}-${selectedUrgency}`);
|
|
setTimeout(() => setLastSent(null), 3000);
|
|
} catch (error) {
|
|
console.error('Failed to send demo notification:', error);
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
if (isMinimized) {
|
|
return (
|
|
<button
|
|
onClick={() => setIsMinimized(false)}
|
|
className="fixed bottom-4 right-4 z-50 p-3 bg-iron-gray border border-charcoal-outline rounded-full shadow-lg hover:bg-charcoal-outline transition-colors"
|
|
title="Open Dev Toolbar"
|
|
>
|
|
<Wrench className="w-5 h-5 text-primary-blue" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50 w-80 bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-3 bg-iron-gray/50 border-b border-charcoal-outline">
|
|
<div className="flex items-center gap-2">
|
|
<Wrench className="w-4 h-4 text-primary-blue" />
|
|
<span className="text-sm font-semibold text-white">Dev Toolbar</span>
|
|
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-primary-blue/20 text-primary-blue rounded">
|
|
DEMO
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
|
) : (
|
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => setIsMinimized(true)}
|
|
className="p-1.5 hover:bg-charcoal-outline rounded transition-colors"
|
|
>
|
|
<X className="w-4 h-4 text-gray-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{isExpanded && (
|
|
<div className="p-4 space-y-4">
|
|
{/* Notification Type Section */}
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<MessageSquare className="w-4 h-4 text-gray-400" />
|
|
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
|
|
Notification Type
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-1">
|
|
{notificationOptions.map((option) => {
|
|
const Icon = option.icon;
|
|
const isSelected = selectedType === option.type;
|
|
|
|
return (
|
|
<button
|
|
key={option.type}
|
|
onClick={() => setSelectedType(option.type)}
|
|
className={`
|
|
flex flex-col items-center gap-1 p-2 rounded-lg border transition-all text-center
|
|
${isSelected
|
|
? 'bg-primary-blue/20 border-primary-blue/50'
|
|
: 'bg-iron-gray/30 border-charcoal-outline hover:bg-iron-gray/50'
|
|
}
|
|
`}
|
|
>
|
|
<Icon className={`w-4 h-4 ${isSelected ? 'text-primary-blue' : option.color}`} />
|
|
<span className={`text-[10px] font-medium ${isSelected ? 'text-primary-blue' : 'text-gray-400'}`}>
|
|
{option.label.split(' ')[0]}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Urgency Section */}
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<BellRing className="w-4 h-4 text-gray-400" />
|
|
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
|
|
Urgency Level
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-1">
|
|
{urgencyOptions.map((option) => {
|
|
const Icon = option.icon;
|
|
const isSelected = selectedUrgency === option.urgency;
|
|
|
|
return (
|
|
<button
|
|
key={option.urgency}
|
|
onClick={() => setSelectedUrgency(option.urgency)}
|
|
className={`
|
|
flex flex-col items-center gap-1 p-2 rounded-lg border transition-all text-center
|
|
${isSelected
|
|
? option.urgency === 'modal'
|
|
? 'bg-red-500/20 border-red-500/50'
|
|
: option.urgency === 'toast'
|
|
? 'bg-warning-amber/20 border-warning-amber/50'
|
|
: 'bg-gray-500/20 border-gray-500/50'
|
|
: 'bg-iron-gray/30 border-charcoal-outline hover:bg-iron-gray/50'
|
|
}
|
|
`}
|
|
>
|
|
<Icon className={`w-4 h-4 ${
|
|
isSelected
|
|
? option.urgency === 'modal'
|
|
? 'text-red-400'
|
|
: option.urgency === 'toast'
|
|
? 'text-warning-amber'
|
|
: 'text-gray-400'
|
|
: 'text-gray-500'
|
|
}`} />
|
|
<span className={`text-[10px] font-medium ${
|
|
isSelected
|
|
? option.urgency === 'modal'
|
|
? 'text-red-400'
|
|
: option.urgency === 'toast'
|
|
? 'text-warning-amber'
|
|
: 'text-gray-400'
|
|
: 'text-gray-500'
|
|
}`}>
|
|
{option.label}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-[10px] text-gray-600 mt-1">
|
|
{urgencyOptions.find(o => o.urgency === selectedUrgency)?.description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Send Button */}
|
|
<button
|
|
onClick={handleSendNotification}
|
|
disabled={sending}
|
|
className={`
|
|
w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
${lastSent
|
|
? 'bg-performance-green/20 border border-performance-green/30 text-performance-green'
|
|
: 'bg-primary-blue hover:bg-primary-blue/80 text-white'
|
|
}
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
`}
|
|
>
|
|
{sending ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
Sending...
|
|
</>
|
|
) : lastSent ? (
|
|
<>
|
|
✓ Notification Sent!
|
|
</>
|
|
) : (
|
|
<>
|
|
<Bell className="w-4 h-4" />
|
|
Send Demo Notification
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Info */}
|
|
<div className="p-3 rounded-lg bg-iron-gray/50 border border-charcoal-outline">
|
|
<p className="text-[10px] text-gray-500">
|
|
<strong className="text-gray-400">Silent:</strong> Notification center only<br/>
|
|
<strong className="text-gray-400">Toast:</strong> Temporary popup (auto-dismisses)<br/>
|
|
<strong className="text-gray-400">Modal:</strong> Blocking popup (requires action)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Login Section */}
|
|
<div className="pt-4 border-t border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<LogIn className="w-4 h-4 text-gray-400" />
|
|
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
|
|
Demo Login
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<button
|
|
onClick={handleLoginAsDriver}
|
|
disabled={loggingIn || loginMode === 'driver'}
|
|
className={`
|
|
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
|
|
${loginMode === 'driver'
|
|
? 'bg-primary-blue/20 border-primary-blue/50 text-primary-blue'
|
|
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
|
|
}
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
`}
|
|
>
|
|
<User className="w-4 h-4" />
|
|
{loginMode === 'driver' ? 'Logged in as Driver' : 'Login as Driver'}
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleLoginAsSponsor}
|
|
disabled={loggingIn || loginMode === 'sponsor'}
|
|
className={`
|
|
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
|
|
${loginMode === 'sponsor'
|
|
? 'bg-performance-green/20 border-performance-green/50 text-performance-green'
|
|
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
|
|
}
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
`}
|
|
>
|
|
<Building2 className="w-4 h-4" />
|
|
{loginMode === 'sponsor' ? 'Logged in as Sponsor' : 'Login as Sponsor'}
|
|
</button>
|
|
|
|
{loginMode !== 'none' && (
|
|
<button
|
|
onClick={handleLogout}
|
|
disabled={loggingIn}
|
|
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg border border-red-500/30 bg-red-500/10 text-red-400 text-sm font-medium hover:bg-red-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Logout
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-[10px] text-gray-600 mt-2">
|
|
Switch between driver and sponsor views for demo purposes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Collapsed state hint */}
|
|
{!isExpanded && (
|
|
<div className="px-4 py-2 text-xs text-gray-500">
|
|
Click ↑ to expand dev tools
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |