feat: Add advanced analytics UI components in frontend
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Add comprehensive API client methods for all advanced analytics endpoints
- Create CircadianRhythmCard component for sleep pattern visualization
- Create AnomalyAlertsPanel for anomaly detection and alerts
- Create GrowthPercentileChart with WHO/CDC percentiles
- Create CorrelationInsights for activity correlations
- Create TrendAnalysisChart with predictions
- Add advanced analytics page with all new components
- Add UI component library (shadcn/ui) setup
- Add navigation link to advanced analytics from insights page

All advanced analytics features are now accessible from the frontend UI.
This commit is contained in:
2025-10-06 11:46:05 +00:00
parent 56d2d83418
commit b0ac2f71df
19 changed files with 3112 additions and 13 deletions

View File

@@ -0,0 +1,365 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useAuth } from '@/lib/auth/AuthContext';
import { childrenApi, Child } from '@/lib/api/children';
import {
analyticsApi,
CircadianRhythm,
AnomalyDetection,
GrowthAnalysis,
CorrelationAnalysis,
TrendAnalysis,
} from '@/lib/api/analytics';
import { CircadianRhythmCard } from '@/components/analytics/CircadianRhythmCard';
import { AnomalyAlertsPanel } from '@/components/analytics/AnomalyAlertsPanel';
import { GrowthPercentileChart } from '@/components/analytics/GrowthPercentileChart';
import { CorrelationInsights } from '@/components/analytics/CorrelationInsights';
import { TrendAnalysisChart } from '@/components/analytics/TrendAnalysisChart';
import {
Card,
CardContent,
CardHeader,
Button,
Alert,
AlertTitle,
Tabs,
Tab,
Box,
Select,
MenuItem,
FormControl,
InputLabel,
} from '@mui/material';
import { Loader2, RefreshCw, Activity, Brain, TrendingUp, Baby, Link } from 'lucide-react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function AdvancedAnalyticsPage() {
const { user } = useAuth();
const [children, setChildren] = useState<Child[]>([]);
const [selectedChildId, setSelectedChildId] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
// Analytics data states
const [circadianData, setCircadianData] = useState<CircadianRhythm | null>(null);
const [anomalyData, setAnomalyData] = useState<AnomalyDetection | null>(null);
const [growthData, setGrowthData] = useState<GrowthAnalysis | null>(null);
const [correlationData, setCorrelationData] = useState<CorrelationAnalysis | null>(null);
const [sleepTrendData, setSleepTrendData] = useState<TrendAnalysis | null>(null);
const [feedingTrendData, setFeedingTrendData] = useState<TrendAnalysis | null>(null);
// Loading states for each component
const [circadianLoading, setCircadianLoading] = useState(false);
const [anomalyLoading, setAnomalyLoading] = useState(false);
const [growthLoading, setGrowthLoading] = useState(false);
const [correlationLoading, setCorrelationLoading] = useState(false);
const [trendLoading, setTrendLoading] = useState(false);
// Error states for each component
const [circadianError, setCircadianError] = useState<Error | null>(null);
const [anomalyError, setAnomalyError] = useState<Error | null>(null);
const [growthError, setGrowthError] = useState<Error | null>(null);
const [correlationError, setCorrelationError] = useState<Error | null>(null);
const [trendError, setTrendError] = useState<Error | null>(null);
const familyId = user?.families?.[0]?.familyId;
useEffect(() => {
if (familyId) {
loadChildren();
}
}, [familyId]);
useEffect(() => {
if (selectedChildId && children.length > 0) {
const childExists = children.some(child => child.id === selectedChildId);
if (childExists) {
loadAllAnalytics();
} else {
console.warn('[AdvancedAnalytics] Selected child not found, resetting');
setSelectedChildId(children[0].id);
}
}
}, [selectedChildId, children]);
const loadChildren = async () => {
if (!familyId) {
setLoading(false);
setError('No family found');
return;
}
try {
console.log('[AdvancedAnalytics] Loading children for familyId:', familyId);
const data = await childrenApi.getChildren(familyId);
console.log('[AdvancedAnalytics] Loaded children:', data);
setChildren(data);
if (data.length > 0) {
const existingChildStillValid = data.some(child => child.id === selectedChildId);
if (!selectedChildId || !existingChildStillValid) {
setSelectedChildId(data[0].id);
}
}
setError('');
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load children:', error);
setError('Failed to load children');
} finally {
setLoading(false);
}
};
const loadAllAnalytics = () => {
loadCircadianRhythm();
loadAnomalies();
loadGrowthAnalysis();
loadCorrelations();
loadTrends();
};
const loadCircadianRhythm = async () => {
if (!selectedChildId) return;
setCircadianLoading(true);
setCircadianError(null);
try {
const data = await analyticsApi.getCircadianRhythm(selectedChildId, 14);
setCircadianData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load circadian rhythm:', error);
setCircadianError(error as Error);
} finally {
setCircadianLoading(false);
}
};
const loadAnomalies = async () => {
if (!selectedChildId) return;
setAnomalyLoading(true);
setAnomalyError(null);
try {
const data = await analyticsApi.getAnomalies(selectedChildId, 30);
setAnomalyData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load anomalies:', error);
setAnomalyError(error as Error);
} finally {
setAnomalyLoading(false);
}
};
const loadGrowthAnalysis = async () => {
if (!selectedChildId) return;
setGrowthLoading(true);
setGrowthError(null);
try {
const data = await analyticsApi.getGrowthAnalysis(selectedChildId);
setGrowthData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load growth analysis:', error);
setGrowthError(error as Error);
} finally {
setGrowthLoading(false);
}
};
const loadCorrelations = async () => {
if (!selectedChildId) return;
setCorrelationLoading(true);
setCorrelationError(null);
try {
const data = await analyticsApi.getCorrelations(selectedChildId, 14);
setCorrelationData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load correlations:', error);
setCorrelationError(error as Error);
} finally {
setCorrelationLoading(false);
}
};
const loadTrends = async () => {
if (!selectedChildId) return;
setTrendLoading(true);
setTrendError(null);
try {
const [sleepTrend, feedingTrend] = await Promise.all([
analyticsApi.getTrends(selectedChildId, 'sleep'),
analyticsApi.getTrends(selectedChildId, 'feeding'),
]);
setSleepTrendData(sleepTrend);
setFeedingTrendData(feedingTrend);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load trends:', error);
setTrendError(error as Error);
} finally {
setTrendLoading(false);
}
};
if (loading) {
return (
<ProtectedRoute>
<AppShell>
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
</AppShell>
</ProtectedRoute>
);
}
if (children.length === 0) {
return (
<ProtectedRoute>
<AppShell>
<div className="p-6">
<Alert>
<AlertTitle>No Children Found</AlertTitle>
<AlertDescription>
Add a child to your family to view advanced analytics.
</AlertDescription>
</Alert>
</div>
</AppShell>
</ProtectedRoute>
);
}
return (
<ProtectedRoute>
<AppShell>
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Advanced Analytics</h1>
<p className="text-gray-600 mt-1">
AI-powered insights and deep analysis of your child's patterns
</p>
</div>
<Button onClick={loadAllAnalytics} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{/* Child Selector */}
<Card>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<label htmlFor="child-select" className="text-sm font-medium">
Select Child:
</label>
<Select
value={selectedChildId}
onValueChange={setSelectedChildId}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select a child" />
</SelectTrigger>
<SelectContent>
{children.map((child) => (
<SelectItem key={child.id} value={child.id}>
{child.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Analytics Tabs */}
<Tabs defaultValue="circadian" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="circadian">
<Brain className="h-4 w-4 mr-2" />
Sleep Rhythm
</TabsTrigger>
<TabsTrigger value="anomalies">
<Activity className="h-4 w-4 mr-2" />
Anomalies
</TabsTrigger>
<TabsTrigger value="growth">
<Baby className="h-4 w-4 mr-2" />
Growth
</TabsTrigger>
<TabsTrigger value="correlations">
<Link className="h-4 w-4 mr-2" />
Correlations
</TabsTrigger>
<TabsTrigger value="trends">
<TrendingUp className="h-4 w-4 mr-2" />
Trends
</TabsTrigger>
</TabsList>
<TabsContent value="circadian" className="space-y-4">
<CircadianRhythmCard
data={circadianData}
loading={circadianLoading}
error={circadianError}
/>
</TabsContent>
<TabsContent value="anomalies" className="space-y-4">
<AnomalyAlertsPanel
data={anomalyData}
loading={anomalyLoading}
error={anomalyError}
/>
</TabsContent>
<TabsContent value="growth" className="space-y-4">
<GrowthPercentileChart
data={growthData}
loading={growthLoading}
error={growthError}
/>
</TabsContent>
<TabsContent value="correlations" className="space-y-4">
<CorrelationInsights
data={correlationData}
loading={correlationLoading}
error={correlationError}
/>
</TabsContent>
<TabsContent value="trends" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<TrendAnalysisChart
data={sleepTrendData}
activityType="Sleep"
loading={trendLoading}
error={trendError}
/>
<TrendAnalysisChart
data={feedingTrendData}
activityType="Feeding"
loading={trendLoading}
error={trendError}
/>
</div>
</TabsContent>
</Tabs>
</div>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -0,0 +1,242 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
AlertTriangle,
AlertCircle,
Info,
TrendingUp,
Activity,
Clock,
ChevronRight,
} from 'lucide-react';
import { AnomalyDetection } from '@/lib/api/analytics';
import { formatDistanceToNow } from 'date-fns';
interface AnomalyAlertsPanelProps {
data: AnomalyDetection | null;
loading?: boolean;
error?: Error | null;
}
export function AnomalyAlertsPanel({ data, loading, error }: AnomalyAlertsPanelProps) {
const [expandedAnomaly, setExpandedAnomaly] = useState<string | null>(null);
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing patterns for anomalies...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading anomaly detection data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
case 'critical':
return 'bg-red-100 text-red-800 border-red-300';
case 'medium':
case 'warning':
return 'bg-yellow-100 text-yellow-800 border-yellow-300';
case 'low':
case 'info':
return 'bg-blue-100 text-blue-800 border-blue-300';
default:
return 'bg-gray-100 text-gray-800 border-gray-300';
}
};
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'high':
case 'critical':
return <AlertTriangle className="h-4 w-4" />;
case 'medium':
case 'warning':
return <AlertCircle className="h-4 w-4" />;
default:
return <Info className="h-4 w-4" />;
}
};
const criticalAlerts = data.alerts.filter(a => a.severity === 'critical');
const warningAlerts = data.alerts.filter(a => a.severity === 'warning');
const infoAlerts = data.alerts.filter(a => a.severity === 'info');
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Anomaly Detection
</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Confidence</span>
<Badge variant="outline">
{Math.round(data.confidenceScore * 100)}%
</Badge>
</div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Confidence Score Bar */}
<div className="space-y-2">
<Progress value={data.confidenceScore * 100} className="h-2" />
<p className="text-xs text-gray-500">
Analysis based on {data.anomalies.length} detected patterns
</p>
</div>
{/* Alert Summary */}
{(criticalAlerts.length > 0 || warningAlerts.length > 0 || infoAlerts.length > 0) && (
<div className="grid grid-cols-3 gap-2 mb-4">
{criticalAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-red-50 rounded-lg">
<AlertTriangle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-700">
{criticalAlerts.length} Critical
</span>
</div>
)}
{warningAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-yellow-50 rounded-lg">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<span className="text-sm font-medium text-yellow-700">
{warningAlerts.length} Warning
</span>
</div>
)}
{infoAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded-lg">
<Info className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-700">
{infoAlerts.length} Info
</span>
</div>
)}
</div>
)}
<Tabs defaultValue="alerts" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="alerts">Alerts ({data.alerts.length})</TabsTrigger>
<TabsTrigger value="anomalies">Anomalies ({data.anomalies.length})</TabsTrigger>
</TabsList>
<TabsContent value="alerts" className="space-y-3">
{data.alerts.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No alerts detected - everything looks normal!
</div>
) : (
data.alerts.map((alert, index) => (
<Alert
key={index}
className={`${getSeverityColor(alert.severity)} border`}
>
<div className="flex items-start gap-3">
{getSeverityIcon(alert.severity)}
<div className="flex-1">
<AlertTitle className="mb-2">{alert.type}</AlertTitle>
<AlertDescription>{alert.message}</AlertDescription>
{alert.recommendations && alert.recommendations.length > 0 && (
<div className="mt-3 space-y-1">
<p className="text-xs font-medium">Recommendations:</p>
<ul className="text-xs space-y-1">
{alert.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-start gap-1">
<ChevronRight className="h-3 w-3 mt-0.5 flex-shrink-0" />
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
</Alert>
))
)}
</TabsContent>
<TabsContent value="anomalies" className="space-y-3">
{data.anomalies.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No anomalies detected in recent activities
</div>
) : (
data.anomalies.map((anomaly) => (
<div
key={anomaly.activityId}
className="border rounded-lg p-3 space-y-2 hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => setExpandedAnomaly(
expandedAnomaly === anomaly.activityId ? null : anomaly.activityId
)}
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-2">
<Badge className={getSeverityColor(anomaly.severity)}>
{anomaly.severity}
</Badge>
<div>
<p className="text-sm font-medium capitalize">
{anomaly.type} Activity
</p>
<p className="text-xs text-gray-500 mt-1">
<Clock className="h-3 w-3 inline mr-1" />
{formatDistanceToNow(anomaly.timestamp, { addSuffix: true })}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Deviation</p>
<p className="text-sm font-medium">
{anomaly.deviation.toFixed(1)}σ
</p>
</div>
</div>
{expandedAnomaly === anomaly.activityId && (
<div className="pt-2 border-t space-y-2">
<p className="text-sm text-gray-600">{anomaly.description}</p>
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-gray-400" />
<span className="text-xs text-gray-500">
Statistical deviation: {anomaly.deviation.toFixed(2)} standard deviations from normal
</span>
</div>
</div>
)}
</div>
))
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,197 @@
'use client';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Moon, Sun, Clock, Brain, AlertCircle, CheckCircle2 } from 'lucide-react';
import { CircadianRhythm } from '@/lib/api/analytics';
interface CircadianRhythmCardProps {
data: CircadianRhythm | null;
loading?: boolean;
error?: Error | null;
}
export function CircadianRhythmCard({ data, loading, error }: CircadianRhythmCardProps) {
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Loading circadian rhythm analysis...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading circadian rhythm data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getChronotypeIcon = () => {
switch (data.chronotype) {
case 'early_bird':
return <Sun className="h-5 w-5 text-yellow-500" />;
case 'night_owl':
return <Moon className="h-5 w-5 text-indigo-500" />;
default:
return <Clock className="h-5 w-5 text-gray-500" />;
}
};
const getChronotypeColor = () => {
switch (data.chronotype) {
case 'early_bird':
return 'bg-yellow-100 text-yellow-800';
case 'night_owl':
return 'bg-indigo-100 text-indigo-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const formatTime = (time: string) => {
// Convert HH:MM to 12-hour format
const [hours, minutes] = time.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
return `${displayHour}:${minutes} ${ampm}`;
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Brain className="h-5 w-5" />
Circadian Rhythm Analysis
</span>
<Badge className={getChronotypeColor()}>
<span className="flex items-center gap-1">
{getChronotypeIcon()}
{data.chronotype.replace('_', ' ')}
</span>
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Sleep Consistency Score */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Sleep Consistency</span>
<span className="font-medium">{Math.round(data.consistency * 100)}%</span>
</div>
<Progress value={data.consistency * 100} className="h-2" />
<p className="text-xs text-gray-500">
{data.consistency > 0.8
? 'Excellent - Very consistent sleep schedule'
: data.consistency > 0.6
? 'Good - Fairly consistent schedule'
: 'Needs improvement - Irregular sleep pattern'}
</p>
</div>
{/* Optimal Times */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Moon className="h-4 w-4 text-blue-500" />
<span className="text-sm text-gray-600">Optimal Bedtime</span>
</div>
<p className="text-lg font-semibold">{formatTime(data.optimalBedtime)}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Sun className="h-4 w-4 text-yellow-500" />
<span className="text-sm text-gray-600">Optimal Wake Time</span>
</div>
<p className="text-lg font-semibold">{formatTime(data.optimalWakeTime)}</p>
</div>
</div>
{/* Sleep Phase Shift */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Sleep Phase Shift</span>
<span className="text-sm font-medium">
{data.sleepPhaseShift > 0 ? '+' : ''}{data.sleepPhaseShift.toFixed(1)} hours
</span>
</div>
<div className="flex items-center gap-2">
{Math.abs(data.sleepPhaseShift) < 1 ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<AlertCircle className="h-4 w-4 text-yellow-500" />
)}
<p className="text-xs text-gray-500">
{Math.abs(data.sleepPhaseShift) < 1
? 'Sleep schedule aligned with typical patterns'
: data.sleepPhaseShift > 0
? `Bedtime is ${Math.abs(data.sleepPhaseShift).toFixed(1)} hours later than typical`
: `Bedtime is ${Math.abs(data.sleepPhaseShift).toFixed(1)} hours earlier than typical`}
</p>
</div>
</div>
{/* Melatonin Onset */}
<div className="p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Brain className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium text-purple-900">Estimated Melatonin Onset</span>
</div>
<p className="text-lg font-semibold text-purple-700">{formatTime(data.melatoninOnset)}</p>
<p className="text-xs text-purple-600 mt-1">
Natural sleepiness begins around this time
</p>
</div>
{/* Recommended Schedule */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700">Recommended Daily Schedule</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Wake Time</span>
<span className="font-medium">{formatTime(data.recommendedSchedule.wakeTime)}</span>
</div>
{data.recommendedSchedule.morningNap && (
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Morning Nap</span>
<span className="font-medium">
{formatTime(data.recommendedSchedule.morningNap.start)} ({data.recommendedSchedule.morningNap.duration} min)
</span>
</div>
)}
{data.recommendedSchedule.afternoonNap && (
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Afternoon Nap</span>
<span className="font-medium">
{formatTime(data.recommendedSchedule.afternoonNap.start)} ({data.recommendedSchedule.afternoonNap.duration} min)
</span>
</div>
)}
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Bedtime</span>
<span className="font-medium">{formatTime(data.recommendedSchedule.bedtime)}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-600">Daily Sleep Target</span>
<span className="font-medium">{Math.round(data.recommendedSchedule.totalSleepTarget / 60)} hours</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,235 @@
'use client';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
Link,
Link2Off,
TrendingUp,
TrendingDown,
Moon,
Utensils,
Baby,
Activity,
Info,
CheckCircle,
} from 'lucide-react';
import { CorrelationAnalysis } from '@/lib/api/analytics';
interface CorrelationInsightsProps {
data: CorrelationAnalysis | null;
loading?: boolean;
error?: Error | null;
}
export function CorrelationInsights({ data, loading, error }: CorrelationInsightsProps) {
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing activity correlations...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<Link2Off className="h-5 w-5 mr-2" />
Error loading correlation data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getCorrelationStrength = (value: number) => {
const absValue = Math.abs(value);
if (absValue > 0.7) return 'Strong';
if (absValue > 0.4) return 'Moderate';
if (absValue > 0.2) return 'Weak';
return 'Negligible';
};
const getCorrelationColor = (value: number) => {
const absValue = Math.abs(value);
if (absValue > 0.7) return 'bg-purple-100 text-purple-800';
if (absValue > 0.4) return 'bg-blue-100 text-blue-800';
if (absValue > 0.2) return 'bg-gray-100 text-gray-800';
return 'bg-gray-50 text-gray-600';
};
const getCorrelationIcon = (value: number) => {
if (value > 0.3) return <TrendingUp className="h-4 w-4 text-green-500" />;
if (value < -0.3) return <TrendingDown className="h-4 w-4 text-red-500" />;
return <Activity className="h-4 w-4 text-gray-400" />;
};
const formatCorrelation = (value: number) => {
return (value * 100).toFixed(0) + '%';
};
const correlations = [
{
name: 'Feeding & Sleep',
value: data.feedingSleepCorrelation,
icon1: <Utensils className="h-4 w-4" />,
icon2: <Moon className="h-4 w-4" />,
description: data.feedingSleepCorrelation > 0
? 'Better feeding patterns correlate with better sleep'
: data.feedingSleepCorrelation < 0
? 'More feedings may be disrupting sleep patterns'
: 'No significant relationship detected',
},
{
name: 'Activity & Diapers',
value: data.activityDiaperCorrelation,
icon1: <Activity className="h-4 w-4" />,
icon2: <Baby className="h-4 w-4" />,
description: data.activityDiaperCorrelation > 0
? 'More activity correlates with more diaper changes'
: data.activityDiaperCorrelation < 0
? 'Less active periods show more diaper changes'
: 'No clear pattern between activity and diapers',
},
...(data.sleepMoodCorrelation !== undefined ? [{
name: 'Sleep & Mood',
value: data.sleepMoodCorrelation,
icon1: <Moon className="h-4 w-4" />,
icon2: <Activity className="h-4 w-4" />,
description: data.sleepMoodCorrelation > 0
? 'Better sleep strongly correlates with better mood'
: data.sleepMoodCorrelation < 0
? 'Sleep patterns inversely affect mood'
: 'Sleep and mood appear independent',
}] : []),
];
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Link className="h-5 w-5" />
Activity Correlations
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Correlation Visualizations */}
<div className="space-y-4">
{correlations.map((correlation) => (
<div key={correlation.name} className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-gray-600">
{correlation.icon1}
<span className="text-sm font-medium">&</span>
{correlation.icon2}
</div>
<span className="text-sm font-medium">{correlation.name}</span>
</div>
<div className="flex items-center gap-2">
{getCorrelationIcon(correlation.value)}
<Badge className={getCorrelationColor(correlation.value)}>
{getCorrelationStrength(correlation.value)}
</Badge>
</div>
</div>
{/* Correlation Bar */}
<div className="relative h-8 bg-gray-100 rounded-full overflow-hidden">
<div
className="absolute top-0 h-full bg-gradient-to-r from-red-400 via-gray-300 to-green-400"
style={{
width: '100%',
opacity: 0.3,
}}
/>
<div
className="absolute top-0 h-full flex items-center justify-center"
style={{
left: '50%',
transform: 'translateX(-50%)',
}}
>
<div className="w-0.5 h-full bg-gray-400" />
</div>
<div
className="absolute top-0 h-full flex items-center"
style={{
left: correlation.value > 0 ? '50%' : `${50 + correlation.value * 50}%`,
width: `${Math.abs(correlation.value) * 50}%`,
}}
>
<div
className={`h-4 rounded-full ${
correlation.value > 0 ? 'bg-green-500' : 'bg-red-500'
}`}
style={{ width: '100%' }}
/>
</div>
<div
className="absolute top-0 h-full flex items-center justify-end pr-2"
style={{
left: correlation.value > 0 ? `${50 + correlation.value * 50}%` : '50%',
transform: correlation.value > 0 ? 'translateX(-100%)' : 'none',
}}
>
<span className="text-xs font-medium text-white drop-shadow">
{formatCorrelation(correlation.value)}
</span>
</div>
</div>
<p className="text-xs text-gray-600">{correlation.description}</p>
</div>
))}
</div>
{/* Correlation Scale Legend */}
<div className="p-3 bg-gray-50 rounded-lg space-y-2">
<h4 className="text-xs font-medium text-gray-700 uppercase tracking-wider">
Correlation Scale
</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500" />
<span>Positive: Activities increase together</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500" />
<span>Negative: One increases, other decreases</span>
</div>
</div>
</div>
{/* Insights */}
{data.insights.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Info className="h-4 w-4" />
Key Insights
</h4>
<div className="space-y-2">
{data.insights.map((insight, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-blue-50 rounded-lg"
>
<CheckCircle className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-900">{insight}</p>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,307 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
Area,
AreaChart,
} from 'recharts';
import {
Ruler,
Weight,
TrendingUp,
Baby,
AlertCircle,
ChevronUp,
ChevronDown,
Minus,
} from 'lucide-react';
import { GrowthAnalysis, GrowthPercentile } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface GrowthPercentileChartProps {
data: GrowthAnalysis | null;
loading?: boolean;
error?: Error | null;
}
export function GrowthPercentileChart({ data, loading, error }: GrowthPercentileChartProps) {
const [selectedMetric, setSelectedMetric] = useState<'weight' | 'height' | 'headCircumference'>('weight');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Loading growth analysis...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading growth data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getPercentileColor = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'text-red-600';
if (percentile < 15 || percentile > 85) return 'text-yellow-600';
return 'text-green-600';
};
const getPercentileBadge = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'bg-red-100 text-red-800';
if (percentile < 15 || percentile > 85) return 'bg-yellow-100 text-yellow-800';
return 'bg-green-100 text-green-800';
};
const getVelocityIcon = (value: number) => {
if (value > 0) return <ChevronUp className="h-4 w-4 text-green-500" />;
if (value < 0) return <ChevronDown className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-gray-500" />;
};
const formatMeasurement = (value: number | undefined, metric: string) => {
if (!value) return 'N/A';
switch (metric) {
case 'weight':
return `${value.toFixed(1)} kg`;
case 'height':
return `${value.toFixed(1)} cm`;
case 'headCircumference':
return `${value.toFixed(1)} cm`;
default:
return value.toFixed(1);
}
};
// Prepare chart data
const chartData = data.growthCurve.measurements.map(m => ({
date: format(m.date, 'MMM dd'),
weight: m.weight,
height: m.height,
headCircumference: m.headCircumference,
p3: data.growthCurve.percentileCurves[selectedMetric]?.p3?.[0],
p15: data.growthCurve.percentileCurves[selectedMetric]?.p15?.[0],
p50: data.growthCurve.percentileCurves[selectedMetric]?.p50?.[0],
p85: data.growthCurve.percentileCurves[selectedMetric]?.p85?.[0],
p97: data.growthCurve.percentileCurves[selectedMetric]?.p97?.[0],
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Baby className="h-5 w-5" />
Growth Analysis
</span>
{data.alerts.length > 0 && (
<Badge variant="destructive">
{data.alerts.length} Alert{data.alerts.length > 1 ? 's' : ''}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Current Percentiles */}
{data.currentPercentiles && (
<div className="grid grid-cols-3 gap-4">
{data.currentPercentiles.weight && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Weight className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Weight</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.weight.value.toFixed(1)} kg
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.weight.percentile)}>
{Math.round(data.currentPercentiles.weight.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.weight.interpretation}
</p>
</div>
)}
{data.currentPercentiles.height && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Ruler className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Height</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.height.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.height.percentile)}>
{Math.round(data.currentPercentiles.height.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.height.interpretation}
</p>
</div>
)}
{data.currentPercentiles.headCircumference && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Baby className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Head Circ.</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.headCircumference.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.headCircumference.percentile)}>
{Math.round(data.currentPercentiles.headCircumference.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.headCircumference.interpretation}
</p>
</div>
)}
</div>
)}
{/* Growth Velocity */}
{data.growthVelocity && (
<div className="p-4 bg-blue-50 rounded-lg space-y-3">
<h4 className="text-sm font-medium text-blue-900">Growth Velocity</h4>
<div className="grid grid-cols-2 gap-4">
{data.growthVelocity.weightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Weight Gain</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.weightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.weightVelocity.value.toFixed(2)} {data.growthVelocity.weightVelocity.unit}
</span>
</div>
</div>
)}
{data.growthVelocity.heightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Height Growth</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.heightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.heightVelocity.value.toFixed(2)} {data.growthVelocity.heightVelocity.unit}
</span>
</div>
</div>
)}
</div>
<p className="text-xs text-blue-600">{data.growthVelocity.interpretation}</p>
</div>
)}
{/* Growth Chart */}
<Tabs value={selectedMetric} onValueChange={(v) => setSelectedMetric(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="weight">Weight</TabsTrigger>
<TabsTrigger value="height">Height</TabsTrigger>
<TabsTrigger value="headCircumference">Head Circ.</TabsTrigger>
</TabsList>
<TabsContent value={selectedMetric} className="mt-4">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
{/* Percentile reference lines */}
<ReferenceLine
y={chartData[0]?.p50}
stroke="#22c55e"
strokeDasharray="3 3"
label="50th %ile"
/>
<ReferenceLine
y={chartData[0]?.p15}
stroke="#eab308"
strokeDasharray="3 3"
label="15th %ile"
/>
<ReferenceLine
y={chartData[0]?.p85}
stroke="#eab308"
strokeDasharray="3 3"
label="85th %ile"
/>
{/* Child's measurements */}
<Line
type="monotone"
dataKey={selectedMetric}
stroke="#3b82f6"
strokeWidth={3}
dot={{ fill: '#3b82f6', r: 6 }}
name={`Child's ${selectedMetric}`}
/>
</LineChart>
</ResponsiveContainer>
</TabsContent>
</Tabs>
{/* Alerts */}
{data.alerts.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Growth Alerts</h4>
{data.alerts.map((alert, index) => (
<Alert key={index} className={
alert.severity === 'high' ? 'border-red-300 bg-red-50' :
alert.severity === 'medium' ? 'border-yellow-300 bg-yellow-50' :
'border-blue-300 bg-blue-50'
}>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium">{alert.message}</p>
<p className="text-sm mt-1">{alert.action}</p>
</AlertDescription>
</Alert>
))}
</div>
)}
{/* Recommendations */}
{data.recommendations.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Recommendations</h4>
<ul className="space-y-1">
{data.recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<TrendingUp className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,273 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import {
LineChart,
Line,
Area,
AreaChart,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import {
TrendingUp,
TrendingDown,
Minus,
Calendar,
Target,
ChartLine,
Activity,
} from 'lucide-react';
import { TrendAnalysis } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface TrendAnalysisChartProps {
data: TrendAnalysis | null;
activityType: string;
loading?: boolean;
error?: Error | null;
}
export function TrendAnalysisChart({ data, activityType, loading, error }: TrendAnalysisChartProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState<'short' | 'medium' | 'long'>('short');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing trends...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<Activity className="h-5 w-5 mr-2" />
Error loading trend analysis
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getTrendIcon = (direction: string) => {
switch (direction) {
case 'improving':
return <TrendingUp className="h-4 w-4 text-green-500" />;
case 'declining':
return <TrendingDown className="h-4 w-4 text-red-500" />;
default:
return <Minus className="h-4 w-4 text-gray-500" />;
}
};
const getTrendColor = (direction: string) => {
switch (direction) {
case 'improving':
return 'bg-green-100 text-green-800';
case 'declining':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getTrendData = () => {
switch (selectedTimeframe) {
case 'short':
return data.shortTermTrend;
case 'medium':
return data.mediumTermTrend;
case 'long':
return data.longTermTrend;
}
};
const currentTrend = getTrendData();
// Prepare chart data for predictions
const chartData = data.prediction.next7Days.map((point, index) => ({
day: format(point.date, 'MMM dd'),
predicted: point.predictedValue,
upperBound: point.confidenceInterval.upper,
lowerBound: point.confidenceInterval.lower,
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<ChartLine className="h-5 w-5" />
{activityType} Trend Analysis
</span>
<Badge className={getTrendColor(currentTrend.direction)}>
<span className="flex items-center gap-1">
{getTrendIcon(currentTrend.direction)}
{currentTrend.direction}
</span>
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Timeframe Tabs */}
<Tabs value={selectedTimeframe} onValueChange={(v) => setSelectedTimeframe(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="short">Short (7 days)</TabsTrigger>
<TabsTrigger value="medium">Medium (14 days)</TabsTrigger>
<TabsTrigger value="long">Long (30 days)</TabsTrigger>
</TabsList>
<TabsContent value={selectedTimeframe} className="space-y-4 mt-4">
{/* Trend Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Change</span>
<span className="font-medium">
{currentTrend.changePercent > 0 ? '+' : ''}{currentTrend.changePercent.toFixed(1)}%
</span>
</div>
<Progress
value={Math.abs(currentTrend.changePercent)}
className="h-2"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Confidence</span>
<span className="font-medium">{(currentTrend.confidence * 100).toFixed(0)}%</span>
</div>
<Progress value={currentTrend.confidence * 100} className="h-2" />
</div>
</div>
{/* Statistical Details */}
<div className="p-3 bg-gray-50 rounded-lg grid grid-cols-3 gap-3 text-sm">
<div>
<p className="text-gray-600">Slope</p>
<p className="font-medium">{currentTrend.slope.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">R² Score</p>
<p className="font-medium">{currentTrend.r2Score.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">Trend</p>
<p className="font-medium capitalize">{currentTrend.direction}</p>
</div>
</div>
</TabsContent>
</Tabs>
{/* Prediction Chart */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Target className="h-4 w-4" />
7-Day Forecast
<Badge variant="outline" className="ml-auto">
{(data.prediction.confidence * 100).toFixed(0)}% confidence
</Badge>
</h4>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Legend />
{/* Confidence interval area */}
<Area
type="monotone"
dataKey="upperBound"
stroke="none"
fill="#e0e7ff"
fillOpacity={0.3}
name="Upper bound"
/>
<Area
type="monotone"
dataKey="lowerBound"
stroke="none"
fill="#ffffff"
fillOpacity={1}
name="Lower bound"
/>
{/* Predicted trend line */}
<Line
type="monotone"
dataKey="predicted"
stroke="#6366f1"
strokeWidth={2}
dot={{ fill: '#6366f1', r: 4 }}
name="Predicted"
/>
</AreaChart>
</ResponsiveContainer>
{/* Prediction Factors */}
{data.prediction.factors.length > 0 && (
<div className="p-3 bg-blue-50 rounded-lg space-y-2">
<p className="text-xs font-medium text-blue-900 uppercase tracking-wider">
Factors Influencing Prediction
</p>
<div className="flex flex-wrap gap-2">
{data.prediction.factors.map((factor, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</div>
{/* Seasonal Patterns */}
{data.seasonalPatterns && data.seasonalPatterns.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Seasonal Patterns Detected
</h4>
<div className="space-y-2">
{data.seasonalPatterns.map((pattern, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div>
<p className="text-sm font-medium text-purple-900 capitalize">
{pattern.type} Pattern
</p>
<p className="text-xs text-purple-700">{pattern.pattern}</p>
</div>
<div className="text-right">
<p className="text-xs text-purple-600">Strength</p>
<p className="text-sm font-medium text-purple-900">
{(pattern.strength * 100).toFixed(0)}%
</p>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -14,6 +14,7 @@ import {
Alert,
Grid,
IconButton,
Button,
} from '@mui/material';
import { Timeline, TrendingUp, Assessment, ArrowBack } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
@@ -170,18 +171,28 @@ export function UnifiedInsightsDashboard() {
>
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
{/* Header with Back Button */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
<ArrowBack />
</IconButton>
<Box>
<Typography variant="h4" fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
<ArrowBack />
</IconButton>
<Box>
<Typography variant="h4" fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
</Box>
</Box>
<Button
variant="outlined"
color="primary"
onClick={() => router.push('/analytics/advanced')}
startIcon={<TrendingUp />}
>
Advanced Analytics
</Button>
</Box>
{/* Error Alert */}

View File

@@ -22,7 +22,7 @@ export const TabBar = () => {
{ label: t('navigation.home'), icon: <Home />, value: '/' },
{ label: t('navigation.track'), icon: <Timeline />, value: '/track' },
{ label: '', icon: null, value: 'voice' }, // Placeholder for center button
{ label: t('navigation.insights'), icon: <Insights />, value: '/insights' },
{ label: t('navigation.insights'), icon: <Insights />, value: pathname.startsWith('/analytics') ? pathname : '/insights' },
{ label: t('navigation.aiChat'), icon: <Chat />, value: '/ai-assistant' },
];

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -112,6 +112,177 @@ export interface MonthlyReport {
trends: string[];
}
// Advanced Analytics Interfaces
export interface CircadianRhythm {
sleepPhaseShift: number;
consistency: number;
optimalBedtime: string;
optimalWakeTime: string;
chronotype: 'early_bird' | 'night_owl' | 'typical';
melatoninOnset: string;
recommendedSchedule: {
wakeTime: string;
morningNap?: { start: string; duration: number };
afternoonNap?: { start: string; duration: number };
bedtime: string;
totalSleepTarget: number;
};
}
export interface AnomalyDetection {
anomalies: Array<{
activityId: string;
type: string;
timestamp: Date;
severity: 'low' | 'medium' | 'high';
description: string;
deviation: number;
}>;
alerts: Array<{
type: string;
severity: 'info' | 'warning' | 'critical';
message: string;
recommendations: string[];
}>;
confidenceScore: number;
}
export interface PatternCluster {
clusterId: string;
label: string;
activities: any[];
centroid: {
averageTime: string;
averageDuration: number;
characteristics: Record<string, any>;
};
confidence: number;
}
export interface CorrelationAnalysis {
feedingSleepCorrelation: number;
activityDiaperCorrelation: number;
sleepMoodCorrelation?: number;
insights: string[];
}
export interface TrendAnalysis {
shortTermTrend: {
direction: 'improving' | 'stable' | 'declining';
slope: number;
confidence: number;
r2Score: number;
changePercent: number;
};
mediumTermTrend: {
direction: 'improving' | 'stable' | 'declining';
slope: number;
confidence: number;
r2Score: number;
changePercent: number;
};
longTermTrend: {
direction: 'improving' | 'stable' | 'declining';
slope: number;
confidence: number;
r2Score: number;
changePercent: number;
};
seasonalPatterns?: Array<{
type: 'weekly' | 'monthly';
pattern: string;
strength: number;
}>;
prediction: {
next7Days: Array<{
date: Date;
predictedValue: number;
confidenceInterval: { lower: number; upper: number };
}>;
confidence: number;
factors: string[];
};
}
export interface GrowthPercentile {
weight?: {
value: number;
percentile: number;
zScore: number;
interpretation: string;
};
height?: {
value: number;
percentile: number;
zScore: number;
interpretation: string;
};
headCircumference?: {
value: number;
percentile: number;
zScore: number;
interpretation: string;
};
bmi?: {
value: number;
percentile: number;
zScore: number;
interpretation: string;
};
}
export interface GrowthAnalysis {
currentPercentiles: GrowthPercentile;
growthVelocity: {
weightVelocity?: {
value: number;
unit: string;
percentile: number;
};
heightVelocity?: {
value: number;
unit: string;
percentile: number;
};
interpretation: string;
concerns: string[];
};
growthCurve: {
measurements: Array<{
date: Date;
weight?: number;
height?: number;
headCircumference?: number;
}>;
percentileCurves: {
weight: Record<string, number[]>;
height: Record<string, number[]>;
headCircumference: Record<string, number[]>;
};
};
projections: {
threeMonths: GrowthPercentile;
sixMonths: GrowthPercentile;
confidence: number;
};
recommendations: string[];
alerts: Array<{
type: string;
severity: 'low' | 'medium' | 'high';
message: string;
action: string;
}>;
}
export interface AdvancedAnalyticsDashboard {
circadianRhythm: CircadianRhythm;
anomalies: AnomalyDetection;
correlations: CorrelationAnalysis;
growthAnalysis: GrowthAnalysis;
trends: Record<string, TrendAnalysis>;
clusters: Record<string, PatternCluster[]>;
}
export const analyticsApi = {
// Get pattern insights
getInsights: async (childId: string, days: number = 7): Promise<PatternInsights> => {
@@ -221,6 +392,139 @@ export const analyticsApi = {
});
return response.data.data;
},
// Advanced Analytics Methods
// Get circadian rhythm analysis
getCircadianRhythm: async (childId: string, days: number = 14): Promise<CircadianRhythm> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/circadian/${childId}`, {
params: { days },
});
return response.data.data;
},
// Get anomaly detection
getAnomalies: async (childId: string, days: number = 30): Promise<AnomalyDetection> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/anomalies/${childId}`, {
params: { days },
});
const data = response.data.data;
// Convert timestamps
return {
...data,
anomalies: data.anomalies.map((a: any) => ({
...a,
timestamp: new Date(a.timestamp),
})),
};
},
// Get activity clusters
getClusters: async (
childId: string,
activityType: string,
days: number = 30,
): Promise<PatternCluster[]> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/clusters/${childId}`, {
params: { activityType, days },
});
return response.data.data;
},
// Get correlation analysis
getCorrelations: async (childId: string, days: number = 14): Promise<CorrelationAnalysis> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/correlations/${childId}`, {
params: { days },
});
return response.data.data;
},
// Get trend analysis
getTrends: async (childId: string, activityType: string): Promise<TrendAnalysis> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/trends/${childId}`, {
params: { activityType },
});
const data = response.data.data;
// Convert dates in predictions
return {
...data,
prediction: {
...data.prediction,
next7Days: data.prediction.next7Days.map((p: any) => ({
...p,
date: new Date(p.date),
})),
},
};
},
// Get growth percentiles
calculateGrowthPercentiles: async (
childId: string,
measurement: {
date: Date;
weight?: number;
height?: number;
headCircumference?: number;
},
): Promise<GrowthPercentile> => {
const response = await apiClient.post(
`/api/v1/analytics/growth/percentiles/${childId}`,
{
...measurement,
date: measurement.date.toISOString(),
},
);
return response.data.data;
},
// Get growth analysis
getGrowthAnalysis: async (childId: string): Promise<GrowthAnalysis> => {
const response = await apiClient.get(`/api/v1/analytics/growth/analysis/${childId}`);
const data = response.data.data;
// Convert dates in measurements
return {
...data,
growthCurve: {
...data.growthCurve,
measurements: data.growthCurve.measurements.map((m: any) => ({
...m,
date: new Date(m.date),
})),
},
};
},
// Get comprehensive analytics dashboard
getAdvancedDashboard: async (childId: string): Promise<AdvancedAnalyticsDashboard> => {
const response = await apiClient.get(`/api/v1/analytics/advanced/dashboard/${childId}`);
const data = response.data.data;
// Process all date conversions
return {
...data,
anomalies: {
...data.anomalies,
anomalies: data.anomalies.anomalies.map((a: any) => ({
...a,
timestamp: new Date(a.timestamp),
})),
},
growthAnalysis: {
...data.growthAnalysis,
growthCurve: {
...data.growthAnalysis.growthCurve,
measurements: data.growthAnalysis.growthCurve.measurements.map((m: any) => ({
...m,
date: new Date(m.date),
})),
},
},
};
},
};
export enum ComparisonMetric {

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -15,10 +15,17 @@
"@mui/icons-material": "^7.3.3",
"@mui/material": "^7.3.3",
"@mui/material-nextjs": "^7.3.3",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@reduxjs/toolkit": "^2.9.0",
"@simplewebauthn/browser": "^13.2.0",
"@tanstack/react-query": "^5.90.2",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"focus-trap-react": "^11.0.4",
@@ -26,6 +33,7 @@
"graphql": "^16.11.0",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.544.0",
"next": "^15.5.4",
"next-pwa": "^5.6.0",
"react": "^19.2.0",
@@ -39,6 +47,7 @@
"redux-persist": "^6.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"web-vitals": "^5.1.0",
"workbox-webpack-plugin": "^7.3.0",
"workbox-window": "^7.3.0",
@@ -2346,6 +2355,44 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
@@ -3910,6 +3957,675 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@reduxjs/toolkit": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
@@ -5630,6 +6346,18 @@
"sprintf-js": "~1.0.2"
}
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -6459,6 +7187,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"node_modules/clean-webpack-plugin": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz",
@@ -8865,6 +9605,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-own-enumerable-property-symbols": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
@@ -11643,6 +12392,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.544.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -14239,6 +14997,75 @@
}
}
},
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -15740,6 +16567,16 @@
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "3.4.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",

View File

@@ -22,10 +22,17 @@
"@mui/icons-material": "^7.3.3",
"@mui/material": "^7.3.3",
"@mui/material-nextjs": "^7.3.3",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"@reduxjs/toolkit": "^2.9.0",
"@simplewebauthn/browser": "^13.2.0",
"@tanstack/react-query": "^5.90.2",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"focus-trap-react": "^11.0.4",
@@ -33,6 +40,7 @@
"graphql": "^16.11.0",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.544.0",
"next": "^15.5.4",
"next-pwa": "^5.6.0",
"react": "^19.2.0",
@@ -46,6 +54,7 @@
"redux-persist": "^6.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"web-vitals": "^5.1.0",
"workbox-webpack-plugin": "^7.3.0",
"workbox-window": "^7.3.0",

File diff suppressed because one or more lines are too long