Files
gridpilot.gg/apps/website/components/dev/DevToolbar.tsx
2025-12-31 19:55:43 +01:00

759 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import {
AlertCircle,
AlertTriangle,
Award,
Bell,
BellRing,
Building2,
ChevronDown,
ChevronUp,
LogIn,
LogOut,
MessageSquare,
Shield,
TrendingUp,
User,
Vote,
Wrench,
X,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required' | 'race_performance_summary' | 'race_final_results';
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',
},
{
type: 'race_performance_summary',
label: 'Race Performance Summary',
description: 'Immediate results after main race',
icon: TrendingUp,
color: 'text-primary-blue',
},
{
type: 'race_final_results',
label: 'Race Final Results',
description: 'Final results after stewarding closes',
icon: Award,
color: 'text-warning-amber',
},
];
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 (may require response)',
icon: AlertCircle,
},
];
type LoginMode = 'none' | 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin';
export default function DevToolbar() {
const router = useRouter();
const { addNotification } = useNotifications();
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 session state on mount
useEffect(() => {
if (typeof document !== 'undefined') {
// Check for actual session cookie first
const cookies = document.cookie.split(';');
const sessionCookie = cookies.find(c => c.trim().startsWith('gp_session='));
if (sessionCookie) {
// User has a session cookie, check if it's valid by calling the API
fetch('/api/auth/session', {
method: 'GET',
credentials: 'include'
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error('No valid session');
})
.then(session => {
if (session && session.user) {
// Determine login mode based on user email patterns
const email = session.user.email?.toLowerCase() || '';
const displayName = session.user.displayName?.toLowerCase() || '';
let mode: LoginMode = 'none';
if (email.includes('sponsor') || displayName.includes('sponsor')) {
mode = 'sponsor';
} else if (email.includes('league-owner') || displayName.includes('owner')) {
mode = 'league-owner';
} else if (email.includes('league-steward') || displayName.includes('steward')) {
mode = 'league-steward';
} else if (email.includes('league-admin') || displayName.includes('admin')) {
mode = 'league-admin';
} else if (email.includes('system-owner') || displayName.includes('system owner')) {
mode = 'system-owner';
} else if (email.includes('super-admin') || displayName.includes('super admin')) {
mode = 'super-admin';
} else if (email.includes('driver') || displayName.includes('demo')) {
mode = 'driver';
}
setLoginMode(mode);
} else {
setLoginMode('none');
}
})
.catch(() => {
// Session invalid or expired
setLoginMode('none');
});
} else {
// No session cookie means not logged in
setLoginMode('none');
}
}
}, []);
const handleDemoLogin = async (role: LoginMode) => {
if (role === 'none') return;
setLoggingIn(true);
try {
// Use the demo login API
const response = await fetch('/api/auth/demo-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }),
});
if (!response.ok) {
throw new Error('Demo login failed');
}
setLoginMode(role);
// Navigate based on role
if (role === 'sponsor') {
window.location.href = '/sponsor/dashboard';
} else {
// For driver and league roles, go to dashboard
window.location.href = '/dashboard';
}
} catch (error) {
console.error('Demo login failed:', error);
alert('Demo login failed. Please check the console for details.');
} finally {
setLoggingIn(false);
}
};
const handleLogout = async () => {
setLoggingIn(true);
try {
// Call logout API
await fetch('/api/auth/logout', { method: 'POST' });
setLoginMode('none');
// Refresh to update all components
window.location.href = '/';
} catch (error) {
console.error('Logout failed:', error);
alert('Logout failed. Please check the console for details.');
} finally {
setLoggingIn(false);
}
};
// Only show in development
if (process.env.NODE_ENV === 'production') {
return null;
}
const handleSendNotification = async () => {
setSending(true);
try {
const actionUrlByType: Record<DemoNotificationType, string> = {
protest_filed: '/races',
defense_requested: '/races',
vote_required: '/leagues',
race_performance_summary: '/races',
race_final_results: '/races',
};
const titleByType: Record<DemoNotificationType, string> = {
protest_filed: 'Protest Filed Against You',
defense_requested: 'Defense Requested',
vote_required: 'Vote Required',
race_performance_summary: 'Race Performance Summary',
race_final_results: 'Race Final Results',
};
const messageByType: Record<DemoNotificationType, string> = {
protest_filed: 'A protest has been filed against you. Please review the incident details.',
defense_requested: 'A steward requests your defense. Please respond within the deadline.',
vote_required: 'A protest vote is pending. Please review and vote.',
race_performance_summary: 'Your race is complete. View your provisional results.',
race_final_results: 'Stewarding is closed. Your final results are available.',
};
const notificationTypeByDemoType: Record<DemoNotificationType, string> = {
protest_filed: 'protest_filed',
defense_requested: 'protest_defense_requested',
vote_required: 'protest_vote_required',
race_performance_summary: 'race_performance_summary',
race_final_results: 'race_final_results',
};
const variant: NotificationVariant = selectedUrgency === 'modal' ? 'modal' : 'toast';
addNotification({
type: notificationTypeByDemoType[selectedType],
title: titleByType[selectedType],
message: messageByType[selectedType],
variant,
actionUrl: actionUrlByType[selectedType],
data: {
driverId: currentDriverId,
demo: true,
},
});
setLastSent(`${selectedType}-${selectedUrgency}`);
setTimeout(() => setLastSent(null), 3000);
} catch (error) {
console.error('Failed to send demo notification:', error);
} finally {
setSending(false);
}
};
// 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) => race.status === 'completed');
// const scheduledRaces = allRaces.filter((race) => race.status === 'scheduled');
// const primaryRace = completedRaces[0] ?? allRaces[0];
// const secondaryRace = scheduledRaces[0] ?? allRaces[1] ?? primaryRace;
// const primaryLeague = allLeagues[0];
// const notificationDeadline =
// selectedUrgency === 'modal'
// ? new Date(Date.now() + 48 * 60 * 60 * 1000)
// : undefined;
// let title: string;
// let body: string;
// let notificationType: 'protest_filed' | 'protest_defense_requested' | 'protest_vote_required' | 'race_performance_summary' | 'race_final_results';
// 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;
// }
// case 'race_performance_summary': {
// const raceId = primaryRace?.id;
// const leagueId = primaryLeague?.id;
// title = '🏁 Race Complete: Performance Summary';
// body =
// 'Your Monza Grand Prix race is finished! You finished P1 with 0 incidents. Provisional rating: +25 points. View full results and standings.';
// notificationType = 'race_performance_summary';
// actionUrl = raceId ? `/races/${raceId}` : '/races';
// break;
// }
// case 'race_final_results': {
// const leagueId = primaryLeague?.id;
// title = '🏆 Final Results: Monza Grand Prix';
// body =
// 'Stewarding is now closed. Your final result: P1 (+25 rating). No penalties were applied. View championship standings.';
// notificationType = 'race_final_results';
// actionUrl = leagueId ? `/leagues/${leagueId}/standings` : '/leagues';
// break;
// }
// }
// const actions =
// selectedUrgency === 'modal'
// ? selectedType.startsWith('race_')
// ? [
// { label: selectedType === 'race_performance_summary' ? '🏁 View Race Results' : '🏆 View Standings', type: 'primary' as const, href: actionUrl, actionId: 'view' },
// { label: '🎉 Share Achievement', type: 'secondary' as const, actionId: 'share' },
// ]
// : [
// { label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
// { label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
// ]
// : [];
// await sendNotification.execute({
// recipientId: currentDriverId,
// type: notificationType,
// title,
// body,
// actionUrl,
// urgency: selectedUrgency as NotificationUrgency,
// requiresResponse: selectedUrgency === 'modal' && !selectedType.startsWith('race_'),
// actions,
// data: {
// ...(selectedType.startsWith('protest_') ? {
// protestId: `demo-protest-${Date.now()}`,
// } : {}),
// ...(selectedType.startsWith('race_') ? {
// raceEventId: `demo-race-event-${Date.now()}`,
// sessionId: `demo-session-${Date.now()}`,
// position: 1,
// positionChange: 0,
// incidents: 0,
// provisionalRatingChange: 25,
// finalRatingChange: 25,
// hadPenaltiesApplied: false,
// } : {}),
// raceId: primaryRace?.id ?? '',
// leagueId: primaryLeague?.id ?? '',
// ...(notificationDeadline && selectedType.startsWith('protest_') ? { deadline: notificationDeadline } : {}),
// },
// });
// 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-2 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 (may require 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">
{/* Driver Login */}
<button
onClick={() => handleDemoLogin('driver')}
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' ? '✓ Driver' : 'Login as Driver'}
</button>
{/* League Owner Login */}
<button
onClick={() => handleDemoLogin('league-owner')}
disabled={loggingIn || loginMode === 'league-owner'}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${loginMode === 'league-owner'
? 'bg-purple-500/20 border-purple-500/50 text-purple-400'
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
<span className="text-xs">👑</span>
{loginMode === 'league-owner' ? '✓ League Owner' : 'Login as League Owner'}
</button>
{/* League Steward Login */}
<button
onClick={() => handleDemoLogin('league-steward')}
disabled={loggingIn || loginMode === 'league-steward'}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${loginMode === 'league-steward'
? 'bg-amber-500/20 border-amber-500/50 text-amber-400'
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
<Shield className="w-4 h-4" />
{loginMode === 'league-steward' ? '✓ Steward' : 'Login as Steward'}
</button>
{/* League Admin Login */}
<button
onClick={() => handleDemoLogin('league-admin')}
disabled={loggingIn || loginMode === 'league-admin'}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${loginMode === 'league-admin'
? 'bg-red-500/20 border-red-500/50 text-red-400'
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
<span className="text-xs"></span>
{loginMode === 'league-admin' ? '✓ Admin' : 'Login as Admin'}
</button>
{/* Sponsor Login */}
<button
onClick={() => handleDemoLogin('sponsor')}
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' ? '✓ Sponsor' : 'Login as Sponsor'}
</button>
{/* System Owner Login */}
<button
onClick={() => handleDemoLogin('system-owner')}
disabled={loggingIn || loginMode === 'system-owner'}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${loginMode === 'system-owner'
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
<span className="text-xs">👑</span>
{loginMode === 'system-owner' ? '✓ System Owner' : 'Login as System Owner'}
</button>
{/* Super Admin Login */}
<button
onClick={() => handleDemoLogin('super-admin')}
disabled={loggingIn || loginMode === 'super-admin'}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${loginMode === 'super-admin'
? 'bg-pink-500/20 border-pink-500/50 text-pink-400'
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
<span className="text-xs"></span>
{loginMode === 'super-admin' ? '✓ Super Admin' : 'Login as Super Admin'}
</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">
Test different user roles for demo purposes. Dashboard works for all roles.
</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>
);
}