From 5a81c1e9f01fa9b64217a5c57eda04ed7a174d0b Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 12 May 2026 13:21:57 +0200 Subject: [PATCH] =?UTF-8?q?Module=20Memo=20:=20int=C3=A9gration=20PercoMem?= =?UTF-8?q?o=20dans=20HouseHub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SPA PHP/MySQL avec rendu Markdown client-side (marked.js) - Notes par famille : titre, contenu Markdown, tags, pièces jointes - Attachements : images, fichiers, URLs avec galerie lightbox - Recherche full-text (LIKE) + filtrage par tag dans la sidebar - Preview live Markdown en split-view lors de l'édition - Drag & drop images + paste depuis presse-papier - Tables pf_memo_notes + pf_memo_attachments dans schema_family.sql - Migration des 2 notes existantes depuis PercoMemo Co-Authored-By: Claude Sonnet 4.6 --- docker/schema_family.sql | 24 ++ header.php | 2 + includes/lang/ca.php | 3 + includes/lang/en.php | 3 + includes/lang/fr.php | 3 + index.php | 9 + memo.php | 139 ++++++++++++ modules/memo/api.php | 209 ++++++++++++++++++ modules/memo/assets/memo.css | 279 ++++++++++++++++++++++++ modules/memo/assets/memo.js | 410 +++++++++++++++++++++++++++++++++++ settings.php | 3 +- 11 files changed, 1083 insertions(+), 1 deletion(-) create mode 100644 memo.php create mode 100644 modules/memo/api.php create mode 100644 modules/memo/assets/memo.css create mode 100644 modules/memo/assets/memo.js diff --git a/docker/schema_family.sql b/docker/schema_family.sql index 0a7ce87..7816274 100644 --- a/docker/schema_family.sql +++ b/docker/schema_family.sql @@ -293,3 +293,27 @@ CREATE TABLE IF NOT EXISTS pf_parts ( FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL, FOREIGN KEY (maintenance_id) REFERENCES pf_maintenances(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Notes / Memo ───────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_memo_notes ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content LONGTEXT DEFAULT '', + tags VARCHAR(1000) DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FULLTEXT KEY ft_notes (title, content, tags) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_memo_attachments ( + id INT AUTO_INCREMENT PRIMARY KEY, + note_id INT NOT NULL, + type ENUM('image','file','url') NOT NULL DEFAULT 'file', + filename VARCHAR(255) DEFAULT NULL, + original_name VARCHAR(255) DEFAULT NULL, + url TEXT DEFAULT NULL, + label VARCHAR(255) DEFAULT NULL, + size INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (note_id) REFERENCES pf_memo_notes(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/header.php b/header.php index 74c84e8..ed8f0ca 100644 --- a/header.php +++ b/header.php @@ -53,6 +53,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; + @@ -98,6 +99,7 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; 🏖️ 🎁 🚗 + 📝 ⚙️ Paramètres 🛡️ Admin diff --git a/includes/lang/ca.php b/includes/lang/ca.php index 5a563ca..04e195a 100644 --- a/includes/lang/ca.php +++ b/includes/lang/ca.php @@ -90,6 +90,9 @@ return [ 'mod_budget_desc' => 'Seguiment de despeses fixes, ingressos i balanç del compte familiar.', 'mod_garage_name' => 'Garatge', 'mod_garage_desc' => 'Seguiu el manteniment dels vostres vehicles, les peces i els costos.', + 'mod_memo_name' => 'Notes', + 'mod_memo_desc' => 'Els vostres memos, receptes, instruccions i informació diversa amb Markdown.', + 'menu_memo' => 'Notes', 'cta_open' => 'Obrir', 'cta_explore' => 'Explorar', 'cta_view_lists' => 'Veure les llistes', diff --git a/includes/lang/en.php b/includes/lang/en.php index 08190dd..ebee33f 100644 --- a/includes/lang/en.php +++ b/includes/lang/en.php @@ -91,6 +91,9 @@ return [ 'mod_budget_desc' => 'Track fixed expenses, income and family account balance.', 'mod_garage_name' => 'Garage', 'mod_garage_desc' => 'Track vehicle maintenance, parts used and costs.', + 'mod_memo_name' => 'Notes', + 'mod_memo_desc' => 'Your memos, recipes, instructions and miscellaneous info with Markdown and attachments.', + 'menu_memo' => 'Notes', 'cta_open' => 'Open', 'cta_explore' => 'Explore', 'cta_view_lists' => 'View lists', diff --git a/includes/lang/fr.php b/includes/lang/fr.php index 59b9c8e..a0dcb76 100644 --- a/includes/lang/fr.php +++ b/includes/lang/fr.php @@ -93,6 +93,9 @@ return [ 'mod_budget_desc' => 'Suivi des dépenses fixes, revenus et équilibre du compte familial.', 'mod_garage_name' => 'Garage', 'mod_garage_desc' => 'Suivez l\'entretien de vos véhicules, les pièces utilisées et les coûts.', + 'mod_memo_name' => 'Notes', + 'mod_memo_desc' => 'Vos mémos, recettes, instructions et infos diverses avec Markdown et pièces jointes.', + 'menu_memo' => 'Notes', 'cta_open' => 'Ouvrir', 'cta_explore' => 'Explorer', 'cta_view_lists' => 'Voir les listes', diff --git a/index.php b/index.php index f817291..72c7acb 100644 --- a/index.php +++ b/index.php @@ -91,6 +91,15 @@ if ($_has_custom_bg): ?> + + +
📝
+

+
+ +
+ + diff --git a/memo.php b/memo.php new file mode 100644 index 0000000..2b8392c --- /dev/null +++ b/memo.php @@ -0,0 +1,139 @@ + + + +
+ +
+ + + + + +
+ + +
+
+
+
📝 Notes
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ + +
+
+
+

Nouvelle note

+
+ + +
+
+
+ + +
+ + +
+ + +
+ +
+ +
+
+
+ + +
+ +
+
+ +
+
Appuyez sur Entrée ou virgule pour valider
+
+ + +
+ +
+ 📎 Glissez-déposez des fichiers ou cliquez pour choisir (images, PDF…) + +
+
+
+
+ + +
+ +
+
+ +
+
+
+ +
+
+ + + + + + + + + + diff --git a/modules/memo/api.php b/modules/memo/api.php new file mode 100644 index 0000000..f6e8d22 --- /dev/null +++ b/modules/memo/api.php @@ -0,0 +1,209 @@ + false, 'error' => $e->getMessage()]); exit; +}); + +require_once dirname(__DIR__, 2) . '/includes/db.php'; + +$UPLOAD_DIR = '/uploads/memo/'; +if (!is_dir($UPLOAD_DIR)) @mkdir($UPLOAD_DIR, 0755, true); + +$method = $_SERVER['REQUEST_METHOD']; +$action = $_GET['action'] ?? ''; + +function mOk($d) { echo json_encode(['ok' => true, 'data' => $d], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); exit; } +function mErr($m, $c = 400) { http_response_code($c); echo json_encode(['ok' => false, 'error' => $m]); exit; } +function mBody() { return json_decode(file_get_contents('php://input'), true) ?? []; } + +function parseTags(string $raw): string { + $tags = array_unique(array_filter(array_map(function($t) { + return strtolower(trim(ltrim($t, '#'), " \t\n\r,")); + }, preg_split('/[\s,]+/', $raw)))); + return implode(',', $tags); +} + +function imgExts(): array { return ['jpg','jpeg','png','gif','webp','svg','bmp']; } +function isImage(string $fname): bool { return in_array(strtolower(pathinfo($fname, PATHINFO_EXTENSION)), imgExts()); } + +function handleUpload(string $field, string $dir): ?array { + if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) return null; + $orig = $_FILES[$field]['name']; + $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION)); + $allowed = array_merge(imgExts(), ['pdf','doc','docx','txt','csv','zip','mp3','mp4']); + if (!in_array($ext, $allowed)) return null; + $fname = uniqid('m', true) . '.' . $ext; + if (!move_uploaded_file($_FILES[$field]['tmp_name'], $dir . $fname)) return null; + return ['filename' => $fname, 'original_name' => $orig, 'size' => filesize($dir . $fname)]; +} + +// ── NOTES ───────────────────────────────────────────────────────────────────── +if ($action === 'notes') { + if ($method === 'GET') { + $id = $_GET['id'] ?? null; + $q = trim($_GET['q'] ?? ''); + $tag = trim($_GET['tag'] ?? ''); + $page = max(1, (int)($_GET['page'] ?? 1)); + $perPage = 24; + $offset = ($page - 1) * $perPage; + + if ($id) { + $n = $pdo->prepare("SELECT * FROM pf_memo_notes WHERE id = ?"); $n->execute([$id]); + $note = $n->fetch(); if (!$note) mErr('Note introuvable', 404); + $a = $pdo->prepare("SELECT * FROM pf_memo_attachments WHERE note_id = ? ORDER BY created_at"); + $a->execute([$id]); $note['attachments'] = $a->fetchAll(); + mOk($note); + } + + if ($q) { + $like = '%' . $q . '%'; + $stmt = $pdo->prepare("SELECT id, title, tags, updated_at, LEFT(content,200) as snippet + FROM pf_memo_notes WHERE title LIKE ? OR content LIKE ? OR tags LIKE ? + ORDER BY updated_at DESC LIMIT ? OFFSET ?"); + $stmt->execute([$like, $like, $like, $perPage, $offset]); + $total = $pdo->prepare("SELECT COUNT(*) FROM pf_memo_notes WHERE title LIKE ? OR content LIKE ? OR tags LIKE ?"); + $total->execute([$like, $like, $like]); + } elseif ($tag) { + $stmt = $pdo->prepare("SELECT id, title, tags, updated_at, LEFT(content,200) as snippet + FROM pf_memo_notes WHERE FIND_IN_SET(?, tags) > 0 + ORDER BY updated_at DESC LIMIT ? OFFSET ?"); + $stmt->execute([$tag, $perPage, $offset]); + $total = $pdo->prepare("SELECT COUNT(*) FROM pf_memo_notes WHERE FIND_IN_SET(?, tags) > 0"); + $total->execute([$tag]); + } else { + $stmt = $pdo->prepare("SELECT id, title, tags, updated_at, LEFT(content,200) as snippet + FROM pf_memo_notes ORDER BY updated_at DESC LIMIT ? OFFSET ?"); + $stmt->execute([$perPage, $offset]); + $total = $pdo->query("SELECT COUNT(*) FROM pf_memo_notes"); + } + + mOk(['notes' => $stmt->fetchAll(), 'total' => (int)$total->fetchColumn(), + 'page' => $page, 'per_page' => $perPage]); + } + + if ($method === 'POST') { + $d = $_POST ?: mBody(); + $title = trim($d['title'] ?? ''); + if (!$title) mErr('Titre requis'); + $tags = parseTags($d['tags'] ?? ''); + $pdo->prepare("INSERT INTO pf_memo_notes (title, content, tags) VALUES (?,?,?)") + ->execute([$title, $d['content'] ?? '', $tags]); + $id = $pdo->lastInsertId(); + + // Files + if (!empty($_FILES)) { + foreach ($_FILES as $key => $f) { + if ($f['error'] === UPLOAD_ERR_OK) { + $up = handleUpload($key, $UPLOAD_DIR); + if ($up) { + $type = isImage($up['filename']) ? 'image' : 'file'; + $pdo->prepare("INSERT INTO pf_memo_attachments (note_id,type,filename,original_name,size) VALUES (?,?,?,?,?)") + ->execute([$id, $type, $up['filename'], $up['original_name'], $up['size']]); + } + } + } + } + // URLs from JSON body + foreach (($d['urls'] ?? []) as $urlItem) { + if (!empty($urlItem['url'])) { + $pdo->prepare("INSERT INTO pf_memo_attachments (note_id,type,url,label) VALUES (?,?,?,?)") + ->execute([$id, 'url', $urlItem['url'], $urlItem['label'] ?? '']); + } + } + mOk(['id' => (int)$id]); + } + + if ($method === 'PUT') { + $id = $_GET['id'] ?? null; if (!$id) mErr('ID manquant'); + $d = mBody(); + $title = trim($d['title'] ?? ''); + if (!$title) mErr('Titre requis'); + $tags = parseTags($d['tags'] ?? ''); + $pdo->prepare("UPDATE pf_memo_notes SET title=?, content=?, tags=?, updated_at=NOW() WHERE id=?") + ->execute([$title, $d['content'] ?? '', $tags, $id]); + mOk(['updated' => true]); + } + + if ($method === 'DELETE') { + $id = $_GET['id'] ?? null; if (!$id) mErr('ID manquant'); + $rows = $pdo->prepare("SELECT filename FROM pf_memo_attachments WHERE note_id=? AND filename IS NOT NULL"); + $rows->execute([$id]); + foreach ($rows->fetchAll() as $r) @unlink($UPLOAD_DIR . $r['filename']); + $pdo->prepare("DELETE FROM pf_memo_notes WHERE id=?")->execute([$id]); + mOk(['deleted' => true]); + } +} + +// ── TAGS ────────────────────────────────────────────────────────────────────── +if ($action === 'tags') { + $rows = $pdo->query("SELECT tags FROM pf_memo_notes WHERE tags != ''")->fetchAll(); + $counts = []; + foreach ($rows as $r) { + foreach (explode(',', $r['tags']) as $t) { + $t = trim($t); + if ($t) $counts[$t] = ($counts[$t] ?? 0) + 1; + } + } + arsort($counts); + mOk(array_map(fn($t, $c) => ['tag' => $t, 'count' => $c], array_keys($counts), $counts)); +} + +// ── ATTACHMENTS ─────────────────────────────────────────────────────────────── +if ($action === 'attachments') { + if ($method === 'POST') { + $id = $_POST['note_id'] ?? null; if (!$id) mErr('note_id manquant'); + // File upload + if (!empty($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) { + $up = handleUpload('file', $UPLOAD_DIR); + if (!$up) mErr('Upload échoué ou format non autorisé'); + $type = isImage($up['filename']) ? 'image' : 'file'; + $pdo->prepare("INSERT INTO pf_memo_attachments (note_id,type,filename,original_name,size) VALUES (?,?,?,?,?)") + ->execute([$id, $type, $up['filename'], $up['original_name'], $up['size']]); + mOk(['id' => (int)$pdo->lastInsertId(), 'type' => $type, + 'filename' => $up['filename'], 'original_name' => $up['original_name']]); + } + // URL + $url = trim($_POST['url'] ?? ''); + if ($url) { + $label = trim($_POST['label'] ?? ''); + $pdo->prepare("INSERT INTO pf_memo_attachments (note_id,type,url,label) VALUES (?,?,?,?)") + ->execute([$id, 'url', $url, $label]); + mOk(['id' => (int)$pdo->lastInsertId(), 'type' => 'url', 'url' => $url, 'label' => $label]); + } + mErr('Aucun fichier ou URL fourni'); + } + if ($method === 'DELETE') { + $id = $_GET['id'] ?? null; if (!$id) mErr('ID manquant'); + $r = $pdo->prepare("SELECT filename FROM pf_memo_attachments WHERE id=?"); $r->execute([$id]); + $row = $r->fetch(); + if ($row && $row['filename']) @unlink($UPLOAD_DIR . $row['filename']); + $pdo->prepare("DELETE FROM pf_memo_attachments WHERE id=?")->execute([$id]); + mOk(['deleted' => true]); + } +} + +// ── FILE SERVE ──────────────────────────────────────────────────────────────── +if ($action === 'file') { + $id = $_GET['id'] ?? null; + $row = null; + if ($id) { $s = $pdo->prepare("SELECT * FROM pf_memo_attachments WHERE id=?"); $s->execute([$id]); $row = $s->fetch(); } + if (!$row || !$row['filename'] || !file_exists($UPLOAD_DIR . $row['filename'])) { http_response_code(404); exit; } + $ext = strtolower(pathinfo($row['filename'], PATHINFO_EXTENSION)); + $mime = [ + 'jpg'=>'image/jpeg','jpeg'=>'image/jpeg','png'=>'image/png','gif'=>'image/gif', + 'webp'=>'image/webp','svg'=>'image/svg+xml','pdf'=>'application/pdf', + 'txt'=>'text/plain','mp4'=>'video/mp4','mp3'=>'audio/mpeg', + ][$ext] ?? 'application/octet-stream'; + header('Content-Type: ' . $mime); + header('Content-Disposition: inline; filename="' . rawurlencode($row['original_name'] ?? $row['filename']) . '"'); + header('Cache-Control: private, max-age=3600'); + readfile($UPLOAD_DIR . $row['filename']); exit; +} + +mErr('Action inconnue', 404); diff --git a/modules/memo/assets/memo.css b/modules/memo/assets/memo.css new file mode 100644 index 0000000..9e87467 --- /dev/null +++ b/modules/memo/assets/memo.css @@ -0,0 +1,279 @@ +/* Memo module */ + +.memo-layout { + display: grid; + grid-template-columns: 220px 1fr; + height: calc(100vh - 64px); + overflow: hidden; +} +.memo-sidebar { + border-right: 1px solid var(--border-light); + background: var(--bg-panel); + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0; +} +.memo-sidebar-top { + padding: 1rem; + border-bottom: 1px solid var(--border-light); + display: flex; + flex-direction: column; + gap: .6rem; + position: sticky; + top: 0; + background: var(--bg-panel); + z-index: 1; +} +.memo-search { + display: flex; + align-items: center; + gap: .4rem; + background: var(--bg-page); + border: 1px solid var(--border-light); + border-radius: 8px; + padding: .4rem .6rem; +} +.memo-search input { + border: none; + background: transparent; + color: var(--text-main); + font-size: .875rem; + outline: none; + flex: 1; + min-width: 0; +} +.memo-search input::placeholder { color: var(--text-muted); } +.memo-tags-list { padding: .5rem 0; } +.memo-tag-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: .4rem 1rem; + cursor: pointer; + font-size: .82rem; + color: var(--text-muted); + border-radius: 0; + transition: background .12s; + text-decoration: none; +} +.memo-tag-item:hover { background: var(--bg-page); color: var(--text-main); } +.memo-tag-item.active { background: #eff6ff; color: var(--primary); font-weight: 600; } +.memo-tag-count { + font-size: .72rem; + background: var(--bg-page); + color: var(--text-muted); + padding: 1px 6px; + border-radius: 999px; + min-width: 20px; + text-align: center; +} +.memo-tag-item.active .memo-tag-count { background: #dbeafe; color: var(--primary); } + +/* Main area */ +.memo-main { overflow-y: auto; background: var(--bg-page); } + +/* Pages */ +.memo-page { display: none; } +.memo-page.active { display: block; } + +/* Note list */ +.memo-list-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + background: var(--bg-panel); + border-bottom: 1px solid var(--border-light); + gap: .75rem; +} +.memo-list-header h2 { font-size: 1rem; font-weight: 700; margin: 0; } +.memo-list-subtitle { font-size: .82rem; color: var(--text-muted); } +.notes-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + padding: 1.25rem 1.5rem; +} +.note-card { + background: var(--bg-panel); + border: 1px solid var(--border-light); + border-radius: 10px; + padding: 1rem 1.1rem; + cursor: pointer; + transition: border-color .15s, box-shadow .15s, transform .1s; + display: flex; + flex-direction: column; + gap: .4rem; + text-decoration: none; +} +.note-card:hover { border-color: var(--primary); box-shadow: 0 4px 12px rgba(59,130,246,.1); transform: translateY(-1px); } +.note-card-title { font-size: .95rem; font-weight: 700; color: var(--text-main); } +.note-card-snippet { font-size: .8rem; color: var(--text-muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.note-card-meta { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-top: .25rem; } +.note-card-date { font-size: .72rem; color: var(--text-muted); margin-left: auto; } +.note-tag-chip { + font-size: .7rem; font-weight: 600; + background: #eff6ff; color: var(--primary); + padding: .15rem .45rem; border-radius: 999px; + cursor: pointer; +} +.note-tag-chip:hover { background: #dbeafe; } + +/* Note view */ +.memo-view-header { + padding: 1.5rem 1.75rem 1rem; + border-bottom: 1px solid var(--border-light); + background: var(--bg-panel); +} +.memo-view-title { font-size: 1.6rem; font-weight: 800; color: var(--text-main); margin-bottom: .5rem; line-height: 1.2; } +.memo-view-meta { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; font-size: .8rem; color: var(--text-muted); } +.memo-view-actions { display: flex; gap: .5rem; margin-left: auto; } +.memo-content-area { + padding: 1.5rem 1.75rem; + background: var(--bg-panel); + margin: 1rem 1.5rem; + border: 1px solid var(--border-light); + border-radius: 12px; +} +.memo-content-area.empty { color: var(--text-muted); font-style: italic; } + +/* Markdown rendered content */ +.md-body { color: var(--text-main); line-height: 1.7; font-size: .95rem; } +.md-body h1,.md-body h2,.md-body h3,.md-body h4 { font-weight: 700; margin: 1.25em 0 .5em; line-height: 1.3; color: var(--text-main); } +.md-body h1 { font-size: 1.5rem; } +.md-body h2 { font-size: 1.25rem; } +.md-body h3 { font-size: 1.05rem; } +.md-body p { margin: .75em 0; } +.md-body ul,.md-body ol { padding-left: 1.5rem; margin: .75em 0; } +.md-body li { margin: .25em 0; } +.md-body code { font-family: monospace; background: #f1f5f9; color: #c7254e; padding: .1em .35em; border-radius: 4px; font-size: .88em; } +.md-body pre { background: #1e293b; color: #e2e8f0; padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 1em 0; } +.md-body pre code { background: none; color: inherit; padding: 0; } +.md-body blockquote { border-left: 3px solid var(--primary); margin: 1em 0; padding: .5em 1em; background: #eff6ff; color: var(--text-muted); border-radius: 0 6px 6px 0; } +.md-body a { color: var(--primary); } +.md-body hr { border: none; border-top: 1px solid var(--border-light); margin: 1.5em 0; } +.md-body table { width: 100%; border-collapse: collapse; margin: 1em 0; } +.md-body table th { background: #f8fafc; text-align: left; padding: .5rem .75rem; border: 1px solid var(--border-light); font-weight: 600; } +.md-body table td { padding: .5rem .75rem; border: 1px solid var(--border-light); } +.md-body mark { background: #fef3c7; color: #92400e; padding: .1em .2em; border-radius: 3px; } +.md-body img { max-width: 100%; border-radius: 8px; margin: .5em 0; } + +/* Attachments */ +.memo-attachments { padding: 0 1.5rem 1.5rem; } +.memo-att-section { margin-bottom: 1.25rem; } +.memo-att-title { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: .6rem; } +.att-img-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: .6rem; } +.att-img-item { position: relative; border-radius: 8px; overflow: hidden; aspect-ratio: 1; background: #f1f5f9; cursor: pointer; } +.att-img-item img { width: 100%; height: 100%; object-fit: cover; } +.att-img-item .att-del { position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,.5); color: #fff; border: none; border-radius: 50%; width: 22px; height: 22px; font-size: .75rem; cursor: pointer; display: none; line-height: 1; } +.att-img-item:hover .att-del { display: flex; align-items: center; justify-content: center; } +.att-file-list { display: flex; flex-direction: column; gap: .4rem; } +.att-file-item { display: flex; align-items: center; gap: .6rem; padding: .4rem .7rem; background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px; font-size: .82rem; } +.att-file-item a { color: var(--primary); flex: 1; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.att-file-item a:hover { text-decoration: underline; } +.att-file-size { color: var(--text-muted); font-size: .72rem; white-space: nowrap; } +.att-url-list { display: flex; flex-direction: column; gap: .4rem; } +.att-url-item { display: flex; align-items: center; gap: .6rem; padding: .4rem .7rem; background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px; font-size: .82rem; } +.att-url-item a { color: var(--primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* Edit form */ +.memo-edit-area { padding: 1.25rem 1.5rem; } +.memo-edit-form { background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 12px; padding: 1.5rem; } +.form-group { display: flex; flex-direction: column; gap: .35rem; margin-bottom: 1.1rem; } +.form-label { font-size: .82rem; font-weight: 600; color: var(--text-muted); } +.form-control { background: #fff; border: 1px solid var(--border-light); color: var(--text-main); border-radius: 8px; padding: .5rem .75rem; font-size: .875rem; width: 100%; outline: none; transition: border-color .15s, box-shadow .15s; font-family: inherit; } +.form-control:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,.1); } +.form-control::placeholder { color: #cbd5e1; } +.edit-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } +.edit-textarea { font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: .82rem; min-height: 320px; resize: vertical; line-height: 1.6; } +.md-preview-box { min-height: 320px; padding: .75rem 1rem; background: #fafafa; border: 1px solid var(--border-light); border-radius: 8px; overflow-y: auto; } +.tags-input-wrap { display: flex; flex-wrap: wrap; gap: .35rem; padding: .35rem .6rem; border: 1px solid var(--border-light); border-radius: 8px; background: #fff; min-height: 42px; align-items: center; cursor: text; transition: border-color .15s; } +.tags-input-wrap:focus-within { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,.1); } +.tags-input-wrap input { border: none; outline: none; background: transparent; font-size: .875rem; color: var(--text-main); min-width: 80px; flex: 1; } +.tag-chip { display: inline-flex; align-items: center; gap: .25rem; background: #eff6ff; color: var(--primary); font-size: .78rem; font-weight: 600; padding: .2rem .5rem; border-radius: 999px; } +.tag-chip button { background: none; border: none; cursor: pointer; font-size: .8rem; color: var(--primary); line-height: 1; padding: 0; } +.attach-drop { border: 2px dashed var(--border-light); border-radius: 10px; padding: 1.25rem; text-align: center; cursor: pointer; transition: border-color .15s, background .15s; color: var(--text-muted); font-size: .875rem; } +.attach-drop:hover,.attach-drop.drag-over { border-color: var(--primary); background: #eff6ff; color: var(--primary); } +.attach-preview-list { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; } +.attach-preview-item { position: relative; } +.attach-preview-item img { width: 64px; height: 64px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border-light); } +.attach-preview-item .rm { position: absolute; top: -6px; right: -6px; background: var(--danger); color: #fff; border: none; border-radius: 50%; width: 18px; height: 18px; font-size: .65rem; cursor: pointer; display: flex; align-items: center; justify-content: center; } +.url-row { display: flex; gap: .5rem; } +.url-row input:first-child { flex: 1.5; } +.url-row input:last-child { flex: 1; } + +/* Tags autocomplete */ +.tags-suggestions { position: absolute; z-index: 100; background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.1); max-height: 160px; overflow-y: auto; min-width: 160px; } +.tags-suggestions div { padding: .4rem .75rem; cursor: pointer; font-size: .82rem; color: var(--text-main); } +.tags-suggestions div:hover { background: var(--bg-page); } + +/* Lightbox */ +.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.88); z-index: 10000; display: none; align-items: center; justify-content: center; cursor: zoom-out; } +.lightbox.show { display: flex; } +.lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 8px; object-fit: contain; } + +/* Empty state */ +.memo-empty { text-align: center; padding: 4rem 2rem; color: var(--text-muted); } +.memo-empty .icon { font-size: 3rem; opacity: .3; margin-bottom: 1rem; } + +/* Btns */ +.btn { display: inline-flex; align-items: center; gap: .4rem; padding: .45rem .9rem; border-radius: 8px; font-size: .875rem; font-weight: 500; cursor: pointer; border: 1px solid transparent; transition: all .15s; white-space: nowrap; } +.btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); } +.btn-primary:hover { background: var(--primary-dark); } +.btn-secondary { background: var(--bg-panel); color: var(--text-main); border-color: var(--border-light); } +.btn-secondary:hover { background: var(--bg-page); } +.btn-danger { background: #fff; color: var(--danger); border-color: #fecaca; } +.btn-danger:hover { background: #fef2f2; } +.btn-sm { padding: .28rem .65rem; font-size: .8rem; } + +/* Toast */ +.memo-toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999; display: flex; flex-direction: column; gap: .5rem; } +.memo-toast { background: var(--bg-panel); border: 1px solid var(--border-light); border-radius: 10px; padding: .7rem 1rem; font-size: .875rem; min-width: 200px; color: var(--text-main); box-shadow: 0 4px 16px rgba(0,0,0,.12); border-left: 4px solid var(--primary); animation: toastIn .2s ease; } +.memo-toast.error { border-left-color: var(--danger); } +@keyframes toastIn { from { opacity:0;transform:translateX(16px); } to { opacity:1;transform:none; } } + +/* Responsive */ +@media (max-width: 768px) { + .memo-layout { grid-template-columns: 1fr; height: auto; } + .memo-sidebar { display: none; } + .memo-sidebar.open { display: flex; position: fixed; inset: 0; z-index: 200; width: 260px; height: 100vh; box-shadow: 4px 0 20px rgba(0,0,0,.2); } + .edit-split { grid-template-columns: 1fr; } + .notes-grid { grid-template-columns: 1fr; padding: 1rem; } + .memo-view-header { padding: 1rem; } + .memo-content-area { margin: .75rem; } + .memo-attachments { padding: 0 .75rem .75rem; } + .memo-edit-area { padding: .75rem; } +} + +/* Dark mode */ +[data-theme="dark"] .memo-sidebar { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .memo-sidebar-top { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .memo-search { background: var(--bg-page); border-color: var(--border-light); } +[data-theme="dark"] .memo-search input { color: var(--text-main); } +[data-theme="dark"] .memo-tag-item { color: var(--text-muted); } +[data-theme="dark"] .memo-tag-item:hover { background: var(--bg-page); } +[data-theme="dark"] .memo-tag-item.active { background: rgba(59,130,246,.15); color: var(--primary); } +[data-theme="dark"] .memo-main { background: var(--bg-page); } +[data-theme="dark"] .memo-list-header { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .note-card { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .note-card:hover { border-color: var(--primary); } +[data-theme="dark"] .note-card-title { color: var(--text-main); } +[data-theme="dark"] .memo-view-header { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .memo-content-area { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .md-body code { background: #1c2128; color: #f87171; } +[data-theme="dark"] .md-body blockquote { background: rgba(59,130,246,.08); } +[data-theme="dark"] .md-body table th { background: #1c2128; border-color: var(--border-light); } +[data-theme="dark"] .md-body table td { border-color: var(--border-light); } +[data-theme="dark"] .att-file-item,.att-url-item { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .memo-edit-form { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .form-control { background: #1c2128; border-color: var(--border-light); color: var(--text-main); } +[data-theme="dark"] .tags-input-wrap { background: #1c2128; border-color: var(--border-light); } +[data-theme="dark"] .md-preview-box { background: #1c2128; border-color: var(--border-light); } +[data-theme="dark"] .attach-drop { border-color: var(--border-light); } +[data-theme="dark"] .attach-drop:hover,.attach-drop.drag-over { background: rgba(59,130,246,.1); } +[data-theme="dark"] .btn-secondary { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .btn-secondary:hover { background: rgba(255,255,255,.06); } +[data-theme="dark"] .tags-suggestions { background: var(--bg-panel); border-color: var(--border-light); } +[data-theme="dark"] .tags-suggestions div:hover { background: var(--bg-page); } +[data-theme="dark"] .memo-toast { background: var(--bg-panel); border-color: var(--border-light); color: var(--text-main); } diff --git a/modules/memo/assets/memo.js b/modules/memo/assets/memo.js new file mode 100644 index 0000000..3bdf097 --- /dev/null +++ b/modules/memo/assets/memo.js @@ -0,0 +1,410 @@ +// HouseHub — Memo module +const API = '/modules/memo/api.php'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── +function escHtml(s){ const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML; } +function fmtDate(d){ if(!d)return'--'; try{return new Date(d).toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});}catch{return d;} } +function fmtSize(b){ if(!b)return''; if(b<1024)return b+'o'; if(b<1048576)return Math.round(b/1024)+'ko'; return (b/1048576).toFixed(1)+'Mo'; } +function toast(msg,type='success'){ + const c=document.getElementById('memo-toasts')||document.body; + const t=document.createElement('div');t.className='memo-toast'+(type==='error'?' error':'');t.textContent=msg; + c.appendChild(t);setTimeout(()=>t.remove(),3000); +} +async function api(action,method='GET',data=null,extra=''){ + const opts={method,headers:{}}; + if(data&&!(data instanceof FormData)){opts.headers['Content-Type']='application/json';opts.body=JSON.stringify(data);} + else if(data instanceof FormData){opts.body=data;} + const r=await fetch(API+'?action='+action+extra,opts); + const j=await r.json(); + if(!j.ok)throw new Error(j.error||'Erreur API'); + return j.data; +} + +// ─── State ─────────────────────────────────────────────────────────────────── +let currentNoteId = null; +let currentPage = 'list'; +let currentQ = ''; +let currentTag = ''; +let allTagsList = []; +let pendingFiles = []; +let pendingUrls = []; + +// ─── Navigation ────────────────────────────────────────────────────────────── +function showPage(name){ + document.querySelectorAll('.memo-page').forEach(p=>p.classList.remove('active')); + const el=document.getElementById('memo-page-'+name); + if(el)el.classList.add('active'); + currentPage=name; +} + +// ─── Sidebar ───────────────────────────────────────────────────────────────── +async function loadSidebar(){ + try{ + allTagsList=await api('tags'); + const el=document.getElementById('memo-tags-list'); + if(!el)return; + const allActive=currentTag===''&¤tQ===''; + let html=`
+ 📝 Toutes les notes
`; + allTagsList.forEach(t=>{ + const act=currentTag===t.tag?' active':''; + html+=`
+ # ${escHtml(t.tag)}${t.count}
`; + }); + el.innerHTML=html; + }catch(e){} +} + +// ─── Note list ──────────────────────────────────────────────────────────────── +async function loadList(q='',tag='',page=1){ + currentQ=q;currentTag=tag;showPage('list'); + loadSidebar(); + try{ + let extra='&page='+page; + if(q)extra+='&q='+encodeURIComponent(q); + if(tag)extra+='&tag='+encodeURIComponent(tag); + const data=await api('notes','GET',null,extra); + const el=document.getElementById('memo-notes-grid'); + const count=document.getElementById('memo-all-count'); + const title=document.getElementById('memo-list-title'); + const subtitle=document.getElementById('memo-list-subtitle'); + if(count)count.textContent=data.total; + if(title){ + title.textContent = q?`🔍 "${q}"` : tag?`# ${tag}` : '📝 Notes'; + } + if(subtitle)subtitle.textContent=data.total+' note'+(data.total!==1?'s':''); + if(!data.notes.length){ + el.innerHTML='
📝

'+(q||tag?'Aucune note trouvée.':'Aucune note. Créez-en une !')+'

'; + return; + } + el.innerHTML=data.notes.map(n=>noteCardHtml(n)).join(''); + // Pagination + const total_pages=Math.ceil(data.total/data.per_page); + const pag=document.getElementById('memo-pagination'); + if(pag){ + pag.innerHTML=total_pages>1? + (page>1?``:'')+ + `Page ${page} / ${total_pages}`+ + (pageSuivant →`:'') + : ''; + } + }catch(e){toast(e.message,'error');} +} + +function noteCardHtml(n){ + const tags=(n.tags||'').split(',').filter(t=>t.trim()).map(t=> + `#${escHtml(t.trim())}`).join(''); + const snippet=n.snippet?(escHtml(n.snippet.replace(/\n/g,' '))):''; + return `
+
${escHtml(n.title)}
+ ${snippet?`
${snippet}
`:''} +
${tags}${fmtDate(n.updated_at)}
+
`; +} + +function filterTag(tag){ + currentTag=tag;currentQ=''; + document.getElementById('memo-search-input')?.value!=null&&(document.getElementById('memo-search-input').value=''); + loadList('',tag); +} + +function doSearch(){ + const q=document.getElementById('memo-search-input')?.value.trim()||''; + loadList(q,''); +} + +// ─── Note view ──────────────────────────────────────────────────────────────── +async function viewNote(id){ + currentNoteId=id;showPage('view'); + try{ + const note=await api('notes','GET',null,'&id='+id); + document.getElementById('view-title').textContent=note.title; + document.getElementById('view-date').textContent='Modifié '+fmtDate(note.updated_at); + // Tags + const tagEl=document.getElementById('view-tags'); + tagEl.innerHTML=(note.tags||'').split(',').filter(t=>t.trim()).map(t=> + `#${escHtml(t.trim())}`).join(''); + // Markdown rendering + const mdEl=document.getElementById('view-md'); + if(note.content&&window.marked){ + mdEl.innerHTML=marked.parse(note.content); + mdEl.classList.remove('empty'); + } else if(note.content){ + mdEl.textContent=note.content;mdEl.classList.remove('empty'); + } else { + mdEl.innerHTML='Aucun contenu.';mdEl.classList.add('empty'); + } + // Attachments + renderAttachmentsView(note.attachments||[],id); + }catch(e){toast(e.message,'error');} +} + +function renderAttachmentsView(atts,noteId){ + const images=atts.filter(a=>a.type==='image'); + const files =atts.filter(a=>a.type==='file'); + const urls =atts.filter(a=>a.type==='url'); + const el=document.getElementById('view-attachments'); + el.innerHTML=''; + if(!atts.length){el.style.display='none';return;} + el.style.display='block'; + + if(images.length){ + el.innerHTML+=`
+
🖼️ Images
+
${images.map(a=>` +
+ ${escHtml(a.original_name||'')} + +
`).join('')}
`; + } + if(files.length){ + el.innerHTML+=`
+
📎 Fichiers
+
${files.map(a=>` +
+ ${escHtml(a.original_name||a.filename)} + ${fmtSize(a.size)} + +
`).join('')}
`; + } + if(urls.length){ + el.innerHTML+=`
+
🔗 Liens
+
${urls.map(a=>` + `).join('')}
`; + } +} + +async function deleteAttachment(attId,noteId){ + if(!confirm('Supprimer cet attachement ?'))return; + try{ await api('attachments','DELETE',null,'&id='+attId); toast('Supprimé'); viewNote(noteId); } + catch(e){toast(e.message,'error');} +} + +// Lightbox +function openLightbox(src){ + const lb=document.getElementById('memo-lightbox'); + const img=lb.querySelector('img'); + img.src=src;lb.classList.add('show'); +} +function closeLightbox(){ document.getElementById('memo-lightbox')?.classList.remove('show'); } + +// ─── Edit / Create ──────────────────────────────────────────────────────────── +let editingId=null; +let editTags=[]; +let tagAutocomplete=[]; + +function openCreate(){ + editingId=null;pendingFiles=[];pendingUrls=[];editTags=[]; + document.getElementById('edit-page-title').textContent='Nouvelle note'; + document.getElementById('edit-title').value=''; + document.getElementById('edit-content').value=''; + document.getElementById('md-live-preview').innerHTML=''; + document.getElementById('edit-tags-chips').innerHTML=''; + document.getElementById('edit-tags-input').value=''; + document.getElementById('edit-attach-preview').innerHTML=''; + document.getElementById('edit-url-rows').innerHTML=addUrlRowHtml(); + renderTagChips(); + showPage('edit'); + document.getElementById('edit-title')?.focus(); +} + +async function openEdit(id){ + editingId=id;pendingFiles=[];pendingUrls=[];editTags=[]; + try{ + const note=await api('notes','GET',null,'&id='+id); + document.getElementById('edit-page-title').textContent='Modifier la note'; + document.getElementById('edit-title').value=note.title; + document.getElementById('edit-content').value=note.content||''; + editTags=(note.tags||'').split(',').filter(t=>t.trim()); + renderTagChips(); + updatePreview(); + // existing attachments shown below form + renderAttachmentsEdit(note.attachments||[]); + document.getElementById('edit-url-rows').innerHTML=addUrlRowHtml(); + showPage('edit'); + }catch(e){toast(e.message,'error');} +} + +function renderAttachmentsEdit(atts){ + const el=document.getElementById('edit-existing-attachments'); + if(!atts.length){el.innerHTML='';return;} + el.innerHTML=`
Attachements existants
`+ + atts.map(a=>`
+ ${a.type==='url'? + `${escHtml(a.label||a.url)}`: + `${escHtml(a.original_name||a.filename)} + ${fmtSize(a.size)}`} + +
`).join(''); +} + +function updatePreview(){ + const content=document.getElementById('edit-content')?.value||''; + const el=document.getElementById('md-live-preview'); + if(!el)return; + el.innerHTML=window.marked?marked.parse(content):'Preview chargement…'; +} + +// Tags +function renderTagChips(){ + const wrap=document.getElementById('edit-tags-chips'); + wrap.innerHTML=editTags.map(t=> + `#${escHtml(t)}` + ).join(''); +} +function removeTag(t){ editTags=editTags.filter(x=>x!==t);renderTagChips(); } +function addTagFromInput(){ + const inp=document.getElementById('edit-tags-input'); + const val=inp.value.trim().replace(/^#/,'').toLowerCase(); + if(val&&!editTags.includes(val)){editTags.push(val);renderTagChips();} + inp.value=''; + document.getElementById('tags-suggestions')?.remove(); +} +function onTagKeydown(e){ + if(e.key==='Enter'||e.key===','||e.key===' '){e.preventDefault();addTagFromInput();} + else if(e.key==='Backspace'&&!e.target.value&&editTags.length){ + editTags.pop();renderTagChips(); + } else showTagSuggestions(e.target.value); +} +function showTagSuggestions(q){ + document.getElementById('tags-suggestions')?.remove(); + if(!q||!allTagsList.length)return; + const matches=allTagsList.filter(t=>t.tag.includes(q.toLowerCase())).slice(0,6); + if(!matches.length)return; + const inp=document.getElementById('edit-tags-input'); + const box=document.createElement('div'); + box.id='tags-suggestions';box.className='tags-suggestions'; + box.style.cssText=`position:absolute;top:${inp.offsetTop+inp.offsetHeight+4}px;left:${inp.offsetLeft}px`; + matches.forEach(t=>{const d=document.createElement('div');d.textContent='#'+t.tag;d.onclick=()=>{ + if(!editTags.includes(t.tag)){editTags.push(t.tag);renderTagChips();} + inp.value='';box.remove(); + };box.appendChild(d);}); + inp.closest('.tags-input-wrap').style.position='relative'; + inp.closest('.tags-input-wrap').appendChild(box); +} + +// File drop +function initDropZone(){ + const zone=document.getElementById('edit-drop-zone'); + const inp=document.getElementById('edit-file-input'); + if(!zone)return; + zone.addEventListener('click',()=>inp?.click()); + zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('drag-over');}); + zone.addEventListener('dragleave',()=>zone.classList.remove('drag-over')); + zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('drag-over');handleFileSelect(e.dataTransfer.files);}); + inp?.addEventListener('change',()=>handleFileSelect(inp.files)); + document.getElementById('edit-content')?.addEventListener('paste',e=>{ + const files=[...e.clipboardData.files].filter(f=>f.type.startsWith('image/')); + if(files.length){e.preventDefault();handleFileSelect(files);} + }); +} +function handleFileSelect(files){ + const preview=document.getElementById('edit-attach-preview'); + [...files].forEach(f=>{ + pendingFiles.push(f); + const item=document.createElement('div');item.className='attach-preview-item'; + if(f.type.startsWith('image/')){ + const img=document.createElement('img'); + const reader=new FileReader(); + reader.onload=ev=>{img.src=ev.target.result;}; + reader.readAsDataURL(f); + item.appendChild(img); + }else{ + item.innerHTML=`
${escHtml(f.name)}
`; + } + const rm=document.createElement('button');rm.className='rm';rm.textContent='✕'; + rm.onclick=()=>{pendingFiles=pendingFiles.filter(x=>x!==f);item.remove();}; + item.appendChild(rm); + preview.appendChild(item); + }); +} + +// URL rows +function addUrlRowHtml(){ + return `
+ + + +
`; +} +function addUrlRow(){ + const wrap=document.getElementById('edit-url-rows'); + wrap.insertAdjacentHTML('beforeend',addUrlRowHtml()); +} + +// Save +async function saveNote(){ + const title=document.getElementById('edit-title')?.value.trim(); + if(!title){toast('Titre requis','error');return;} + const content=document.getElementById('edit-content')?.value||''; + const tags=editTags.join(','); + + // Collect URLs + const urlRows=document.querySelectorAll('#edit-url-rows .url-row'); + const urls=[]; + urlRows.forEach(row=>{ + const u=row.querySelector('.url-input')?.value.trim(); + const l=row.querySelector('.url-label')?.value.trim()||''; + if(u)urls.push({url:u,label:l}); + }); + + try{ + let noteId; + if(editingId){ + await api('notes','PUT',{title,content,tags},'&id='+editingId); + noteId=editingId;toast('Note mise à jour'); + }else{ + const r=await api('notes','POST',JSON.stringify({title,content,tags,urls})); + noteId=r.id;toast('Note créée'); + } + + // Upload pending files + for(const f of pendingFiles){ + const fd=new FormData();fd.append('file',f);fd.append('note_id',noteId); + try{await api('attachments','POST',fd);}catch(e){toast('Erreur upload: '+f.name,'error');} + } + // Add URLs (for edit mode) + if(editingId){ + for(const u of urls){ + const fd=new FormData();fd.append('note_id',noteId);fd.append('url',u.url);fd.append('label',u.label); + try{await api('attachments','POST',fd);}catch{} + } + } + + loadList(currentQ,currentTag); + viewNote(noteId); + }catch(e){toast(e.message,'error');} +} + +async function deleteNote(id){ + if(!confirm('Supprimer cette note définitivement ?'))return; + try{ + await api('notes','DELETE',null,'&id='+id); + toast('Note supprimée');loadList(currentQ,currentTag); + }catch(e){toast(e.message,'error');} +} + +// ─── Init ───────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded',()=>{ + loadList(); + loadSidebar(); + initDropZone(); + // Search + const sinp=document.getElementById('memo-search-input'); + if(sinp){ + let t;sinp.addEventListener('input',()=>{clearTimeout(t);t=setTimeout(()=>doSearch(),300);}); + sinp.addEventListener('keydown',e=>{if(e.key==='Enter')doSearch();}); + } + // Content live preview + document.getElementById('edit-content')?.addEventListener('input',updatePreview); + // Lightbox close + document.getElementById('memo-lightbox')?.addEventListener('click',closeLightbox); + // Tags input + document.getElementById('edit-tags-input')?.addEventListener('keydown',onTagKeydown); + document.getElementById('edit-tags-input')?.addEventListener('blur',()=>setTimeout(()=>document.getElementById('tags-suggestions')?.remove(),150)); + document.addEventListener('click',e=>{if(!e.target.closest('.tags-input-wrap'))document.getElementById('tags-suggestions')?.remove();}); +}); diff --git a/settings.php b/settings.php index 5d49d7f..875c397 100644 --- a/settings.php +++ b/settings.php @@ -15,7 +15,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'set_modules' && $family_id) { - $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage']; + $all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo']; $enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m]))); if (empty($enabled)) { $error = "Vous devez garder au moins un module actif."; @@ -187,6 +187,7 @@ require __DIR__ . '/header.php'; 'holidays' => ['icon' => '🏖️', 'label' => tr('menu_holidays')], 'gifts' => ['icon' => '🎁', 'label' => tr('menu_gifts')], 'garage' => ['icon' => '🚗', 'label' => tr('menu_garage')], + 'memo' => ['icon' => '📝', 'label' => tr('menu_memo')], ]; ?>