Skip to content

Commit ae98966

Browse files
Fixed the XP Pop up issue with notification bell icon feature addition
1 parent e3cfb3e commit ae98966

2 files changed

Lines changed: 98 additions & 32 deletions

File tree

public/hub.html

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
<button id="replayTourBtn" class="secondary-btn" onclick="replayTour()" title="Replay the onboarding tour">🧭 Tour</button>
3636
<button class="secondary-btn" onclick="showHowToPlay()" title="How to play CyberArena" style="padding: 5px 10px; font-size: 10px;">🎮 Guide</button>
3737
<button class="secondary-btn" onclick="window.location.href='profile.html'" title="Your Profile" style="padding: 5px 10px; font-size: 10px;">👤 Profile</button>
38+
<div style="position:relative;display:inline-block;">
39+
<button id="bellBtn" class="secondary-btn" onclick="toggleBell()" title="Notifications" style="padding:5px 10px;font-size:14px;">🔔</button>
40+
<span id="bellBadge" style="display:none;position:absolute;top:-4px;right:-4px;background:#ff003c;color:#fff;font-size:9px;font-family:var(--font-main);border-radius:50%;width:16px;height:16px;display:flex;align-items:center;justify-content:center;pointer-events:none;">0</span>
41+
</div>
3842
</div>
3943
<button class="logout-btn" onclick="logout()">Logout</button>
4044
</div>
@@ -76,6 +80,15 @@ <h3 style="color:var(--neon-magenta); font-size:14px; margin-top:20px;">💡 Pro
7680
</div>
7781
</div>
7882

83+
<!-- Notification dropdown -->
84+
<div id="bellPanel" style="display:none;position:fixed;top:95px;right:15px;z-index:9998;background:rgba(0,0,0,0.95);border:1px solid var(--neon-green);border-radius:4px;min-width:260px;max-width:320px;font-size:12px;box-shadow:0 0 20px rgba(57,255,20,0.15);">
85+
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid #1a1a1a;">
86+
<span style="color:var(--neon-green);font-family:var(--font-cyber);font-size:11px;letter-spacing:1px;">🔔 UNLOCKS</span>
87+
<button onclick="clearBell()" style="font-size:9px;padding:2px 8px;clip-path:none;color:#555;border-color:#333;">Mark all read</button>
88+
</div>
89+
<div id="bellList" style="max-height:300px;overflow-y:auto;padding:8px 0;"></div>
90+
</div>
91+
7992
<div class="mission-section" id="tour-social">
8093
<h3 class="section-title">🎭 Social Engineering</h3>
8194
<div class="missions">
@@ -128,7 +141,62 @@ <h3 class="section-title">&#x1F6E0;&#xFE0F; Tools &amp; Knowledge</h3>
128141
import { onAuthStateChanged } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
129142
import { getUserData } from "./js/xp.js";
130143
import { initTour } from "./js/tour.js";
131-
import { applyLockStates, getNewlyUnlocked, showUnlockToast } from "./js/unlocks.js";
144+
import { applyLockStates, getNewlyUnlocked, addUnlockNotification, getUnreadCount, markAllRead, getNotifications } from "./js/unlocks.js";
145+
146+
function updateBell() {
147+
const count = getUnreadCount();
148+
const badge = document.getElementById('bellBadge');
149+
if (count > 0) {
150+
badge.style.display = 'flex';
151+
badge.textContent = count;
152+
} else {
153+
badge.style.display = 'none';
154+
}
155+
}
156+
157+
window.toggleBell = function() {
158+
const panel = document.getElementById('bellPanel');
159+
const open = panel.style.display === 'block';
160+
if (!open) {
161+
renderBellList();
162+
panel.style.display = 'block';
163+
} else {
164+
panel.style.display = 'none';
165+
}
166+
};
167+
168+
function renderBellList() {
169+
const list = document.getElementById('bellList');
170+
const notifications = getNotifications();
171+
if (!notifications.length) {
172+
list.innerHTML = '<div style="padding:12px 14px;color:#555;">No unlocks yet. Keep earning XP!</div>';
173+
return;
174+
}
175+
list.innerHTML = notifications.slice().reverse().map(n => `
176+
<div style="padding:10px 14px;border-bottom:1px solid #111;display:flex;gap:10px;align-items:flex-start;opacity:${n.read ? '0.5' : '1'};">
177+
<span style="font-size:16px;">${n.read ? '✅' : '🔓'}</span>
178+
<div>
179+
<div style="color:var(--neon-green);font-family:var(--font-cyber);font-size:11px;">${n.label}</div>
180+
<div style="color:#888;font-size:10px;margin-top:2px;">${n.description.replace('Reach','Reached')}</div>
181+
</div>
182+
</div>
183+
`).join('');
184+
}
185+
186+
window.clearBell = function() {
187+
markAllRead();
188+
updateBell();
189+
renderBellList();
190+
};
191+
192+
// Close bell panel when clicking outside
193+
document.addEventListener('click', (e) => {
194+
const panel = document.getElementById('bellPanel');
195+
const btn = document.getElementById('bellBtn');
196+
if (panel && !panel.contains(e.target) && e.target !== btn) {
197+
panel.style.display = 'none';
198+
}
199+
});
132200

133201
function startActivityTicker() {
134202
const activities = [
@@ -160,13 +228,14 @@ <h3 class="section-title">&#x1F6E0;&#xFE0F; Tools &amp; Knowledge</h3>
160228
if (userData) {
161229
const xp = userData.xp || 0;
162230
applyLockStates(xp);
163-
const prevXP = parseInt(localStorage.getItem('prevXP') || '0');
164-
if (prevXP < xp) {
165-
getNewlyUnlocked(prevXP, xp).forEach((u, i) => {
166-
setTimeout(() => showUnlockToast(u), i * 1000);
167-
});
231+
// Store new unlocks into notification bell (never show popup)
232+
const seenUnlocks = JSON.parse(localStorage.getItem('seenUnlocks') || '[]');
233+
const newItems = getNewlyUnlocked(0, xp).filter(u => !seenUnlocks.includes(u.id));
234+
newItems.forEach(u => addUnlockNotification(u));
235+
if (newItems.length) {
236+
localStorage.setItem('seenUnlocks', JSON.stringify([...seenUnlocks, ...newItems.map(u => u.id)]));
168237
}
169-
localStorage.setItem('prevXP', xp);
238+
updateBell();
170239
}
171240
startActivityTicker();
172241
if (!localStorage.getItem('cyberarena_guide_seen')) {

public/js/unlocks.js

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -75,29 +75,26 @@ export function applyLockStates(xp) {
7575
});
7676
}
7777

78-
// ── SHOW UNLOCK TOAST ─────────────────────────────────────────
79-
export function showUnlockToast(unlock) {
80-
const isHC = document.documentElement.classList.contains('high-contrast');
81-
const toast = document.createElement('div');
82-
toast.style.cssText = `
83-
position:fixed;top:70px;left:50%;transform:translateX(-50%);
84-
background:${isHC ? '#fff' : 'rgba(0,0,0,0.95)'};
85-
border:2px solid ${isHC ? '#000' : 'var(--neon-green)'};
86-
padding:14px 24px;border-radius:4px;z-index:99999;
87-
font-family:var(--font-cyber);font-size:13px;
88-
color:${isHC ? '#000' : 'var(--neon-green)'};
89-
box-shadow:${isHC ? 'none' : '0 0 20px rgba(57,255,20,0.3)'};
90-
text-align:center;animation:slideIn 0.4s ease-out;
91-
`;
92-
toast.innerHTML = `
93-
<div style="font-size:10px;letter-spacing:2px;color:${isHC ? '#000' : '#555'};margin-bottom:4px;">UNLOCKED</div>
94-
<div style="color:${isHC ? '#000' : 'var(--neon-green)'}">${unlock.label}</div>
95-
<div style="font-size:11px;color:${isHC ? '#333' : '#888'};margin-top:4px;font-family:var(--font-main);">${unlock.description.replace('Reach','Reached')}</div>
96-
`;
97-
document.body.appendChild(toast);
98-
setTimeout(() => {
99-
toast.style.transition = 'opacity 0.5s';
100-
toast.style.opacity = '0';
101-
setTimeout(() => toast.remove(), 500);
102-
}, 3500);
78+
// ── NOTIFICATION BELL ─────────────────────────────────────────
79+
export function addUnlockNotification(unlock) {
80+
const notifications = JSON.parse(localStorage.getItem('unlockNotifications') || '[]');
81+
// Don't add duplicates
82+
if (notifications.find(n => n.id === unlock.id)) return;
83+
notifications.push({ id: unlock.id, label: unlock.label, description: unlock.description, read: false });
84+
localStorage.setItem('unlockNotifications', JSON.stringify(notifications));
85+
}
86+
87+
export function getUnreadCount() {
88+
const notifications = JSON.parse(localStorage.getItem('unlockNotifications') || '[]');
89+
return notifications.filter(n => !n.read).length;
90+
}
91+
92+
export function markAllRead() {
93+
const notifications = JSON.parse(localStorage.getItem('unlockNotifications') || '[]');
94+
notifications.forEach(n => n.read = true);
95+
localStorage.setItem('unlockNotifications', JSON.stringify(notifications));
96+
}
97+
98+
export function getNotifications() {
99+
return JSON.parse(localStorage.getItem('unlockNotifications') || '[]');
103100
}

0 commit comments

Comments
 (0)