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?.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(
);