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,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>
);
}