Spaces:
Running
Running
| "use client"; | |
| import React from "react"; | |
| import { BarChart3, TrendingUp, AlertTriangle, Sparkles, Download, Send, Loader2, AlertCircle } from "lucide-react"; | |
| interface TrendData { | |
| day: string; | |
| count: number; | |
| } | |
| interface TrendChartProps { | |
| data: TrendData[]; | |
| total_episodes: number; | |
| worst_day: string; | |
| insight?: string; | |
| doctorName?: string; | |
| doctorEmail?: string; | |
| onSend?: () => void; | |
| } | |
| export function TrendChart({ data, total_episodes, worst_day, insight, doctorName, doctorEmail, onSend }: TrendChartProps) { | |
| const maxCount = Math.max(...data.map(d => d.count), 1); | |
| const canSend = !!doctorEmail; | |
| const [isExporting, setIsExporting] = React.useState(false); | |
| const handleExportPDF = async () => { | |
| if (isExporting) return; | |
| setIsExporting(true); | |
| try { | |
| const html2pdf = (await import("html2pdf.js")).default; | |
| const dateStr = new Date().toLocaleDateString("en-GB", { | |
| day: "numeric", | |
| month: "long", | |
| year: "numeric", | |
| }); | |
| const rows = data.map(d => | |
| `<tr><td style="padding: 8px 16px; border-bottom: 1px solid #eee;">${d.day}</td><td style="padding: 8px 16px; border-bottom: 1px solid #eee; text-align: center; font-weight: 600;">${d.count}</td></tr>` | |
| ).join(""); | |
| const container = document.createElement("div"); | |
| container.innerHTML = ` | |
| <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 700px; margin: 0 auto; padding: 32px; color: #1a1a1a; line-height: 1.8;"> | |
| <h1 style="font-size: 1.5rem; border-bottom: 2px solid #00d4aa; padding-bottom: 8px; margin-bottom: 4px;">Headache Trend Analysis</h1> | |
| <p style="color: #666; font-size: 0.85rem; margin-bottom: 24px;">Generated ${dateStr}${doctorName ? ` · For ${doctorName}` : ""}</p> | |
| <h2 style="font-size: 1.2rem; margin-top: 2rem; color: #333;">Overview</h2> | |
| <p><strong>Total episodes (30 days):</strong> ${total_episodes}</p> | |
| <p><strong>Peak trigger day:</strong> ${worst_day}</p> | |
| <h2 style="font-size: 1.2rem; margin-top: 2rem; color: #333;">Episodes by Day of Week</h2> | |
| <table style="width: 100%; border-collapse: collapse; margin-top: 12px;"> | |
| <thead><tr style="background: #f5f5f5;"><th style="padding: 8px 16px; text-align: left; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: #555;">Day</th><th style="padding: 8px 16px; text-align: center; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: #555;">Episodes</th></tr></thead> | |
| <tbody>${rows}</tbody> | |
| </table> | |
| ${insight ? `<h2 style="font-size: 1.2rem; margin-top: 2rem; color: #333;">AI Insight</h2><p style="font-style: italic;">"${insight}"</p>` : ""} | |
| </div> | |
| `; | |
| await html2pdf() | |
| .set({ | |
| margin: [10, 10, 10, 10], | |
| filename: `Headache_Trends_${new Date().toISOString().slice(0, 10)}.pdf`, | |
| image: { type: "jpeg", quality: 0.98 }, | |
| html2canvas: { scale: 2, useCORS: true }, | |
| jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }, | |
| }) | |
| .from(container) | |
| .save(); | |
| } catch (err) { | |
| console.error("PDF export failed:", err); | |
| } finally { | |
| setIsExporting(false); | |
| } | |
| }; | |
| return ( | |
| <div className="bg-surface-elevated/40 border border-surface-overlay rounded-2xl shadow-2xl relative overflow-hidden group flex flex-col"> | |
| <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-accent-pink/20 via-accent-cyan to-cta/20 opacity-30" /> | |
| <div className="p-6 flex-1"> | |
| <div className="flex items-center justify-between mb-8"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-accent-cyan/10 text-accent-cyan rounded-xl border border-accent-cyan/20"> | |
| <BarChart3 className="w-5 h-5" /> | |
| </div> | |
| <div> | |
| <h3 className="text-base font-bold text-primary tracking-tight">Cerebral Patterns</h3> | |
| <p className="text-[10px] text-muted uppercase tracking-[0.15em] font-bold">Last 7 Reporting Periods</p> | |
| </div> | |
| </div> | |
| <div className="px-3 py-1 bg-surface-overlay rounded-lg border border-surface-overlay flex flex-col items-center"> | |
| <span className="text-[10px] text-muted uppercase font-black tracking-tighter">Events</span> | |
| <span className="text-sm font-bold text-accent-cyan leading-none">{total_episodes}</span> | |
| </div> | |
| </div> | |
| {/* Bar Chart */} | |
| <div className="flex items-end justify-between gap-3 h-32 mb-8 px-1"> | |
| {data.map((item) => { | |
| const height = (item.count / maxCount) * 100; | |
| const isWorst = item.day === worst_day; | |
| return ( | |
| <div key={item.day} className="flex-1 flex flex-col items-center gap-3 group/bar"> | |
| <div className="w-full relative flex flex-col justify-end h-full"> | |
| <div | |
| className={` | |
| w-full rounded-t-lg transition-all duration-700 ease-out | |
| ${isWorst | |
| ? 'bg-accent-pink shadow-[0_0_20px_rgba(255,107,157,0.3)]' | |
| : 'bg-surface-overlay group-hover/bar:bg-accent-cyan/40' | |
| } | |
| `} | |
| style={{ height: `${Math.max(height, 5)}%` }} | |
| /> | |
| {item.count > 0 && ( | |
| <span className={` | |
| absolute -top-7 left-1/2 -translate-x-1/2 text-[9px] font-black px-1.5 py-0.5 rounded border transition-all duration-300 | |
| ${isWorst | |
| ? 'bg-accent-pink text-black border-accent-pink scale-110' | |
| : 'bg-surface-elevated text-muted border-surface-overlay opacity-0 group-hover/bar:opacity-100' | |
| } | |
| `}> | |
| {item.count} | |
| </span> | |
| )} | |
| </div> | |
| <span className={`text-[9px] font-black uppercase tracking-[0.15em] ${isWorst ? 'text-accent-pink' : 'text-muted'}`}> | |
| {item.day.substring(0, 3)} | |
| </span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Insights */} | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="bg-surface-subtle/60 rounded-xl p-4 border border-surface-overlay flex flex-col gap-1"> | |
| <div className="flex items-center gap-2 text-accent-pink opacity-80"> | |
| <AlertTriangle className="w-3.5 h-3.5" /> | |
| <span className="text-[9px] font-black uppercase tracking-widest">Peak Triggers</span> | |
| </div> | |
| <p className="text-xs text-primary font-bold">{worst_day}</p> | |
| </div> | |
| <div className="bg-surface-subtle/60 rounded-xl p-4 border border-surface-overlay flex flex-col gap-1"> | |
| <div className="flex items-center gap-2 text-accent-cyan opacity-80"> | |
| <TrendingUp className="w-3.5 h-3.5" /> | |
| <span className="text-[9px] font-black uppercase tracking-widest">Trend Status</span> | |
| </div> | |
| <p className="text-xs text-primary font-bold">Stable Analysis</p> | |
| </div> | |
| {insight && ( | |
| <div className="col-span-2 bg-cta/5 rounded-2xl p-4 border border-cta/20 flex gap-4 items-start group/insight hover:bg-cta/10 transition-colors"> | |
| <div className="w-8 h-8 rounded-lg bg-cta/20 flex items-center justify-center shrink-0"> | |
| <Sparkles className="w-4 h-4 text-cta" /> | |
| </div> | |
| <div className="space-y-1"> | |
| <p className="text-[10px] font-black uppercase tracking-[0.2em] text-cta/70">AI Correlation</p> | |
| <p className="text-sm text-primary leading-[1.6] italic"> | |
| "{insight}" | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Footer / Actions */} | |
| <div className="px-6 py-5 bg-surface-subtle border-t border-surface-overlay flex flex-col gap-4"> | |
| <div className="flex items-center gap-4"> | |
| <button | |
| onClick={handleExportPDF} | |
| disabled={isExporting} | |
| className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-surface-elevated border border-surface-overlay hover:border-text-muted disabled:opacity-30 rounded-xl text-[11px] font-bold text-secondary transition-all" | |
| > | |
| {isExporting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />} | |
| {isExporting ? "Generating..." : "Export PDF"} | |
| </button> | |
| <button | |
| onClick={onSend} | |
| disabled={!canSend} | |
| className={` | |
| flex-[1.5] flex items-center justify-center gap-2 px-6 py-3 rounded-xl text-[11px] font-black uppercase tracking-widest transition-all shadow-xl | |
| ${canSend | |
| ? 'bg-cta text-black hover:shadow-cta/20 active:scale-95' | |
| : 'bg-surface-overlay text-muted opacity-50' | |
| } | |
| `} | |
| > | |
| <Send className="w-4 h-4" /> | |
| {doctorName ? `Send to ${doctorName}` : "Send to Provider"} | |
| </button> | |
| </div> | |
| {!doctorEmail && ( | |
| <div className="flex items-center gap-3 px-4 py-3 bg-accent-pink/5 border border-accent-pink/10 rounded-xl"> | |
| <AlertCircle className="w-4 h-4 text-accent-pink shrink-0" /> | |
| <p className="text-[10px] text-accent-pink font-medium leading-tight"> | |
| <strong>Action Required:</strong> Add your neurologist's email in <span className="underline cursor-pointer">Settings</span> to enable direct clinical sharing. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |