feat: add inline edit functionality for unlocked events

- EntryList now supports inline editing with pencil icon
- Click pencil to edit, Save/Cancel buttons appear
- Edit works on Home and Day pages
This commit is contained in:
lotherk
2026-03-27 09:28:47 +00:00
parent 0b4d5bad58
commit 18ba0a47e1
3 changed files with 87 additions and 14 deletions

View File

@@ -1,12 +1,17 @@
import { useState } from 'react';
import type { Event } from '../lib/api'; import type { Event } from '../lib/api';
interface Props { interface Props {
events: Event[]; events: Event[];
onDelete: (id: string) => void; onDelete: (id: string) => void;
onEdit: (id: string, content: string) => Promise<void>;
readOnly?: boolean; readOnly?: boolean;
} }
export default function EntryList({ events, onDelete, readOnly }: Props) { export default function EntryList({ events, onDelete, onEdit, readOnly }: Props) {
const [editingId, setEditingId] = useState<string | null>(null);
const [editContent, setEditContent] = useState('');
const formatTime = (dateStr: string) => { const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); return new Date(dateStr).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}; };
@@ -20,9 +25,26 @@ export default function EntryList({ events, onDelete, readOnly }: Props) {
} }
}; };
const startEdit = (event: Event) => {
setEditingId(event.id);
setEditContent(event.content);
};
const cancelEdit = () => {
setEditingId(null);
setEditContent('');
};
const saveEdit = async () => {
if (editingId && editContent.trim()) {
await onEdit(editingId, editContent.trim());
setEditingId(null);
setEditContent('');
}
};
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{events.map((event) => ( {events.map((event) => (
<div <div
key={event.id} key={event.id}
@@ -35,9 +57,36 @@ export default function EntryList({ events, onDelete, readOnly }: Props) {
<span className="text-xs text-slate-500">{formatTime(event.createdAt)}</span> <span className="text-xs text-slate-500">{formatTime(event.createdAt)}</span>
<span className="text-xs px-2 py-0.5 bg-slate-800 rounded text-slate-400 capitalize">{event.type}</span> <span className="text-xs px-2 py-0.5 bg-slate-800 rounded text-slate-400 capitalize">{event.type}</span>
</div> </div>
<p className="text-slate-200">{event.content}</p>
{event.mediaPath && ( {editingId === event.id ? (
<div className="mt-2">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-200 resize-none focus:border-purple-500 focus:outline-none"
rows={3}
autoFocus
/>
<div className="flex gap-2 mt-2">
<button
onClick={saveEdit}
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 rounded text-sm font-medium transition"
>
Save
</button>
<button
onClick={cancelEdit}
className="px-3 py-1 bg-slate-700 hover:bg-slate-600 rounded text-sm font-medium transition"
>
Cancel
</button>
</div>
</div>
) : (
<p className="text-slate-200">{event.content}</p>
)}
{event.mediaPath && editingId !== event.id && (
<div className="mt-3"> <div className="mt-3">
{event.type === 'photo' && ( {event.type === 'photo' && (
<img <img
@@ -56,19 +105,29 @@ export default function EntryList({ events, onDelete, readOnly }: Props) {
</div> </div>
)} )}
{(event.placeName || (event.latitude && event.longitude)) && ( {(event.placeName || (event.latitude && event.longitude)) && editingId !== event.id && (
<div className="mt-2 text-xs text-slate-500"> <div className="mt-2 text-xs text-slate-500">
📍 {event.placeName || `${event.latitude?.toFixed(4)}, ${event.longitude?.toFixed(4)}`} 📍 {event.placeName || `${event.latitude?.toFixed(4)}, ${event.longitude?.toFixed(4)}`}
</div> </div>
)} )}
</div> </div>
{!readOnly && ( {!readOnly && editingId !== event.id && (
<div className="flex gap-2">
<button
onClick={() => startEdit(event)}
className="text-slate-500 hover:text-purple-400 text-sm transition"
title="Edit"
>
</button>
<button <button
onClick={() => onDelete(event.id)} onClick={() => onDelete(event.id)}
className="text-slate-500 hover:text-red-400 text-sm transition" className="text-slate-500 hover:text-red-400 text-sm transition"
title="Delete"
> >
× ×
</button> </button>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -48,6 +48,13 @@ export default function Day() {
} }
}; };
const handleEditEvent = async (id: string, content: string) => {
const res = await api.updateEvent(id, content);
if (res.data) {
setEvents((prev) => prev.map((e) => e.id === id ? res.data! : e));
}
};
const handleDeleteJournal = async () => { const handleDeleteJournal = async () => {
if (!date) return; if (!date) return;
if (!confirm('Delete diary page? This will unlock events for editing.')) return; if (!confirm('Delete diary page? This will unlock events for editing.')) return;
@@ -110,7 +117,7 @@ export default function Day() {
<p className="text-slate-400">No events for this day</p> <p className="text-slate-400">No events for this day</p>
</div> </div>
) : ( ) : (
<EntryList events={events} onDelete={handleDeleteEvent} readOnly={hasJournal} /> <EntryList events={events} onDelete={handleDeleteEvent} onEdit={handleEditEvent} readOnly={hasJournal} />
)} )}
</div> </div>
); );

View File

@@ -46,6 +46,13 @@ export default function Home() {
} }
}; };
const handleEditEvent = async (id: string, content: string) => {
const res = await api.updateEvent(id, content);
if (res.data) {
setEvents((prev) => prev.map((e) => e.id === id ? res.data! : e));
}
};
const handleDeleteJournal = async () => { const handleDeleteJournal = async () => {
if (!confirm('Delete diary page? This will unlock events for editing.')) return; if (!confirm('Delete diary page? This will unlock events for editing.')) return;
const res = await api.deleteJournal(today); const res = await api.deleteJournal(today);
@@ -124,7 +131,7 @@ export default function Home() {
<p className="text-slate-500 text-sm">Start capturing your day above</p> <p className="text-slate-500 text-sm">Start capturing your day above</p>
</div> </div>
) : ( ) : (
<EntryList events={events} onDelete={handleDeleteEvent} readOnly={hasJournal} /> <EntryList events={events} onDelete={handleDeleteEvent} onEdit={handleEditEvent} readOnly={hasJournal} />
)} )}
{events.length > 0 && hasJournal && ( {events.length > 0 && hasJournal && (