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';
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user