feat: menu Notas -- apuntes rapidos personales
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>
This commit is contained in:
@@ -113,6 +113,12 @@
|
|||||||
- `GET /api/attachments?entityType=activity&entityId=1` · `POST /api/attachments` (multipart)
|
- `GET /api/attachments?entityType=activity&entityId=1` · `POST /api/attachments` (multipart)
|
||||||
- `GET /api/attachments/:id/download` · `DELETE /api/attachments/:id`
|
- `GET /api/attachments/:id/download` · `DELETE /api/attachments/:id`
|
||||||
|
|
||||||
|
### Notas (`/api/notes`)
|
||||||
|
- `GET /api/notes` — solo las del usuario autenticado, ordenadas por `updated_at` descendente.
|
||||||
|
- `POST /api/notes` — `{ content }`. `PUT /api/notes/:id` — `{ content }`. `DELETE /api/notes/:id`.
|
||||||
|
- Siempre privadas: una nota de otro usuario responde `404` (no `403`, para no revelar que
|
||||||
|
existe). Sin restricción de rol — cualquier autenticado tiene las suyas.
|
||||||
|
|
||||||
### Notificaciones (`/api/notifications`)
|
### Notificaciones (`/api/notifications`)
|
||||||
- `GET /api/notifications` · `PATCH /api/notifications/:id/read` · `PATCH /api/notifications/read-all`
|
- `GET /api/notifications` · `PATCH /api/notifications/:id/read` · `PATCH /api/notifications/read-all`
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
| `notifications` | notificaciones in-app por usuario |
|
| `notifications` | notificaciones in-app por usuario |
|
||||||
| `audit_log` | auditoría central (quién, qué, sobre qué, cuándo) |
|
| `audit_log` | auditoría central (quién, qué, sobre qué, cuándo) |
|
||||||
| `error_logs` | errores de backend (excepciones 500) y frontend (JS/promesas sin catch), panel "Errores del sistema" en Configuración |
|
| `error_logs` | errores de backend (excepciones 500) y frontend (JS/promesas sin catch), panel "Errores del sistema" en Configuración |
|
||||||
|
| `notes` | apuntes rápidos personales, siempre privados de cada usuario, menú propio "Notas" |
|
||||||
|
|
||||||
## Relaciones clave
|
## Relaciones clave
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [1.23.0] - 2026-09-17
|
||||||
|
### Added
|
||||||
|
- Menú "Notas" nuevo: apuntes rápidos y simples, siempre privados de cada usuario (nadie más los
|
||||||
|
ve). Crear, editar y eliminar, con fecha y hora de creación (y de última edición, si aplica).
|
||||||
|
Sin permisos especiales — cualquier usuario autenticado tiene los suyos.
|
||||||
|
- Nuevo `notes` (tabla) + `GET/POST/PUT/DELETE /api/notes`.
|
||||||
|
|
||||||
## [1.22.0] - 2026-09-17
|
## [1.22.0] - 2026-09-17
|
||||||
### Added
|
### Added
|
||||||
- La app ahora es instalable desde el navegador (PWA): ícono propio, se abre en su propia ventana
|
- La app ahora es instalable desde el navegador (PWA): ícono propio, se abre en su propia ventana
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Apuntes rapidos personales (menu "Notas"): siempre privados de cada usuario, sin permisos
|
||||||
|
-- especiales de rol -- cualquiera autenticado ve/edita/borra solo los suyos.
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_notes_user (user_id),
|
||||||
|
CONSTRAINT fk_notes_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "1.22.0",
|
"version": "1.23.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import recurringTasksRoutes from './modules/recurring-tasks/routes.js';
|
|||||||
import searchRoutes from './modules/search/routes.js';
|
import searchRoutes from './modules/search/routes.js';
|
||||||
import updaterRoutes from './modules/updater/routes.js';
|
import updaterRoutes from './modules/updater/routes.js';
|
||||||
import errorLogsRoutes from './modules/errorLogs/routes.js';
|
import errorLogsRoutes from './modules/errorLogs/routes.js';
|
||||||
|
import notesRoutes from './modules/notes/routes.js';
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ app.use('/api/recurring-tasks', recurringTasksRoutes);
|
|||||||
app.use('/api/search', searchRoutes);
|
app.use('/api/search', searchRoutes);
|
||||||
app.use('/api/updater', updaterRoutes);
|
app.use('/api/updater', updaterRoutes);
|
||||||
app.use('/api/error-logs', errorLogsRoutes);
|
app.use('/api/error-logs', errorLogsRoutes);
|
||||||
|
app.use('/api/notes', notesRoutes);
|
||||||
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
|
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
|
||||||
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
|
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
|
||||||
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
|
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ok, asyncHandler } from '../../utils/http.js';
|
||||||
|
import * as service from './service.js';
|
||||||
|
|
||||||
|
export const getNotes = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.listNotes(req.currentUser));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const postNote = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.createNote(req.body.content, req.currentUser), 201);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const putNote = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.updateNote(Number(req.params.id), req.body.content, req.currentUser));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteNote = asyncHandler(async (req, res) => {
|
||||||
|
await service.removeNote(Number(req.params.id), req.currentUser);
|
||||||
|
ok(res, { deleted: true });
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { pool } from '../../config/db.js';
|
||||||
|
|
||||||
|
export async function findByUser(userId) {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT id, user_id, content, created_at, updated_at FROM notes WHERE user_id = ? ORDER BY updated_at DESC',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findById(id) {
|
||||||
|
const [rows] = await pool.query('SELECT id, user_id, content, created_at, updated_at FROM notes WHERE id = ?', [id]);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(userId, content) {
|
||||||
|
const [result] = await pool.query('INSERT INTO notes (user_id, content) VALUES (?, ?)', [userId, content]);
|
||||||
|
return findById(result.insertId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(id, content) {
|
||||||
|
await pool.query('UPDATE notes SET content = ? WHERE id = ?', [content, id]);
|
||||||
|
return findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(id) {
|
||||||
|
await pool.query('DELETE FROM notes WHERE id = ?', [id]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { requireAuth } from '../../middleware/auth.js';
|
||||||
|
import { getNotes, postNote, putNote, deleteNote } from './controller.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Sin restriccion de rol: cualquier usuario autenticado tiene sus propias notas.
|
||||||
|
router.use(requireAuth);
|
||||||
|
router.get('/', getNotes);
|
||||||
|
router.post('/', postNote);
|
||||||
|
router.put('/:id', putNote);
|
||||||
|
router.delete('/:id', deleteNote);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as repo from './repository.js';
|
||||||
|
import { HttpError } from '../../utils/http.js';
|
||||||
|
|
||||||
|
// Siempre privadas: cada quien ve y toca solo sus propias notas, sin importar el rol. Una nota
|
||||||
|
// de otro usuario responde 404 (no 403) para no revelar que existe.
|
||||||
|
async function getOwnNoteOr404(id, currentUser) {
|
||||||
|
const note = await repo.findById(id);
|
||||||
|
if (!note || note.user_id !== currentUser.id) throw new HttpError(404, 'Nota no encontrada');
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listNotes = (currentUser) => repo.findByUser(currentUser.id);
|
||||||
|
|
||||||
|
export async function createNote(content, currentUser) {
|
||||||
|
const trimmed = (content || '').trim();
|
||||||
|
if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
|
||||||
|
return repo.create(currentUser.id, trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateNote(id, content, currentUser) {
|
||||||
|
await getOwnNoteOr404(id, currentUser);
|
||||||
|
const trimmed = (content || '').trim();
|
||||||
|
if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
|
||||||
|
return repo.update(id, trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeNote(id, currentUser) {
|
||||||
|
await getOwnNoteOr404(id, currentUser);
|
||||||
|
await repo.remove(id);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.22.0",
|
"version": "1.23.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import ProyectosPage from '../pages/Proyectos/ProyectosPage.jsx';
|
|||||||
import ProyectoDetallePage from '../pages/Proyectos/ProyectoDetallePage.jsx';
|
import ProyectoDetallePage from '../pages/Proyectos/ProyectoDetallePage.jsx';
|
||||||
import ConfiguracionPage from '../pages/Configuracion/ConfiguracionPage.jsx';
|
import ConfiguracionPage from '../pages/Configuracion/ConfiguracionPage.jsx';
|
||||||
import RecurringTasksPage from '../pages/RecurringTasks/RecurringTasksPage.jsx';
|
import RecurringTasksPage from '../pages/RecurringTasks/RecurringTasksPage.jsx';
|
||||||
|
import NotasPage from '../pages/Notas/NotasPage.jsx';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -39,6 +40,7 @@ export default function App() {
|
|||||||
<Route path="proyectos" element={<ProyectosPage />} />
|
<Route path="proyectos" element={<ProyectosPage />} />
|
||||||
<Route path="proyectos/:id" element={<ProyectoDetallePage />} />
|
<Route path="proyectos/:id" element={<ProyectoDetallePage />} />
|
||||||
<Route path="tareas-recurrentes" element={<RecurringTasksPage />} />
|
<Route path="tareas-recurrentes" element={<RecurringTasksPage />} />
|
||||||
|
<Route path="notas" element={<NotasPage />} />
|
||||||
<Route path="organizacion" element={<OrganizacionPage />} />
|
<Route path="organizacion" element={<OrganizacionPage />} />
|
||||||
<Route path="configuracion" element={<ConfiguracionPage />} />
|
<Route path="configuracion" element={<ConfiguracionPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { LayoutDashboard, FolderKanban, Building2, Settings, Repeat } from 'lucide-react';
|
import { LayoutDashboard, FolderKanban, Building2, Settings, Repeat, StickyNote } from 'lucide-react';
|
||||||
import { useUi } from '../context/ui-context.jsx';
|
import { useUi } from '../context/ui-context.jsx';
|
||||||
import { useAuth } from '../context/auth-context.jsx';
|
import { useAuth } from '../context/auth-context.jsx';
|
||||||
import { useBranding } from '../context/branding-context.jsx';
|
import { useBranding } from '../context/branding-context.jsx';
|
||||||
@@ -12,6 +12,7 @@ const ITEMS = [
|
|||||||
{ to: '/', icon: LayoutDashboard, labelKey: 'nav_dashboard', end: true },
|
{ to: '/', icon: LayoutDashboard, labelKey: 'nav_dashboard', end: true },
|
||||||
{ to: '/proyectos', icon: FolderKanban, labelKey: 'nav_projects' },
|
{ to: '/proyectos', icon: FolderKanban, labelKey: 'nav_projects' },
|
||||||
{ to: '/tareas-recurrentes', icon: Repeat, labelKey: 'nav_recurring_tasks' },
|
{ to: '/tareas-recurrentes', icon: Repeat, labelKey: 'nav_recurring_tasks' },
|
||||||
|
{ to: '/notas', icon: StickyNote, labelKey: 'nav_notes' },
|
||||||
{ to: '/organizacion', icon: Building2, labelKey: 'nav_organization' },
|
{ to: '/organizacion', icon: Building2, labelKey: 'nav_organization' },
|
||||||
{ to: '/configuracion', icon: Settings, labelKey: 'nav_config', superadminOnly: true },
|
{ to: '/configuracion', icon: Settings, labelKey: 'nav_config', superadminOnly: true },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -172,6 +172,12 @@ export const messages = {
|
|||||||
project_filter_due_soon: 'Solo próximos a vencer (7 días)',
|
project_filter_due_soon: 'Solo próximos a vencer (7 días)',
|
||||||
|
|
||||||
nav_recurring_tasks: 'Tareas recurrentes',
|
nav_recurring_tasks: 'Tareas recurrentes',
|
||||||
|
nav_notes: 'Notas',
|
||||||
|
notes_title: 'Notas',
|
||||||
|
notes_placeholder: 'Escribe un apunte rápido...',
|
||||||
|
notes_add: 'Agregar',
|
||||||
|
notes_empty: 'No tienes notas todavía.',
|
||||||
|
notes_updated: 'Editado',
|
||||||
recurring_task_new: 'Nueva tarea recurrente',
|
recurring_task_new: 'Nueva tarea recurrente',
|
||||||
recurring_task_project: 'Proyecto de origen',
|
recurring_task_project: 'Proyecto de origen',
|
||||||
recurring_task_every: 'Cada',
|
recurring_task_every: 'Cada',
|
||||||
@@ -448,6 +454,12 @@ export const messages = {
|
|||||||
project_filter_due_soon: 'Due soon only (7 days)',
|
project_filter_due_soon: 'Due soon only (7 days)',
|
||||||
|
|
||||||
nav_recurring_tasks: 'Recurring tasks',
|
nav_recurring_tasks: 'Recurring tasks',
|
||||||
|
nav_notes: 'Notes',
|
||||||
|
notes_title: 'Notes',
|
||||||
|
notes_placeholder: 'Write a quick note...',
|
||||||
|
notes_add: 'Add',
|
||||||
|
notes_empty: 'You don\'t have any notes yet.',
|
||||||
|
notes_updated: 'Edited',
|
||||||
recurring_task_new: 'New recurring task',
|
recurring_task_new: 'New recurring task',
|
||||||
recurring_task_project: 'Source project',
|
recurring_task_project: 'Source project',
|
||||||
recurring_task_every: 'Every',
|
recurring_task_every: 'Every',
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { api } from './api.js';
|
||||||
|
|
||||||
|
export const listNotes = () => api.get('/notes');
|
||||||
|
export const createNote = (content) => api.post('/notes', { content });
|
||||||
|
export const updateNote = (id, content) => api.put(`/notes/${id}`, { content });
|
||||||
|
export const deleteNote = (id) => api.delete(`/notes/${id}`);
|
||||||
Reference in New Issue
Block a user