/* @component-map
* App — Main container, tab navigation [app.jsx]
* SymptomLogger — Log new symptoms with type, severity, time, triggers, notes [components/SymptomLogger.jsx]
* Timeline — Chronological timeline view of logged symptoms [components/Timeline.jsx]
* Heatmap — Severity heatmap by time of day [components/Heatmap.jsx]
* @end-component-map */
// DEPLOY_CONFIG: {"cron": [{"name": "weekly_health_insight_report", "schedule": "0 9 * * 1", "action": "event", "config": {"event_type": "analyze_weekly_symptom_patterns"}}], "triggers": [{"name": "send_weekly_health_insight_report_email", "on": "event.completed", "collection": "symptom_logs", "actions": [{"type": "email", "to": "{{config.recipient_emails}}", "subject": "Weekly Health Insight Report", "body": "Your weekly health insight report has been generated. It includes pattern analysis for symptom timing, likely triggers, and symptom clusters. Please review the attached/generated report in your app."}]}]}
import { useEffect, useMemo, useState } from 'react';
import { useAuth, useCollection, renderChart } from '@deplixo/sdk';
import { SymptomLogger } from './components/SymptomLogger.jsx';
import { Timeline } from './components/Timeline.jsx';
import { Heatmap } from './components/Heatmap.jsx';
function ChartPanel({ title, subtitle, children }) {
return (
{title}
{subtitle ?
{subtitle}
: null}
{children}
);
}
function FrequencyChart({ symptoms }) {
const canvasRef = useState(null)[0];
const ref = useMemo(() => ({ current: null }), []);
ref.current = canvasRef;
const chartData = useMemo(() => {
const counts = new Map();
symptoms.forEach(item => {
const type = item?.type || 'Unknown';
counts.set(type, (counts.get(type) || 0) + 1);
});
const labels = Array.from(counts.keys());
return {
labels,
datasets: [{
label: 'Symptom Count',
data: labels.map(label => counts.get(label) || 0),
}],
};
}, [symptoms]);
useEffect(() => {
if (!ref.current || chartData.labels.length === 0) return;
renderChart(ref.current, {
type: 'bar',
data: chartData,
});
}, [chartData, ref]);
return ;
}
function SeverityTrendChart({ symptoms }) {
const canvasRef = useState(null)[0];
const ref = useMemo(() => ({ current: null }), []);
ref.current = canvasRef;
const chartData = useMemo(() => {
const byDay = new Map();
symptoms.forEach(item => {
const date = item?.time ? new Date(item.time) : null;
if (!date || Number.isNaN(date.getTime())) return;
const key = date.toISOString().slice(0, 10);
const prev = byDay.get(key) || { sum: 0, count: 0 };
prev.sum += Number(item?.severity || 0);
prev.count += 1;
byDay.set(key, prev);
});
const labels = Array.from(byDay.keys()).sort();
return {
labels,
datasets: [{
label: 'Average Severity',
data: labels.map(label => {
const v = byDay.get(label);
return v && v.count ? Number((v.sum / v.count).toFixed(2)) : 0;
}),
}],
};
}, [symptoms]);
useEffect(() => {
if (!ref.current || chartData.labels.length === 0) return;
renderChart(ref.current, {
type: 'line',
data: chartData,
});
}, [chartData, ref]);
return ;
}
function TriggerCorrelationChart({ symptoms }) {
const canvasRef = useState(null)[0];
const ref = useMemo(() => ({ current: null }), []);
ref.current = canvasRef;
const chartData = useMemo(() => {
const triggerTotals = new Map();
symptoms.forEach(item => {
const severity = Number(item?.severity || 0);
const triggers = Array.isArray(item?.triggers) ? item.triggers : [];
triggers.forEach(trigger => {
const key = String(trigger || 'Unknown');
const prev = triggerTotals.get(key) || { sum: 0, count: 0 };
prev.sum += severity;
prev.count += 1;
triggerTotals.set(key, prev);
});
});
const entries = Array.from(triggerTotals.entries())
.map(([label, value]) => ({ label, avg: value.count ? value.sum / value.count : 0, count: value.count }))
.sort((a, b) => b.avg - a.avg)
.slice(0, 10);
return {
labels: entries.map(entry => entry.label),
datasets: [{
label: 'Avg Severity When Trigger Present',
data: entries.map(entry => Number(entry.avg.toFixed(2))),
}],
};
}, [symptoms]);
useEffect(() => {
if (!ref.current || chartData.labels.length === 0) return;
renderChart(ref.current, {
type: 'bar',
data: chartData,
});
}, [chartData, ref]);
return ;
}
function App() {
const { user, loading, login, logout } = useAuth();
const { items: allSymptoms, loading: symptomsLoading } = useCollection('symptoms', { personal: true });
const [activeTab, setActiveTab] = useState('timeline');
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState('all');
const [dateRange, setDateRange] = useState('all');
const symptomItems = useMemo(() => Array.isArray(allSymptoms) ? allSymptoms : [], [allSymptoms]);
const symptomTypes = useMemo(() => {
const types = new Set();
symptomItems.forEach(item => {
if (item?.type) types.add(item.type);
});
return ['all', ...Array.from(types).sort()];
}, [symptomItems]);
const filteredSymptoms = useMemo(() => {
const term = searchTerm.trim().toLowerCase();
const now = new Date();
return symptomItems.filter(item => {
const itemDate = item?.time ? new Date(item.time) : null;
const matchesSearch = !term || [item?.type, item?.notes, ...(Array.isArray(item?.triggers) ? item.triggers : [])]
.filter(Boolean)
.some(value => String(value).toLowerCase().includes(term));
const matchesType = selectedType === 'all' || item?.type === selectedType;
let matchesDate = true;
if (dateRange !== 'all' && itemDate && !Number.isNaN(itemDate.getTime())) {
const diffMs = now.getTime() - itemDate.getTime();
const diffDays = diffMs / (1000 * 60 * 60 * 24);
if (dateRange === 'today') matchesDate = diffDays < 1;
if (dateRange === '7d') matchesDate = diffDays < 7;
if (dateRange === '30d') matchesDate = diffDays < 30;
} else if (dateRange !== 'all' && !itemDate) {
matchesDate = false;
}
return matchesSearch && matchesType && matchesDate;
});
}, [symptomItems, searchTerm, selectedType, dateRange]);
useEffect(() => {
if (activeTab !== 'log') return;
if (selectedType === 'all') return;
const currentTypes = symptomTypes.slice(1);
if (!currentTypes.includes(selectedType)) setSelectedType('all');
}, [activeTab, selectedType, symptomTypes]);
const tabs = [
{ id: 'timeline', label: '📋 Timeline', icon: '📋' },
{ id: 'log', label: '+ Log', icon: '+' },
{ id: 'heatmap', label: '🔥 Heatmap', icon: '🔥' },
];
const sharedViewProps = { user, symptoms: filteredSymptoms, allSymptoms: symptomItems };
if (loading) {
return
Signing in...
;
}
if (!user) {
return (
Symptom Tracker
Sign in with Google to access your private symptom data
);
}
const chartSymptoms = filteredSymptoms;
return (
Symptom Tracker
Monitor your chronic condition patterns
{user.avatar ?
: user.name?.charAt(0) || 'U'}
{user.name}
Search & Filters
{filteredSymptoms.length} result{filteredSymptoms.length === 1 ? '' : 's'}
{activeTab === 'timeline' && }
{activeTab === 'log' && setActiveTab('timeline')} defaultType={selectedType !== 'all' ? selectedType : ''} />}
{activeTab === 'heatmap' && }
);
}
ReactDOM.createRoot(document.getElementById("root")).render();