feat: Add advanced analytics UI components in frontend
- 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:
365
maternal-web/app/analytics/advanced/page.tsx
Normal file
365
maternal-web/app/analytics/advanced/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
242
maternal-web/components/analytics/AnomalyAlertsPanel.tsx
Normal file
242
maternal-web/components/analytics/AnomalyAlertsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
197
maternal-web/components/analytics/CircadianRhythmCard.tsx
Normal file
197
maternal-web/components/analytics/CircadianRhythmCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
235
maternal-web/components/analytics/CorrelationInsights.tsx
Normal file
235
maternal-web/components/analytics/CorrelationInsights.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal file
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
273
maternal-web/components/analytics/TrendAnalysisChart.tsx
Normal file
273
maternal-web/components/analytics/TrendAnalysisChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
59
maternal-web/components/ui/alert.tsx
Normal file
59
maternal-web/components/ui/alert.tsx
Normal 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 }
|
||||
36
maternal-web/components/ui/badge.tsx
Normal file
36
maternal-web/components/ui/badge.tsx
Normal 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 }
|
||||
56
maternal-web/components/ui/button.tsx
Normal file
56
maternal-web/components/ui/button.tsx
Normal 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 }
|
||||
79
maternal-web/components/ui/card.tsx
Normal file
79
maternal-web/components/ui/card.tsx
Normal 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 }
|
||||
28
maternal-web/components/ui/progress.tsx
Normal file
28
maternal-web/components/ui/progress.tsx
Normal 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 }
|
||||
55
maternal-web/components/ui/tabs.tsx
Normal file
55
maternal-web/components/ui/tabs.tsx
Normal 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 }
|
||||
@@ -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 {
|
||||
|
||||
6
maternal-web/lib/utils.ts
Normal file
6
maternal-web/lib/utils.ts
Normal 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))
|
||||
}
|
||||
837
maternal-web/package-lock.json
generated
837
maternal-web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user