531 lines
31 KiB
PHP
531 lines
31 KiB
PHP
<?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]);
|
||
}
|
||
|
||
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]);
|
||
}
|
||
}
|
||
|
||
if(isset($_POST['delete_note'])) {
|
||
$n = query("SELECT owner_id, image_path 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']);
|
||
query("DELETE FROM notes WHERE id=? AND owner_id=?", [$_POST['note_id'], $user_id]);
|
||
}
|
||
}
|
||
|
||
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.";
|
||
if($parts[0] === 'user') notify_user($parts[1], $msg);
|
||
else notify_group($parts[1], $msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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) {
|
||
$lbl = trim((string)($n['label_name'] ?? ''));
|
||
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">
|
||
<input type="text" name="label_name" class="form-control" id="new-note-label" placeholder="Tag/Label name">
|
||
</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>
|
||
|
||
<!-- ── 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">
|
||
<input type="text" name="label_name" id="edit-note-label" class="form-control" placeholder="Tag/Label name">
|
||
</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;">Share this note</label>
|
||
<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>
|
||
|
||
<!-- 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 ? '...' : '');
|
||
?>
|
||
<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-mine="<?= $n['is_mine'] ?>"
|
||
onclick="openNote(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:inline-flex; align-items:center; gap:0.35rem; width:max-content; max-width:100%; 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($n['label_name']) ?>
|
||
</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').textContent = '👁 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';
|
||
|
||
// 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');
|
||
}
|
||
|
||
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();
|
||
const cl = (card.dataset.label || '').toLowerCase();
|
||
const okText = !q || t.includes(q) || c.includes(q) || cl.includes(q);
|
||
const okLabel = !lbl || cl === lbl;
|
||
card.style.display = (okText && okLabel) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function deleteNote(id) {
|
||
if(!confirm('Delete this note permanently?')) return;
|
||
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>
|