import { useState, useEffect, useMemo, useRef } from 'react'; import { useAuth, useCollection } from 'deplixo'; import { generatePDF } from '@deplixo/sdk'; import { JournalEntry } from './components/JournalEntry.jsx'; import { CalendarView } from './components/CalendarView.jsx'; import { RandomReflection } from './components/RandomReflection.jsx'; function getEntryDateKey(entry) { if (!entry) return null; const value = entry.entryDate || entry.date || entry.createdAt || entry.updatedAt; if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d.toISOString().slice(0, 10); } function getWeekStartKey(value) { if (!value) return null; const d = new Date(`${value}T00:00:00`); if (Number.isNaN(d.getTime())) return null; const day = d.getDay(); const diff = (day + 6) % 7; d.setDate(d.getDate() - diff); return d.toISOString().slice(0, 10); } function getMoodTagsFromEntry(entry) { if (!entry) return []; const candidates = [ entry.mood, entry.moodTag, entry.moodTags, entry.moods, entry.tags, entry.labels ]; const moods = candidates.flatMap(value => { if (Array.isArray(value)) return value; if (typeof value === 'string') return [value]; return []; }); return moods .map(value => String(value).trim().toLowerCase()) .filter(Boolean); } function normalizeMoodLabel(label) { const cleaned = String(label || '').trim().toLowerCase(); if (!cleaned) return null; const mapping = { happy: 'Happy', joy: 'Happy', joyful: 'Happy', grateful: 'Happy', calm: 'Calm', peaceful: 'Calm', relaxed: 'Calm', neutral: 'Neutral', okay: 'Neutral', fine: 'Neutral', sad: 'Sad', down: 'Sad', tired: 'Tired', stressed: 'Stressed', anxious: 'Anxious', anxiousness: 'Anxious', excited: 'Excited', hopeful: 'Hopeful', content: 'Content' }; return mapping[cleaned] || cleaned.charAt(0).toUpperCase() + cleaned.slice(1); } function countWordsFromEntry(entry) { if (!entry) return 0; const fields = [ entry.text, entry.entryText, entry.content, entry.gratitude, entry.gratitudes, entry.items, entry.response ]; const text = fields .flatMap(value => { if (Array.isArray(value)) return value; if (typeof value === 'string') return [value]; return []; }) .join(' ') .trim(); if (!text) return 0; return text.split(/\s+/).filter(Boolean).length; } function getEntryDisplayDate(entry) { const key = getEntryDateKey(entry); if (!key) return 'Unknown date'; return new Date(`${key}T00:00:00`).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); } function getEntryTextList(entry) { if (!entry) return []; const fields = [entry.text, entry.entryText, entry.content, entry.gratitude, entry.gratitudes, entry.items, entry.response]; return fields.flatMap(value => { if (Array.isArray(value)) return value; if (typeof value === 'string') return [value]; return []; }).map(value => String(value).trim()).filter(Boolean); } function buildShareLink(entry) { const entryId = entry?.id || ''; const dateKey = getEntryDateKey(entry) || ''; const payload = encodeURIComponent(JSON.stringify({ entryId, dateKey })); return `${window.location.origin}${window.location.pathname}#share=${payload}`; } function EntrySharePanel({ entry, onClose }) { const [copied, setCopied] = useState(false); const shareLink = useMemo(() => buildShareLink(entry), [entry]); const summary = getEntryTextList(entry).slice(0, 3).join(' • '); const handleCopy = async () => { try { await navigator.clipboard.writeText(shareLink); setCopied(true); window.setTimeout(() => setCopied(false), 1800); } catch { setCopied(false); } }; const handleNativeShare = async () => { if (navigator.share) { try { await navigator.share({ title: 'Grateful entry', text: summary || 'A gratitude entry from Grateful', url: shareLink }); } catch { // ignore cancellation } } }; return (

Share this entry

Choose how you want to share this gratitude moment.

{getEntryDisplayDate(entry)}

{summary || 'A private gratitude entry'}

); } function MonthlyExportCard({ entries, user }) { const exportRef = useRef(null); const [exporting, setExporting] = useState(false); const [selectedMonth, setSelectedMonth] = useState(() => { const now = new Date(); return now.toISOString().slice(0, 7); }); const monthEntries = useMemo(() => { return (entries || []) .filter(entry => entry?.userId === user?.id) .filter(entry => getEntryDateKey(entry)?.startsWith(selectedMonth)); }, [entries, selectedMonth, user?.id]); const monthLabel = useMemo(() => { const [year, month] = selectedMonth.split('-').map(Number); const date = new Date(year, (month || 1) - 1, 1); return date.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); }, [selectedMonth]); const handleExportPDF = async () => { if (!exportRef.current) return; setExporting(true); try { await generatePDF(exportRef.current, { filename: `grateful-${selectedMonth}-compilation.pdf`, margin: 10 }); } finally { setExporting(false); } }; return (

Monthly compilation PDF

Export a printable summary of one month

Grateful — {monthLabel}

Monthly gratitude compilation

{monthEntries.length === 0 ? (

No entries were found for this month.

) : (
{monthEntries.map(entry => (
{getEntryDisplayDate(entry)}
    {getEntryTextList(entry).slice(0, 3).map((text, idx) =>
  • {text}
  • )}
))}
)}
); } function App() { const auth = typeof useAuth === 'function' ? useAuth() : {}; const { user, loading: authLoading, login, logout } = auth; const collection = typeof useCollection === 'function' ? useCollection('journalEntries', { personal: true }) : {}; const { items: entries, loading: entriesLoading, add, update } = collection; const [activeTab, setActiveTab] = useState('journal'); const [signedInEntryId, setSignedInEntryId] = useState(null); const [shareEntry, setShareEntry] = useState(null); const tabs = [ { id: 'journal', label: '✏️ Today', icon: '✏️' }, { id: 'calendar', label: '📅 Calendar', icon: '📅' }, { id: 'reflect', label: '🔮 Reflect', icon: '🔮' }, { id: 'charts', label: '📈 Charts', icon: '📈' }, { id: 'stats', label: '📊 Stats', icon: '📊' } ]; useEffect(() => { if (!authLoading && !user) { setActiveTab('journal'); } }, [authLoading, user]); const handleSignIn = async () => { if (typeof login === 'function') { await login({ provider: 'google' }); } }; const handleSignOut = async () => { if (typeof logout === 'function') { await logout(); } }; const handleSaveEntry = async (entryData) => { if (!user) return; const payload = { ...entryData, userId: user.id, userEmail: user.email || '', userName: user.name || '', updatedAt: new Date().toISOString() }; if (signedInEntryId && typeof update === 'function') { await update(signedInEntryId, payload); return; } if (typeof add !== 'function') return; const created = await add({ ...payload, createdAt: new Date().toISOString() }); if (created?.id) setSignedInEntryId(created.id); }; const userEntries = (entries || []).filter(entry => entry?.userId === user?.id); const charts = useMemo(() => { const entriesWithDates = userEntries .map(entry => ({ entry, dateKey: getEntryDateKey(entry) })) .filter(item => Boolean(item.dateKey)) .sort((a, b) => new Date(a.dateKey) - new Date(b.dateKey)); const weekMap = new Map(); entriesWithDates.forEach(({ dateKey }) => { const weekKey = getWeekStartKey(dateKey); if (!weekKey) return; weekMap.set(weekKey, (weekMap.get(weekKey) || 0) + 1); }); const entriesPerWeek = [...weekMap.entries()] .sort((a, b) => new Date(a[0]) - new Date(b[0])) .map(([weekStart, count]) => ({ label: new Date(`${weekStart}T00:00:00`).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), value: count, weekStart })); const moodCounts = new Map(); const moodWordTotals = new Map(); let moodTaggedEntries = 0; userEntries.forEach(entry => { const moods = getMoodTagsFromEntry(entry).map(normalizeMoodLabel).filter(Boolean); if (moods.length === 0) return; moodTaggedEntries += 1; const wordCount = countWordsFromEntry(entry); moods.forEach(mood => { moodCounts.set(mood, (moodCounts.get(mood) || 0) + 1); moodWordTotals.set(mood, (moodWordTotals.get(mood) || 0) + wordCount); }); }); const moodCorrelation = [...moodCounts.entries()] .map(([mood, count]) => ({ mood, count, avgWords: count > 0 ? moodWordTotals.get(mood) / count : 0 })) .sort((a, b) => b.count - a.count || b.avgWords - a.avgWords); return { entriesPerWeek, moodCorrelation, moodTaggedEntries }; }, [userEntries]); const stats = useMemo(() => { const totalEntries = userEntries.length; const sortedByDate = [...userEntries].sort((a, b) => { const aTime = new Date(a.createdAt || a.updatedAt || a.entryDate || 0).getTime(); const bTime = new Date(b.createdAt || b.updatedAt || b.entryDate || 0).getTime(); return aTime - bTime; }); const dateKeys = [...new Set(sortedByDate.map(getEntryDateKey).filter(Boolean))].sort(); let longestStreak = 0; let currentStreak = 0; if (dateKeys.length > 0) { const daySet = new Set(dateKeys); let best = 0; let run = 0; for (let i = 0; i < dateKeys.length; i += 1) { const current = dateKeys[i]; const prev = dateKeys[i - 1]; if (!prev) { run = 1; } else { const prevDate = new Date(`${prev}T00:00:00`); const currentDate = new Date(`${current}T00:00:00`); const diffDays = Math.round((currentDate - prevDate) / 86400000); run = diffDays === 1 ? run + 1 : 1; } best = Math.max(best, run); } longestStreak = best; const today = new Date(); const todayKey = today.toISOString().slice(0, 10); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); const yesterdayKey = yesterday.toISOString().slice(0, 10); if (daySet.has(todayKey) || daySet.has(yesterdayKey)) { let streakCursor = daySet.has(todayKey) ? todayKey : yesterdayKey; let streak = 0; while (daySet.has(streakCursor)) { streak += 1; const cursorDate = new Date(`${streakCursor}T00:00:00`); cursorDate.setDate(cursorDate.getDate() - 1); streakCursor = cursorDate.toISOString().slice(0, 10); } currentStreak = streak; } } const wordsWritten = userEntries.reduce((sum, entry) => sum + countWordsFromEntry(entry), 0); return { totalEntries, longestStreak, currentStreak, wordsWritten }; }, [userEntries]); const maxWeeklyEntries = Math.max(1, ...charts.entriesPerWeek.map(item => item.value)); const maxMoodCount = Math.max(1, ...charts.moodCorrelation.map(item => item.count)); return (

Grateful

Three things. Every day.

{!authLoading && user && (
Signed in as {user.name || user.email}
)}
{authLoading || entriesLoading ? (
) : !user ? (
🔐

Sign in to view your journal

Your gratitude entries are private and tied to your Google account. Sign in to write, review, and reflect on your own entries.

) : ( <> {activeTab === 'journal' && ( )} {activeTab === 'calendar' && ( )} {activeTab === 'reflect' && } {activeTab === 'charts' && (

Your Charts

See weekly consistency and mood patterns over time.

Entries per week

Time-series aggregation
{charts.entriesPerWeek.length === 0 ? (

Not enough dated entries yet to build a weekly chart.

) : (
{charts.entriesPerWeek.map(point => (
{point.value}
{point.label}
))}
)}

Mood correlation

Only entries with mood tags are included
{charts.moodTaggedEntries === 0 ? (

Add mood tags to your entries to see mood correlation here.

Supported fields: mood, moodTag, moodTags, moods, tags, or labels.

) : charts.moodCorrelation.length < 1 ? (

Mood data is present, but there isn’t enough consistent tagging to chart yet.

) : (
{charts.moodCorrelation.map(item => (
{item.mood} {item.count} entries
Avg. {item.avgWords.toFixed(1)} words
))}
)}
)} {activeTab === 'stats' && (

Your Stats

Track consistency and momentum over time.

{stats.totalEntries} Total entries
{stats.currentStreak} Current streak
{stats.longestStreak} Longest streak
{stats.wordsWritten} Words written
)} )}
{shareEntry && (
setShareEntry(null)}>
e.stopPropagation()}> setShareEntry(null)} />
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();