feat: add dev tools page and update sidebar toggle and team mood dashboard UI
This commit is contained in:
@@ -268,11 +268,11 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
<div class="db-stat-link">Open Vault →</div>
|
||||
</a>
|
||||
|
||||
<!-- Projects -->
|
||||
<!-- Files -->
|
||||
<a href="?action=projects" class="db-stat-card" style="border-top:3px solid #F59E0B;">
|
||||
<div class="db-stat-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
|
||||
Projects & Files
|
||||
Files
|
||||
</div>
|
||||
<div class="db-stat-value" style="color:#F59E0B;"><?= $projects_count ?></div>
|
||||
<div class="db-stat-sub">
|
||||
@@ -295,36 +295,45 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ── Team Moods + Activity Log ───────────────────────────────────────────── -->
|
||||
<div class="two-col">
|
||||
|
||||
<!-- Team Moods -->
|
||||
<div class="card">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<h3 style="font-size:1rem; font-weight:700;">👥 Team Mood</h3>
|
||||
<span style="font-size:0.75rem; color:var(--text-light); font-style:italic;">Live from colleagues</span>
|
||||
</div>
|
||||
<?php if(empty($team_moods)): ?>
|
||||
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="team-mood-row">
|
||||
<?php foreach($team_moods as $tm): ?>
|
||||
<div class="team-mood-chip" title="Updated: <?= date('M j, H:i', strtotime($tm['mood_updated_at'])) ?>">
|
||||
<span class="em"><?= $tm['mood'] ?></span>
|
||||
<span><?= htmlspecialchars($tm['username']) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if($my_mood): ?>
|
||||
<div style="margin-top:1rem; padding-top:0.75rem; border-top:1px dashed var(--border); font-size:0.8rem; color:var(--text-light);">
|
||||
Your mood is visible to your team: <strong style="color:var(--text);"><?= $my_mood ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- ── Team Moods ───────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-bottom:1.5rem;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<h3 style="font-size:1rem; font-weight:700;">👥 Team Mood</h3>
|
||||
<span style="font-size:0.75rem; color:var(--text-light); font-style:italic;">Live from colleagues</span>
|
||||
</div>
|
||||
<?php if(empty($team_moods)): ?>
|
||||
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="team-mood-row" style="display:flex; flex-wrap:nowrap; overflow-x:auto; gap:1rem; padding-bottom:0.5rem;">
|
||||
<?php
|
||||
$avatar_colors_tm = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
|
||||
foreach($team_moods as $tm):
|
||||
$initial = strtoupper(mb_substr($tm['username'], 0, 1));
|
||||
$color = $avatar_colors_tm[crc32($tm['username']) % count($avatar_colors_tm)];
|
||||
?>
|
||||
<div style="position:relative; flex-shrink:0; cursor:pointer;" title="<?= htmlspecialchars($tm['username']) ?> (Updated: <?= date('M j, H:i', strtotime($tm['mood_updated_at'])) ?>)">
|
||||
<div style="width:40px; height:40px; border-radius:50%; background:<?= $color ?>; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:1rem;">
|
||||
<?= $initial ?>
|
||||
</div>
|
||||
<div style="position:absolute; top:-4px; right:-4px; background:var(--surface); border-radius:50%; width:20px; height:20px; display:flex; align-items:center; justify-content:center; font-size:0.8rem; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
|
||||
<?= $tm['mood'] ?>
|
||||
</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-light); text-align:center; margin-top:4px; max-width:40px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||||
<?= htmlspecialchars($tm['username']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if($my_mood): ?>
|
||||
<div style="margin-top:0.5rem; font-size:0.8rem; color:var(--text-light);">
|
||||
Your mood is visible to your team: <strong style="color:var(--text);"><?= $my_mood ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Activity Log -->
|
||||
<div class="card">
|
||||
<!-- ── Activity Log ───────────────────────────────────────────── -->
|
||||
<div class="card" style="flex:1; display:flex; flex-direction:column; min-height:0;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<div style="display:flex; align-items:center; gap:0.65rem;">
|
||||
<h3 style="font-size:1rem; font-weight:700;">🕓 Recent Activity</h3>
|
||||
@@ -393,7 +402,6 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><!-- .db-wrapper -->
|
||||
|
||||
<script>
|
||||
@@ -401,7 +409,7 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const el = document.getElementById('current-datetime');
|
||||
if (el) el.textContent = now.toLocaleString('id-ID', {
|
||||
if (el) el.textContent = now.toLocaleString('en-US', {
|
||||
weekday:'long', year:'numeric', month:'long', day:'numeric',
|
||||
hour:'2-digit', minute:'2-digit', second:'2-digit'
|
||||
});
|
||||
|
||||
@@ -163,9 +163,13 @@
|
||||
<i class="fa fa-lock nav-icon"></i>
|
||||
<span class="sidebar-label">Passwords</span>
|
||||
</a>
|
||||
<a href="?action=projects" class="nav-link <?= $action=='projects'?'active':'' ?>" title="Projects">
|
||||
<a href="?action=projects" class="nav-link <?= $action=='projects'?'active':'' ?>" title="Files">
|
||||
<i class="fa fa-folder-open nav-icon"></i>
|
||||
<span class="sidebar-label">Projects</span>
|
||||
<span class="sidebar-label">Files</span>
|
||||
</a>
|
||||
<a href="?action=tools" class="nav-link <?= $action=='tools'?'active':'' ?>" title="Tools">
|
||||
<i class="fa fa-toolbox nav-icon"></i>
|
||||
<span class="sidebar-label">Tools</span>
|
||||
</a>
|
||||
<?php if(!empty($_SESSION['is_admin'])): ?>
|
||||
<a href="?action=admin" class="nav-link <?= $action=='admin'?'active':'' ?>" title="Admin Panel">
|
||||
|
||||
@@ -91,6 +91,14 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
else notify_group($parts[1], $msg);
|
||||
}
|
||||
}
|
||||
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
@@ -287,7 +295,8 @@ $note_colors = [
|
||||
|
||||
<!-- 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>
|
||||
<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): ?>
|
||||
@@ -327,6 +336,11 @@ $note_colors = [
|
||||
<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>
|
||||
|
||||
@@ -343,6 +357,11 @@ $note_colors = [
|
||||
$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, g.name as gname 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) ?>;"
|
||||
@@ -353,6 +372,7 @@ $note_colors = [
|
||||
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'] ?>"
|
||||
onclick="openNote(this)">
|
||||
<?php if($n['image_path'] && file_exists($n['image_path'])): ?>
|
||||
@@ -479,6 +499,9 @@ function openNote(card) {
|
||||
|
||||
// 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"]');
|
||||
@@ -495,6 +518,35 @@ function openNote(card) {
|
||||
document.getElementById('edit-note-modal').classList.add('active');
|
||||
}
|
||||
|
||||
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 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="removeNoteShare(${Number(sh.id)})">×</button>`;
|
||||
list.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function removeNoteShare(shareId) {
|
||||
if(!shareId) return;
|
||||
if(!confirm('Unshare this note access?')) return;
|
||||
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();
|
||||
|
||||
@@ -41,6 +41,14 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_project'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_project_share'])) {
|
||||
$share_id = (int)$_POST['share_id'];
|
||||
$chk = query("SELECT ps.id FROM project_shares ps JOIN projects p ON ps.project_id=p.id WHERE ps.id=? AND p.owner_id=?", [$share_id, $user_id])->fetch();
|
||||
if($chk) {
|
||||
$pdo->prepare("DELETE FROM project_shares WHERE id=?")->execute([$share_id]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fetch current folder content ──────────────────────────────────────────────
|
||||
$user_groups = get_user_groups($user_id);
|
||||
$my_group_ids = array_column($user_groups, 'id');
|
||||
@@ -138,6 +146,11 @@ function get_file_icon($name, $is_folder) {
|
||||
<input type="hidden" name="item_id" id="delete-item-id">
|
||||
</form>
|
||||
|
||||
<form method="POST" id="remove-share-form" style="display:none;">
|
||||
<input type="hidden" name="remove_project_share" value="1">
|
||||
<input type="hidden" name="share_id" id="remove-share-id">
|
||||
</form>
|
||||
|
||||
<!-- Hidden folder create form -->
|
||||
<form method="POST" id="new-folder-form" style="display:none;">
|
||||
<input type="hidden" name="create_folder" value="1">
|
||||
@@ -227,10 +240,13 @@ function get_file_icon($name, $is_folder) {
|
||||
<td style="font-size:0.8rem; color:var(--text-light);">
|
||||
<?php
|
||||
// Show shares
|
||||
$shares = query("SELECT ps.share_type, ps.share_id, u.username, g.name as gname FROM project_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.project_id=?", [$item['id']])->fetchAll();
|
||||
$shares = query("SELECT ps.id, ps.share_type, ps.share_id, u.username, g.name as gname FROM project_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.project_id=?", [$item['id']])->fetchAll();
|
||||
foreach($shares as $sh) {
|
||||
echo '<span style="font-size:0.72rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 5px; border-radius:8px; margin:1px; display:inline-block;">';
|
||||
echo '<span style="font-size:0.72rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 5px; border-radius:8px; margin:1px; display:inline-flex; align-items:center; gap:3px;">';
|
||||
echo $sh['share_type']==='user' ? ('👤 '.htmlspecialchars($sh['username']??'')) : ('👥 '.htmlspecialchars($sh['gname']??''));
|
||||
if($item['is_mine']) {
|
||||
echo '<button type="button" style="border:none;background:transparent;color:inherit;cursor:pointer;line-height:1;" title="Unshare" onclick="removeProjectShare('.$sh['id'].')">×</button>';
|
||||
}
|
||||
echo '</span>';
|
||||
}
|
||||
if(!$shares) echo '<span style="color:var(--text-light); font-size:0.8rem;">—</span>';
|
||||
@@ -283,6 +299,12 @@ function openShare(id) {
|
||||
document.getElementById('share-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function removeProjectShare(shareId) {
|
||||
if(!confirm('Unshare this item?')) return;
|
||||
document.getElementById('remove-share-id').value = shareId;
|
||||
document.getElementById('remove-share-form').submit();
|
||||
}
|
||||
|
||||
function showNewFolderDialog() {
|
||||
const name = prompt('New folder name:');
|
||||
if(name && name.trim()) {
|
||||
|
||||
74
views/tools.php
Normal file
74
views/tools.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// Tools page
|
||||
?>
|
||||
<div class="header">
|
||||
<h1 style="font-size:1.5rem; font-weight:700;"><i class="fa fa-toolbox" style="color:var(--primary);"></i> Tools</h1>
|
||||
</div>
|
||||
|
||||
<div class="db-grid">
|
||||
<!-- Base64 encode/decode -->
|
||||
<a href="#" class="db-stat-card" style="border-top:3px solid #6366F1;" onclick="openTool('base64'); return false;">
|
||||
<div class="db-stat-label"><i class="fa fa-code"></i> Base64 En/Decode</div>
|
||||
<div class="db-stat-sub" style="margin-top:auto;"><span style="background:rgba(99,102,241,0.12); color:#6366F1;">Text utility</span></div>
|
||||
</a>
|
||||
|
||||
<!-- UUID Generator -->
|
||||
<a href="#" class="db-stat-card" style="border-top:3px solid #10B981;" onclick="openTool('uuid'); return false;">
|
||||
<div class="db-stat-label"><i class="fa fa-fingerprint"></i> UUID Generator</div>
|
||||
<div class="db-stat-sub" style="margin-top:auto;"><span style="background:rgba(16,185,129,0.12); color:#10B981;">ID utility</span></div>
|
||||
</a>
|
||||
|
||||
<!-- JSON Formatter -->
|
||||
<a href="#" class="db-stat-card" style="border-top:3px solid #F59E0B;" onclick="openTool('json'); return false;">
|
||||
<div class="db-stat-label"><i class="fa fa-code"></i> JSON Formatter</div>
|
||||
<div class="db-stat-sub" style="margin-top:auto;"><span style="background:rgba(245,158,11,0.12); color:#F59E0B;">Dev utility</span></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Modal for tools -->
|
||||
<div class="modal-overlay" id="tool-modal">
|
||||
<div class="modal-content" style="max-width:600px; display:flex; flex-direction:column;">
|
||||
<h3 id="tool-title" style="margin-bottom:1rem; font-size:1.1rem;"></h3>
|
||||
<div id="tool-body" style="flex:1;"></div>
|
||||
<button class="btn btn-secondary" style="margin-top:1rem; width:100%;" onclick="document.getElementById('tool-modal').classList.remove('active')">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTool(tool) {
|
||||
const title = document.getElementById('tool-title');
|
||||
const body = document.getElementById('tool-body');
|
||||
|
||||
if (tool === 'base64') {
|
||||
title.innerHTML = '<i class="fa fa-code"></i> Base64 Encoder/Decoder';
|
||||
body.innerHTML = `
|
||||
<textarea id="b64-input" class="form-control" rows="4" placeholder="Enter text here..."></textarea>
|
||||
<div style="display:flex; gap:0.5rem; margin:1rem 0;">
|
||||
<button class="btn" onclick="document.getElementById('b64-output').value = btoa(unescape(encodeURIComponent(document.getElementById('b64-input').value)))">Encode</button>
|
||||
<button class="btn" onclick="try{document.getElementById('b64-output').value = decodeURIComponent(escape(atob(document.getElementById('b64-input').value)))}catch(e){alert('Invalid base64')}">Decode</button>
|
||||
</div>
|
||||
<textarea id="b64-output" class="form-control" rows="4" placeholder="Result..." readonly></textarea>
|
||||
`;
|
||||
} else if (tool === 'uuid') {
|
||||
title.innerHTML = '<i class="fa fa-fingerprint"></i> UUID Generator';
|
||||
body.innerHTML = `
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:1rem;">
|
||||
<input type="text" id="uuid-output" class="form-control" readonly style="font-family:monospace; font-size:1.1rem; text-align:center;">
|
||||
<button class="btn" onclick="navigator.clipboard.writeText(document.getElementById('uuid-output').value); showToast('Copied!')"><i class="fa fa-copy"></i></button>
|
||||
</div>
|
||||
<button class="btn" style="width:100%;" onclick="document.getElementById('uuid-output').value = crypto.randomUUID()">Generate New UUID</button>
|
||||
`;
|
||||
setTimeout(() => document.getElementById('uuid-output').value = crypto.randomUUID(), 10);
|
||||
} else if (tool === 'json') {
|
||||
title.innerHTML = '<i class="fa fa-code"></i> JSON Formatter';
|
||||
body.innerHTML = `
|
||||
<textarea id="json-input" class="form-control" rows="8" placeholder="Paste JSON here..." style="font-family:monospace;"></textarea>
|
||||
<div style="margin:1rem 0; text-align:right;">
|
||||
<button class="btn" onclick="try{document.getElementById('json-input').value = JSON.stringify(JSON.parse(document.getElementById('json-input').value), null, 4)}catch(e){alert('Invalid JSON')}">Format JSON</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('tool-modal').classList.add('active');
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user