ceda779b75
Nuevo menu "Notas" en el sidebar: crear, editar y eliminar apuntes rapidos, con fecha y hora de creacion (y de ultima edicion si aplica). Siempre privadas de cada usuario -- una nota de otro usuario responde 404, no 403, para no revelar que existe. Sin restriccion de rol. Nuevo notes (tabla) + GET/POST/PUT/DELETE /api/notes. Probado de punta a punta en dev: crear, editar, borrar y el formato de fecha/hora funcionan correctamente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
126 lines
4.3 KiB
React
126 lines
4.3 KiB
React
import { useState } from 'react';
|
|
import { Plus, Pencil, Save, X, Trash2 } from 'lucide-react';
|
|
import { useUi } from '../../context/ui-context.jsx';
|
|
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
|
import { listNotes, createNote, updateNote, deleteNote } from '../../services/notes.js';
|
|
import { Button } from '../../components/Button.jsx';
|
|
|
|
function formatDateTime(value) {
|
|
return String(value).slice(0, 16).replace('T', ' ');
|
|
}
|
|
|
|
export default function NotasPage() {
|
|
const { t, pushToast, openDialog } = useUi();
|
|
const runAction = useAction();
|
|
const { data: notes, reload } = useAsync(listNotes, []);
|
|
|
|
const [newContent, setNewContent] = useState('');
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [editingContent, setEditingContent] = useState('');
|
|
|
|
async function addNote() {
|
|
if (!newContent.trim()) return;
|
|
await runAction(async () => {
|
|
await createNote(newContent);
|
|
setNewContent('');
|
|
await reload();
|
|
});
|
|
}
|
|
|
|
function startEdit(note) {
|
|
setEditingId(note.id);
|
|
setEditingContent(note.content);
|
|
}
|
|
|
|
function cancelEdit() {
|
|
setEditingId(null);
|
|
setEditingContent('');
|
|
}
|
|
|
|
async function saveEdit(id) {
|
|
if (!editingContent.trim()) return;
|
|
await runAction(async () => {
|
|
await updateNote(id, editingContent);
|
|
cancelEdit();
|
|
await reload();
|
|
});
|
|
}
|
|
|
|
function confirmDelete(note) {
|
|
openDialog({
|
|
tone: 'warning',
|
|
title: t('common_confirm_delete_title'),
|
|
message: t('common_confirm_delete_msg'),
|
|
actions: [
|
|
{ label: t('common_cancel'), variant: 'standard' },
|
|
{
|
|
label: t('common_delete'), variant: 'danger',
|
|
onClick: () => runAction(async () => {
|
|
await deleteNote(note.id);
|
|
await reload();
|
|
pushToast({ tone: 'success', message: t('common_saved') });
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
if (!notes) return <div className="text-muted">{t('common_loading')}</div>;
|
|
|
|
return (
|
|
<div>
|
|
<h2 className="page-title mt-0" style={{ marginBottom: 16 }}>{t('notes_title')}</h2>
|
|
|
|
<div className="panel-card" style={{ marginBottom: 16 }}>
|
|
<textarea
|
|
className="textarea"
|
|
rows={3}
|
|
placeholder={t('notes_placeholder')}
|
|
value={newContent}
|
|
onChange={(e) => setNewContent(e.target.value)}
|
|
/>
|
|
<div style={{ marginTop: 8 }}>
|
|
<Button variant="accent" icon={Plus} onClick={addNote} disabled={!newContent.trim()}>
|
|
{t('notes_add')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{notes.length === 0 && <div className="text-muted">{t('notes_empty')}</div>}
|
|
|
|
{notes.map((note) => (
|
|
<div key={note.id} className="panel-card" style={{ marginBottom: 10 }}>
|
|
{editingId === note.id ? (
|
|
<>
|
|
<textarea
|
|
className="textarea" rows={3} autoFocus
|
|
value={editingContent}
|
|
onChange={(e) => setEditingContent(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === 'Escape') cancelEdit(); }}
|
|
/>
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
|
<Button variant="accent" icon={Save} onClick={() => saveEdit(note.id)}>{t('common_save')}</Button>
|
|
<Button variant="standard" icon={X} onClick={cancelEdit}>{t('common_cancel')}</Button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div style={{ fontSize: '.85rem', whiteSpace: 'pre-wrap', marginBottom: 8 }}>{note.content}</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<span className="text-muted" style={{ fontSize: '.7rem' }}>
|
|
{note.created_at === note.updated_at
|
|
? formatDateTime(note.created_at)
|
|
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
|
|
</span>
|
|
<div style={{ flex: 1 }} />
|
|
<button className="icon-btn" title={t('common_edit')} onClick={() => startEdit(note)}><Pencil size={15} /></button>
|
|
<button className="icon-btn" title={t('common_delete')} onClick={() => confirmDelete(note)}><Trash2 size={15} /></button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|