'use client' /** * ErrorTrendChart - #43 錯誤趨勢圖表 * =================================== * Phase 10: Sentry + OpenClaw + UI 整合 * * Nothing.tech 視覺規範: * - 純白底色 (bg-white) * - 極細淺灰邊框 (border border-gray-200) * - 無圓角或微圓角 (rounded-sm) * - 嚴禁陰影 (shadow-none) * * 建立: 2026-03-26 (台北時區) * 建立者: Claude Code (#43 Error UI) */ import { useMemo } from 'react' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { TrendingUp, TrendingDown, Minus, Activity } from 'lucide-react' import type { ErrorTrendResponse, ErrorTrendPoint } from '@/lib/api-client' // ============================================================================= // Component Props // ============================================================================= interface ErrorTrendChartProps { data: ErrorTrendResponse | null loading?: boolean error?: string | null onPeriodChange?: (period: '24h' | '7d' | '30d') => void activePeriod?: '24h' | '7d' | '30d' className?: string } // ============================================================================= // Sparkline Component // ============================================================================= function Sparkline({ data, width = 200, height = 60, }: { data: ErrorTrendPoint[] width?: number height?: number }) { const path = useMemo(() => { if (data.length === 0) return '' const maxCount = Math.max(...data.map((d) => d.count), 1) const minCount = Math.min(...data.map((d) => d.count), 0) const range = maxCount - minCount || 1 const points = data.map((d, i) => { const x = (i / (data.length - 1)) * width const y = height - ((d.count - minCount) / range) * height return `${x},${y}` }) return `M ${points.join(' L ')}` }, [data, width, height]) const areaPath = useMemo(() => { if (data.length === 0) return '' const maxCount = Math.max(...data.map((d) => d.count), 1) const minCount = Math.min(...data.map((d) => d.count), 0) const range = maxCount - minCount || 1 const points = data.map((d, i) => { const x = (i / (data.length - 1)) * width const y = height - ((d.count - minCount) / range) * height return `${x},${y}` }) return `M 0,${height} L ${points.join(' L ')} L ${width},${height} Z` }, [data, width, height]) if (data.length === 0) { return ( ) } return ( ) } // ============================================================================= // Period Selector Component // ============================================================================= function PeriodSelector({ activePeriod, onChange, }: { activePeriod: '24h' | '7d' | '30d' onChange?: (period: '24h' | '7d' | '30d') => void }) { const periods: Array<{ value: '24h' | '7d' | '30d'; label: string }> = [ { value: '24h', label: '24h' }, { value: '7d', label: '7d' }, { value: '30d', label: '30d' }, ] return (
{t('noTrendData')}