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
This commit is contained in:
154
frontend/src/pages/Diary.tsx
Normal file
154
frontend/src/pages/Diary.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user