feat: add dev tools page and update sidebar toggle and team mood dashboard UI

This commit is contained in:
root
2026-03-29 15:04:44 +07:00
parent 2a670d80d1
commit f4f842d658
6 changed files with 201 additions and 39 deletions

View File

@@ -47,16 +47,17 @@ a:hover { text-decoration: underline; }
/* Toggle button pinned to right edge */
.sidebar-toggle-btn {
position: absolute; top: 1.5rem; right: -14px;
position: fixed; top: 1.5rem; left: 226px;
width: 28px; height: 28px; border-radius: 50%;
border: 1px solid var(--border); background: var(--surface);
color: var(--text-light); font-size: 0.65rem;
cursor: pointer; display: flex; align-items: center; justify-content: center;
z-index: 20; transition: background 0.15s, color 0.15s, border-color 0.15s;
z-index: 20; transition: left 0.25s cubic-bezier(.4,0,.2,1), background 0.15s, color 0.15s, border-color 0.15s;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.sidebar-toggle-btn:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
/* Nav link: icon always visible, label collapses */
.nav-link {
padding: 0.6rem 0.75rem; border-radius: 0.5rem;
@@ -75,6 +76,7 @@ body.sidebar-collapsed .sidebar { width: 64px; }
body.sidebar-collapsed .sidebar-label { opacity: 0; width: 0; pointer-events: none; }
body.sidebar-collapsed .nav-link { justify-content: center; padding: 0.65rem; gap: 0; }
body.sidebar-collapsed .nav-link .badge { position: absolute; top: 2px; right: 2px; padding: 1px 4px; font-size: 0.6rem; }
body.sidebar-collapsed .sidebar-toggle-btn { left: 50px; }
body.sidebar-collapsed .sidebar-toggle-btn .fa-chevron-left { display: block; }
/* Tooltip on collapsed nav links */

View File

@@ -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'
});

View File

@@ -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">

View File

@@ -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();

View File

@@ -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
View 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>