- Automatic browser geolocation capture on event creation - Reverse geocoding via Nominatim API for place names - Full-text search with SQLite FTS5 - Calendar view for browsing past entries - DateNavigator component for day navigation - SearchModal with Ctrl+K shortcut - QuickAddWidget with Ctrl+J shortcut - Starlight documentation site with GitHub Pages deployment - Multiple AI provider support (Groq, OpenAI, Anthropic, Ollama, LM Studio) - Multi-user registration support BREAKING: Events now include latitude/longitude/placeName fields
155 lines
5.7 KiB
TypeScript
155 lines
5.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { api, Journal } from '../lib/api';
|
|
|
|
const PAGE_SIZES = [10, 50, 100];
|
|
|
|
export default function Diary() {
|
|
const [journals, setJournals] = useState<Journal[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [page, setPage] = useState(1);
|
|
const [limit, setLimit] = useState(10);
|
|
const [total, setTotal] = useState(0);
|
|
const [totalPages, setTotalPages] = useState(0);
|
|
|
|
useEffect(() => {
|
|
loadJournals();
|
|
}, [page, limit]);
|
|
|
|
const loadJournals = async () => {
|
|
setLoading(true);
|
|
const res = await api.getJournals(page, limit);
|
|
if (res.data) {
|
|
setJournals(res.data.journals);
|
|
setTotal(res.data.total);
|
|
setTotalPages(res.data.totalPages);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
const d = new Date(dateStr + 'T12:00:00');
|
|
return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
|
|
};
|
|
|
|
const formatDateShort = (dateStr: string) => {
|
|
const d = new Date(dateStr + 'T12:00:00');
|
|
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto p-4">
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold mb-2">Diary</h1>
|
|
<p className="text-slate-400 text-sm">Read your diary pages</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-sm text-slate-400">Show:</label>
|
|
<select
|
|
value={limit}
|
|
onChange={(e) => { setLimit(parseInt(e.target.value)); setPage(1); }}
|
|
className="bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm focus:outline-none focus:border-purple-500"
|
|
>
|
|
{PAGE_SIZES.map(size => (
|
|
<option key={size} value={size}>{size} per page</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="text-sm text-slate-400">
|
|
{total} diary pages total
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-slate-400">Loading...</div>
|
|
) : journals.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-slate-400 mb-2">No diary pages yet</p>
|
|
<p className="text-slate-500 text-sm">Generate diary pages from your events to see them here</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{journals.map((journal) => (
|
|
<div key={journal.id} className="bg-slate-900 rounded-xl p-6 border border-slate-800">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div>
|
|
<a href={`/journal/${journal.date}`} className="text-lg font-semibold hover:text-purple-400 transition">
|
|
{journal.title || formatDateShort(journal.date)}
|
|
</a>
|
|
<p className="text-sm text-slate-400">{formatDate(journal.date)}</p>
|
|
</div>
|
|
<div className="text-xs text-slate-500">
|
|
{journal.eventCount} events
|
|
</div>
|
|
</div>
|
|
<div className="prose prose-invert prose-sm prose-slate max-w-none">
|
|
<div className="text-slate-300 whitespace-pre-wrap leading-relaxed">
|
|
{journal.content}
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-800 flex justify-between items-center">
|
|
<span className="text-xs text-slate-500">
|
|
Generated {new Date(journal.generatedAt).toLocaleString()}
|
|
</span>
|
|
<a
|
|
href={`/journal/${journal.date}`}
|
|
className="text-sm text-purple-400 hover:text-purple-300 transition"
|
|
>
|
|
View & Edit →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-center gap-2 mt-8">
|
|
<button
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed transition"
|
|
>
|
|
Previous
|
|
</button>
|
|
<div className="flex items-center gap-1">
|
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
let pageNum: number;
|
|
if (totalPages <= 5) {
|
|
pageNum = i + 1;
|
|
} else if (page <= 3) {
|
|
pageNum = i + 1;
|
|
} else if (page >= totalPages - 2) {
|
|
pageNum = totalPages - 4 + i;
|
|
} else {
|
|
pageNum = page - 2 + i;
|
|
}
|
|
return (
|
|
<button
|
|
key={pageNum}
|
|
onClick={() => setPage(pageNum)}
|
|
className={`w-10 h-10 rounded text-sm transition ${
|
|
page === pageNum
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-slate-800 hover:bg-slate-700'
|
|
}`}
|
|
>
|
|
{pageNum}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<button
|
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
|
disabled={page === totalPages}
|
|
className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded text-sm disabled:opacity-50 disabled:cursor-not-allowed transition"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|