Skip to content

Commit 345d18e

Browse files
update
1 parent fe5e2ed commit 345d18e

File tree

1 file changed

+90
-73
lines changed

1 file changed

+90
-73
lines changed

js/main.js

Lines changed: 90 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ const firebaseConfig = {
1111
measurementId: "G-S40XF238WM"
1212
};
1313

14-
// Initialize Instance
14+
// Initialize Firebase
1515
if (!firebase.apps.length) { firebase.initializeApp(firebaseConfig); }
1616
const auth = firebase.auth();
1717
const db = firebase.firestore();
1818
const provider = new firebase.auth.GoogleAuthProvider();
1919

20-
// GLOBAL LOGIN FUNCTION (Accessible to buttons injected via innerHTML)
20+
// GLOBAL LOGIN FUNCTION (Required for UI-injected buttons)
2121
const handleLogin = () => {
2222
auth.signInWithPopup(provider).catch(() => auth.signInWithRedirect(provider));
2323
};
@@ -32,7 +32,6 @@ let currentUser = null;
3232
let currentFilter = 'All';
3333
let searchTerm = '';
3434

35-
// Path routing for the Documentation Hub folders
3635
const DOC_PATHS = {
3736
'jmeter': 'performance', 'neoload': 'performance', 'loadrunner': 'performance', 'k6': 'performance', 'locust': 'performance',
3837
'kubernetes': 'sre', 'aks': 'sre', 'azure': 'sre', 'grafana': 'sre', 'datadog': 'sre', 'dynatrace': 'sre',
@@ -41,7 +40,7 @@ const DOC_PATHS = {
4140
'ai-in-perf-testing': 'trends_ai', 'generative-ai-sre': 'trends_ai'
4241
};
4342

44-
// Helper: Progress Stats Engine
43+
// Helper: Calculate progress percentage
4544
function getStats(course) {
4645
if (!course.videos || course.videos.length === 0) return { done: 0, total: 0, percent: 0 };
4746
const total = course.videos.length;
@@ -65,7 +64,7 @@ document.addEventListener('DOMContentLoaded', () => {
6564
const res = await fetch('data/playlists.json');
6665
playlists = await res.json();
6766
render();
68-
} catch (e) { console.error("Error loading JSON:", e); }
67+
} catch (e) { console.error("Course Data missing."); }
6968
};
7069

7170
auth.onAuthStateChanged(async (user) => {
@@ -75,12 +74,14 @@ document.addEventListener('DOMContentLoaded', () => {
7574
if (userPic) userPic.src = user.photoURL;
7675
if (loginBtn) loginBtn.style.display = 'none';
7776

78-
const doc = await db.collection('users').doc(user.uid).get();
79-
if (doc.exists) {
80-
completed = doc.data().completed || [];
81-
favorites = doc.data().favorites || [];
82-
}
83-
loadHourlyTeamBriefing();
77+
// Sync from Cloud
78+
try {
79+
const doc = await db.collection('users').doc(user.uid).get();
80+
if (doc.exists) {
81+
completed = doc.data().completed || [];
82+
favorites = doc.data().favorites || [];
83+
}
84+
} catch(e) { console.warn("Firestore access error."); }
8485
} else {
8586
currentUser = null;
8687
if (userProfile) userProfile.style.display = 'none';
@@ -89,64 +90,68 @@ document.addEventListener('DOMContentLoaded', () => {
8990
render();
9091
});
9192

93+
const syncCloud = async () => {
94+
if (currentUser) {
95+
await db.collection('users').doc(currentUser.uid).set({ completed, favorites });
96+
} else {
97+
localStorage.setItem('ll-completed', JSON.stringify(completed));
98+
}
99+
};
100+
92101
async function loadHourlyTeamBriefing() {
102+
if (currentFilter === 'All') return; // Skip loading if we are on Home page
103+
93104
try {
94105
const doc = await db.collection('admin_data').doc('hourly_tip').get();
95106
if (doc.exists) {
96107
const el = document.getElementById('ai-tip-banner');
97108
if (el) {
98109
el.style.display = 'block';
99-
el.innerHTML = `<h4>💡 Little's Law Team | Expert Field Manual</h4>
100-
<div style="font-family:serif; line-height:1.7;">${doc.data().content}</div>`;
110+
const contentHtml = (typeof marked !== 'undefined') ? marked.parse(doc.data().content) : doc.data().content;
111+
el.innerHTML = `<h4 style="color:#e52e2e; font-size:0.8rem; letter-spacing:1px; margin-bottom:15px;">LITTLE'S LAW TEAM | EXPERT FIELD MANUAL</h4>
112+
<div class="team-content-style">${contentHtml}</div>`;
101113
}
102114
}
103-
} catch(e) {}
115+
} catch(e) { console.warn("Team briefing not found."); }
104116
}
105117

106-
const syncCloud = async () => {
107-
if (currentUser) {
108-
await db.collection('users').doc(currentUser.uid).set({ completed, favorites });
109-
} else {
110-
localStorage.setItem('ll-completed', JSON.stringify(completed));
111-
}
112-
};
113-
114-
// RENDERING DASHBOARD
118+
// MAIN RENDER ENGINE
115119
function render() {
116120
if (!currentUser) {
117121
container.innerHTML = `
118122
<div id="locked-view" style="grid-column:1/-1; text-align:center; padding:100px;">
119-
<h1>🎓 Welcome to Little's Law Academy</h1>
120-
<p style="margin:20px; color:#666;">Sign in with Google to unlock your SRE & Performance training track.</p>
121-
<button class="auth-btn" style="padding:15px 40px; font-size:1.1rem; cursor:pointer;" onclick="handleLogin()">Unlock My Journey</button>
123+
<h1 style="font-size:3rem;">🎓 Little's Law Academy</h1>
124+
<p style="margin:20px; color:#666; font-size:1.1rem;">Sign in to master Performance, SRE, and DevOps across 900+ professional modules.</p>
125+
<button class="auth-btn" style="padding:15px 35px; font-size:1.1rem; cursor:pointer;" onclick="handleLogin()">🔓 Unlock Academy Journey</button>
122126
</div>`;
123127
return;
124128
}
125129

126130
let hubHeader = '';
131+
let bannerDiv = '';
132+
133+
// Conditionally show banner space ONLY if not on "All" view
127134
if (currentFilter !== 'All') {
128135
hubHeader = `
129136
<div class="hub-header" style="grid-column: 1 / -1; background: #111; color: white; padding: 40px; border-radius: 12px; margin-bottom: 30px; display: flex; justify-content: space-between; align-items: center;">
130-
<div><small style="color:red">TEAM HUB</small><h2 style="margin:0">${currentFilter}</h2></div>
131-
<button class="back-btn" data-filter="All" style="background:transparent; border:1px solid white; color:white; padding:8px 15px; cursor:pointer; border-radius:5px;">← All Paths</button>
137+
<div><small style="color:red; font-weight:bold;">TRACK HUB</small><h2 style="margin:0">${currentFilter}</h2></div>
138+
<button class="back-btn" data-filter="All" style="background:transparent; border:1px solid #444; color:white; padding:10px 20px; cursor:pointer; border-radius:6px;">← Back to Home</button>
132139
</div>`;
140+
bannerDiv = `<div id="ai-tip-banner" style="grid-column:1/-1; display:none; border-left:10px solid #111; padding:40px; margin-bottom:40px; background:#fff; box-shadow:0 10px 30px rgba(0,0,0,0.05);"></div>`;
133141
}
134142

135-
container.innerHTML = hubHeader + `<div id="ai-tip-banner" class="stats-bar" style="grid-column:1/-1; display:none; border-left:10px solid #e52e2e; padding:30px; margin-bottom:40px; background:#fff; white-space:pre-wrap;"></div>`;
143+
container.innerHTML = hubHeader + bannerDiv;
136144

137145
const filtered = playlists.filter(p => {
138-
const filterKey = currentFilter.toLowerCase().trim();
139-
const pool = (p.title + p.tool + (p.category || '')).toLowerCase();
140-
const matchesSearch = pool.includes(searchTerm);
141-
142-
const isToolMatch = p.tool && p.tool.toLowerCase() === filterKey;
143-
const isCatMatch = p.category && p.category.toLowerCase() === filterKey;
144-
145-
return (filterKey === 'all' || isToolMatch || isCatMatch) && matchesSearch;
146+
const fKey = currentFilter.toLowerCase().trim();
147+
const pool = (p.title + (p.tool || "") + (p.category || "")).toLowerCase();
148+
const isToolMatch = p.tool && p.tool.toLowerCase() === fKey;
149+
const isCatMatch = p.category && p.category.toLowerCase() === fKey;
150+
return (fKey === 'all' || isToolMatch || isCatMatch) && pool.includes(searchTerm);
146151
});
147152

148153
if (filtered.length === 0 && currentFilter !== 'All') {
149-
container.innerHTML += `<div style="grid-column:1/-1; text-align:center; padding:50px;"><h3>No modules found in the ${currentFilter} track.</h3></div>`;
154+
container.innerHTML += `<div style="grid-column:1/-1; text-align:center; padding:50px;"><h3>Searching the team archive... try a different tool filter.</h3></div>`;
150155
}
151156

152157
filtered.forEach(p => {
@@ -157,9 +162,9 @@ document.addEventListener('DOMContentLoaded', () => {
157162
<div class="card-tag">${p.tool} <span class="badge ${p.level.toLowerCase()}">${p.level}</span></div>
158163
<h2>${p.title}</h2>
159164
<div class="card-progress"><div class="progress-fill" style="width: ${stats.percent}%"></div></div>
160-
<small>${stats.percent}% Journey Complete (${stats.done}/${stats.total} videos)</small>
165+
<small>${stats.percent}% Processed (${stats.done}/${stats.total} modules)</small>
161166
<p>${p.description}</p>
162-
<button class="lms-btn" data-cid="${p.courseId}">▶ Enter Journey</button>
167+
<button class="lms-btn" data-cid="${p.courseId}">▶ Open Syllabus</button>
163168
`;
164169
container.appendChild(card);
165170
});
@@ -168,14 +173,17 @@ document.addEventListener('DOMContentLoaded', () => {
168173
if(document.getElementById('progress-count')) document.getElementById('progress-count').textContent = completed.length;
169174
}
170175

171-
// INTERACTION HANDLER
176+
// ==========================================
177+
// 4. INTERACTION SYSTEM
178+
// ==========================================
172179
document.addEventListener('click', async (e) => {
173180
const t = e.target;
174181

175-
// A. Handle Filters
182+
// Navigation
176183
const filterBtn = t.closest('[data-filter]');
177184
if (filterBtn) {
178185
e.preventDefault();
186+
if(!currentUser) return;
179187
currentFilter = filterBtn.dataset.filter;
180188
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
181189
(t.closest('.dropdown')?.querySelector('.nav-item') || filterBtn).classList.add('active');
@@ -184,60 +192,62 @@ document.addEventListener('DOMContentLoaded', () => {
184192
return;
185193
}
186194

187-
// B. Handle Team Wiki (Docs)
195+
// Docs Wiki Loader
188196
const docBtn = t.closest('[data-doc]');
189197
if (docBtn) {
190198
e.preventDefault();
191199
const tool = docBtn.dataset.doc.toLowerCase();
192200
const folder = DOC_PATHS[tool] || 'performance';
193201
try {
194-
const res = await fetch(`docs/${folder}/${tool}.md`);
202+
const res = await fetch(`docs/${folder}/${tool.replace(/-/g,'_')}.md`);
195203
if(!res.ok) throw new Error();
196204
const mdText = await res.text();
197205

198-
document.getElementById('current-lesson-title').textContent = tool.toUpperCase() + " Technical Wiki";
199206
const mediaArea = document.querySelector('.lms-video-area');
200-
mediaArea.innerHTML = `<div class="doc-viewer" style="background:#fff; padding:60px; overflow-y:auto; height:100%; color:#222; font-size:1.1rem; line-height:1.8;">${marked.parse(mdText)}</div>`;
207+
document.getElementById('current-lesson-title').textContent = tool.toUpperCase() + " Technical Document";
208+
mediaArea.innerHTML = `<div class="doc-viewer" style="background:#fff; padding:60px; overflow-y:auto; height:100%; color:#222; font-size:1.1rem; line-height:1.8;">
209+
${typeof marked !== 'undefined' ? marked.parse(mdText) : mdText}</div>`;
201210
document.getElementById('video-overlay').style.display = 'flex';
202-
} catch (err) { alert("Documentation being curated by the Team. Check back next hour!"); }
211+
} catch (err) { alert("Our team is updating this module. Check back shortly!"); }
203212
return;
204213
}
205214

206-
// C. LMS Sidebar & Progress
215+
// LMS Bootcamp Open
207216
if (t.classList.contains('lms-btn')) {
208217
const course = playlists.find(p => p.courseId === t.dataset.cid);
209218
if(!course) return;
210219

211-
const list = document.getElementById('curriculum-list');
212-
list.innerHTML = '';
220+
const listEl = document.getElementById('curriculum-list');
221+
listEl.innerHTML = '';
213222
course.videos.forEach(v => {
214223
const gid = `${course.courseId}_${v.id}`;
215224
const checked = completed.includes(gid);
216225
const li = document.createElement('li');
217226
li.className = `lesson-item ${checked ? 'completed' : ''}`;
218227
li.innerHTML = `<input type="checkbox" ${checked ? 'checked' : ''} data-gid="${gid}">
219228
<span class="lesson-link" data-vid="${v.id}">${v.title}</span>`;
220-
list.appendChild(li);
229+
listEl.appendChild(li);
221230
});
222231
document.getElementById('course-title-label').textContent = course.title;
223232
updateModalUI(course.courseId);
224233
document.getElementById('video-overlay').style.display = 'flex';
225234
document.body.style.overflow = 'hidden';
226235
}
227236

228-
// Lesson switch
237+
// Switch Video logic
229238
if (t.classList.contains('lesson-link')) {
230239
const vidId = t.dataset.vid;
240+
// CORRECTED: Fixed source builder syntax
231241
document.getElementById('video-player').src = `https://www.youtube.com/embed/${vidId}?autoplay=1&rel=0&origin=${window.location.origin}`;
232242
document.getElementById('current-lesson-title').textContent = t.textContent;
233243
document.querySelectorAll('.lesson-item').forEach(li => li.classList.remove('active'));
234244
t.closest('.lesson-item').classList.add('active');
235245
}
236246

237-
// Checkbox click
247+
// Checklist completion
238248
if (t.type === 'checkbox' && t.dataset.gid) {
239249
const gid = t.dataset.gid;
240-
completed = t.checked ? [...completed, gid] : completed.filter(i => i !== gid);
250+
completed = t.checked ? [...completed, gid] : completed.filter(id => id !== gid);
241251
await syncCloud();
242252
updateModalUI(gid.split('_')[0]);
243253
render();
@@ -249,35 +259,42 @@ document.addEventListener('DOMContentLoaded', () => {
249259
const bar = document.getElementById('modal-progress-bar');
250260
const text = document.getElementById('modal-progress-text');
251261
if(bar) bar.style.width = stats.percent + '%';
252-
if(text) text.textContent = `${stats.percent}% Ready to Certificate`;
253-
254-
const header = document.querySelector('.sidebar-header');
255-
const oldCert = document.getElementById('dl-cert-btn'); if(oldCert) oldCert.remove();
262+
if(text) text.textContent = `${stats.percent}% Mastery achieved`;
256263

264+
// CERTIFICATE AWARDING
257265
if (stats.percent === 100) {
258-
const btn = document.createElement('button');
259-
btn.id = 'dl-cert-btn';
260-
btn.className = 'cert-download-btn visible';
261-
btn.style = "background:#28a745; color:white; border:none; padding:12px; width:100%; cursor:pointer; margin-top:20px; font-weight:bold; border-radius:5px;";
262-
btn.innerHTML = '🥇 Download My Team Certificate';
263-
btn.onclick = () => {
264-
document.getElementById('cert-user-name').textContent = currentUser.displayName;
265-
document.getElementById('cert-course-name').textContent = playlists.find(p => p.courseId === cid).title;
266-
document.getElementById('cert-date').textContent = "Validated: " + new Date().toLocaleDateString();
267-
const element = document.getElementById('certificate-template');
268-
element.style.display = 'block';
269-
html2pdf().from(element).set({ margin:0.5, filename:'TeamCertificate.pdf', jsPDF:{orientation:'landscape'} }).save().then(() => element.style.display = 'none');
270-
};
271-
header.appendChild(btn);
266+
handleCertDownload(playlists.find(p => p.courseId === cid).title);
267+
} else {
268+
const oldCert = document.getElementById('dl-cert-btn'); if(oldCert) oldCert.remove();
272269
}
273270
}
274271

275-
searchInput.oninput = (e) => { searchTerm = e.target.value.toLowerCase(); render(); };
272+
function handleCertDownload(courseTitle) {
273+
if (document.getElementById('dl-cert-btn')) return;
274+
const btn = document.createElement('button');
275+
btn.id = 'dl-cert-btn';
276+
btn.className = 'cert-download-btn visible';
277+
btn.style = "background:#28a745; color:white; border:none; padding:12px; width:100%; cursor:pointer; margin-top:20px; font-weight:bold; border-radius:5px;";
278+
btn.innerHTML = '🥇 Claim Team Certificate';
279+
btn.onclick = () => {
280+
document.getElementById('cert-user-name').textContent = currentUser.displayName;
281+
document.getElementById('cert-course-name').textContent = courseTitle;
282+
document.getElementById('cert-date').textContent = "Issued: " + new Date().toLocaleDateString();
283+
const certView = document.getElementById('certificate-template');
284+
certView.style.display = 'block';
285+
html2pdf().from(certView).set({ margin: 0.5, filename: `Certificate_${courseTitle}.pdf`, jsPDF: { orientation: 'landscape' } }).save().then(() => certView.style.display = 'none');
286+
};
287+
document.querySelector('.sidebar-header').appendChild(btn);
288+
}
289+
290+
// Modal Close
276291
document.querySelector('.close-modal').onclick = () => {
277292
document.getElementById('video-overlay').style.display = 'none';
278293
document.getElementById('video-player').src = '';
279294
document.body.style.overflow = 'auto';
280295
};
281296

297+
if(searchInput) searchInput.oninput = (e) => { searchTerm = e.target.value.toLowerCase(); render(); };
298+
282299
loadData();
283300
});

0 commit comments

Comments
 (0)