website refactor
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
|
||||
interface AccordionProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
children: ReactNode;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) {
|
||||
return (
|
||||
<div className="border border-charcoal-outline rounded-lg overflow-hidden bg-iron-gray/30">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Bug, X, Settings, Shield, Activity } from 'lucide-react';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||
import type { GlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import type { ApiRequestLogger } from '@/lib/infrastructure/ApiRequestLogger';
|
||||
|
||||
// Extend Window interface for debug globals
|
||||
declare global {
|
||||
interface Window {
|
||||
__GRIDPILOT_FETCH_LOGGED__?: boolean;
|
||||
__GRIDPILOT_GLOBAL_HANDLER__?: GlobalErrorHandler;
|
||||
__GRIDPILOT_API_LOGGER__?: ApiRequestLogger;
|
||||
__GRIDPILOT_REACT_ERRORS__?: Array<{ error: unknown; componentStack?: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
interface DebugModeToggleProps {
|
||||
/**
|
||||
* Whether to show the toggle (auto-detected from environment)
|
||||
*/
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Mode Toggle Component
|
||||
* Provides a floating interface to control debug features and view real-time metrics
|
||||
*/
|
||||
export function DebugModeToggle({ show }: DebugModeToggleProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
const [metrics, setMetrics] = useState({
|
||||
errors: 0,
|
||||
apiRequests: 0,
|
||||
apiFailures: 0,
|
||||
});
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const shouldShow = show ?? isDev;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShow) return;
|
||||
|
||||
// Load debug state from localStorage
|
||||
const saved = localStorage.getItem('gridpilot_debug_enabled');
|
||||
if (saved === 'true') {
|
||||
setDebugEnabled(true);
|
||||
initializeDebugFeatures();
|
||||
}
|
||||
|
||||
// Update metrics every 2 seconds
|
||||
const interval = setInterval(updateMetrics, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [shouldShow]);
|
||||
|
||||
useEffect(() => {
|
||||
// Save debug state
|
||||
if (shouldShow) {
|
||||
localStorage.setItem('gridpilot_debug_enabled', debugEnabled.toString());
|
||||
}
|
||||
}, [debugEnabled, shouldShow]);
|
||||
|
||||
const updateMetrics = () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const errorStats = globalHandler.getStats();
|
||||
const apiStats = apiLogger.getStats();
|
||||
|
||||
setMetrics({
|
||||
errors: errorStats.total,
|
||||
apiRequests: apiStats.total,
|
||||
apiFailures: apiStats.failed,
|
||||
});
|
||||
};
|
||||
|
||||
const initializeDebugFeatures = () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
// Initialize global error handler
|
||||
globalHandler.initialize();
|
||||
|
||||
// Override fetch with logging
|
||||
if (!window.__GRIDPILOT_FETCH_LOGGED__) {
|
||||
const loggedFetch = apiLogger.createLoggedFetch();
|
||||
window.fetch = loggedFetch as typeof fetch;
|
||||
window.__GRIDPILOT_FETCH_LOGGED__ = true;
|
||||
}
|
||||
|
||||
// Expose to window for easy access
|
||||
window.__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
||||
window.__GRIDPILOT_API_LOGGER__ = apiLogger;
|
||||
|
||||
console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;');
|
||||
console.log('Available globals:', {
|
||||
__GRIDPILOT_GLOBAL_HANDLER__: globalHandler,
|
||||
__GRIDPILOT_API_LOGGER__: apiLogger,
|
||||
__GRIDPILOT_REACT_ERRORS__: window.__GRIDPILOT_REACT_ERRORS__ || [],
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDebug = () => {
|
||||
const newEnabled = !debugEnabled;
|
||||
setDebugEnabled(newEnabled);
|
||||
|
||||
if (newEnabled) {
|
||||
initializeDebugFeatures();
|
||||
} else {
|
||||
// Disable debug features
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.destroy();
|
||||
|
||||
console.log('%c[DEBUG MODE] Disabled', 'color: #ff4444; font-weight: bold; font-size: 14px;');
|
||||
}
|
||||
};
|
||||
|
||||
const triggerTestError = () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
// Trigger a test API error
|
||||
const testError = new Error('This is a test error for debugging');
|
||||
(testError as any).type = 'TEST_ERROR';
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.report(testError, { test: true, timestamp: Date.now() });
|
||||
|
||||
console.log('%c[TEST] Error triggered', 'color: #ffaa00; font-weight: bold;', testError);
|
||||
};
|
||||
|
||||
const triggerTestApiCall = async () => {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
try {
|
||||
// This will fail and be logged
|
||||
await fetch('https://httpstat.us/500');
|
||||
} catch (error) {
|
||||
// Already logged by interceptor
|
||||
console.log('%c[TEST] API call completed', 'color: #00aaff; font-weight: bold;');
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllLogs = () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
globalHandler.clearHistory();
|
||||
apiLogger.clearHistory();
|
||||
|
||||
setMetrics({ errors: 0, apiRequests: 0, apiFailures: 0 });
|
||||
|
||||
console.log('%c[DEBUG] All logs cleared', 'color: #00ff88; font-weight: bold;');
|
||||
};
|
||||
|
||||
const copyDebugInfo = async () => {
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
|
||||
const debugInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: {
|
||||
mode: process.env.NODE_ENV,
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
},
|
||||
browser: {
|
||||
userAgent: navigator.userAgent,
|
||||
language: navigator.language,
|
||||
platform: navigator.platform,
|
||||
},
|
||||
errors: globalHandler.getStats(),
|
||||
api: apiLogger.getStats(),
|
||||
reactErrors: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
|
||||
};
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
|
||||
console.log('%c[DEBUG] Debug info copied to clipboard', 'color: #00ff88; font-weight: bold;');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-50">
|
||||
{/* Main Toggle Button */}
|
||||
{!isOpen && (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className={`p-3 rounded-full shadow-lg transition-all ${
|
||||
debugEnabled
|
||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||||
: 'bg-iron-gray hover:bg-charcoal-outline text-gray-300'
|
||||
}`}
|
||||
title={debugEnabled ? 'Debug Mode Active' : 'Enable Debug Mode'}
|
||||
>
|
||||
<Bug className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Debug Panel */}
|
||||
{isOpen && (
|
||||
<div className="w-80 bg-deep-graphite border border-charcoal-outline rounded-xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-iron-gray/50 border-b border-charcoal-outline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bug className="w-4 h-4 text-green-400" />
|
||||
<span className="text-sm font-semibold text-white">Debug Control</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
>
|
||||
<X className="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Debug Toggle */}
|
||||
<div className="flex items-center justify-between bg-iron-gray/30 p-2 rounded border border-charcoal-outline">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className={`w-4 h-4 ${debugEnabled ? 'text-green-400' : 'text-gray-500'}`} />
|
||||
<span className="text-sm font-medium">Debug Mode</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleDebug}
|
||||
className={`px-3 py-1 rounded text-xs font-bold transition-colors ${
|
||||
debugEnabled
|
||||
? 'bg-green-600 text-white hover:bg-green-700'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{debugEnabled ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
{debugEnabled && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">Errors</div>
|
||||
<div className="text-lg font-bold text-red-400">{metrics.errors}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">API</div>
|
||||
<div className="text-lg font-bold text-blue-400">{metrics.apiRequests}</div>
|
||||
</div>
|
||||
<div className="bg-iron-gray border border-charcoal-outline rounded p-2 text-center">
|
||||
<div className="text-[10px] text-gray-500">Failures</div>
|
||||
<div className="text-lg font-bold text-yellow-400">{metrics.apiFailures}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{debugEnabled && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-gray-400">Test Actions</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={triggerTestError}
|
||||
className="px-2 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-xs font-medium"
|
||||
>
|
||||
Test Error
|
||||
</button>
|
||||
<button
|
||||
onClick={triggerTestApiCall}
|
||||
className="px-2 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium"
|
||||
>
|
||||
Test API
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-semibold text-gray-400 mt-2">Utilities</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={copyDebugInfo}
|
||||
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
|
||||
>
|
||||
Copy Info
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAllLogs}
|
||||
className="px-2 py-1.5 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded text-xs font-medium border border-charcoal-outline"
|
||||
>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Links */}
|
||||
{debugEnabled && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-semibold text-gray-400">Quick Access</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono space-y-0.5">
|
||||
<div>• window.__GRIDPILOT_GLOBAL_HANDLER__</div>
|
||||
<div>• window.__GRIDPILOT_API_LOGGER__</div>
|
||||
<div>• window.__GRIDPILOT_REACT_ERRORS__</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
<div className="text-[10px] text-gray-500 text-center pt-2 border-t border-charcoal-outline">
|
||||
{debugEnabled ? 'Debug features active' : 'Debug mode disabled'}
|
||||
{isDev && ' • Development Environment'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for programmatic debug control
|
||||
*/
|
||||
export function useDebugMode() {
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('gridpilot_debug_enabled');
|
||||
setDebugEnabled(saved === 'true');
|
||||
}, []);
|
||||
|
||||
const enable = () => {
|
||||
setDebugEnabled(true);
|
||||
localStorage.setItem('gridpilot_debug_enabled', 'true');
|
||||
|
||||
// Initialize debug features
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.initialize();
|
||||
|
||||
const apiLogger = getGlobalApiLogger();
|
||||
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
|
||||
const loggedFetch = apiLogger.createLoggedFetch();
|
||||
window.fetch = loggedFetch as any;
|
||||
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
|
||||
}
|
||||
|
||||
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
|
||||
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
|
||||
};
|
||||
|
||||
const disable = () => {
|
||||
setDebugEnabled(false);
|
||||
localStorage.setItem('gridpilot_debug_enabled', 'false');
|
||||
|
||||
const globalHandler = getGlobalErrorHandler();
|
||||
globalHandler.destroy();
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
if (debugEnabled) {
|
||||
disable();
|
||||
} else {
|
||||
enable();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: debugEnabled,
|
||||
enable,
|
||||
disable,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
|
||||
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, AlertTriangle } 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';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
||||
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||
|
||||
// Import our new components
|
||||
import { Accordion } from './Accordion';
|
||||
import { Accordion } from '@/ui/Accordion';
|
||||
import { NotificationTypeSection } from './sections/NotificationTypeSection';
|
||||
import { UrgencySection } from './sections/UrgencySection';
|
||||
import { NotificationSendSection } from './sections/NotificationSendSection';
|
||||
import { APIStatusSection } from './sections/APIStatusSection';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
// Import types
|
||||
import type { DemoNotificationType, DemoUrgency } from './types';
|
||||
|
||||
export default function DevToolbar() {
|
||||
const router = useRouter();
|
||||
export function DevToolbar() {
|
||||
const { addNotification } = useNotifications();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
@@ -225,140 +230,155 @@ export default function DevToolbar() {
|
||||
|
||||
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>
|
||||
<Box position="fixed" bottom="4" right="4" zIndex={50}>
|
||||
<IconButton
|
||||
icon={Wrench}
|
||||
onClick={() => setIsMinimized(false)}
|
||||
variant="secondary"
|
||||
title="Open Dev Toolbar"
|
||||
size="lg"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<Box
|
||||
position="fixed"
|
||||
bottom="4"
|
||||
right="4"
|
||||
zIndex={50}
|
||||
w="80"
|
||||
bg="bg-deep-graphite"
|
||||
border
|
||||
borderColor="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">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" px={4} py={3} bg="bg-iron-gray/50" borderBottom borderColor="border-charcoal-outline">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Wrench} size={4} color="rgb(59, 130, 246)" />
|
||||
<Text size="sm" weight="semibold" color="text-white">Dev Toolbar</Text>
|
||||
<Badge variant="primary" size="xs">
|
||||
DEMO
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<IconButton
|
||||
icon={isExpanded ? ChevronDown : ChevronUp}
|
||||
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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
<IconButton
|
||||
icon={X}
|
||||
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>
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Notification Section - Accordion */}
|
||||
<Accordion
|
||||
title="Notifications"
|
||||
icon={<MessageSquare className="w-4 h-4 text-gray-400" />}
|
||||
isOpen={openAccordion === 'notifications'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'notifications' ? null : 'notifications')}
|
||||
>
|
||||
<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>
|
||||
<Box p={4}>
|
||||
<Stack gap={3}>
|
||||
{/* Notification Section - Accordion */}
|
||||
<Accordion
|
||||
title="Notifications"
|
||||
icon={<Icon icon={MessageSquare} size={4} color="rgb(156, 163, 175)" />}
|
||||
isOpen={openAccordion === 'notifications'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'notifications' ? null : 'notifications')}
|
||||
>
|
||||
<Stack gap={3}>
|
||||
<NotificationTypeSection
|
||||
selectedType={selectedType}
|
||||
onSelectType={setSelectedType}
|
||||
/>
|
||||
<UrgencySection
|
||||
selectedUrgency={selectedUrgency}
|
||||
onSelectUrgency={setSelectedUrgency}
|
||||
/>
|
||||
<NotificationSendSection
|
||||
selectedType={selectedType}
|
||||
selectedUrgency={selectedUrgency}
|
||||
sending={sending}
|
||||
lastSent={lastSent}
|
||||
onSend={handleSendNotification}
|
||||
/>
|
||||
</Stack>
|
||||
</Accordion>
|
||||
|
||||
{/* API Status Section - Accordion */}
|
||||
<Accordion
|
||||
title="API Status"
|
||||
icon={<Activity className="w-4 h-4 text-gray-400" />}
|
||||
isOpen={openAccordion === 'apiStatus'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'apiStatus' ? null : 'apiStatus')}
|
||||
>
|
||||
<APIStatusSection
|
||||
apiStatus={apiStatus}
|
||||
apiHealth={apiHealth}
|
||||
circuitBreakers={circuitBreakers}
|
||||
checkingHealth={checkingHealth}
|
||||
onHealthCheck={handleApiHealthCheck}
|
||||
onResetStats={handleResetApiStats}
|
||||
onTestError={handleTestApiError}
|
||||
/>
|
||||
</Accordion>
|
||||
{/* API Status Section - Accordion */}
|
||||
<Accordion
|
||||
title="API Status"
|
||||
icon={<Icon icon={Activity} size={4} color="rgb(156, 163, 175)" />}
|
||||
isOpen={openAccordion === 'apiStatus'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'apiStatus' ? null : 'apiStatus')}
|
||||
>
|
||||
<APIStatusSection
|
||||
apiStatus={apiStatus}
|
||||
apiHealth={apiHealth}
|
||||
circuitBreakers={circuitBreakers}
|
||||
checkingHealth={checkingHealth}
|
||||
onHealthCheck={handleApiHealthCheck}
|
||||
onResetStats={handleResetApiStats}
|
||||
onTestError={handleTestApiError}
|
||||
/>
|
||||
</Accordion>
|
||||
|
||||
{/* Error Stats Section - Accordion */}
|
||||
<Accordion
|
||||
title="Error Stats"
|
||||
icon={<AlertTriangle className="w-4 h-4 text-gray-400" />}
|
||||
isOpen={openAccordion === 'errors'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'errors' ? null : 'errors')}
|
||||
>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex justify-between items-center p-2 bg-iron-gray/30 rounded">
|
||||
<span className="text-gray-400">Total Errors</span>
|
||||
<span className="font-mono font-bold text-red-400">{errorStats.total}</span>
|
||||
</div>
|
||||
{Object.keys(errorStats.byType).length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{Object.entries(errorStats.byType).map(([type, count]) => (
|
||||
<div key={type} className="flex justify-between items-center p-1.5 bg-deep-graphite rounded">
|
||||
<span className="text-gray-300">{type}</span>
|
||||
<span className="font-mono text-yellow-400">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-2">No errors yet</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const handler = getGlobalErrorHandler();
|
||||
handler.clearHistory();
|
||||
setErrorStats({ total: 0, byType: {} });
|
||||
}}
|
||||
className="w-full p-2 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline text-xs"
|
||||
>
|
||||
Clear Error History
|
||||
</button>
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
</div>
|
||||
{/* Error Stats Section - Accordion */}
|
||||
<Accordion
|
||||
title="Error Stats"
|
||||
icon={<Icon icon={AlertTriangle} size={4} color="rgb(156, 163, 175)" />}
|
||||
isOpen={openAccordion === 'errors'}
|
||||
onToggle={() => setOpenAccordion(openAccordion === 'errors' ? null : 'errors')}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" justifyContent="between" alignItems="center" p={2} bg="bg-iron-gray/30" rounded="md">
|
||||
<Text size="xs" color="text-gray-400">Total Errors</Text>
|
||||
<Text size="xs" font="mono" weight="bold" color="text-red-400">{errorStats.total}</Text>
|
||||
</Box>
|
||||
{Object.keys(errorStats.byType).length > 0 ? (
|
||||
<Stack gap={1}>
|
||||
{Object.entries(errorStats.byType).map(([type, count]) => (
|
||||
<Box key={type} display="flex" justifyContent="between" alignItems="center" p={1.5} bg="bg-deep-graphite" rounded="md">
|
||||
<Text size="xs" color="text-gray-300">{type}</Text>
|
||||
<Text size="xs" font="mono" color="text-warning-amber">{count}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Box textAlign="center" py={2}>
|
||||
<Text size="xs" color="text-gray-500">No errors yet</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const handler = getGlobalErrorHandler();
|
||||
handler.clearHistory();
|
||||
setErrorStats({ total: 0, byType: {} });
|
||||
}}
|
||||
fullWidth
|
||||
size="sm"
|
||||
>
|
||||
Clear Error History
|
||||
</Button>
|
||||
</Stack>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Collapsed state hint */}
|
||||
{!isExpanded && (
|
||||
<div className="px-4 py-2 text-xs text-gray-500">
|
||||
Click ↑ to expand dev tools
|
||||
</div>
|
||||
<Box px={4} py={2}>
|
||||
<Text size="xs" color="text-gray-500">Click ↑ to expand dev tools</Text>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
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';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { StatusIndicator, StatRow, Badge } from '@/ui/StatusIndicator';
|
||||
|
||||
interface APIStatusSectionProps {
|
||||
apiStatus: string;
|
||||
apiHealth: any;
|
||||
circuitBreakers: Record<string, any>;
|
||||
apiHealth: {
|
||||
successfulRequests: number;
|
||||
totalRequests: number;
|
||||
averageResponseTime: number;
|
||||
consecutiveFailures: number;
|
||||
lastCheck: number | Date | null;
|
||||
};
|
||||
circuitBreakers: Record<string, { state: string; failures: number }>;
|
||||
checkingHealth: boolean;
|
||||
onHealthCheck: () => void;
|
||||
onResetStats: () => void;
|
||||
@@ -25,121 +34,137 @@ export function APIStatusSection({
|
||||
onResetStats,
|
||||
onTestError
|
||||
}: APIStatusSectionProps) {
|
||||
const reliability = apiHealth.totalRequests === 0
|
||||
? 0
|
||||
: (apiHealth.successfulRequests / apiHealth.totalRequests);
|
||||
|
||||
const reliabilityLabel = apiHealth.totalRequests === 0 ? 'N/A' : 'Calculated';
|
||||
|
||||
const getReliabilityColor = () => {
|
||||
if (apiHealth.totalRequests === 0) return 'text-gray-500';
|
||||
if (reliability >= 0.95) return 'text-performance-green';
|
||||
if (reliability >= 0.8) return 'text-warning-amber';
|
||||
return 'text-red-400';
|
||||
};
|
||||
|
||||
const getStatusVariant = () => {
|
||||
if (apiStatus === 'connected') return 'success';
|
||||
if (apiStatus === 'degraded') return 'warning';
|
||||
return 'danger';
|
||||
};
|
||||
|
||||
const statusLabel = apiStatus.toUpperCase();
|
||||
const healthSummary = `${apiHealth.successfulRequests}/${apiHealth.totalRequests} req`;
|
||||
const avgResponseLabel = `${apiHealth.averageResponseTime.toFixed(0)}ms`;
|
||||
const lastCheckLabel = apiHealth.lastCheck ? 'Recently' : 'Never';
|
||||
const consecutiveFailuresLabel = String(apiHealth.consecutiveFailures);
|
||||
|
||||
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">
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={3}>
|
||||
<Icon icon={Activity} size={4} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400" uppercase letterSpacing="wide">
|
||||
API Status
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 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>
|
||||
<StatusIndicator
|
||||
icon={Wifi}
|
||||
label={statusLabel}
|
||||
subLabel={healthSummary}
|
||||
variant={getStatusVariant()}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<Box mt={2}>
|
||||
{/* Reliability */}
|
||||
<StatRow
|
||||
label="Reliability"
|
||||
value={reliabilityLabel}
|
||||
valueColor={getReliabilityColor()}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
{/* Response Time */}
|
||||
<StatRow
|
||||
label="Avg Response"
|
||||
value={avgResponseLabel}
|
||||
valueColor="text-primary-blue"
|
||||
valueFont="mono"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 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>
|
||||
<Box mt={2}>
|
||||
<StatusIndicator
|
||||
icon={Activity}
|
||||
label="Consecutive Failures"
|
||||
subLabel={consecutiveFailuresLabel}
|
||||
variant="danger"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Circuit Breakers */}
|
||||
<div className="mt-2">
|
||||
<div className="text-[10px] text-gray-500 mb-1">Circuit Breakers:</div>
|
||||
<Box mt={2}>
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>Circuit Breakers:</Text>
|
||||
{Object.keys(circuitBreakers).length === 0 ? (
|
||||
<div className="text-[10px] text-gray-500 italic">None active</div>
|
||||
<Text size="xs" color="text-gray-500" italic>None active</Text>
|
||||
) : (
|
||||
<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'
|
||||
}`}>
|
||||
<Stack gap={1} maxHeight="4rem" overflow="auto">
|
||||
{Object.entries(circuitBreakers).map(([endpoint, status]) => (
|
||||
<Box key={endpoint} display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="xs" color="text-gray-400" truncate flexGrow={1}>
|
||||
{endpoint}
|
||||
</Text>
|
||||
<Badge variant={status.state === 'CLOSED' ? 'success' : status.state === 'OPEN' ? 'danger' : 'warning'}>
|
||||
{status.state}
|
||||
</span>
|
||||
</Badge>
|
||||
{status.failures > 0 && (
|
||||
<span className="text-red-400 ml-1">({status.failures})</span>
|
||||
<Text size="xs" color="text-red-400" ml={1}>({status.failures})</Text>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{/* API Actions */}
|
||||
<div className="grid grid-cols-2 gap-2 mt-3">
|
||||
<button
|
||||
<Box display="grid" gridCols={2} gap={2} mt={3}>
|
||||
<Button
|
||||
variant="primary"
|
||||
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"
|
||||
size="sm"
|
||||
icon={<Icon icon={RefreshCw} size={3} animate={checkingHealth ? 'spin' : 'none'} />}
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${checkingHealth ? 'animate-spin' : ''}`} />
|
||||
Health Check
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
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"
|
||||
size="sm"
|
||||
>
|
||||
Reset Stats
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 mt-2">
|
||||
<button
|
||||
<Box display="grid" gridCols={1} gap={2} mt={2}>
|
||||
<Button
|
||||
variant="danger"
|
||||
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"
|
||||
size="sm"
|
||||
icon={<Icon icon={Terminal} size={3} />}
|
||||
>
|
||||
<Terminal className="w-3 h-3" />
|
||||
Test Error Handler
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<div className="text-[10px] text-gray-600 mt-2">
|
||||
Last Check: {apiHealth.lastCheck ? new Date(apiHealth.lastCheck).toLocaleTimeString() : 'Never'}
|
||||
</div>
|
||||
</div>
|
||||
<Box mt={2}>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
Last Check: {lastCheckLabel}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Bell } from 'lucide-react';
|
||||
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
|
||||
import { useNotifications } from '@/components/notifications/NotificationProvider';
|
||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||
import React from 'react';
|
||||
import { Bell, Loader2 } from 'lucide-react';
|
||||
import type { DemoNotificationType, DemoUrgency } from '../types';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface NotificationSendSectionProps {
|
||||
selectedType: DemoNotificationType;
|
||||
@@ -15,50 +17,36 @@ interface NotificationSendSectionProps {
|
||||
}
|
||||
|
||||
export function NotificationSendSection({
|
||||
selectedType,
|
||||
selectedUrgency,
|
||||
sending,
|
||||
lastSent,
|
||||
onSend
|
||||
}: NotificationSendSectionProps) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
<Box>
|
||||
<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
|
||||
`}
|
||||
variant={lastSent ? 'secondary' : 'primary'}
|
||||
fullWidth
|
||||
bg={lastSent ? 'bg-performance-green/20' : undefined}
|
||||
borderColor={lastSent ? 'border-performance-green/30' : undefined}
|
||||
color={lastSent ? 'text-performance-green' : undefined}
|
||||
icon={sending ? <Icon icon={Loader2} size={4} animate="spin" /> : lastSent ? undefined : <Icon icon={Bell} size={4} />}
|
||||
>
|
||||
{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>
|
||||
{sending ? 'Sending...' : lastSent ? '✓ Notification Sent!' : '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>
|
||||
<Box p={3} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" mt={2}>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
<Text weight="bold" color="text-gray-400">Silent:</Text> Notification center only
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
<Text weight="bold" color="text-gray-400">Toast:</Text> Temporary popup (auto-dismisses)
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
<Text weight="bold" color="text-gray-400">Modal:</Text> Blocking popup (may require action)
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { MessageSquare, AlertTriangle, Shield, Vote, TrendingUp, Award } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { MessageSquare, AlertTriangle, Shield, Vote, TrendingUp, Award, LucideIcon } from 'lucide-react';
|
||||
import type { DemoNotificationType } from '../types';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface NotificationOption {
|
||||
type: DemoNotificationType;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
}
|
||||
|
||||
@@ -22,73 +26,85 @@ export const notificationOptions: NotificationOption[] = [
|
||||
label: 'Protest Against You',
|
||||
description: 'A protest was filed against you',
|
||||
icon: AlertTriangle,
|
||||
color: 'text-red-400',
|
||||
color: 'rgb(239, 68, 68)',
|
||||
},
|
||||
{
|
||||
type: 'defense_requested',
|
||||
label: 'Defense Requested',
|
||||
description: 'A steward requests your defense',
|
||||
icon: Shield,
|
||||
color: 'text-warning-amber',
|
||||
color: 'rgb(245, 158, 11)',
|
||||
},
|
||||
{
|
||||
type: 'vote_required',
|
||||
label: 'Vote Required',
|
||||
description: 'You need to vote on a protest',
|
||||
icon: Vote,
|
||||
color: 'text-primary-blue',
|
||||
color: 'rgb(59, 130, 246)',
|
||||
},
|
||||
{
|
||||
type: 'race_performance_summary',
|
||||
label: 'Race Performance Summary',
|
||||
description: 'Immediate results after main race',
|
||||
icon: TrendingUp,
|
||||
color: 'text-primary-blue',
|
||||
color: 'rgb(59, 130, 246)',
|
||||
},
|
||||
{
|
||||
type: 'race_final_results',
|
||||
label: 'Race Final Results',
|
||||
description: 'Final results after stewarding closes',
|
||||
icon: Award,
|
||||
color: 'text-warning-amber',
|
||||
color: 'rgb(245, 158, 11)',
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={MessageSquare} size={4} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400" uppercase letterSpacing="wide">
|
||||
Notification Type
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<Box display="grid" gridCols={2} gap={1}>
|
||||
{notificationOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = selectedType === option.type;
|
||||
|
||||
return (
|
||||
<button
|
||||
<Box
|
||||
key={option.type}
|
||||
as="button"
|
||||
type="button"
|
||||
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'
|
||||
}
|
||||
`}
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
p={2}
|
||||
rounded="lg"
|
||||
border
|
||||
transition
|
||||
bg={isSelected ? 'bg-primary-blue/20' : 'bg-iron-gray/30'}
|
||||
borderColor={isSelected ? 'border-primary-blue/50' : 'border-charcoal-outline'}
|
||||
>
|
||||
<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'}`}>
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
size={4}
|
||||
color={isSelected ? 'rgb(59, 130, 246)' : option.color}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
weight="medium"
|
||||
color={isSelected ? 'text-primary-blue' : 'text-gray-400'}
|
||||
>
|
||||
{option.label.split(' ')[0]}
|
||||
</span>
|
||||
</button>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Play, Copy, Trash2, Download, Clock } from 'lucide-react';
|
||||
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface ReplayEntry {
|
||||
id: string;
|
||||
@@ -11,7 +19,6 @@ interface ReplayEntry {
|
||||
|
||||
export function ReplaySection() {
|
||||
const [replays, setReplays] = useState<ReplayEntry[]>([]);
|
||||
const [selectedReplay, setSelectedReplay] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -78,83 +85,89 @@ export function ReplaySection() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-400">Error Replay</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
<Stack gap={2}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Text size="xs" weight="semibold" color="text-gray-400">Error Replay</Text>
|
||||
<Box display="flex" gap={1}>
|
||||
<IconButton
|
||||
icon={Clock}
|
||||
onClick={loadReplays}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Refresh"
|
||||
>
|
||||
<Clock className="w-3 h-3 text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
/>
|
||||
<IconButton
|
||||
icon={Trash2}
|
||||
onClick={handleClearAll}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Clear All"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
color="rgb(239, 68, 68)"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{replays.length === 0 ? (
|
||||
<div className="text-xs text-gray-500 text-center py-2">
|
||||
No replays available
|
||||
</div>
|
||||
<Box textAlign="center" py={2}>
|
||||
<Text size="xs" color="text-gray-500">No replays available</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-48 overflow-auto">
|
||||
<Stack gap={1}>
|
||||
{replays.map((replay) => (
|
||||
<div
|
||||
<Box
|
||||
key={replay.id}
|
||||
className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs"
|
||||
bg="bg-deep-graphite"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
rounded="md"
|
||||
p={2}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-red-400 font-bold truncate">
|
||||
{replay.type}
|
||||
</div>
|
||||
<div className="text-gray-300 truncate">{replay.error}</div>
|
||||
<div className="text-gray-500 text-[10px]">
|
||||
{new Date(replay.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<button
|
||||
<Box mb={1}>
|
||||
<Text size="xs" font="mono" weight="bold" color="text-red-400" block truncate>
|
||||
{replay.type}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-300" block truncate>{replay.error}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
{new Date(replay.timestamp).toLocaleTimeString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" gap={1} mt={1}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleReplay(replay.id)}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-green-600 hover:bg-green-700 text-white rounded"
|
||||
size="sm"
|
||||
icon={<Icon icon={Play} size={3} />}
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Replay
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleExport(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
size="sm"
|
||||
icon={<Icon icon={Download} size={3} />}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleCopy(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
size="sm"
|
||||
icon={<Icon icon={Copy} size={3} />}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<IconButton
|
||||
icon={Trash2}
|
||||
onClick={() => handleDelete(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { Bell, BellRing, AlertCircle } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Bell, BellRing, AlertCircle, LucideIcon } from 'lucide-react';
|
||||
import type { DemoUrgency } from '../types';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface UrgencyOption {
|
||||
urgency: DemoUrgency;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
interface UrgencySectionProps {
|
||||
@@ -38,62 +42,72 @@ export const urgencyOptions: UrgencyOption[] = [
|
||||
|
||||
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">
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Icon icon={BellRing} size={4} color="rgb(156, 163, 175)" />
|
||||
<Text size="xs" weight="semibold" color="text-gray-400" uppercase letterSpacing="wide">
|
||||
Urgency Level
|
||||
</span>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<Box display="grid" gridCols={3} gap={1}>
|
||||
{urgencyOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = selectedUrgency === option.urgency;
|
||||
|
||||
const getSelectedBg = () => {
|
||||
if (option.urgency === 'modal') return 'bg-red-500/20';
|
||||
if (option.urgency === 'toast') return 'bg-warning-amber/20';
|
||||
return 'bg-gray-500/20';
|
||||
};
|
||||
|
||||
const getSelectedBorder = () => {
|
||||
if (option.urgency === 'modal') return 'border-red-500/50';
|
||||
if (option.urgency === 'toast') return 'border-warning-amber/50';
|
||||
return 'border-gray-500/50';
|
||||
};
|
||||
|
||||
const getSelectedColor = () => {
|
||||
if (option.urgency === 'modal') return 'rgb(239, 68, 68)';
|
||||
if (option.urgency === 'toast') return 'rgb(245, 158, 11)';
|
||||
return 'rgb(156, 163, 175)';
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
<Box
|
||||
key={option.urgency}
|
||||
as="button"
|
||||
type="button"
|
||||
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'
|
||||
}
|
||||
`}
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
p={2}
|
||||
rounded="lg"
|
||||
border
|
||||
transition
|
||||
bg={isSelected ? getSelectedBg() : 'bg-iron-gray/30'}
|
||||
borderColor={isSelected ? getSelectedBorder() : 'border-charcoal-outline'}
|
||||
>
|
||||
<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'
|
||||
}`}>
|
||||
<Icon
|
||||
icon={option.icon}
|
||||
size={4}
|
||||
color={isSelected ? getSelectedColor() : 'rgb(107, 114, 128)'}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
weight="medium"
|
||||
color={isSelected ? (option.urgency === 'modal' ? 'text-red-400' : option.urgency === 'toast' ? 'text-warning-amber' : 'text-gray-400') : 'text-gray-500'}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</button>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-600 mt-1">
|
||||
</Box>
|
||||
<Text size="xs" color="text-gray-600" mt={1} block>
|
||||
{urgencyOptions.find(o => o.urgency === selectedUrgency)?.description}
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required' | 'race_performance_summary' | 'race_final_results';
|
||||
export type DemoUrgency = 'silent' | 'toast' | 'modal';
|
||||
@@ -7,7 +7,7 @@ export interface NotificationOption {
|
||||
type: DemoNotificationType;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ export interface UrgencyOption {
|
||||
urgency: DemoUrgency;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
Reference in New Issue
Block a user