Enhance note-taking features by adding label support, improving modal styles, and refining CSS for better responsiveness. Update login page title and adjust password management UI for better usability.
This commit is contained in:
@@ -219,8 +219,17 @@ body.dark-mode .project-item:hover { background: rgba(255,255,255,0.03); }
|
|||||||
.note-editor-toolbar { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem; padding: 0.35rem; background: var(--bg); border-radius: 0.5rem; border: 1px solid var(--border); }
|
.note-editor-toolbar { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem; padding: 0.35rem; background: var(--bg); border-radius: 0.5rem; border: 1px solid var(--border); }
|
||||||
.note-toolbar-btn { background: none; border: none; cursor: pointer; padding: 4px 7px; border-radius: 4px; color: var(--text-light); font-size: 0.85rem; transition: background 0.15s, color 0.15s; }
|
.note-toolbar-btn { background: none; border: none; cursor: pointer; padding: 4px 7px; border-radius: 4px; color: var(--text-light); font-size: 0.85rem; transition: background 0.15s, color 0.15s; }
|
||||||
.note-toolbar-btn:hover { background: rgba(var(--primary-rgb),0.1); color: var(--primary); }
|
.note-toolbar-btn:hover { background: rgba(var(--primary-rgb),0.1); color: var(--primary); }
|
||||||
.note-content-area { min-height: 160px; border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem; outline: none; font-size: 0.9rem; line-height: 1.6; background: var(--input-bg); color: var(--text); resize: vertical; }
|
.note-content-area { width: 100%; min-height: 160px; border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem; outline: none; font-size: 0.9rem; line-height: 1.6; background: var(--input-bg); color: var(--text); resize: vertical; }
|
||||||
.note-content-area:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--primary-rgb),0.1); }
|
.note-content-area:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--primary-rgb),0.1); }
|
||||||
|
.note-modal-compact { padding: 1rem 1.1rem; }
|
||||||
|
.note-modal-compact .form-group { margin-bottom: 0.75rem; }
|
||||||
|
.note-modal-compact .note-editor-toolbar { margin-bottom: 0.45rem; padding: 0.28rem; }
|
||||||
|
.note-modal-compact .note-toolbar-btn { padding: 3px 6px; font-size: 0.82rem; }
|
||||||
|
.note-modal-compact .form-control { padding: 0.55rem 0.68rem; font-size: 0.88rem; }
|
||||||
|
.note-modal-compact .note-content-area { min-height: 200px; }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.note-modal-top-fields { grid-template-columns: 1fr !important; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Note color palette */
|
/* Note color palette */
|
||||||
.note-color-palette { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.5rem; }
|
.note-color-palette { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.5rem; }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<h1>Welcome Back</h1>
|
<h1>Login</h1>
|
||||||
<?php if($error): ?><div class="alert"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
<?php if($error): ?><div class="alert"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -23,7 +23,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" style="width:100%;">Sign In</button>
|
<button type="submit" class="btn" style="width:100%;">Sign In</button>
|
||||||
</form>
|
</form>
|
||||||
|
<!--
|
||||||
<p style="text-align:center; margin-top:1rem; font-size:0.875rem; color:var(--text-light);">First time? Run <a href="?action=setup">setup</a> to install Database.</p>
|
<p style="text-align:center; margin-top:1rem; font-size:0.875rem; color:var(--text-light);">First time? Run <a href="?action=setup">setup</a> to install Database.</p>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
try { $pdo->exec("ALTER TABLE notes ADD COLUMN color TEXT DEFAULT NULL"); } catch(Exception $e) {}
|
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 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 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 {
|
try {
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS notes (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS notes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -40,8 +41,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) $image_path = $dest;
|
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) VALUES (?, ?, ?, ?, ?)");
|
$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]);
|
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $user_id, $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($_POST['update_note'])) {
|
if(isset($_POST['update_note'])) {
|
||||||
@@ -64,8 +65,8 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if($image_path && file_exists($image_path)) @unlink($image_path);
|
if($image_path && file_exists($image_path)) @unlink($image_path);
|
||||||
$image_path = null;
|
$image_path = null;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("UPDATE notes SET title=?, content=?, color=?, image_path=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND owner_id=?");
|
$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, $_POST['note_id'], $user_id]);
|
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $_POST['color'] ?? null, $image_path, trim($_POST['label_name'] ?? '') ?: null, $_POST['note_id'], $user_id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +112,12 @@ $notes = $stmt->fetchAll();
|
|||||||
|
|
||||||
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
|
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
|
||||||
$all_groups = query("SELECT * FROM groups")->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
|
// Define note colors — Google Keep inspired
|
||||||
$note_colors = [
|
$note_colors = [
|
||||||
@@ -122,31 +129,53 @@ $note_colors = [
|
|||||||
'#b3e5fc' => ['bg'=>'#b3e5fc', 'label'=>'Sky', 'hex'=>'#b3e5fc'],
|
'#b3e5fc' => ['bg'=>'#b3e5fc', 'label'=>'Sky', 'hex'=>'#b3e5fc'],
|
||||||
'#cce5ff' => ['bg'=>'#cce5ff', 'label'=>'Blue', 'hex'=>'#cce5ff'],
|
'#cce5ff' => ['bg'=>'#cce5ff', 'label'=>'Blue', 'hex'=>'#cce5ff'],
|
||||||
'#e2d9f3' => ['bg'=>'#e2d9f3', 'label'=>'Lavender', 'hex'=>'#e2d9f3'],
|
'#e2d9f3' => ['bg'=>'#e2d9f3', 'label'=>'Lavender', 'hex'=>'#e2d9f3'],
|
||||||
|
'#f3e5f5' => ['bg'=>'#f3e5f5', 'label'=>'Orchid', 'hex'=>'#f3e5f5'],
|
||||||
'#f8d7da' => ['bg'=>'#f8d7da', 'label'=>'Rose', 'hex'=>'#f8d7da'],
|
'#f8d7da' => ['bg'=>'#f8d7da', 'label'=>'Rose', 'hex'=>'#f8d7da'],
|
||||||
'#ef9a9a' => ['bg'=>'#ef9a9a', 'label'=>'Red', 'hex'=>'#ef9a9a'],
|
'#ef9a9a' => ['bg'=>'#ef9a9a', 'label'=>'Red', 'hex'=>'#ef9a9a'],
|
||||||
'#ffcc80' => ['bg'=>'#ffcc80', 'label'=>'Peach', 'hex'=>'#ffcc80'],
|
'#ffcc80' => ['bg'=>'#ffcc80', 'label'=>'Peach', 'hex'=>'#ffcc80'],
|
||||||
'#fde8d8' => ['bg'=>'#fde8d8', 'label'=>'Orange', 'hex'=>'#fde8d8'],
|
'#fde8d8' => ['bg'=>'#fde8d8', 'label'=>'Orange', 'hex'=>'#fde8d8'],
|
||||||
'#d7ccc8' => ['bg'=>'#d7ccc8', 'label'=>'Sand', 'hex'=>'#d7ccc8'],
|
'#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:flex-end; align-items:center; margin-bottom:1rem;">
|
<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')">
|
<button class="btn" onclick="document.getElementById('new-note-modal').classList.add('active')">
|
||||||
<i class="fa fa-plus"></i> New Note
|
<i class="fa fa-plus"></i> New Note
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ── New Note Modal ─────────────────────────────────────────────────────── -->
|
<!-- ── New Note Modal ─────────────────────────────────────────────────────── -->
|
||||||
<div class="modal-overlay" id="new-note-modal">
|
<div class="modal-overlay" id="new-note-modal">
|
||||||
<div class="modal-content" style="max-width:580px; max-height:92vh; overflow-y:auto; padding:1.5rem;">
|
<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;">
|
<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>
|
<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>
|
<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>
|
</div>
|
||||||
<form method="POST" enctype="multipart/form-data" id="new-note-form">
|
<form method="POST" enctype="multipart/form-data" id="new-note-form">
|
||||||
<input type="hidden" name="create_note" value="1">
|
<input type="hidden" name="create_note" value="1">
|
||||||
<div class="form-group">
|
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
|
||||||
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
|
<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>
|
</div>
|
||||||
<!-- WYSIWYG Toolbar -->
|
<!-- WYSIWYG Toolbar -->
|
||||||
<div class="note-editor-toolbar" id="new-note-toolbar">
|
<div class="note-editor-toolbar" id="new-note-toolbar">
|
||||||
@@ -196,13 +225,21 @@ $note_colors = [
|
|||||||
|
|
||||||
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
|
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
|
||||||
<div class="modal-overlay" id="edit-note-modal">
|
<div class="modal-overlay" id="edit-note-modal">
|
||||||
<div class="modal-content" style="max-width:600px; max-height:90vh; overflow-y:auto;">
|
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:90vh; overflow-y:auto;">
|
||||||
<h3 style="margin-bottom:1rem;">Edit Note</h3>
|
<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">
|
<form method="POST" enctype="multipart/form-data" id="edit-note-form">
|
||||||
<input type="hidden" name="update_note" value="1">
|
<input type="hidden" name="update_note" value="1">
|
||||||
<input type="hidden" name="note_id" id="edit-note-id">
|
<input type="hidden" name="note_id" id="edit-note-id">
|
||||||
<div class="form-group">
|
<div class="note-modal-top-fields" style="display:grid; grid-template-columns:2fr 1fr; gap:0.75rem;">
|
||||||
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
|
<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>
|
||||||
<div class="note-editor-toolbar">
|
<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','bold')"><b>B</b></button>
|
||||||
@@ -307,11 +344,13 @@ $note_colors = [
|
|||||||
$preview = strip_tags(isset($n['content']) ? (function_exists('marked') ? $n['content'] : $n['content']) : '');
|
$preview = strip_tags(isset($n['content']) ? (function_exists('marked') ? $n['content'] : $n['content']) : '');
|
||||||
$preview = mb_substr($preview, 0, 120) . (mb_strlen($preview) > 120 ? '...' : '');
|
$preview = mb_substr($preview, 0, 120) . (mb_strlen($preview) > 120 ? '...' : '');
|
||||||
?>
|
?>
|
||||||
<div class="note-card"
|
<div class="note-card note-filter-item"
|
||||||
style="background:<?= htmlspecialchars($bg) ?>;"
|
style="background:<?= htmlspecialchars($bg) ?>;"
|
||||||
data-id="<?= $n['id'] ?>"
|
data-id="<?= $n['id'] ?>"
|
||||||
data-title="<?= htmlspecialchars(addslashes((string)$n['title'])) ?>"
|
data-title="<?= htmlspecialchars(addslashes((string)$n['title'])) ?>"
|
||||||
data-content="<?= htmlspecialchars(addslashes((string)$n['content'])) ?>"
|
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-color="<?= htmlspecialchars((string)$n['color']) ?>"
|
||||||
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
|
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
|
||||||
data-mine="<?= $n['is_mine'] ?>"
|
data-mine="<?= $n['is_mine'] ?>"
|
||||||
@@ -322,6 +361,11 @@ $note_colors = [
|
|||||||
<?php if($n['title']): ?>
|
<?php if($n['title']): ?>
|
||||||
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
|
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
|
||||||
<?php endif; ?>
|
<?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-preview"><?= htmlspecialchars($preview) ?></div>
|
||||||
<div class="note-card-footer">
|
<div class="note-card-footer">
|
||||||
<div style="display:flex; align-items:center; gap:0.4rem;">
|
<div style="display:flex; align-items:center; gap:0.4rem;">
|
||||||
@@ -403,12 +447,14 @@ function openNote(card) {
|
|||||||
const title = card.dataset.title;
|
const title = card.dataset.title;
|
||||||
const content = card.dataset.content;
|
const content = card.dataset.content;
|
||||||
const color = card.dataset.color;
|
const color = card.dataset.color;
|
||||||
|
const label = card.dataset.labelRaw || '';
|
||||||
const image = card.dataset.image;
|
const image = card.dataset.image;
|
||||||
const isMine = card.dataset.mine === '1';
|
const isMine = card.dataset.mine === '1';
|
||||||
|
|
||||||
document.getElementById('edit-note-id').value = id;
|
document.getElementById('edit-note-id').value = id;
|
||||||
document.getElementById('edit-note-title').value = title;
|
document.getElementById('edit-note-title').value = title;
|
||||||
document.getElementById('edit-note-content').value = content;
|
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-color-input').value = color;
|
||||||
document.getElementById('edit-note-preview').style.display = 'none';
|
document.getElementById('edit-note-preview').style.display = 'none';
|
||||||
document.getElementById('edit-note-content').style.display = 'block';
|
document.getElementById('edit-note-content').style.display = 'block';
|
||||||
@@ -449,6 +495,19 @@ function openNote(card) {
|
|||||||
document.getElementById('edit-note-modal').classList.add('active');
|
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) {
|
function deleteNote(id) {
|
||||||
if(!confirm('Delete this note permanently?')) return;
|
if(!confirm('Delete this note permanently?')) return;
|
||||||
document.getElementById('delete-note-id').value = id;
|
document.getElementById('delete-note-id').value = id;
|
||||||
|
|||||||
@@ -217,11 +217,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
|||||||
<input type="hidden" name="delete_password" value="1">
|
<input type="hidden" name="delete_password" value="1">
|
||||||
<input type="hidden" name="password_id" id="delete-pw-id">
|
<input type="hidden" name="password_id" id="delete-pw-id">
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" id="remove-share-form" style="display:none;">
|
||||||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; flex-wrap:wrap; margin-bottom:1rem;">
|
<input type="hidden" name="remove_password_share" value="1">
|
||||||
<button class="btn btn-secondary" onclick="showAddFolderPrompt()"><i class="fa fa-folder-plus"></i> New Folder</button>
|
<input type="hidden" name="share_id" id="remove-share-id">
|
||||||
<button class="btn" onclick="openAddPassword()"><i class="fa fa-plus"></i> Add Entry</button>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add folder form (hidden) -->
|
<!-- Add folder form (hidden) -->
|
||||||
<form method="POST" id="add-folder-form" style="display:none;">
|
<form method="POST" id="add-folder-form" style="display:none;">
|
||||||
@@ -234,7 +233,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="background:var(--surface); border-radius:1rem; box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border); overflow:hidden;">
|
<div style="background:var(--surface); border-radius:1rem; box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border); overflow:hidden;">
|
||||||
<div class="vault-layout" style="min-height:calc(100vh - 200px);">
|
<div class="vault-layout" style="min-height:calc(100vh - 130px);">
|
||||||
<!-- Vault Sidebar (Folders) -->
|
<!-- Vault Sidebar (Folders) -->
|
||||||
<div class="vault-sidebar">
|
<div class="vault-sidebar">
|
||||||
<div style="font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light); font-weight:700; padding:0.25rem 0.65rem; margin-bottom:0.25rem;">Folders</div>
|
<div style="font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light); font-weight:700; padding:0.25rem 0.65rem; margin-bottom:0.25rem;">Folders</div>
|
||||||
@@ -265,6 +264,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
|||||||
|
|
||||||
<!-- Vault Main -->
|
<!-- Vault Main -->
|
||||||
<div class="vault-main">
|
<div class="vault-main">
|
||||||
|
<div style="display:flex; justify-content:flex-end; gap:0.5rem; flex-wrap:wrap; margin-bottom:1rem;">
|
||||||
|
<button class="btn btn-secondary" onclick="showAddFolderPrompt()"><i class="fa fa-folder-plus"></i> New Folder</button>
|
||||||
|
<button class="btn" onclick="openAddPassword()"><i class="fa fa-plus"></i> Add Entry</button>
|
||||||
|
</div>
|
||||||
<!-- Search + Bulk Actions -->
|
<!-- Search + Bulk Actions -->
|
||||||
<div class="vault-search">
|
<div class="vault-search">
|
||||||
<div style="position:relative; flex:1;">
|
<div style="position:relative; flex:1;">
|
||||||
@@ -347,11 +350,20 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
|||||||
$domain = preg_replace('/^www\./', '', $domain);
|
$domain = preg_replace('/^www\./', '', $domain);
|
||||||
}
|
}
|
||||||
$favicon = $domain ? "https://www.google.com/s2/favicons?domain={$domain}&sz=32" : '';
|
$favicon = $domain ? "https://www.google.com/s2/favicons?domain={$domain}&sz=32" : '';
|
||||||
|
$pw_shares = [];
|
||||||
|
if($p['is_mine']) {
|
||||||
|
$pw_shares = query("SELECT ps.id, ps.share_type, ps.share_id, u.username, g.name as gname FROM password_shares ps LEFT JOIN users u ON (ps.share_type='user' AND ps.share_id=u.id) LEFT JOIN groups g ON (ps.share_type='group' AND ps.share_id=g.id) WHERE ps.password_id=?", [$p['id']])->fetchAll();
|
||||||
|
}
|
||||||
|
$shares_json = htmlspecialchars(json_encode($pw_shares), ENT_QUOTES, 'UTF-8');
|
||||||
?>
|
?>
|
||||||
<div class="vault-entry pw-row"
|
<div class="vault-entry pw-row"
|
||||||
data-pw="<?= htmlspecialchars(addslashes($p['password'])) ?>"
|
data-pw="<?= htmlspecialchars(addslashes($p['password'])) ?>"
|
||||||
data-usr="<?= htmlspecialchars((string)$p['username']) ?>"
|
data-usr="<?= htmlspecialchars((string)$p['username']) ?>"
|
||||||
data-id="<?= $p['id'] ?>"
|
data-id="<?= $p['id'] ?>"
|
||||||
|
data-url="<?= htmlspecialchars((string)($p['url'] ?? '')) ?>"
|
||||||
|
data-notes="<?= htmlspecialchars(addslashes((string)($p['notes'] ?? ''))) ?>"
|
||||||
|
data-folder-id="<?= htmlspecialchars((string)($p['folder_id'] ?? '')) ?>"
|
||||||
|
data-shares="<?= $shares_json ?>"
|
||||||
onclick="openViewEntry(this)">
|
onclick="openViewEntry(this)">
|
||||||
<!-- Checkbox (only for owned items) -->
|
<!-- Checkbox (only for owned items) -->
|
||||||
<?php if($p['is_mine']): ?>
|
<?php if($p['is_mine']): ?>
|
||||||
@@ -463,8 +475,8 @@ function openViewEntry(row) {
|
|||||||
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
|
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
|
||||||
const urlEl = document.getElementById('pw-view-url');
|
const urlEl = document.getElementById('pw-view-url');
|
||||||
urlEl.textContent = '';
|
urlEl.textContent = '';
|
||||||
// try to show url from data attribute if we add it - just show username for now
|
urlEl.textContent = row.dataset.url || '';
|
||||||
document.getElementById('pw-view-notes').textContent = '';
|
document.getElementById('pw-view-notes').textContent = row.dataset.notes || '';
|
||||||
document.getElementById('password-view-modal').classList.add('active');
|
document.getElementById('password-view-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +504,8 @@ function openAddPassword() {
|
|||||||
document.getElementById('pw-edit-url').value = '';
|
document.getElementById('pw-edit-url').value = '';
|
||||||
document.getElementById('pw-edit-notes').value = '';
|
document.getElementById('pw-edit-notes').value = '';
|
||||||
document.getElementById('pw-edit-folder').value = '';
|
document.getElementById('pw-edit-folder').value = '';
|
||||||
|
document.getElementById('pw-shares-section').style.display = 'none';
|
||||||
|
document.getElementById('pw-shares-list').innerHTML = '';
|
||||||
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
|
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
|
||||||
document.getElementById('pw-edit-pw').dispatchEvent(new Event('input'));
|
document.getElementById('pw-edit-pw').dispatchEvent(new Event('input'));
|
||||||
document.getElementById('password-edit-modal').classList.add('active');
|
document.getElementById('password-edit-modal').classList.add('active');
|
||||||
@@ -517,13 +531,48 @@ function openEditEntry(row) {
|
|||||||
document.getElementById('pw-edit-uname').value = row.dataset.usr;
|
document.getElementById('pw-edit-uname').value = row.dataset.usr;
|
||||||
document.getElementById('pw-edit-pw').value = row.dataset.pw;
|
document.getElementById('pw-edit-pw').value = row.dataset.pw;
|
||||||
document.getElementById('pw-edit-pw').type = 'password';
|
document.getElementById('pw-edit-pw').type = 'password';
|
||||||
document.getElementById('pw-edit-url').value = '';
|
document.getElementById('pw-edit-url').value = row.dataset.url || '';
|
||||||
document.getElementById('pw-edit-notes').value = '';
|
document.getElementById('pw-edit-notes').value = row.dataset.notes || '';
|
||||||
document.getElementById('pw-edit-folder').value = '';
|
document.getElementById('pw-edit-folder').value = row.dataset.folderId || '';
|
||||||
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
|
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
|
||||||
|
renderPasswordShares(row.dataset.shares || '[]');
|
||||||
document.getElementById('password-edit-modal').classList.add('active');
|
document.getElementById('password-edit-modal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPasswordShares(jsonShares) {
|
||||||
|
const sec = document.getElementById('pw-shares-section');
|
||||||
|
const list = document.getElementById('pw-shares-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
let shares = [];
|
||||||
|
try { shares = JSON.parse(jsonShares || '[]'); } catch(e) { shares = []; }
|
||||||
|
if(!shares || shares.length === 0) {
|
||||||
|
sec.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shares.forEach(sh => {
|
||||||
|
const label = sh.share_type === 'user' ? ('👤 ' + (sh.username || 'User')) : ('👥 ' + (sh.gname || 'Group'));
|
||||||
|
const chip = document.createElement('div');
|
||||||
|
chip.style.display = 'inline-flex';
|
||||||
|
chip.style.alignItems = 'center';
|
||||||
|
chip.style.gap = '0.35rem';
|
||||||
|
chip.style.fontSize = '0.72rem';
|
||||||
|
chip.style.padding = '3px 7px';
|
||||||
|
chip.style.borderRadius = '999px';
|
||||||
|
chip.style.background = 'rgba(var(--primary-rgb),0.12)';
|
||||||
|
chip.style.color = 'var(--primary)';
|
||||||
|
chip.innerHTML = `<span>${label}</span><button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;" title="Unshare" onclick="removePasswordShare(${Number(sh.id)})">×</button>`;
|
||||||
|
list.appendChild(chip);
|
||||||
|
});
|
||||||
|
sec.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePasswordShare(shareId) {
|
||||||
|
if(!shareId) return;
|
||||||
|
if(!confirm('Unshare this password access?')) return;
|
||||||
|
document.getElementById('remove-share-id').value = String(shareId);
|
||||||
|
document.getElementById('remove-share-form').submit();
|
||||||
|
}
|
||||||
|
|
||||||
function deletePassword() {
|
function deletePassword() {
|
||||||
if(!confirm('Delete this entry permanently?')) return;
|
if(!confirm('Delete this entry permanently?')) return;
|
||||||
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
|
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
|
||||||
|
|||||||
@@ -138,17 +138,6 @@ function get_file_icon($name, $is_folder) {
|
|||||||
<input type="hidden" name="item_id" id="delete-item-id">
|
<input type="hidden" name="item_id" id="delete-item-id">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="header" style="margin-bottom:0.75rem;">
|
|
||||||
<h1 style="font-size:1.5rem; font-weight:700;">📁 Project Explorer</h1>
|
|
||||||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
|
||||||
<label class="btn btn-secondary" style="cursor:pointer;">
|
|
||||||
<i class="fa fa-arrow-up-from-bracket"></i> Upload File
|
|
||||||
<input type="file" id="file-upload-input" style="display:none;" onchange="handleFileSelect(this)">
|
|
||||||
</label>
|
|
||||||
<button class="btn" onclick="showNewFolderDialog()"><i class="fa fa-folder-plus"></i> New Folder</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden folder create form -->
|
<!-- Hidden folder create form -->
|
||||||
<form method="POST" id="new-folder-form" style="display:none;">
|
<form method="POST" id="new-folder-form" style="display:none;">
|
||||||
<input type="hidden" name="create_folder" value="1">
|
<input type="hidden" name="create_folder" value="1">
|
||||||
@@ -156,7 +145,7 @@ function get_file_icon($name, $is_folder) {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Explorer Toolbar / Breadcrumb -->
|
<!-- Explorer Toolbar / Breadcrumb -->
|
||||||
<div class="explorer-toolbar">
|
<div class="explorer-toolbar" style="margin-bottom:0.75rem;">
|
||||||
<a href="?action=projects" class="btn btn-secondary btn-sm" title="Home">
|
<a href="?action=projects" class="btn btn-secondary btn-sm" title="Home">
|
||||||
<i class="fa fa-house"></i>
|
<i class="fa fa-house"></i>
|
||||||
</a>
|
</a>
|
||||||
@@ -173,21 +162,24 @@ function get_file_icon($name, $is_folder) {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search -->
|
|
||||||
<input type="text" id="explorer-search" placeholder="Search..." class="form-control" style="width:180px; padding:0.4rem 0.6rem; font-size:0.85rem;" oninput="filterExplorer(this.value)">
|
<input type="text" id="explorer-search" placeholder="Search..." class="form-control" style="width:180px; padding:0.4rem 0.6rem; font-size:0.85rem;" oninput="filterExplorer(this.value)">
|
||||||
|
<label class="btn btn-secondary btn-sm" style="cursor:pointer;">
|
||||||
|
<i class="fa fa-arrow-up-from-bracket"></i> Upload File
|
||||||
|
<input type="file" id="file-upload-input" style="display:none;" onchange="handleFileSelect(this)">
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-sm" onclick="showNewFolderDialog()"><i class="fa fa-folder-plus"></i> New Folder</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drop Zone -->
|
<div id="drag-upload-overlay" style="position:fixed; inset:0; z-index:1200; display:none; align-items:center; justify-content:center; pointer-events:none; background:rgba(15,23,42,0.45); backdrop-filter:blur(2px);">
|
||||||
<div id="dropzone" class="dropzone" style="padding:1.5rem; margin-bottom:0.75rem;"
|
<div style="background:var(--surface); border:2px dashed var(--primary); border-radius:1rem; padding:2rem 2.5rem; color:var(--text); text-align:center; box-shadow:0 12px 30px rgba(0,0,0,0.2);">
|
||||||
ondragover="this.classList.add('dragover'); return false;"
|
<i class="fa fa-cloud-arrow-up" style="font-size:2rem; margin-bottom:0.6rem; display:block; color:var(--primary);"></i>
|
||||||
ondragleave="this.classList.remove('dragover');"
|
<div style="font-weight:700; margin-bottom:0.2rem;">Drop files to upload</div>
|
||||||
ondrop="handleDrop(event)">
|
<div style="font-size:0.84rem; color:var(--text-light);">Release your file anywhere on this page</div>
|
||||||
<i class="fa fa-cloud-arrow-up" style="font-size:1.5rem; margin-bottom:0.5rem; display:block;"></i>
|
</div>
|
||||||
Drag & drop files here to upload
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail Table -->
|
<!-- Detail Table -->
|
||||||
<div class="card" style="padding:0; overflow:hidden;">
|
<div class="card" style="padding:0; overflow:hidden; min-height:calc(100vh - 190px);">
|
||||||
<table class="explorer-table" id="explorer-table">
|
<table class="explorer-table" id="explorer-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -305,7 +297,7 @@ function handleFileSelect(input) {
|
|||||||
|
|
||||||
function handleDrop(e) {
|
function handleDrop(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.currentTarget.classList.remove('dragover');
|
hideDragOverlay();
|
||||||
if(e.dataTransfer.files.length > 0) uploadFile(e.dataTransfer.files[0]);
|
if(e.dataTransfer.files.length > 0) uploadFile(e.dataTransfer.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +310,35 @@ function uploadFile(file) {
|
|||||||
.then(() => window.location.reload());
|
.then(() => window.location.reload());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dragCounter = 0;
|
||||||
|
const dragOverlay = document.getElementById('drag-upload-overlay');
|
||||||
|
function showDragOverlay() {
|
||||||
|
if(dragOverlay) dragOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
function hideDragOverlay() {
|
||||||
|
if(dragOverlay) dragOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
document.addEventListener('dragenter', (e) => {
|
||||||
|
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
|
||||||
|
dragCounter++;
|
||||||
|
showDragOverlay();
|
||||||
|
});
|
||||||
|
document.addEventListener('dragover', (e) => {
|
||||||
|
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
|
||||||
|
dragCounter = Math.max(0, dragCounter - 1);
|
||||||
|
if(dragCounter === 0) hideDragOverlay();
|
||||||
|
});
|
||||||
|
document.addEventListener('drop', (e) => {
|
||||||
|
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter = 0;
|
||||||
|
handleDrop(e);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Select all ────────────────────────────────────────────────────────────────
|
// ── Select all ────────────────────────────────────────────────────────────────
|
||||||
function explorerSelectAll(chk) {
|
function explorerSelectAll(chk) {
|
||||||
document.querySelectorAll('.explorer-check').forEach(c => c.checked = chk.checked);
|
document.querySelectorAll('.explorer-check').forEach(c => c.checked = chk.checked);
|
||||||
|
|||||||
Reference in New Issue
Block a user