Files
gridpilot.gg/apps/website/components/dev/DevToolbar.tsx
2025-12-10 15:41:44 +01:00

510 lines
18 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { getSendNotificationUseCase, getRaceRepository, getLeagueRepository } 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();
const raceRepository = getRaceRepository();
const leagueRepository = getLeagueRepository();
const [allRaces, allLeagues] = await Promise.all([
raceRepository.findAll(),
leagueRepository.findAll(),
]);
const completedRaces = allRaces.filter((race: any) => race.status === 'completed');
const scheduledRaces = allRaces.filter((race: any) => race.status === 'scheduled');
const primaryRace = completedRaces[0] ?? allRaces[0];
const secondaryRace = scheduledRaces[0] ?? allRaces[1] ?? primaryRace;
const primaryLeague = allLeagues[0];
let title: string;
let body: string;
let notificationType: 'protest_filed' | 'protest_defense_requested' | 'protest_vote_required';
let actionUrl: string;
switch (selectedType) {
case 'protest_filed': {
const raceId = primaryRace?.id;
title = '🚨 Protest Filed Against You';
body =
'A protest has been filed against you for unsafe rejoining during a recent race. Please review the incident details.';
notificationType = 'protest_filed';
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
break;
}
case 'defense_requested': {
const raceId = secondaryRace?.id ?? primaryRace?.id;
title = '⚖️ Defense Requested';
body =
'A steward has requested your defense regarding a recent incident. Please provide your side of the story within 48 hours.';
notificationType = 'protest_defense_requested';
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
break;
}
case 'vote_required': {
const leagueId = primaryLeague?.id;
title = '🗳️ Your Vote Required';
body =
'As a league steward, you are required to vote on an open protest. Please review the case and cast your vote.';
notificationType = 'protest_vote_required';
actionUrl = leagueId ? `/leagues/${leagueId}/stewarding` : '/leagues';
break;
}
}
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: primaryRace?.id,
leagueId: primaryLeague?.id,
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>
);
}