feat: add dev tools page and update sidebar toggle and team mood dashboard UI
This commit is contained in:
@@ -47,16 +47,17 @@ a:hover { text-decoration: underline; }
|
|||||||
|
|
||||||
/* Toggle button pinned to right edge */
|
/* Toggle button pinned to right edge */
|
||||||
.sidebar-toggle-btn {
|
.sidebar-toggle-btn {
|
||||||
position: absolute; top: 1.5rem; right: -14px;
|
position: fixed; top: 1.5rem; left: 226px;
|
||||||
width: 28px; height: 28px; border-radius: 50%;
|
width: 28px; height: 28px; border-radius: 50%;
|
||||||
border: 1px solid var(--border); background: var(--surface);
|
border: 1px solid var(--border); background: var(--surface);
|
||||||
color: var(--text-light); font-size: 0.65rem;
|
color: var(--text-light); font-size: 0.65rem;
|
||||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
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);
|
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
||||||
}
|
}
|
||||||
.sidebar-toggle-btn:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
|
.sidebar-toggle-btn:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||||
|
|
||||||
|
|
||||||
/* Nav link: icon always visible, label collapses */
|
/* Nav link: icon always visible, label collapses */
|
||||||
.nav-link {
|
.nav-link {
|
||||||
padding: 0.6rem 0.75rem; border-radius: 0.5rem;
|
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 .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 { 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 .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; }
|
body.sidebar-collapsed .sidebar-toggle-btn .fa-chevron-left { display: block; }
|
||||||
|
|
||||||
/* Tooltip on collapsed nav links */
|
/* Tooltip on collapsed nav links */
|
||||||
|
|||||||
@@ -268,11 +268,11 @@ if (empty($_SESSION['dash_logged_today'])) {
|
|||||||
<div class="db-stat-link">Open Vault →</div>
|
<div class="db-stat-link">Open Vault →</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Projects -->
|
<!-- Files -->
|
||||||
<a href="?action=projects" class="db-stat-card" style="border-top:3px solid #F59E0B;">
|
<a href="?action=projects" class="db-stat-card" style="border-top:3px solid #F59E0B;">
|
||||||
<div class="db-stat-label">
|
<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>
|
<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>
|
||||||
<div class="db-stat-value" style="color:#F59E0B;"><?= $projects_count ?></div>
|
<div class="db-stat-value" style="color:#F59E0B;"><?= $projects_count ?></div>
|
||||||
<div class="db-stat-sub">
|
<div class="db-stat-sub">
|
||||||
@@ -295,11 +295,8 @@ if (empty($_SESSION['dash_logged_today'])) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Team Moods + Activity Log ───────────────────────────────────────────── -->
|
<!-- ── Team Moods ───────────────────────────────────────────── -->
|
||||||
<div class="two-col">
|
<div class="card" style="margin-bottom:1.5rem;">
|
||||||
|
|
||||||
<!-- Team Moods -->
|
|
||||||
<div class="card">
|
|
||||||
<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;">👥 Team Mood</h3>
|
<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>
|
<span style="font-size:0.75rem; color:var(--text-light); font-style:italic;">Live from colleagues</span>
|
||||||
@@ -307,24 +304,36 @@ if (empty($_SESSION['dash_logged_today'])) {
|
|||||||
<?php if(empty($team_moods)): ?>
|
<?php if(empty($team_moods)): ?>
|
||||||
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
|
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="team-mood-row">
|
<div class="team-mood-row" style="display:flex; flex-wrap:nowrap; overflow-x:auto; gap:1rem; padding-bottom:0.5rem;">
|
||||||
<?php foreach($team_moods as $tm): ?>
|
<?php
|
||||||
<div class="team-mood-chip" title="Updated: <?= date('M j, H:i', strtotime($tm['mood_updated_at'])) ?>">
|
$avatar_colors_tm = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
|
||||||
<span class="em"><?= $tm['mood'] ?></span>
|
foreach($team_moods as $tm):
|
||||||
<span><?= htmlspecialchars($tm['username']) ?></span>
|
$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>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if($my_mood): ?>
|
<?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);">
|
<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>
|
Your mood is visible to your team: <strong style="color:var(--text);"><?= $my_mood ?></strong>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Activity Log -->
|
<!-- ── Activity Log ───────────────────────────────────────────── -->
|
||||||
<div class="card">
|
<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; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||||
<div style="display:flex; align-items:center; gap:0.65rem;">
|
<div style="display:flex; align-items:center; gap:0.65rem;">
|
||||||
<h3 style="font-size:1rem; font-weight:700;">🕓 Recent Activity</h3>
|
<h3 style="font-size:1rem; font-weight:700;">🕓 Recent Activity</h3>
|
||||||
@@ -393,7 +402,6 @@ if (empty($_SESSION['dash_logged_today'])) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div><!-- .db-wrapper -->
|
</div><!-- .db-wrapper -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -401,7 +409,7 @@ if (empty($_SESSION['dash_logged_today'])) {
|
|||||||
function updateTime() {
|
function updateTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const el = document.getElementById('current-datetime');
|
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',
|
weekday:'long', year:'numeric', month:'long', day:'numeric',
|
||||||
hour:'2-digit', minute:'2-digit', second:'2-digit'
|
hour:'2-digit', minute:'2-digit', second:'2-digit'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,9 +163,13 @@
|
|||||||
<i class="fa fa-lock nav-icon"></i>
|
<i class="fa fa-lock nav-icon"></i>
|
||||||
<span class="sidebar-label">Passwords</span>
|
<span class="sidebar-label">Passwords</span>
|
||||||
</a>
|
</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>
|
<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>
|
</a>
|
||||||
<?php if(!empty($_SESSION['is_admin'])): ?>
|
<?php if(!empty($_SESSION['is_admin'])): ?>
|
||||||
<a href="?action=admin" class="nav-link <?= $action=='admin'?'active':'' ?>" title="Admin Panel">
|
<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);
|
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 ───────────────────────────────────────────────────────────────
|
// ── Fetch Notes ───────────────────────────────────────────────────────────────
|
||||||
@@ -287,7 +295,8 @@ $note_colors = [
|
|||||||
|
|
||||||
<!-- Share section (only for owner) -->
|
<!-- 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;">
|
<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;">
|
<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;">
|
<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): ?>
|
<?php foreach($all_users as $u): ?>
|
||||||
@@ -327,6 +336,11 @@ $note_colors = [
|
|||||||
<input type="hidden" name="share_target" id="share-note-standalone-target">
|
<input type="hidden" name="share_target" id="share-note-standalone-target">
|
||||||
</form>
|
</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 -->
|
<!-- Markdown Libs -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
|
||||||
@@ -343,6 +357,11 @@ $note_colors = [
|
|||||||
$bg = $n['color'] ?: 'var(--surface)';
|
$bg = $n['color'] ?: 'var(--surface)';
|
||||||
$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 ? '...' : '');
|
||||||
|
$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"
|
<div class="note-card note-filter-item"
|
||||||
style="background:<?= htmlspecialchars($bg) ?>;"
|
style="background:<?= htmlspecialchars($bg) ?>;"
|
||||||
@@ -353,6 +372,7 @@ $note_colors = [
|
|||||||
data-label-raw="<?= htmlspecialchars(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-shares="<?= $shares_json ?>"
|
||||||
data-mine="<?= $n['is_mine'] ?>"
|
data-mine="<?= $n['is_mine'] ?>"
|
||||||
onclick="openNote(this)">
|
onclick="openNote(this)">
|
||||||
<?php if($n['image_path'] && file_exists($n['image_path'])): ?>
|
<?php if($n['image_path'] && file_exists($n['image_path'])): ?>
|
||||||
@@ -479,6 +499,9 @@ function openNote(card) {
|
|||||||
|
|
||||||
// Share section
|
// Share section
|
||||||
document.getElementById('edit-share-section').style.display = isMine ? 'block' : 'none';
|
document.getElementById('edit-share-section').style.display = isMine ? 'block' : 'none';
|
||||||
|
if(isMine) {
|
||||||
|
renderNoteShares(card.dataset.shares || '[]');
|
||||||
|
}
|
||||||
|
|
||||||
// Wire up share button
|
// Wire up share button
|
||||||
const shareBtn = document.querySelector('#edit-note-form button[name="share_note"]');
|
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');
|
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() {
|
function filterNotes() {
|
||||||
const q = (document.getElementById('notes-search-input')?.value || '').toLowerCase().trim();
|
const q = (document.getElementById('notes-search-input')?.value || '').toLowerCase().trim();
|
||||||
const lbl = (document.getElementById('notes-label-filter')?.value || '').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 ──────────────────────────────────────────────
|
// ── Fetch current folder content ──────────────────────────────────────────────
|
||||||
$user_groups = get_user_groups($user_id);
|
$user_groups = get_user_groups($user_id);
|
||||||
$my_group_ids = array_column($user_groups, '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">
|
<input type="hidden" name="item_id" id="delete-item-id">
|
||||||
</form>
|
</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 -->
|
<!-- 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">
|
||||||
@@ -227,10 +240,13 @@ function get_file_icon($name, $is_folder) {
|
|||||||
<td style="font-size:0.8rem; color:var(--text-light);">
|
<td style="font-size:0.8rem; color:var(--text-light);">
|
||||||
<?php
|
<?php
|
||||||
// Show shares
|
// 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) {
|
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']??''));
|
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>';
|
echo '</span>';
|
||||||
}
|
}
|
||||||
if(!$shares) echo '<span style="color:var(--text-light); font-size:0.8rem;">—</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');
|
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() {
|
function showNewFolderDialog() {
|
||||||
const name = prompt('New folder name:');
|
const name = prompt('New folder name:');
|
||||||
if(name && name.trim()) {
|
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