@@ -11,13 +11,13 @@ const firebaseConfig = {
1111 measurementId : "G-S40XF238WM"
1212} ;
1313
14- // Initialize Instance
14+ // Initialize Firebase
1515if ( ! firebase . apps . length ) { firebase . initializeApp ( firebaseConfig ) ; }
1616const auth = firebase . auth ( ) ;
1717const db = firebase . firestore ( ) ;
1818const 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 )
2121const handleLogin = ( ) => {
2222 auth . signInWithPopup ( provider ) . catch ( ( ) => auth . signInWithRedirect ( provider ) ) ;
2323} ;
@@ -32,7 +32,6 @@ let currentUser = null;
3232let currentFilter = 'All' ;
3333let searchTerm = '' ;
3434
35- // Path routing for the Documentation Hub folders
3635const 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
4544function 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