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:
@@ -1,12 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import type { Event } from '../lib/api';
|
||||
|
||||
interface Props {
|
||||
events: Event[];
|
||||
onDelete: (id: string) => void;
|
||||
onEdit: (id: string, content: string) => Promise<void>;
|
||||
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) => {
|
||||
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 (
|
||||
<div className="space-y-3">
|
||||
|
||||
{events.map((event) => (
|
||||
<div
|
||||
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 px-2 py-0.5 bg-slate-800 rounded text-slate-400 capitalize">{event.type}</span>
|
||||
</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">
|
||||
{event.type === 'photo' && (
|
||||
<img
|
||||
@@ -56,19 +105,29 @@ export default function EntryList({ events, onDelete, readOnly }: Props) {
|
||||
</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">
|
||||
📍 {event.placeName || `${event.latitude?.toFixed(4)}, ${event.longitude?.toFixed(4)}`}
|
||||
</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
|
||||
onClick={() => onDelete(event.id)}
|
||||
className="text-slate-500 hover:text-red-400 text-sm transition"
|
||||
title="Delete"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 () => {
|
||||
if (!date) 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>
|
||||
</div>
|
||||
) : (
|
||||
<EntryList events={events} onDelete={handleDeleteEvent} readOnly={hasJournal} />
|
||||
<EntryList events={events} onDelete={handleDeleteEvent} onEdit={handleEditEvent} readOnly={hasJournal} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 () => {
|
||||
if (!confirm('Delete diary page? This will unlock events for editing.')) return;
|
||||
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>
|
||||
</div>
|
||||
) : (
|
||||
<EntryList events={events} onDelete={handleDeleteEvent} readOnly={hasJournal} />
|
||||
<EntryList events={events} onDelete={handleDeleteEvent} onEdit={handleEditEvent} readOnly={hasJournal} />
|
||||
)}
|
||||
|
||||
{events.length > 0 && hasJournal && (
|
||||
|
||||
Reference in New Issue
Block a user