Skip to content

Commit 1754d65

Browse files
authored
feat: Initial commit for W01 E-Learning Prototype (VibeCoding)
Implemented a functional front-end prototype for the mini e-learning platform using AI-assisted VibeCoding. The solution includes course listing, detail views, lesson progress tracking, and state persistence via localStorage (HTML, CSS, JS).
1 parent 78e61c8 commit 1754d65

File tree

3 files changed

+560
-0
lines changed

3 files changed

+560
-0
lines changed

app.js

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
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("&", "&amp;")
327+
.replaceAll("<", "&lt;")
328+
.replaceAll(">", "&gt;");
329+
}
330+
331+
// start
332+
init();

index.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>Mini e-Learning Prototype</title>
7+
<link rel="stylesheet" href="styles.css">
8+
</head>
9+
<body>
10+
<header class="site-header">
11+
<div class="container">
12+
<h1 class="brand">Mini e-Learning</h1>
13+
<nav>
14+
<button id="homeBtn" class="btn btn-ghost">Home</button>
15+
</nav>
16+
</div>
17+
</header>
18+
19+
<main id="app" class="container">
20+
<!-- App content rendered by JavaScript -->
21+
</main>
22+
23+
<footer class="site-footer">
24+
<div class="container">
25+
<small>Prototype — HTML, CSS, JavaScript · Persisted in localStorage</small>
26+
</div>
27+
</footer>
28+
29+
<script src="app.js"></script>
30+
</body>
31+
</html>

0 commit comments

Comments
 (0)