dev setup

This commit is contained in:
2025-12-31 21:24:42 +01:00
parent 167e82a52b
commit 16e0bdaec1
26 changed files with 4076 additions and 646 deletions

View File

@@ -0,0 +1,42 @@
'use client';
import { ReactNode, useState } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
interface AccordionProps {
title: string;
icon: ReactNode;
children: ReactNode;
defaultOpen?: boolean;
}
export function Accordion({ title, icon, children, defaultOpen = false }: AccordionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border border-charcoal-outline rounded-lg overflow-hidden bg-iron-gray/30">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-3 py-2 hover:bg-iron-gray/50 transition-colors"
>
<div className="flex items-center gap-2">
{icon}
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
{title}
</span>
</div>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-gray-400" />
) : (
<ChevronUp className="w-4 h-4 text-gray-400" />
)}
</button>
{isOpen && (
<div className="p-3 border-t border-charcoal-outline">
{children}
</div>
)}
</div>
);
}

View File

@@ -3,106 +3,22 @@
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 { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required' | 'race_performance_summary' | 'race_final_results';
type DemoUrgency = 'silent' | 'toast' | 'modal';
// Import our new components
import { Accordion } from './Accordion';
import { NotificationTypeSection } from './sections/NotificationTypeSection';
import { UrgencySection } from './sections/UrgencySection';
import { NotificationSendSection } from './sections/NotificationSendSection';
import { APIStatusSection } from './sections/APIStatusSection';
import { LoginSection } from './sections/LoginSection';
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';
// Import types
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
export default function DevToolbar() {
const router = useRouter();
@@ -116,6 +32,12 @@ export default function DevToolbar() {
const [loginMode, setLoginMode] = useState<LoginMode>('none');
const [loggingIn, setLoggingIn] = useState(false);
// API Status Monitoring State
const [apiStatus, setApiStatus] = useState(() => ApiConnectionMonitor.getInstance().getStatus());
const [apiHealth, setApiHealth] = useState(() => ApiConnectionMonitor.getInstance().getHealth());
const [circuitBreakers, setCircuitBreakers] = useState(() => CircuitBreakerRegistry.getInstance().getStatus());
const [checkingHealth, setCheckingHealth] = useState(false);
const currentDriverId = useEffectiveDriverId();
// Sync login mode with actual session state on mount
@@ -176,6 +98,101 @@ export default function DevToolbar() {
}
}, []);
// API Status Monitoring Effects
useEffect(() => {
const monitor = ApiConnectionMonitor.getInstance();
const registry = CircuitBreakerRegistry.getInstance();
const updateStatus = () => {
setApiStatus(monitor.getStatus());
setApiHealth(monitor.getHealth());
setCircuitBreakers(registry.getStatus());
};
// Initial update
updateStatus();
// Listen for status changes
monitor.on('connected', updateStatus);
monitor.on('disconnected', updateStatus);
monitor.on('degraded', updateStatus);
monitor.on('success', updateStatus);
monitor.on('failure', updateStatus);
// Poll for updates every 2 seconds
const interval = setInterval(updateStatus, 2000);
return () => {
monitor.off('connected', updateStatus);
monitor.off('disconnected', updateStatus);
monitor.off('degraded', updateStatus);
monitor.off('success', updateStatus);
monitor.off('failure', updateStatus);
clearInterval(interval);
};
}, []);
// API Health Check Handler
const handleApiHealthCheck = async () => {
setCheckingHealth(true);
try {
const monitor = ApiConnectionMonitor.getInstance();
const result = await monitor.performHealthCheck();
addNotification({
type: result.healthy ? 'api_healthy' : 'api_unhealthy',
title: result.healthy ? 'API Health Check Passed' : 'API Health Check Failed',
message: result.healthy
? `API responded in ${result.responseTime}ms`
: `Health check failed: ${result.error}`,
variant: 'toast',
});
} catch (error) {
addNotification({
type: 'api_error',
title: 'Health Check Error',
message: 'Failed to perform health check',
variant: 'toast',
});
} finally {
setCheckingHealth(false);
}
};
// Reset API Stats
const handleResetApiStats = () => {
ApiConnectionMonitor.getInstance().reset();
CircuitBreakerRegistry.getInstance().resetAll();
addNotification({
type: 'api_reset',
title: 'API Stats Reset',
message: 'All API connection statistics have been reset',
variant: 'toast',
});
};
// Test API Error
const handleTestApiError = async () => {
try {
// This will intentionally fail to test error handling
const response = await fetch('http://localhost:3001/api/nonexistent', {
method: 'GET',
});
if (!response.ok) {
throw new Error(`Test error: ${response.status}`);
}
} catch (error) {
addNotification({
type: 'api_test_error',
title: 'Test Error Triggered',
message: 'This is a test API error to demonstrate error handling',
variant: 'toast',
});
}
};
const handleDemoLogin = async (role: LoginMode) => {
if (role === 'none') return;
@@ -202,8 +219,7 @@ export default function DevToolbar() {
window.location.href = '/dashboard';
}
} catch (error) {
console.error('Demo login failed:', error);
alert('Demo login failed. Please check the console for details.');
alert('Demo login failed. Please check the API server status.');
} finally {
setLoggingIn(false);
}
@@ -219,8 +235,7 @@ export default function DevToolbar() {
// Refresh to update all components
window.location.href = '/';
} catch (error) {
console.error('Logout failed:', error);
alert('Logout failed. Please check the console for details.');
alert('Logout failed. Please check the API server status.');
} finally {
setLoggingIn(false);
}
@@ -283,142 +298,11 @@ export default function DevToolbar() {
setLastSent(`${selectedType}-${selectedUrgency}`);
setTimeout(() => setLastSent(null), 3000);
} catch (error) {
console.error('Failed to send demo notification:', error);
} finally {
// Silent failure for demo notifications
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
@@ -464,286 +348,62 @@ export default function DevToolbar() {
{/* 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
`}
<div className="p-4 space-y-3">
{/* Notification Section - Accordion */}
<Accordion
title="Notifications"
icon={<MessageSquare className="w-4 h-4 text-gray-400" />}
defaultOpen={true}
>
{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 className="space-y-3">
<NotificationTypeSection
selectedType={selectedType}
onSelectType={setSelectedType}
/>
<UrgencySection
selectedUrgency={selectedUrgency}
onSelectUrgency={setSelectedUrgency}
/>
<NotificationSendSection
selectedType={selectedType}
selectedUrgency={selectedUrgency}
sending={sending}
lastSent={lastSent}
onSend={handleSendNotification}
/>
</div>
</Accordion>
<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>
{/* API Status Section - Accordion */}
<Accordion
title="API Status"
icon={<Activity className="w-4 h-4 text-gray-400" />}
defaultOpen={false}
>
<APIStatusSection
apiStatus={apiStatus}
apiHealth={apiHealth}
circuitBreakers={circuitBreakers}
checkingHealth={checkingHealth}
onHealthCheck={handleApiHealthCheck}
onResetStats={handleResetApiStats}
onTestError={handleTestApiError}
/>
</Accordion>
{/* 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>
{/* Login Section - Accordion */}
<Accordion
title="Demo Login"
icon={<LogIn className="w-4 h-4 text-gray-400" />}
defaultOpen={false}
>
<LoginSection
loginMode={loginMode}
loggingIn={loggingIn}
onDemoLogin={handleDemoLogin}
onLogout={handleLogout}
/>
</Accordion>
</div>
)}
@@ -755,4 +415,4 @@ export default function DevToolbar() {
)}
</div>
);
}
}

View File

@@ -0,0 +1,145 @@
'use client';
import { Activity, Wifi, RefreshCw, Terminal } from 'lucide-react';
import { useState } from 'react';
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import { useNotifications } from '@/components/notifications/NotificationProvider';
interface APIStatusSectionProps {
apiStatus: string;
apiHealth: any;
circuitBreakers: Record<string, any>;
checkingHealth: boolean;
onHealthCheck: () => void;
onResetStats: () => void;
onTestError: () => void;
}
export function APIStatusSection({
apiStatus,
apiHealth,
circuitBreakers,
checkingHealth,
onHealthCheck,
onResetStats,
onTestError
}: APIStatusSectionProps) {
return (
<div>
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-gray-400" />
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
API Status
</span>
</div>
{/* Status Indicator */}
<div className={`flex items-center justify-between p-2 rounded-lg mb-2 ${
apiStatus === 'connected' ? 'bg-green-500/10 border border-green-500/30' :
apiStatus === 'degraded' ? 'bg-yellow-500/10 border border-yellow-500/30' :
'bg-red-500/10 border border-red-500/30'
}`}>
<div className="flex items-center gap-2">
<Wifi className={`w-4 h-4 ${
apiStatus === 'connected' ? 'text-green-400' :
apiStatus === 'degraded' ? 'text-yellow-400' :
'text-red-400'
}`} />
<span className="text-sm font-semibold text-white">{apiStatus.toUpperCase()}</span>
</div>
<span className="text-xs text-gray-400">
{apiHealth.successfulRequests}/{apiHealth.totalRequests} req
</span>
</div>
{/* Reliability */}
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-gray-500">Reliability</span>
<span className={`font-bold ${
apiHealth.totalRequests === 0 ? 'text-gray-500' :
(apiHealth.successfulRequests / apiHealth.totalRequests) >= 0.95 ? 'text-green-400' :
(apiHealth.successfulRequests / apiHealth.totalRequests) >= 0.8 ? 'text-yellow-400' :
'text-red-400'
}`}>
{apiHealth.totalRequests === 0 ? 'N/A' :
((apiHealth.successfulRequests / apiHealth.totalRequests) * 100).toFixed(1) + '%'}
</span>
</div>
{/* Response Time */}
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-gray-500">Avg Response</span>
<span className="text-blue-400 font-mono">
{apiHealth.averageResponseTime.toFixed(0)}ms
</span>
</div>
{/* Consecutive Failures */}
{apiHealth.consecutiveFailures > 0 && (
<div className="flex items-center justify-between text-xs mb-2 bg-red-500/10 rounded px-2 py-1">
<span className="text-red-400">Consecutive Failures</span>
<span className="text-red-400 font-bold">{apiHealth.consecutiveFailures}</span>
</div>
)}
{/* Circuit Breakers */}
<div className="mt-2">
<div className="text-[10px] text-gray-500 mb-1">Circuit Breakers:</div>
{Object.keys(circuitBreakers).length === 0 ? (
<div className="text-[10px] text-gray-500 italic">None active</div>
) : (
<div className="space-y-1 max-h-16 overflow-auto">
{Object.entries(circuitBreakers).map(([endpoint, status]: [string, any]) => (
<div key={endpoint} className="flex items-center justify-between text-[10px]">
<span className="text-gray-400 truncate flex-1">{endpoint.split('/').pop() || endpoint}</span>
<span className={`px-1 rounded ${
status.state === 'CLOSED' ? 'bg-green-500/20 text-green-400' :
status.state === 'OPEN' ? 'bg-red-500/20 text-red-400' :
'bg-yellow-500/20 text-yellow-400'
}`}>
{status.state}
</span>
{status.failures > 0 && (
<span className="text-red-400 ml-1">({status.failures})</span>
)}
</div>
))}
</div>
)}
</div>
{/* API Actions */}
<div className="grid grid-cols-2 gap-2 mt-3">
<button
onClick={onHealthCheck}
disabled={checkingHealth}
className="px-2 py-1.5 bg-primary-blue hover:bg-primary-blue/80 text-white text-xs rounded transition-colors disabled:opacity-50 flex items-center justify-center gap-1"
>
<RefreshCw className={`w-3 h-3 ${checkingHealth ? 'animate-spin' : ''}`} />
Health Check
</button>
<button
onClick={onResetStats}
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 text-xs rounded transition-colors border border-charcoal-outline"
>
Reset Stats
</button>
</div>
<div className="grid grid-cols-1 gap-2 mt-2">
<button
onClick={onTestError}
className="px-2 py-1.5 bg-red-600 hover:bg-red-700 text-white text-xs rounded transition-colors flex items-center justify-center gap-1"
>
<Terminal className="w-3 h-3" />
Test Error Handler
</button>
</div>
<div className="text-[10px] text-gray-600 mt-2">
Last Check: {apiHealth.lastCheck ? new Date(apiHealth.lastCheck).toLocaleTimeString() : 'Never'}
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
'use client';
import { LogIn, LogOut, User, Shield, Building2 } from 'lucide-react';
import type { LoginMode } from '../types';
interface LoginSectionProps {
loginMode: LoginMode;
loggingIn: boolean;
onDemoLogin: (role: LoginMode) => void;
onLogout: () => void;
}
export function LoginSection({ loginMode, loggingIn, onDemoLogin, onLogout }: LoginSectionProps) {
const loginOptions = [
{ mode: 'driver' as LoginMode, label: 'Driver', icon: User, color: 'primary-blue', emoji: null },
{ mode: 'league-owner' as LoginMode, label: 'League Owner', icon: null, color: 'purple-500', emoji: '👑' },
{ mode: 'league-steward' as LoginMode, label: 'Steward', icon: Shield, color: 'amber-500', emoji: null },
{ mode: 'league-admin' as LoginMode, label: 'Admin', icon: null, color: 'red-500', emoji: '⚙️' },
{ mode: 'sponsor' as LoginMode, label: 'Sponsor', icon: Building2, color: 'performance-green', emoji: null },
{ mode: 'system-owner' as LoginMode, label: 'System Owner', icon: null, color: 'indigo-500', emoji: '👑' },
{ mode: 'super-admin' as LoginMode, label: 'Super Admin', icon: null, color: 'pink-500', emoji: '⚡' },
];
return (
<div>
<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">
{loginOptions.map((option) => {
const Icon = option.icon;
const isSelected = loginMode === option.mode;
return (
<button
key={option.mode}
onClick={() => onDemoLogin(option.mode)}
disabled={loggingIn || isSelected}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${isSelected
? `bg-${option.color}/20 border-${option.color}/50 text-${option.color}`
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{option.emoji ? (
<span className="text-xs">{option.emoji}</span>
) : Icon ? (
<Icon className="w-4 h-4" />
) : null}
{isSelected ? `${option.label}` : `Login as ${option.label}`}
</button>
);
})}
{loginMode !== 'none' && (
<button
onClick={onLogout}
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>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import { Bell } from 'lucide-react';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import type { DemoNotificationType, DemoUrgency } from '../types';
interface NotificationSendSectionProps {
selectedType: DemoNotificationType;
selectedUrgency: DemoUrgency;
sending: boolean;
lastSent: string | null;
onSend: () => void;
}
export function NotificationSendSection({
selectedType,
selectedUrgency,
sending,
lastSent,
onSend
}: NotificationSendSectionProps) {
return (
<div>
<button
onClick={onSend}
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>
<div className="p-3 rounded-lg bg-iron-gray/50 border border-charcoal-outline mt-2">
<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>
</div>
);
}

View File

@@ -0,0 +1,94 @@
'use client';
import { MessageSquare, AlertTriangle, Shield, Vote, TrendingUp, Award } from 'lucide-react';
import type { DemoNotificationType } from '../types';
interface NotificationOption {
type: DemoNotificationType;
label: string;
description: string;
icon: any;
color: string;
}
interface NotificationTypeSectionProps {
selectedType: DemoNotificationType;
onSelectType: (type: DemoNotificationType) => void;
}
export 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',
},
];
export function NotificationTypeSection({ selectedType, onSelectType }: NotificationTypeSectionProps) {
return (
<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={() => onSelectType(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>
);
}

View File

@@ -0,0 +1,99 @@
'use client';
import { Bell, BellRing, AlertCircle } from 'lucide-react';
import type { DemoUrgency } from '../types';
interface UrgencyOption {
urgency: DemoUrgency;
label: string;
description: string;
icon: any;
}
interface UrgencySectionProps {
selectedUrgency: DemoUrgency;
onSelectUrgency: (urgency: DemoUrgency) => void;
}
export 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,
},
];
export function UrgencySection({ selectedUrgency, onSelectUrgency }: UrgencySectionProps) {
return (
<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={() => onSelectUrgency(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>
);
}

View File

@@ -0,0 +1,20 @@
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
export type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required' | 'race_performance_summary' | 'race_final_results';
export type DemoUrgency = 'silent' | 'toast' | 'modal';
export type LoginMode = 'none' | 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin';
export interface NotificationOption {
type: DemoNotificationType;
label: string;
description: string;
icon: any;
color: string;
}
export interface UrgencyOption {
urgency: DemoUrgency;
label: string;
description: string;
icon: any;
}