import { useEffect, useMemo, useRef, useState } from 'react'; import { useAuth, useCollection, renderChart, generatePDF, exportCSV, exportJSON } from '@deplixo/sdk'; import { JournalEntry } from './components/JournalEntry.jsx'; import { CalendarView } from './components/CalendarView.jsx'; import { EntryList } from './components/EntryList.jsx'; import { EntryDetail } from './components/EntryDetail.jsx'; const MOOD_VALUES = { awful: 1, bad: 2, okay: 3, good: 4, great: 5 }; const MOOD_LABELS = { awful: 'Awful', bad: 'Bad', okay: 'Okay', good: 'Good', great: 'Great' }; const MOOD_COLORS = { awful: '#BCAAA4', bad: '#D7CCC8', okay: '#FFF176', good: '#FFEE58', great: '#FDD835' }; const ENTRY_COLLECTION = 'journal_entries'; function getEntryDate(entry) { return new Date(entry?.date || entry?.createdAt || entry?.updatedAt || Date.now()); } function getWordCount(entry) { const content = String(entry?.content || ''); const plainText = content .replace(/<[^>]*>/g, ' ') .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); if (!plainText) return 0; return plainText.split(' ').filter(Boolean).length; } function formatShortDate(date) { return new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric' }).format(date); } function formatLongDate(date) { return new Intl.DateTimeFormat('en', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(date); } function formatMonthKey(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; } function formatMonthLabel(date) { return new Intl.DateTimeFormat('en', { month: 'long', year: 'numeric' }).format(date); } function getMoodColor(mood) { return MOOD_COLORS[mood] || MOOD_COLORS.okay; } function ChartPanel({ title, subtitle, children }) { return (

{title}

{subtitle ?

{subtitle}

: null}
{children}
); } function MoodTrendChart({ entries }) { const canvasRef = useRef(null); const chartData = useMemo(() => { const now = new Date(); const start = new Date(now); start.setDate(now.getDate() - 29); start.setHours(0, 0, 0, 0); const days = []; for (let i = 0; i < 30; i += 1) { const d = new Date(start); d.setDate(start.getDate() + i); days.push(d); } const entriesByDay = new Map(); entries.forEach((entry) => { const d = getEntryDate(entry); const key = d.toISOString().slice(0, 10); if (!entriesByDay.has(key)) entriesByDay.set(key, []); entriesByDay.get(key).push(entry); }); const values = days.map((day) => { const key = day.toISOString().slice(0, 10); const dayEntries = entriesByDay.get(key) || []; if (!dayEntries.length) return null; const sum = dayEntries.reduce((acc, entry) => acc + (MOOD_VALUES[entry.mood] || 0), 0); return sum / dayEntries.length; }); return { labels: days.map(formatShortDate), values }; }, [entries]); useEffect(() => { if (canvasRef.current && chartData.labels.length > 0) { renderChart(canvasRef.current, { type: 'line', data: { labels: chartData.labels, datasets: [ { label: 'Mood trend', data: chartData.values, borderColor: '#F9A825', backgroundColor: 'rgba(249, 168, 37, 0.16)', pointBackgroundColor: '#F9A825', pointBorderColor: '#F9A825', pointRadius: 3, tension: 0.35, fill: true } ] } }); } }, [chartData]); return ; } function WordCountChart({ entries }) { const canvasRef = useRef(null); const sortedEntries = useMemo(() => { return [...entries] .sort((a, b) => getEntryDate(a).getTime() - getEntryDate(b).getTime()) .slice(-10) .map((entry) => ({ label: entry.title?.trim() || formatShortDate(getEntryDate(entry)), value: getWordCount(entry) })); }, [entries]); useEffect(() => { if (canvasRef.current && sortedEntries.length > 0) { renderChart(canvasRef.current, { type: 'bar', data: { labels: sortedEntries.map((d) => d.label), datasets: [ { label: 'Word count', data: sortedEntries.map((d) => d.value), backgroundColor: '#FDD835', borderColor: '#F9A825', borderWidth: 1, borderRadius: 8 } ] } }); } }, [sortedEntries]); return ; } function MonthlyExportPanel({ entries }) { const reportRef = useRef(null); const [selectedMonth, setSelectedMonth] = useState(formatMonthKey(new Date())); const [exporting, setExporting] = useState(false); const monthOptions = useMemo(() => { const keys = new Set(); const current = new Date(); for (let i = 0; i < 12; i += 1) { const d = new Date(current.getFullYear(), current.getMonth() - i, 1); keys.add(formatMonthKey(d)); } entries.forEach((entry) => keys.add(formatMonthKey(getEntryDate(entry)))); return [...keys].sort((a, b) => b.localeCompare(a)); }, [entries]); const selectedMonthDate = useMemo(() => { const [year, month] = selectedMonth.split('-').map(Number); return new Date(year, month - 1, 1); }, [selectedMonth]); const monthEntries = useMemo(() => { return entries .filter((entry) => formatMonthKey(getEntryDate(entry)) === selectedMonth) .sort((a, b) => getEntryDate(a).getTime() - getEntryDate(b).getTime()); }, [entries, selectedMonth]); const handleExportPDF = async () => { if (!reportRef.current || !monthEntries.length) return; setExporting(true); try { await generatePDF(reportRef.current, { filename: `journal-${selectedMonth}.pdf`, margin: 10 }); } finally { setExporting(false); } }; return (

Monthly PDF export

Choose a month and download a printable PDF of your journal entries.

My Journal — {formatMonthLabel(selectedMonthDate)}

{monthEntries.length} entry{monthEntries.length === 1 ? '' : 'ies'} included

{monthEntries.length ? (
{monthEntries.map((entry) => { const entryDate = getEntryDate(entry); return (
{MOOD_LABELS[entry.mood] || 'Okay'}

{entry.title?.trim() || 'Untitled entry'}

{formatLongDate(entryDate)}

' }} />
); })}
) : (

No entries found for this month.

)}
); } function DataExportPanel({ entries }) { const [exportingFormat, setExportingFormat] = useState(''); const exportRows = useMemo(() => { return [...entries] .sort((a, b) => getEntryDate(a).getTime() - getEntryDate(b).getTime()) .map((entry) => { const entryDate = getEntryDate(entry); return { id: entry.id || '', title: entry.title?.trim() || 'Untitled entry', mood: entry.mood || 'okay', moodLabel: MOOD_LABELS[entry.mood] || 'Okay', moodValue: MOOD_VALUES[entry.mood] || 3, date: entry.date || entryDate.toISOString(), createdAt: entry.createdAt || '', updatedAt: entry.updatedAt || '', wordCount: getWordCount(entry), content: String(entry.content || '').replace(/<[^>]*>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim() }; }); }, [entries]); const handleCSVExport = () => { if (!exportRows.length) return; setExportingFormat('csv'); try { exportCSV(exportRows, { filename: 'journal-entries.csv', columns: ['id', 'title', 'mood', 'moodLabel', 'moodValue', 'date', 'createdAt', 'updatedAt', 'wordCount', 'content'] }); } finally { setExportingFormat(''); } }; const handleJSONExport = () => { if (!exportRows.length) return; setExportingFormat('json'); try { exportJSON(exportRows, { filename: 'journal-entries.json' }); } finally { setExportingFormat(''); } }; return (

Download your journal data

Export all entries with mood, timestamps, titles, and text content.

{exportRows.length} entr{exportRows.length === 1 ? 'y' : 'ies'} ready for export.

); } function App() { const { user, loading, logout } = useAuth(); const { items: entries = [], loading: entriesLoading } = useCollection(ENTRY_COLLECTION, { personal: true }); const [activeTab, setActiveTab] = useState('journal'); const [viewingEntry, setViewingEntry] = useState(null); const [editingEntry, setEditingEntry] = useState(null); const [selectedDate, setSelectedDate] = useState(null); const tabs = [ { id: 'journal', label: '✍️ Write', icon: '✍️' }, { id: 'calendar', label: '📅 Calendar', icon: '📅' }, { id: 'entries', label: '📖 Entries', icon: '📖' }, { id: 'insights', label: '📈 Insights', icon: '📈' } ]; const metrics = useMemo(() => { const last30Days = new Date(); last30Days.setDate(last30Days.getDate() - 29); last30Days.setHours(0, 0, 0, 0); const recentEntries = entries.filter((entry) => getEntryDate(entry) >= last30Days); const moodTrend = recentEntries.reduce((acc, entry) => acc + (MOOD_VALUES[entry.mood] || 0), 0); const averageMood = recentEntries.length ? moodTrend / recentEntries.length : 0; const totalWords = entries.reduce((acc, entry) => acc + getWordCount(entry), 0); const averageWords = entries.length ? Math.round(totalWords / entries.length) : 0; return { entriesCount: entries.length, recentEntriesCount: recentEntries.length, averageMood, totalWords, averageWords }; }, [entries]); const handleViewEntry = (entry) => { setViewingEntry(entry); setActiveTab('detail'); }; const handleEditEntry = (entry) => { setEditingEntry(entry); setActiveTab('journal'); }; const handleBackFromDetail = () => { setViewingEntry(null); setActiveTab('entries'); }; const handleEntrySaved = () => { setEditingEntry(null); setActiveTab('entries'); }; const handleDateSelect = (date) => { setSelectedDate(date); setActiveTab('entries'); }; const handleTabClick = (tabId) => { setActiveTab(tabId); if (tabId !== 'entries') setSelectedDate(null); if (tabId !== 'detail') setViewingEntry(null); if (tabId !== 'journal') setEditingEntry(null); }; if (loading) { return (

🌻 My Journal

Reflect, track, grow

); } if (!user) { return (

🌻 My Journal

Private reflection space

Sign in to continue

Use Google OAuth to access your private journal entries, calendar, and moods.

You'll be redirected to Deplixo to complete sign-in.

); } return (

🌻 My Journal

Reflect, track, grow

{user.avatar ? {user.name} :
{user.name?.charAt(0) || 'U'}
}
{user.name}
{activeTab === 'journal' && { setEditingEntry(null); setActiveTab('entries'); }} />} {activeTab === 'calendar' && } {activeTab === 'entries' && setSelectedDate(null)} />} {activeTab === 'insights' && (
{metrics.entriesCount}Total entries
{metrics.recentEntriesCount}Last 30 days
{metrics.averageMood ? metrics.averageMood.toFixed(1) : '—'}Avg mood
{metrics.averageWords}Avg words
{entriesLoading ?
: } {entriesLoading ?
: } {entriesLoading ?
: } {entriesLoading ?
: }
)} {activeTab === 'detail' && viewingEntry && }
); } ReactDOM.createRoot(document.getElementById("root")).render();