Files
deardiary/frontend/src/pages/Diary.tsx
lotherk 0bdd71a4ed feat: v0.1.0 - geolocation capture, calendar, search, Starlight docs site
- 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
2026-03-27 02:27:55 +00:00

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>
);
}