1+ // Simple mini e-learning SPA prototype
2+
3+ // Sample course data (can be replaced with JSON or fetched from an API)
4+ const coursesSeed = [
5+ {
6+ id : "c1" ,
7+ title : "Introduction to Web Development" ,
8+ description : "HTML, CSS, and JavaScript basics to get you started." ,
9+ lessons : [
10+ { id : "c1-l1" , title : "HTML Basics" , duration : "10m" } ,
11+ { id : "c1-l2" , title : "CSS Foundations" , duration : "14m" } ,
12+ { id : "c1-l3" , title : "Intro to JavaScript" , duration : "18m" }
13+ ]
14+ } ,
15+ {
16+ id : "c2" ,
17+ title : "Responsive Design" ,
18+ description : "Create layouts that look great on any screen." ,
19+ lessons : [
20+ { id : "c2-l1" , title : "Flexbox" , duration : "12m" } ,
21+ { id : "c2-l2" , title : "Grid" , duration : "16m" } ,
22+ { id : "c2-l3" , title : "Media Queries" , duration : "10m" }
23+ ]
24+ } ,
25+ {
26+ id : "c3" ,
27+ title : "JavaScript Essentials" ,
28+ description : "Core JS concepts: variables, control flow, functions." ,
29+ lessons : [
30+ { id : "c3-l1" , title : "Variables & Types" , duration : "8m" } ,
31+ { id : "c3-l2" , title : "Functions" , duration : "15m" } ,
32+ { id : "c3-l3" , title : "Async Basics" , duration : "20m" }
33+ ]
34+ }
35+ ] ;
36+
37+ const STORAGE_KEY = "mini-elearning:progress" ;
38+ let courses = [ ] ; // will hold courses with progress info
39+ const appEl = document . getElementById ( "app" ) ;
40+ const homeBtn = document . getElementById ( "homeBtn" ) ;
41+
42+ homeBtn . addEventListener ( "click" , ( ) => {
43+ history . pushState ( { view : "home" } , "" , "/" ) ;
44+ renderHome ( ) ;
45+ } ) ;
46+
47+ // Initialize app: hydrate from localStorage or seed
48+ function init ( ) {
49+ const saved = localStorage . getItem ( STORAGE_KEY ) ;
50+ if ( saved ) {
51+ try {
52+ courses = JSON . parse ( saved ) ;
53+ } catch ( e ) {
54+ console . error ( "Failed to parse storage, using seed" , e ) ;
55+ courses = attachProgress ( coursesSeed ) ;
56+ }
57+ } else {
58+ courses = attachProgress ( coursesSeed ) ;
59+ }
60+
61+ // router - handle back/forward
62+ window . addEventListener ( "popstate" , ( e ) => {
63+ const state = ( e . state && e . state . view ) || "home" ;
64+ if ( state === "home" ) renderHome ( ) ;
65+ else if ( state . view === "course" ) renderCourse ( state . courseId ) ;
66+ else renderHome ( ) ;
67+ } ) ;
68+
69+ // initial render
70+ renderHome ( ) ;
71+ }
72+
73+ // Add progress metadata to seed
74+ function attachProgress ( seed ) {
75+ return seed . map ( c => ( {
76+ ...c ,
77+ completed : false ,
78+ lessonsProgress : c . lessons . map ( l => ( { lessonId : l . id , done : false } ) )
79+ } ) ) ;
80+ }
81+
82+ function saveState ( ) {
83+ localStorage . setItem ( STORAGE_KEY , JSON . stringify ( courses ) ) ;
84+ }
85+
86+ function findCourse ( id ) {
87+ return courses . find ( c => c . id === id ) ;
88+ }
89+
90+ function calculatePercent ( course ) {
91+ const total = course . lessons . length ;
92+ const doneCount = course . lessonsProgress . filter ( lp => lp . done ) . length ;
93+ return Math . round ( ( doneCount / total ) * 100 ) ;
94+ }
95+
96+ /* ---------- Renderers ---------- */
97+
98+ function renderHome ( ) {
99+ document . title = "Mini e-Learning — Home" ;
100+ appEl . innerHTML = `
101+ <section>
102+ <div class="space-between">
103+ <div>
104+ <h2>Available Courses</h2>
105+ <p class="muted">Browse courses and track your progress.</p>
106+ </div>
107+ <div class="row">
108+ <button id="resetBtn" class="btn btn-ghost">Reset Progress</button>
109+ </div>
110+ </div>
111+
112+ <div class="grid" id="coursesGrid"></div>
113+ </section>
114+ ` ;
115+
116+ const grid = document . getElementById ( "coursesGrid" ) ;
117+ courses . forEach ( c => {
118+ const percent = calculatePercent ( c ) ;
119+ const card = document . createElement ( "article" ) ;
120+ card . className = "card" ;
121+ card . innerHTML = `
122+ <div class="meta">
123+ <div>
124+ <h3>${ escapeHtml ( c . title ) } </h3>
125+ <p class="muted">${ escapeHtml ( c . description ) } </p>
126+ </div>
127+ <div style="text-align:right">
128+ ${ c . completed ? `<span class="badge success">Completed</span>` : `<span class="small muted">${ percent } %</span>` }
129+ </div>
130+ </div>
131+
132+ <div>
133+ <div class="progress-wrap" aria-hidden="true">
134+ <div class="progress" style="width:${ percent } %"></div>
135+ </div>
136+ </div>
137+
138+ <div class="space-between">
139+ <div class="small muted">${ c . lessons . length } lessons</div>
140+ <div class="row">
141+ ${ c . completed ?
142+ `<button class="btn btn-ghost" data-course="${ c . id } " disabled>Completed</button>` :
143+ `<button class="btn" data-action="mark-complete" data-course="${ c . id } ">Mark Completed</button>`
144+ }
145+ <button class="btn btn-ghost" data-action="view-course" data-course="${ c . id } ">View</button>
146+ </div>
147+ </div>
148+ ` ;
149+
150+ // clicking whole card opens course detail
151+ card . addEventListener ( "click" , ( ev ) => {
152+ // don't trigger when clicking buttons inside card
153+ if ( ev . target . closest ( "button" ) ) return ;
154+ history . pushState ( { view : "course" , courseId : c . id } , "" , `#${ c . id } ` ) ;
155+ renderCourse ( c . id ) ;
156+ } ) ;
157+
158+ grid . appendChild ( card ) ;
159+ } ) ;
160+
161+ // attach event handlers for buttons inside grid
162+ grid . addEventListener ( "click" , ( ev ) => {
163+ const btn = ev . target . closest ( "button" ) ;
164+ if ( ! btn ) return ;
165+ const cId = btn . dataset . course ;
166+ const action = btn . dataset . action ;
167+ if ( action === "mark-complete" ) {
168+ markCourseCompleted ( cId ) ;
169+ renderHome ( ) ;
170+ } else if ( action === "view-course" ) {
171+ history . pushState ( { view : "course" , courseId : cId } , "" , `#${ cId } ` ) ;
172+ renderCourse ( cId ) ;
173+ }
174+ } ) ;
175+
176+ document . getElementById ( "resetBtn" ) . addEventListener ( "click" , ( ) => {
177+ if ( ! confirm ( "Reset all progress?" ) ) return ;
178+ courses = attachProgress ( coursesSeed ) ;
179+ saveState ( ) ;
180+ renderHome ( ) ;
181+ } ) ;
182+ }
183+
184+ function renderCourse ( courseId ) {
185+ const course = findCourse ( courseId ) ;
186+ if ( ! course ) {
187+ appEl . innerHTML = `<div class="empty">Course not found</div>` ;
188+ return ;
189+ }
190+
191+ document . title = `Mini e-Learning — ${ course . title } ` ;
192+
193+ const percent = calculatePercent ( course ) ;
194+ appEl . innerHTML = `
195+ <div class="detail">
196+ <div class="panel">
197+ <div class="space-between">
198+ <div>
199+ <h2>${ escapeHtml ( course . title ) } </h2>
200+ <p class="muted">${ escapeHtml ( course . description ) } </p>
201+ </div>
202+ <div>
203+ <div class="badge ${ course . completed ? "success" : "" } ">${ course . completed ? "Completed" : `${ percent } %` } </div>
204+ </div>
205+ </div>
206+
207+ <div style="margin-top:12px" class="small muted">Lessons</div>
208+ <div class="lessons" id="lessonsList" style="margin-top:8px"></div>
209+
210+ <div style="margin-top:16px" class="row">
211+ <button id="backBtn" class="btn btn-ghost">Back</button>
212+ ${ course . completed ? `<button class="btn btn-ghost" disabled>Completed</button>` : `<button id="completeCourseBtn" class="btn">Mark course completed</button>` }
213+ </div>
214+ </div>
215+
216+ <div class="panel">
217+ <div class="small muted">Course progress</div>
218+ <div style="margin-top:8px;">
219+ <div class="progress-wrap" aria-hidden="true"><div class="progress" style="width:${ percent } %"></div></div>
220+ </div>
221+
222+ <div style="margin-top:12px;">
223+ <div class="muted small">Details</div>
224+ <ul style="margin:8px 0 0 18px;">
225+ <li>${ course . lessons . length } lessons</li>
226+ <li>Completion: ${ percent } %</li>
227+ </ul>
228+ </div>
229+
230+ </div>
231+ </div>
232+ ` ;
233+
234+ const lessonsList = document . getElementById ( "lessonsList" ) ;
235+ course . lessons . forEach ( lesson => {
236+ const lp = course . lessonsProgress . find ( x => x . lessonId === lesson . id ) ;
237+ const lessonEl = document . createElement ( "label" ) ;
238+ lessonEl . className = "lesson" ;
239+ lessonEl . innerHTML = `
240+ <input type="checkbox" ${ lp && lp . done ? "checked" : "" } data-lesson="${ lesson . id } " />
241+ <div style="flex:1">
242+ <div class="title">${ escapeHtml ( lesson . title ) } </div>
243+ <div class="meta muted">${ escapeHtml ( lesson . duration ) } </div>
244+ </div>
245+ <div class="muted small">${ lp && lp . done ? "Done" : "" } </div>
246+ ` ;
247+ lessonsList . appendChild ( lessonEl ) ;
248+ } ) ;
249+
250+ // event handlers
251+ document . getElementById ( "backBtn" ) . addEventListener ( "click" , ( ) => {
252+ history . pushState ( { view : "home" } , "" , "/" ) ;
253+ renderHome ( ) ;
254+ } ) ;
255+
256+ const completeCourseBtn = document . getElementById ( "completeCourseBtn" ) ;
257+ if ( completeCourseBtn ) {
258+ completeCourseBtn . addEventListener ( "click" , ( ) => {
259+ markCourseCompleted ( courseId ) ;
260+ renderCourse ( courseId ) ;
261+ } ) ;
262+ }
263+
264+ lessonsList . addEventListener ( "change" , ( ev ) => {
265+ const cb = ev . target ;
266+ if ( cb && cb . type === "checkbox" ) {
267+ const lessonId = cb . dataset . lesson ;
268+ toggleLesson ( courseId , lessonId , cb . checked ) ;
269+ // re-render progress indicator within detail
270+ const newPercent = calculatePercent ( course ) ;
271+ const progEls = document . querySelectorAll ( ".progress" ) ;
272+ progEls . forEach ( pe => {
273+ // attempt to update widths (simple approach)
274+ pe . style . width = `${ newPercent } %` ;
275+ } ) ;
276+
277+ // Update completed state if all lessons are done
278+ if ( newPercent === 100 ) {
279+ markCourseCompleted ( courseId ) ;
280+ } else {
281+ // If any lesson unchecked, ensure course not marked completed
282+ course . completed = false ;
283+ }
284+ saveState ( ) ;
285+ // Update badge and button state
286+ const badge = document . querySelector ( ".badge" ) ;
287+ if ( badge ) badge . textContent = course . completed ? "Completed" : `${ newPercent } %` ;
288+ if ( course . completed ) {
289+ const ccBtn = document . getElementById ( "completeCourseBtn" ) ;
290+ if ( ccBtn ) ccBtn . disabled = true ;
291+ }
292+ }
293+ } ) ;
294+ }
295+
296+ /* ---------- Actions ---------- */
297+
298+ function toggleLesson ( courseId , lessonId , done ) {
299+ const course = findCourse ( courseId ) ;
300+ if ( ! course ) return ;
301+ const lp = course . lessonsProgress . find ( x => x . lessonId === lessonId ) ;
302+ if ( ! lp ) return ;
303+ lp . done = ! ! done ;
304+ // if any undone, ensure course not completed
305+ if ( ! done ) course . completed = false ;
306+ else {
307+ const percent = calculatePercent ( course ) ;
308+ if ( percent === 100 ) course . completed = true ;
309+ }
310+ saveState ( ) ;
311+ }
312+
313+ function markCourseCompleted ( courseId ) {
314+ const course = findCourse ( courseId ) ;
315+ if ( ! course ) return ;
316+ course . completed = true ;
317+ course . lessonsProgress . forEach ( lp => lp . done = true ) ;
318+ saveState ( ) ;
319+ }
320+
321+ /* ---------- Utilities ---------- */
322+
323+ function escapeHtml ( str ) {
324+ if ( ! str ) return "" ;
325+ return String ( str )
326+ . replaceAll ( "&" , "&" )
327+ . replaceAll ( "<" , "<" )
328+ . replaceAll ( ">" , ">" ) ;
329+ }
330+
331+ // start
332+ init ( ) ;
0 commit comments