Files
Task-Management/views/notes.php

716 lines
42 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ── DB migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE notes ADD COLUMN color TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN image_path TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN label_name TEXT DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
owner_id INTEGER,
color TEXT DEFAULT NULL,
image_path TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT NULL,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS note_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
share_type TEXT,
share_id INTEGER,
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
// ── Handle POST ───────────────────────────────────────────────────────────────
if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(isset($_POST['create_note'])) {
$image_path = null;
if(isset($_FILES['note_image']) && $_FILES['note_image']['error'] === 0) {
$allowed = ['image/jpeg','image/png','image/gif','image/webp'];
if(in_array($_FILES['note_image']['type'], $allowed)) {
$ext = pathinfo($_FILES['note_image']['name'], PATHINFO_EXTENSION);
$fname = 'note_' . $user_id . '_' . time() . '.' . $ext;
$dest = 'uploads/' . $fname;
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) $image_path = $dest;
}
}
$stmt = $pdo->prepare("INSERT INTO notes (title, content, owner_id, color, image_path, label_name) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $user_id, $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null]);
// Note creation only notifies owner in UI, but we can trigger a WAHA self-alert if they have it enabled
notify_user($user_id, "You created a new note: " . ($_POST['title'] ?? 'Untitled'), ['action'=>'Created', 'module'=>'Note', 'item'=>$_POST['title']??'Untitled']);
}
if(isset($_POST['update_note'])) {
$n = query("SELECT owner_id, image_path FROM notes WHERE id=?", [$_POST['note_id']])->fetch();
if($n && $n['owner_id'] == $user_id) {
$image_path = $n['image_path'];
if(isset($_FILES['note_image']) && $_FILES['note_image']['error'] === 0) {
$allowed = ['image/jpeg','image/png','image/gif','image/webp'];
if(in_array($_FILES['note_image']['type'], $allowed)) {
$ext = pathinfo($_FILES['note_image']['name'], PATHINFO_EXTENSION);
$fname = 'note_' . $user_id . '_' . time() . '.' . $ext;
$dest = 'uploads/' . $fname;
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) {
if($image_path && file_exists($image_path)) @unlink($image_path);
$image_path = $dest;
}
}
}
if(isset($_POST['remove_image']) && $_POST['remove_image']) {
if($image_path && file_exists($image_path)) @unlink($image_path);
$image_path = null;
}
$stmt = $pdo->prepare("UPDATE notes SET title=?, content=?, color=?, image_path=?, label_name=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null, $_POST['note_id'], $user_id]);
$ctx = ['action'=>'Updated', 'module'=>'Note', 'item'=>$_POST['title']??'Untitled'];
// Notify owner
notify_user($user_id, "Note '{$_POST['title']}' updated.", $ctx);
// Notify anyone this note is shared with
$shares = query("SELECT share_type, share_id FROM note_shares WHERE note_id=?", [$_POST['note_id']])->fetchAll();
foreach($shares as $sh) {
if($sh['share_type'] === 'user') notify_user($sh['share_id'], "Shared note '{$_POST['title']}' updated.", $ctx);
else notify_group($sh['share_id'], "Shared group note '{$_POST['title']}' updated.", $ctx);
}
}
}
if(isset($_POST['delete_note'])) {
$n = query("SELECT owner_id, image_path, title FROM notes WHERE id=?", [$_POST['note_id']])->fetch();
if($n && $n['owner_id'] == $user_id) {
if($n['image_path'] && file_exists($n['image_path'])) @unlink($n['image_path']);
$ctx = ['action'=>'Deleted', 'module'=>'Note', 'item'=>$n['title']??'Untitled'];
// Notify anyone it was shared with before deleting
$shares = query("SELECT share_type, share_id FROM note_shares WHERE note_id=?", [$_POST['note_id']])->fetchAll();
foreach($shares as $sh) {
if($sh['share_type'] === 'user') notify_user($sh['share_id'], "Shared note '{$n['title']}' has been deleted.", $ctx);
else notify_group($sh['share_id'], "Shared group note '{$n['title']}' has been deleted.", $ctx);
}
query("DELETE FROM notes WHERE id=? AND owner_id=?", [$_POST['note_id'], $user_id]);
notify_user($user_id, "Note '{$n['title']}' has been deleted.", $ctx);
}
}
if(isset($_POST['share_note'])) {
$n_id = $_POST['note_id'];
$share = $_POST['share_target'];
$parts = explode('_', $share, 2);
if(count($parts)===2) {
$stmt = $pdo->prepare("INSERT INTO note_shares (note_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$n_id, $parts[0], $parts[1]]);
$n = query("SELECT title FROM notes WHERE id=?", [$n_id])->fetch();
$msg = "A note '{$n['title']}' has been shared with you.";
$ctx = ['action'=>'Shared', 'module'=>'Note', 'item'=>$n['title']??'Untitled', 'user'=>$_SESSION['username']??'System'];
if($parts[0] === 'user') notify_user($parts[1], $msg, $ctx);
else notify_group($parts[1], $msg, $ctx);
}
}
if(isset($_POST['remove_note_share'])) {
$share_id = (int)$_POST['share_id'];
$chk = query("SELECT ns.id FROM note_shares ns JOIN notes n ON ns.note_id=n.id WHERE ns.id=? AND n.owner_id=?", [$share_id, $user_id])->fetch();
if($chk) {
$pdo->prepare("DELETE FROM note_shares WHERE id=?")->execute([$share_id]);
}
}
}
// ── Fetch Notes ───────────────────────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0';
$sql = "SELECT n.*, (n.owner_id = ?) as is_mine, u.username as owner_name FROM notes n
LEFT JOIN note_shares ns ON n.id = ns.note_id
LEFT JOIN users u ON n.owner_id = u.id
WHERE n.owner_id = ?
OR (ns.share_type='user' AND ns.share_id = ?)
OR (ns.share_type='group' AND ns.share_id IN ($group_in))
GROUP BY n.id ORDER BY n.updated_at DESC, n.created_at DESC";
$params = array_merge([$user_id, $user_id, $user_id], $my_group_ids);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$notes = $stmt->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
$label_names = [];
foreach($notes as $n) {
$lbls = explode(',', (string)($n['label_name'] ?? ''));
foreach($lbls as $lbl) {
$lbl = trim($lbl);
if($lbl !== '') $label_names[strtolower($lbl)] = $lbl;
}
}
natcasesort($label_names);
// Define note colors — Google Keep inspired
$note_colors = [
'' => ['bg'=>'var(--surface)', 'label'=>'Default', 'hex'=>'#ffffff'],
'#fff9c4' => ['bg'=>'#fff9c4', 'label'=>'Yellow', 'hex'=>'#fff9c4'],
'#ccff90' => ['bg'=>'#ccff90', 'label'=>'Lime', 'hex'=>'#ccff90'],
'#d4edda' => ['bg'=>'#d4edda', 'label'=>'Sage', 'hex'=>'#d4edda'],
'#80cbc4' => ['bg'=>'#80cbc4', 'label'=>'Teal', 'hex'=>'#80cbc4'],
'#b3e5fc' => ['bg'=>'#b3e5fc', 'label'=>'Sky', 'hex'=>'#b3e5fc'],
'#cce5ff' => ['bg'=>'#cce5ff', 'label'=>'Blue', 'hex'=>'#cce5ff'],
'#e2d9f3' => ['bg'=>'#e2d9f3', 'label'=>'Lavender', 'hex'=>'#e2d9f3'],
'#f3e5f5' => ['bg'=>'#f3e5f5', 'label'=>'Orchid', 'hex'=>'#f3e5f5'],
'#f8d7da' => ['bg'=>'#f8d7da', 'label'=>'Rose', 'hex'=>'#f8d7da'],
'#ef9a9a' => ['bg'=>'#ef9a9a', 'label'=>'Red', 'hex'=>'#ef9a9a'],
'#ffcc80' => ['bg'=>'#ffcc80', 'label'=>'Peach', 'hex'=>'#ffcc80'],
'#fde8d8' => ['bg'=>'#fde8d8', 'label'=>'Orange', 'hex'=>'#fde8d8'],
'#d7ccc8' => ['bg'=>'#d7ccc8', 'label'=>'Sand', 'hex'=>'#d7ccc8'],
'#f5f5f5' => ['bg'=>'#f5f5f5', 'label'=>'Stone', 'hex'=>'#f5f5f5'],
'#e8f5e9' => ['bg'=>'#e8f5e9', 'label'=>'Mint', 'hex'=>'#e8f5e9'],
'#fff3e0' => ['bg'=>'#fff3e0', 'label'=>'Apricot', 'hex'=>'#fff3e0'],
'#fce4ec' => ['bg'=>'#fce4ec', 'label'=>'Pink', 'hex'=>'#fce4ec'],
'#ede7f6' => ['bg'=>'#ede7f6', 'label'=>'Violet', 'hex'=>'#ede7f6'],
'#e0f7fa' => ['bg'=>'#e0f7fa', 'label'=>'Cyan', 'hex'=>'#e0f7fa'],
'#fffde7' => ['bg'=>'#fffde7', 'label'=>'Butter', 'hex'=>'#fffde7'],
];
?>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; gap:0.75rem; flex-wrap:wrap;">
<button class="btn" onclick="document.getElementById('new-note-modal').classList.add('active')">
<i class="fa fa-plus"></i> New Note
</button>
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
<input type="text" id="notes-search-input" class="form-control" placeholder="Search notes..." style="width:200px; padding:0.45rem 0.6rem; font-size:0.84rem;" oninput="filterNotes()">
<select id="notes-label-filter" class="form-control" style="width:180px; padding:0.45rem 0.6rem; font-size:0.84rem;" onchange="filterNotes()">
<option value="">All labels</option>
<?php foreach($label_names as $lbl): ?>
<option value="<?= htmlspecialchars(strtolower($lbl)) ?>"><?= htmlspecialchars($lbl) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<!-- ── New Note Modal ─────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="new-note-modal">
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:92vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:700;">New Note</h3>
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.2rem;" onclick="document.getElementById('new-note-modal').classList.remove('active')">×</button>
</div>
<form method="POST" enctype="multipart/form-data" id="new-note-form">
<input type="hidden" name="create_note" value="1">
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
<div class="form-group">
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.25rem; display:block;">Tags</label>
<input type="text" name="label_name" class="form-control tagify-input" id="new-note-label" placeholder="Add tags...">
</div>
</div>
<!-- WYSIWYG Toolbar -->
<div class="note-editor-toolbar" id="new-note-toolbar">
<button type="button" class="note-toolbar-btn" onclick="formatNote('new-note-content','bold')" title="Bold"><b>B</b></button>
<button type="button" class="note-toolbar-btn" onclick="formatNote('new-note-content','italic')" title="Italic"><i>I</i></button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','# ')" title="H1">H1</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','## ')" title="H2">H2</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','### ')" title="H3">H3</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','- ')" title="Bullet">• List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','1. ')" title="Numbered">1. List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','- [ ] ')" title="Todo">☑ Todo</button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('new-note-content','`','`')" title="Code"><code>Code</code></button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('new-note-content','```\n','\n```')" title="Code Block">{ } Block</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','> ')" title="Quote">❝ Quote</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','---\n')" title="Divider">─ Line</button>
<button type="button" class="note-toolbar-btn" id="new-preview-btn" onclick="togglePreview('new-note-content','new-note-preview','new-preview-btn')">👁 Preview</button>
</div>
<div class="form-group">
<textarea name="content" id="new-note-content" class="note-content-area" placeholder="Write your note... (Markdown supported)" rows="8"></textarea>
<div id="new-note-preview" class="markdown-body" style="display:none; min-height:160px; border:1px solid var(--border); border-radius:0.5rem; padding:0.75rem; background:var(--bg); font-size:0.9rem;"></div>
</div>
<!-- Color picker -->
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Note Color</label>
<div class="note-color-palette" id="new-note-colors">
<?php foreach($note_colors as $hex => $info): ?>
<div class="color-swatch <?= !$hex ? 'selected' : '' ?>"
style="background:<?= $hex ?: '#fff' ?>; border:2px solid var(--border);"
onclick="selectColor(this,'new-note-color-input','<?= $hex ?>')"
title="<?= $info['label'] ?>"></div>
<?php endforeach; ?>
</div>
<input type="hidden" name="color" id="new-note-color-input" value="">
</div>
<!-- Image upload -->
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Attach Image (optional)</label>
<input type="file" name="note_image" accept="image/*" class="form-control">
</div>
<div style="display:flex; gap:0.5rem; justify-content:flex-end; margin-top:1rem;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('new-note-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-save"></i> Save Note</button>
</div>
</form>
</div>
</div>
<!-- ── View Note Modal ────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="view-note-modal">
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:92vh; overflow-y:auto; padding:0; background:var(--bg);">
<div id="view-note-header" style="padding:1.5rem; border-bottom:1px solid var(--border); background:var(--surface); position:sticky; top:0; z-index:10;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:1rem;">
<div style="flex:1; min-width:0;">
<h2 id="view-note-title" style="margin:0 0 0.5rem 0; font-size:1.4rem; font-weight:800; line-height:1.3; color:var(--text); word-wrap:break-word;"></h2>
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
<span id="view-note-label-container" style="display:none; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.75rem; font-weight:600;">
<i class="fa fa-tag" style="font-size:0.7rem;"></i> <span id="view-note-label"></span>
</span>
<span id="view-note-date" style="font-size:0.8rem; color:var(--text-light);"></span>
<span id="view-note-shared-badge" class="note-card-shared-badge" style="display:none;">Shared</span>
</div>
</div>
<div style="display:flex; align-items:center; gap:0.5rem; flex-shrink:0;">
<button class="btn btn-sm btn-secondary" id="view-note-edit-btn" onclick="transToEditNote()"><i class="fa fa-pencil"></i> Edit</button>
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.6rem; line-height:1; padding:0 4px;" onclick="document.getElementById('view-note-modal').classList.remove('active')">×</button>
</div>
</div>
<div id="view-note-image-container" style="display:none; margin-top:1.25rem; border-radius:0.75rem; overflow:hidden; border:1px solid var(--border);">
<img id="view-note-image" src="" style="width:100%; max-height:450px; object-fit:cover; display:block;">
</div>
</div>
<div style="padding:1.5rem;">
<div id="view-note-content" class="markdown-body" style="font-size:1rem; line-height:1.7; color:var(--text);"></div>
</div>
</div>
</div>
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="edit-note-modal">
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:90vh; overflow-y:auto;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="margin:0;">Edit Note</h3>
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.2rem;" onclick="document.getElementById('edit-note-modal').classList.remove('active')">×</button>
</div>
<form method="POST" enctype="multipart/form-data" id="edit-note-form">
<input type="hidden" name="update_note" value="1">
<input type="hidden" name="note_id" id="edit-note-id">
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
<div class="form-group">
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.25rem; display:block;">Tags</label>
<input type="text" name="label_name" id="edit-note-label" class="form-control tagify-input" placeholder="Add tags...">
</div>
</div>
<div class="note-editor-toolbar">
<button type="button" class="note-toolbar-btn" onclick="formatNote('edit-note-content','bold')"><b>B</b></button>
<button type="button" class="note-toolbar-btn" onclick="formatNote('edit-note-content','italic')"><i>I</i></button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','# ')">H1</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','## ')">H2</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','### ')">H3</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','- ')">• List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','1. ')">1. List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','- [ ] ')">☑ Todo</button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('edit-note-content','`','`')"><code>Code</code></button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('edit-note-content','```\n','\n```')">{ } Block</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','> ')">❝ Quote</button>
<button type="button" class="note-toolbar-btn" id="edit-preview-btn" onclick="togglePreview('edit-note-content','edit-note-preview','edit-preview-btn')">👁 Preview</button>
</div>
<div class="form-group">
<textarea name="content" id="edit-note-content" class="note-content-area" rows="8"></textarea>
<div id="edit-note-preview" class="markdown-body" style="display:none; min-height:160px; border:1px solid var(--border); border-radius:0.5rem; padding:0.75rem; background:var(--bg); font-size:0.9rem;"></div>
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Note Color</label>
<div class="note-color-palette" id="edit-note-colors">
<?php foreach($note_colors as $hex => $info): ?>
<div class="color-swatch"
style="background:<?= $hex ?: '#fff' ?>; border:2px solid var(--border);"
onclick="selectColor(this,'edit-note-color-input','<?= $hex ?>')"
title="<?= $info['label'] ?>"></div>
<?php endforeach; ?>
</div>
<input type="hidden" name="color" id="edit-note-color-input" value="">
</div>
<div id="edit-current-image" style="display:none; margin-bottom:0.75rem;">
<label style="font-size:0.8rem; color:var(--text-light);">Current Image:</label>
<div style="display:flex; align-items:center; gap:0.5rem; margin-top:0.25rem;">
<img id="edit-current-img-preview" src="" alt="Current" style="height:60px; border-radius:0.5rem; object-fit:cover;">
<label style="display:flex; align-items:center; gap:0.25rem; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" name="remove_image" value="1"> Remove image
</label>
</div>
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Replace Image (optional)</label>
<input type="file" name="note_image" accept="image/*" class="form-control">
</div>
<!-- Share section (only for owner) -->
<div id="edit-share-section" style="border-top:1px solid var(--border); padding-top:0.75rem; margin-top:0.5rem;">
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.4rem; display:block;">Shared with</label>
<div id="note-shares-list" style="display:flex; flex-wrap:wrap; gap:0.35rem; margin-bottom:0.75rem;"></div>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
<select name="share_target" id="edit-share-target" class="form-control" style="flex:1; min-width:150px; padding:0.4rem;">
<?php foreach($all_users as $u): ?>
<option value="user_<?= $u['id'] ?>">👤 <?= htmlspecialchars($u['username']) ?></option>
<?php endforeach; ?>
<?php foreach($all_groups as $g): ?>
<option value="group_<?= $g['id'] ?>">👥 <?= htmlspecialchars($g['name']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" name="share_note" value="1" class="btn btn-sm" onclick="this.form.action=''; document.querySelector('[name=update_note]').disabled=true; this.form.onsubmit=null;" formaction="">
<i class="fa fa-share-nodes"></i> Share
</button>
</div>
</div>
<div style="display:flex; gap:0.5rem; justify-content:space-between; margin-top:1rem; flex-wrap:wrap;">
<button type="button" class="btn btn-danger btn-sm" onclick="deleteNote(document.getElementById('edit-note-id').value)"><i class="fa fa-trash"></i> Delete</button>
<div style="display:flex; gap:0.5rem;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-note-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-save"></i> Save Changes</button>
</div>
</div>
</form>
</div>
</div>
<!-- Delete Note Form -->
<form method="POST" id="delete-note-form" style="display:none;">
<input type="hidden" name="delete_note" value="1">
<input type="hidden" name="note_id" id="delete-note-id">
</form>
<!-- Share Note Form (separate POST for share_note) -->
<form method="POST" id="share-note-standalone-form" style="display:none;">
<input type="hidden" name="share_note" value="1">
<input type="hidden" name="note_id" id="share-note-standalone-id">
<input type="hidden" name="share_target" id="share-note-standalone-target">
</form>
<form method="POST" id="remove-note-share-form" style="display:none;">
<input type="hidden" name="remove_note_share" value="1">
<input type="hidden" name="share_id" id="remove-note-share-id">
</form>
<!-- Markdown Libs -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- ── Notes Grid ─────────────────────────────────────────────────────────── -->
<?php if(empty($notes)): ?>
<div style="text-align:center; padding:4rem 2rem; color:var(--text-light);">
<div style="font-size:3rem; margin-bottom:1rem;">📝</div>
<h3 style="margin-bottom:0.5rem;">No notes yet</h3>
<p>Click <strong>New Note</strong> to start writing something.</p>
</div>
<?php else: ?>
<div class="notes-grid">
<?php foreach($notes as $n):
$bg = $n['color'] ?: 'var(--surface)';
$preview = strip_tags(isset($n['content']) ? (function_exists('marked') ? $n['content'] : $n['content']) : '');
$preview = mb_substr($preview, 0, 120) . (mb_strlen($preview) > 120 ? '...' : '');
$note_shares = [];
if($n['is_mine']) {
$note_shares = query("SELECT ns.id, ns.share_type, ns.share_id, u.username, u.profile_pic as upic, g.name as gname, g.profile_pic as gpic FROM note_shares ns LEFT JOIN users u ON (ns.share_type='user' AND ns.share_id=u.id) LEFT JOIN groups g ON (ns.share_type='group' AND ns.share_id=g.id) WHERE ns.note_id=?", [$n['id']])->fetchAll();
}
$shares_json = htmlspecialchars(json_encode($note_shares), ENT_QUOTES, 'UTF-8');
?>
<div class="note-card note-filter-item"
style="background:<?= htmlspecialchars($bg) ?>;"
data-id="<?= $n['id'] ?>"
data-title="<?= htmlspecialchars(addslashes((string)$n['title'])) ?>"
data-content="<?= htmlspecialchars(addslashes((string)$n['content'])) ?>"
data-label="<?= htmlspecialchars(strtolower(trim((string)($n['label_name'] ?? '')))) ?>"
data-label-raw="<?= htmlspecialchars(trim((string)($n['label_name'] ?? ''))) ?>"
data-color="<?= htmlspecialchars((string)$n['color']) ?>"
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
data-shares="<?= $shares_json ?>"
data-mine="<?= $n['is_mine'] ?>"
data-updated="<?= date('M j, Y, H:i', strtotime($n['updated_at'] ?: $n['created_at'])) ?>"
onclick="viewNote(this)">
<?php if($n['image_path'] && file_exists($n['image_path'])): ?>
<img src="<?= htmlspecialchars($n['image_path']) ?>" alt="Note image" class="note-card-img">
<?php endif; ?>
<?php if($n['title']): ?>
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
<?php endif; ?>
<?php if(!empty($n['label_name'])): ?>
<div style="display:flex; flex-wrap:wrap; gap:0.35rem; margin-bottom:0.5rem;">
<?php foreach(explode(',', $n['label_name']) as $lbl): $lbl=trim($lbl); if(!$lbl) continue; ?>
<div style="display:inline-flex; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.7rem; font-weight:600;">
<i class="fa fa-tag" style="font-size:0.65rem;"></i> <?= htmlspecialchars($lbl) ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="note-card-preview"><?= htmlspecialchars($preview) ?></div>
<div class="note-card-footer">
<div style="display:flex; align-items:center; gap:0.4rem;">
<?php if($n['color']): ?><div class="note-color-dot" style="background:<?= htmlspecialchars($n['color']) ?>;"></div><?php endif; ?>
<span style="font-size:0.7rem; color:var(--text-light);"><?= date('M j', strtotime($n['updated_at'] ?: $n['created_at'])) ?></span>
<?php if(!$n['is_mine']): ?>
<span class="note-card-shared-badge">Shared</span>
<?php endif; ?>
</div>
<div class="note-card-actions" onclick="event.stopPropagation()">
<?php if($n['is_mine']): ?>
<button class="btn btn-sm btn-danger" style="padding:3px 7px;" onclick="deleteNote(<?= $n['id'] ?>)" title="Delete"><i class="fa fa-trash"></i></button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<script>
// ── WYSIWYG Markdown Tools ────────────────────────────────────────────────────
function formatNote(textareaId, cmd) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart, end = ta.selectionEnd;
const sel = ta.value.substring(start, end);
let wrap = cmd === 'bold' ? '**' : '_';
const replacement = `${wrap}${sel || 'text'}${wrap}`;
ta.value = ta.value.substring(0, start) + replacement + ta.value.substring(end);
ta.focus();
ta.selectionStart = start + wrap.length;
ta.selectionEnd = start + wrap.length + (sel || 'text').length;
}
function insertMarkdown(textareaId, prefix) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart;
// Go to start of line
const before = ta.value.substring(0, start);
const lineStart = before.lastIndexOf('\n') + 1;
ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
ta.focus();
const newPos = lineStart + prefix.length + (start - lineStart);
ta.selectionStart = ta.selectionEnd = newPos;
}
function wrapMarkdown(textareaId, before, after) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart, end = ta.selectionEnd;
const sel = ta.value.substring(start, end);
ta.value = ta.value.substring(0, start) + before + (sel || 'code') + after + ta.value.substring(end);
ta.focus();
}
function togglePreview(contentId, previewId, btnId) {
const content = document.getElementById(contentId);
const preview = document.getElementById(previewId);
const btn = document.getElementById(btnId);
if(preview.style.display === 'none') {
preview.innerHTML = marked.parse(content.value || '');
preview.style.display = 'block';
content.style.display = 'none';
btn.textContent = '✏️ Edit';
} else {
preview.style.display = 'none';
content.style.display = 'block';
btn.textContent = '👁 Preview';
}
}
function selectColor(el, inputId, color) {
el.closest('.note-color-palette').querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
el.classList.add('selected');
document.getElementById(inputId).value = color;
}
function openNote(card) {
const id = card.dataset.id;
const title = card.dataset.title;
const content = card.dataset.content;
const color = card.dataset.color;
const label = card.dataset.labelRaw || '';
const image = card.dataset.image;
const isMine = card.dataset.mine === '1';
document.getElementById('edit-note-id').value = id;
document.getElementById('edit-note-title').value = title;
document.getElementById('edit-note-content').value = content;
document.getElementById('edit-note-label').value = label;
document.getElementById('edit-note-color-input').value = color;
document.getElementById('edit-note-preview').style.display = 'none';
document.getElementById('edit-note-content').style.display = 'block';
document.getElementById('edit-preview-btn').innerHTML = '👁 Preview';
// Color swatches
document.querySelectorAll('#edit-note-colors .color-swatch').forEach(s => {
s.classList.remove('selected');
const swatchColor = s.getAttribute('onclick').match(/'([^']*)'/g);
if(swatchColor && swatchColor[1] && swatchColor[1].replace(/'/g,'') === color) s.classList.add('selected');
if(!color && s.title === 'Default') s.classList.add('selected');
});
// Image preview
const imgBlock = document.getElementById('edit-current-image');
if(image) {
imgBlock.style.display = 'block';
document.getElementById('edit-current-img-preview').src = image;
} else {
imgBlock.style.display = 'none';
}
// Share section
document.getElementById('edit-share-section').style.display = isMine ? 'block' : 'none';
if(isMine) {
renderNoteShares(card.dataset.shares || '[]');
}
// Wire up share button
const shareBtn = document.querySelector('#edit-note-form button[name="share_note"]');
if(shareBtn) {
shareBtn.onclick = (e) => {
e.preventDefault();
const target = document.getElementById('edit-share-target').value;
document.getElementById('share-note-standalone-id').value = id;
document.getElementById('share-note-standalone-target').value = target;
document.getElementById('share-note-standalone-form').submit();
};
}
document.getElementById('edit-note-modal').classList.add('active');
}
let currentViewedCard = null;
function viewNote(card) {
currentViewedCard = card;
const title = card.dataset.title || 'Untitled Note';
const content = card.dataset.content || '';
const label = card.dataset.labelRaw || '';
const image = card.dataset.image;
const updatedAt = card.dataset.updated;
const isMine = card.dataset.mine === '1';
document.getElementById('view-note-title').textContent = title;
// Clear old tags
document.getElementById('view-note-label-container').innerHTML = '';
if(label) {
document.getElementById('view-note-label-container').style.display = 'flex';
const lbls = label.split(',');
lbls.forEach(l => {
const span = document.createElement('span');
span.style.cssText = 'display:inline-flex; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.75rem; font-weight:600;';
span.innerHTML = `<i class="fa fa-tag" style="font-size:0.7rem;"></i> ${l.trim()}`;
document.getElementById('view-note-label-container').appendChild(span);
});
} else {
document.getElementById('view-note-label-container').style.display = 'none';
}
document.getElementById('view-note-date').textContent = 'Updated ' + updatedAt;
document.getElementById('view-note-shared-badge').style.display = !isMine ? 'inline-block' : 'none';
if(image) {
document.getElementById('view-note-image-container').style.display = 'block';
document.getElementById('view-note-image').src = image;
} else {
document.getElementById('view-note-image-container').style.display = 'none';
}
document.getElementById('view-note-content').innerHTML = marked.parse(content);
document.getElementById('view-note-edit-btn').style.display = isMine ? 'inline-flex' : 'none';
document.getElementById('view-note-modal').classList.add('active');
}
function transToEditNote() {
if(currentViewedCard) {
document.getElementById('view-note-modal').classList.remove('active');
openNote(currentViewedCard);
}
}
function renderNoteShares(jsonShares) {
const list = document.getElementById('note-shares-list');
list.innerHTML = '';
let shares = [];
try { shares = JSON.parse(jsonShares || '[]'); } catch(e) { shares = []; }
if(shares.length === 0) return;
shares.forEach(sh => {
const pic = sh.share_type === 'user' ? sh.upic : sh.gpic;
const name = sh.share_type === 'user' ? (sh.username || 'User') : (sh.gname || 'Group');
const icon = sh.share_type === 'user' ? '👤' : '👥';
const chip = document.createElement('div');
chip.className = 'share-chip';
chip.style.display = 'inline-flex';
chip.style.alignItems = 'center';
chip.style.gap = '0.4rem';
chip.style.fontSize = '0.72rem';
chip.style.padding = '2px 8px 2px 2px';
chip.style.borderRadius = '999px';
chip.style.background = 'rgba(var(--primary-rgb),0.1)';
chip.style.color = 'var(--primary)';
chip.style.border = '1px solid rgba(var(--primary-rgb),0.2)';
let avatarHtml = `<div style="width:20px; height:20px; border-radius:50%; background:var(--primary); color:white; display:flex; align-items:center; justify-content:center; font-size:0.6rem; font-weight:700; overflow:hidden;">${name.charAt(0).toUpperCase()}</div>`;
if(pic) {
avatarHtml = `<img src="${pic}" style="width:20px; height:20px; border-radius:50%; object-fit:cover;">`;
}
chip.innerHTML = `${avatarHtml}<span style="font-weight:600;">${name}</span><button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;margin-left:2px;font-size:0.9rem;" title="Unshare" onclick="removeNoteShare(${Number(sh.id)})">×</button>`;
list.appendChild(chip);
});
}
function removeNoteShare(shareId) {
if(!shareId) return;
appConfirm('Unshare this note access?').then(y => {
if(y) {
document.getElementById('remove-note-share-id').value = String(shareId);
document.getElementById('remove-note-share-form').submit();
}
});
}
function filterNotes() {
const q = (document.getElementById('notes-search-input')?.value || '').toLowerCase().trim();
const lbl = (document.getElementById('notes-label-filter')?.value || '').trim();
document.querySelectorAll('.note-filter-item').forEach(card => {
const t = (card.dataset.title || '').toLowerCase();
const c = (card.dataset.content || '').toLowerCase();
// data-label is now comma separated
const cardLabels = (card.dataset.label || '').split(',').map(s=>s.trim());
const okText = !q || t.includes(q) || c.includes(q) || cardLabels.join(' ').includes(q);
const okLabel = !lbl || cardLabels.includes(lbl);
card.style.display = (okText && okLabel) ? '' : 'none';
});
}
function deleteNote(id) {
appConfirm('Delete this note permanently?').then(y => {
if(y) {
document.getElementById('delete-note-id').value = id;
document.getElementById('delete-note-form').submit();
}
});
}
// Fix note card colors in dark mode
function applyDarkNoteColors() {
const isDark = document.body.classList.contains('dark-mode');
document.querySelectorAll('.note-card').forEach(card => {
const origColor = card.dataset.color;
if(!origColor && isDark) {
card.style.background = 'var(--surface)';
}
});
}
const observer = new MutationObserver(applyDarkNoteColors);
observer.observe(document.body, {attributes: true, attributeFilter: ['class']});
applyDarkNoteColors();
</script>