Files
Task-Management/views/dashboard.php

515 lines
28 KiB
PHP

<?php
// ── DB Migrations (safe, idempotent) ─────────────────────────────────────────
try { $pdo->exec("ALTER TABLE users ADD COLUMN mood TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN mood_updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
detail TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
// ── Task Counts ───────────────────────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, max(count($my_group_ids), 1), '?')) ?: '0';
$params_tasks = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids, [$user_id]);
$sql_tasks = "SELECT status, COUNT(*) as cnt FROM tasks WHERE
(assignee_type='user' AND assignee_id=?)
OR (assignor_type='user' AND assignor_id=?)
OR (assignee_type='group' AND assignee_id IN ($group_in))
OR (assignor_type='group' AND assignor_group_id IN ($group_in))
OR (assignor_id=?)
GROUP BY status";
$stmt = $pdo->prepare($sql_tasks);
$stmt->execute($params_tasks);
$task_counts = ['pending'=>0,'in_progress'=>0,'completed'=>0,'archived'=>0];
foreach($stmt->fetchAll() as $row) {
if (isset($task_counts[$row['status']])) $task_counts[$row['status']] = $row['cnt'];
}
$total_tasks = $task_counts['pending'] + $task_counts['in_progress'] + $task_counts['completed'];
// ── Password & Project counts ─────────────────────────────────────────────────
$stmt = $pdo->prepare("SELECT COUNT(*) FROM passwords p LEFT JOIN password_shares ps ON p.id=ps.password_id WHERE p.owner_id=? OR (ps.share_type='user' AND ps.share_id=?)");
$stmt->execute([$user_id, $user_id]);
$passwords_count = $stmt->fetchColumn();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM projects p LEFT JOIN project_shares ps ON p.id=ps.project_id WHERE p.owner_id=? OR (ps.share_type='user' AND ps.share_id=?)");
$stmt->execute([$user_id, $user_id]);
$projects_count = $stmt->fetchColumn();
// ── Notes count ───────────────────────────────────────────────────────────────
$notes_count = 0;
try {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM notes WHERE owner_id=?");
$stmt->execute([$user_id]);
$notes_count = $stmt->fetchColumn();
} catch(Exception $e) {}
// ── Current user mood ─────────────────────────────────────────────────────────
$me = query("SELECT username, profile_pic, mood, mood_updated_at FROM users WHERE id=?", [$user_id])->fetch();
$my_mood = $me['mood'] ?? null;
// ── Team moods (all users + their mood) ──────────────────────────────────────
$team_moods = query("SELECT id, username, mood, mood_updated_at, profile_pic FROM users WHERE mood IS NOT NULL AND id != ? ORDER BY mood_updated_at DESC", [$user_id])->fetchAll();
// ── Activity log ──────────────────────────────────────────────────────────────
// Admin: No longer sees all activity by default.
// Standard user/Admin: sees own activity + group members activity, BUT we hide admin activity from non-self.
$is_admin_user = !empty($_SESSION['is_admin']);
$peer_ids = [$user_id];
if ($my_group_ids) {
$gin = implode(',', array_fill(0, count($my_group_ids), '?'));
$peers = $pdo->prepare("SELECT DISTINCT user_id FROM user_groups WHERE group_id IN ($gin)");
$peers->execute($my_group_ids);
$peer_ids = array_unique(array_merge($peer_ids, array_column($peers->fetchAll(), 'user_id')));
}
// Ensure at least the user themselves is present
if(empty($peer_ids)) { $peer_ids = [$user_id]; }
// Get all admins except the current user
$other_admins = [];
try {
$other_admins = $pdo->prepare("SELECT id FROM users WHERE is_admin = 1 AND id != ?");
$other_admins->execute([$user_id]);
$other_admins = $other_admins->fetchAll(PDO::FETCH_COLUMN);
} catch(Exception $e) {}
// Filter out other admins from peer ids
$peer_ids = array_filter($peer_ids, function($id) use ($other_admins) {
return !in_array($id, $other_admins);
});
$uid_in = implode(',', array_fill(0, count($peer_ids), '?'));
$recent_logs = [];
try {
$stmt = $pdo->prepare("SELECT al.*, u.username FROM activity_log al LEFT JOIN users u ON al.user_id=u.id WHERE al.user_id IN ($uid_in) ORDER BY al.created_at DESC LIMIT 30");
$stmt->execute(array_values($peer_ids));
$recent_logs = $stmt->fetchAll();
} catch(Exception $e) {}
// ── Task logs ─────────────────────────────────────────────────────────────────
$task_logs = [];
try {
$group_in_tl = $my_group_ids ? implode(',', array_fill(0, count($my_group_ids), '?')) : '0';
$tl_params = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids);
$admin_excl = $other_admins ? "AND t.assignor_id NOT IN (" . implode(',', array_fill(0, count($other_admins), '?')) . ")" : "";
if($other_admins) { $tl_params = array_merge($tl_params, $other_admins); }
$stmt = $pdo->prepare("SELECT t.title, t.status, t.status_updated_at, u.username as assignor
FROM tasks t LEFT JOIN users u ON t.assignor_id=u.id
WHERE t.status_updated_at IS NOT NULL
AND (
t.assignee_id=? OR t.assignor_id=?
OR (t.assignee_type='group' AND t.assignee_id IN ($group_in_tl))
OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in_tl))
)
$admin_excl
ORDER BY t.status_updated_at DESC LIMIT 30");
$stmt->execute($tl_params);
$task_logs = $stmt->fetchAll();
} catch(Exception $e) {}
?>
<style>
.db-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px,1fr)); gap:1.25rem; margin-bottom:1.5rem; }
.db-stat-card {
background:var(--surface); border-radius:1rem; padding:1.25rem 1.5rem;
box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border);
display:flex; flex-direction:column; gap:0.5rem; transition:transform 0.15s, box-shadow 0.15s;
text-decoration:none !important; color:var(--text) !important;
}
.db-stat-card:hover { transform:translateY(-3px); box-shadow:0 8px 15px -3px rgba(0,0,0,0.1); }
.db-stat-label { font-size:0.78rem; color:var(--text-light); font-weight:600; text-transform:uppercase; letter-spacing:0.05em; display:flex; align-items:center; gap:0.4rem; }
.db-stat-value { font-size:2.2rem; font-weight:700; line-height:1; }
.db-stat-sub { display:flex; gap:0.5rem; flex-wrap:wrap; margin-top:0.35rem; }
.db-stat-sub span { font-size:0.72rem; font-weight:600; padding:2px 8px; border-radius:20px; }
.db-stat-link { font-size:0.78rem; color:var(--primary); margin-top:auto; font-weight:500; }
/* Welcome banner */
.db-welcome {
background: linear-gradient(135deg, var(--primary) 0%, #10B981 100%);
border-radius:1rem; padding:1.5rem; color:#fff; margin-bottom:1.5rem;
display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem;
}
.db-welcome h2 { font-size:1.4rem; font-weight:700; }
.db-welcome p { font-size:0.875rem; opacity:0.85; margin-top:0.25rem; }
/* Mood picker */
.mood-row { display:flex; gap:0.4rem; flex-wrap:wrap; align-items:center; }
.mood-btn {
font-size:1.5rem; background:none; border:2px solid transparent;
border-radius:50%; width:40px; height:40px; cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:transform 0.15s, border-color 0.15s, background 0.15s;
line-height:1;
}
.mood-btn:hover { transform:scale(1.15); background:rgba(255,255,255,0.15); }
.mood-btn.active { border-color:rgba(255,255,255,0.9); background:rgba(255,255,255,0.2); transform:scale(1.1); }
/* Team moods */
.team-mood-row { display:flex; flex-wrap:wrap; gap:0.6rem; }
.team-mood-chip {
display:flex; align-items:center; gap:0.4rem; font-size:0.8rem;
background:var(--bg); border:1px solid var(--border); border-radius:20px;
padding:4px 10px; color:var(--text);
}
.team-mood-chip .em { font-size:1.2rem; }
/* Activity log */
.log-list { display:flex; flex-direction:column; gap:0.5rem; overflow-y:auto; flex:1; max-height:360px; }
.log-item { display:flex; gap:0.75rem; align-items:flex-start; font-size:0.8rem; flex-shrink:0; padding:0.35rem 0; }
.log-avatar { width:26px; height:26px; border-radius:50%; background:var(--primary); color:#fff; display:flex; align-items:center; justify-content:center; font-size:0.65rem; font-weight:700; flex-shrink:0; }
.log-body { flex:1; min-width:0; }
.log-body strong { color:var(--text); }
.log-body .log-detail { color:var(--text-light); }
.log-time { font-size:0.7rem; color:var(--text-light); white-space:nowrap; flex-shrink:0; }
/* Full-height bottom section */
.db-wrapper { display:flex; flex-direction:column; }
.two-col {
display:grid; grid-template-columns:1fr 1fr; gap:1.25rem;
flex:1; min-height:0;
}
.two-col > .card {
margin-bottom:0; display:flex; flex-direction:column;
min-height:0;
}
@media(max-width:720px){ .two-col { grid-template-columns:1fr; } }
.admin-activity-badge { display:inline-flex; align-items:center; gap:0.4rem; font-size:0.7rem; padding:2px 8px; border-radius:12px; background:rgba(239,68,68,0.1); color:#EF4444; font-weight:700; }
</style>
<?php
// ── Log a "viewed dashboard" entry (once per session key) ───────────────────
if (empty($_SESSION['dash_logged_today'])) {
try {
$pdo->prepare("INSERT INTO activity_log (user_id, action, detail) VALUES (?, 'visit', 'Opened dashboard')")->execute([$user_id]);
} catch(Exception $e) {}
$_SESSION['dash_logged_today'] = date('Y-m-d');
}
?>
<div class="db-wrapper">
<!-- ── Welcome Banner ──────────────────────────────────────────────────────── -->
<div class="db-welcome">
<div>
<h2>👋 Welcome, <?= htmlspecialchars($current_user['username']) ?>!</h2>
<p id="current-datetime"></p>
<?php if($user_groups): ?>
<p style="margin-top:0.25rem; opacity:0.8; font-size:0.8rem;">
Groups: <?= implode(', ', array_map(fn($g) => htmlspecialchars($g['name']), $user_groups)) ?>
</p>
<?php endif; ?>
</div>
<!-- Mood Picker inline in banner -->
<div style="text-align:right;">
<div style="font-size:0.75rem; font-weight:600; opacity:0.8; margin-bottom:0.4rem; text-transform:uppercase; letter-spacing:0.05em;">Your Mood</div>
<form method="POST" id="mood-form" style="display:inline;">
<input type="hidden" name="set_mood" value="1">
<input type="hidden" name="mood" id="mood-value" value="">
<div class="mood-row" style="justify-content:flex-end;">
<?php
$moods = ['😊','🤩','😎','🥳','😐','🤔','😴','😟','😢','😠'];
foreach($moods as $em):
?>
<button type="button" class="mood-btn <?= $my_mood === $em ? 'active' : '' ?>"
onclick="selectMood('<?= $em ?>')" title="<?= $em ?>">
<?= $em ?>
</button>
<?php endforeach; ?>
<?php if($my_mood): ?>
<button type="button" class="mood-btn" onclick="selectMood('')" title="Clear mood" style="font-size:0.7rem; width:auto; border-radius:12px; padding:0 8px; color:#fff; border-color:rgba(255,255,255,0.4);">✕ Clear</button>
<?php endif; ?>
</div>
</form>
<?php if($my_mood): ?>
<div style="margin-top:0.4rem; font-size:0.8rem; opacity:0.8;">Currently: <?= $my_mood ?></div>
<?php endif; ?>
</div>
</div>
<!-- ── Today's Holiday Badge ──────────────────────────────────────────────── -->
<div id="today-holiday-banner" style="display:none; margin-bottom:1rem; padding:0.6rem 1.25rem; background:linear-gradient(90deg,rgba(245,158,11,0.12),rgba(16,185,129,0.08)); border:1px solid rgba(245,158,11,0.3); border-radius:0.75rem; align-items:center; gap:0.75rem; font-size:0.875rem;">
<span style="font-size:1.3rem;">🎉</span>
<div>
<strong style="color:var(--text);" id="today-holiday-name"></strong>
<span style="color:var(--text-light); margin-left:0.4rem;" id="today-holiday-label"></span>
</div>
</div>
<!-- ── Stat Cards ──────────────────────────────────────────────────────────── -->
<div class="db-grid">
<!-- Tasks -->
<a href="?action=tasks" class="db-stat-card" style="border-top:3px solid var(--primary);">
<div class="db-stat-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>
My Tasks
</div>
<div class="db-stat-value" style="color:var(--primary);"><?= $total_tasks ?></div>
<div class="db-stat-sub">
<span style="background:rgba(245,158,11,0.12); color:#F59E0B;">⏳ <?= $task_counts['pending'] ?> Pending</span>
<span style="background:rgba(59,130,246,0.12); color:#3B82F6;">🔵 <?= $task_counts['in_progress'] ?> In Progress</span>
<span style="background:rgba(16,185,129,0.12); color:#10B981;">✅ <?= $task_counts['completed'] ?> Done</span>
</div>
<div class="db-stat-link">Open Kanban →</div>
</a>
<!-- Passwords -->
<a href="?action=passwords" class="db-stat-card" style="border-top:3px solid #10B981;">
<div class="db-stat-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Password Vault
</div>
<div class="db-stat-value" style="color:#10B981;"><?= $passwords_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(16,185,129,0.12); color:#10B981;">🔐 Stored items</span>
</div>
<div class="db-stat-link">Open Vault →</div>
</a>
<!-- 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>
Files
</div>
<div class="db-stat-value" style="color:#F59E0B;"><?= $projects_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(245,158,11,0.12); color:#F59E0B;">📁 Items</span>
</div>
<div class="db-stat-link">Open Explorer →</div>
</a>
<!-- Notes -->
<a href="?action=notes" class="db-stat-card" style="border-top:3px solid #8B5CF6;">
<div class="db-stat-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
Notes
</div>
<div class="db-stat-value" style="color:#8B5CF6;"><?= $notes_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(139,92,246,0.12); color:#8B5CF6;">📝 Notes saved</span>
</div>
<div class="db-stat-link">Open Notes →</div>
</a>
<!-- Tools -->
<a href="?action=tools" class="db-stat-card" style="border-top:3px solid #EC4899;">
<div class="db-stat-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 9.36l-7.1 7.1a1 1 0 0 1-1.4 0l-2.8-2.8a1 1 0 0 1 0-1.4l7.1-7.1a6 6 0 0 1 9.36-7.94z"></path></svg>
Tools
</div>
<div class="db-stat-value" style="color:#EC4899;">3</div>
<div class="db-stat-sub">
<span style="background:rgba(236,72,153,0.12); color:#EC4899;">🛠 Utilities</span>
</div>
<div class="db-stat-link">Open Tools →</div>
</a>
</div>
<!-- ── 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) && empty($my_mood)): ?>
<p style="color:var(--text-light); font-size:0.85rem;">No team mood activity 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'];
if(!empty($my_mood)):
$m_initial = strtoupper(mb_substr($me['username'], 0, 1));
$m_color = $avatar_colors_tm[crc32($me['username']) % count($avatar_colors_tm)];
?>
<div style="position:relative; flex-shrink:0; cursor:pointer;" title="You (Updated: <?= date('M j, H:i', strtotime($me['mood_updated_at'])) ?>)">
<div style="width:40px; height:40px; border-radius:50%; background:<?= $m_color ?>; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:1rem; overflow:hidden;">
<?php if(!empty($me['profile_pic']) && file_exists($me['profile_pic'])): ?>
<img src="<?= htmlspecialchars($me['profile_pic']) ?>?v=<?= time() ?>" style="width:100%; height:100%; object-fit:cover;">
<?php else: ?>
<?= $m_initial ?>
<?php endif; ?>
</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);">
<?= $my_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; font-style:italic;">
You
</div>
</div>
<?php endif; ?>
<?php
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; overflow:hidden;">
<?php if(!empty($tm['profile_pic']) && file_exists($tm['profile_pic'])): ?>
<img src="<?= htmlspecialchars($tm['profile_pic']) ?>?v=<?= time() ?>" style="width:100%; height:100%; object-fit:cover;">
<?php else: ?>
<?= $initial ?>
<?php endif; ?>
</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; ?>
</div>
<!-- ── 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>
</div>
<span style="font-size:0.75rem; color:var(--text-light);">
You & your groups
</span>
</div>
<?php
// Merge activity_log + task_logs into one unified feed sorted by time
$feed = [];
foreach($recent_logs as $l) {
$feed[] = [
'time' => $l['created_at'],
'user' => $l['username'] ?? 'System',
'action' => $l['action'],
'detail' => $l['detail'] ?? '',
];
}
foreach($task_logs as $l) {
$status_label = ['pending'=>'moved to Pending','in_progress'=>'started In Progress','completed'=>'marked Completed','archived'=>'archived'][$l['status']] ?? $l['status'];
$feed[] = [
'time' => $l['status_updated_at'],
'user' => $l['assignor'] ?? 'Someone',
'action' => 'task_status',
'detail' => '"'.htmlspecialchars(mb_substr($l['title'],0,30)).'" '.$status_label,
];
}
usort($feed, fn($a,$b) => strtotime($b['time']) - strtotime($a['time']));
$feed = array_slice($feed, 0, $is_admin_user ? 50 : 30);
$action_icons = [
'visit' => '🌐',
'mood_set' => '😊',
'task_status' => '✅',
'note_create' => '📝',
'pw_add' => '🔐',
];
// Generate avatar color based on username hash
$avatar_colors = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
?>
<?php if(empty($feed)): ?>
<p style="color:var(--text-light); font-size:0.85rem;">No activity yet.</p>
<?php else: ?>
<div class="log-list">
<?php foreach($feed as $entry):
$initial = strtoupper(mb_substr($entry['user'], 0, 1));
$icon = $action_icons[$entry['action']] ?? '🔔';
$ago = human_time_diff(strtotime($entry['time']));
$color = $avatar_colors[crc32($entry['user']) % count($avatar_colors)];
?>
<div class="log-item">
<div class="log-avatar" style="background:<?= $color ?>;"><?= $initial ?></div>
<div class="log-body">
<strong><?= htmlspecialchars($entry['user']) ?></strong>
<span class="log-detail"> <?= $icon ?> <?= htmlspecialchars($entry['detail']) ?></span>
</div>
<div class="log-time"><?= $ago ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div><!-- .db-wrapper -->
<script>
// ── Live clock ────────────────────────────────────────────────────────────────
function updateTime() {
const now = new Date();
const el = document.getElementById('current-datetime');
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'
});
}
setInterval(updateTime, 1000); updateTime();
// ── Mood picker ───────────────────────────────────────────────────────────────
function selectMood(em) {
document.getElementById('mood-value').value = em;
document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active'));
if (em) {
document.querySelectorAll('.mood-btn').forEach(b => {
if (b.textContent.trim() === em) b.classList.add('active');
});
}
document.getElementById('mood-form').submit();
}
// ── Holiday check ─────────────────────────────────────────────────────────────
(function checkTodayHoliday() {
const year = new Date().getFullYear();
const today = new Date().toISOString().slice(0, 10);
const urls = [
{ url: `https://date.nager.at/api/v3/PublicHolidays/${year}/ID`, flag: '🇮🇩', label: 'Indonesia' },
{ url: `https://date.nager.at/api/v3/PublicHolidays/${year}/UN`, flag: '🌍', label: 'International Day' }
];
Promise.allSettled(urls.map(e => fetch(e.url).then(r => r.json()).then(d => ({...e, data: d}))))
.then(results => {
const banner = document.getElementById('today-holiday-banner');
const nameEl = document.getElementById('today-holiday-name');
const labelEl = document.getElementById('today-holiday-label');
const matches = [];
results.forEach(r => {
if (r.status === 'fulfilled' && Array.isArray(r.value.data)) {
r.value.data.forEach(h => {
if (h.date === today) matches.push(`${r.value.flag} ${h.localName || h.name}`);
});
}
});
if (matches.length > 0) {
nameEl.textContent = matches.join(' · ');
labelEl.textContent = '— Today is a holiday! 🎊';
banner.style.display = 'flex';
}
});
})();
</script>
<?php
// ── Helper: human-readable time difference ───────────────────────────────────
function human_time_diff($ts) {
$diff = time() - $ts;
if ($diff < 60) return 'just now';
if ($diff < 3600) return floor($diff/60).'m ago';
if ($diff < 86400) return floor($diff/3600).'h ago';
return floor($diff/86400).'d ago';
}
?>