skillpath / templates /index.html
harshdhane's picture
Upload 4 files
d95c468 verified
Raw
History Blame Contribute Delete
46.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LearnPath β€” AI Student Learning Generator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0b0f;
--surface: #12141a;
--card: #1a1d26;
--border: #252836;
--accent: #6c63ff;
--accent2: #ff6b6b;
--accent3: #00d4aa;
--text: #e8eaf0;
--muted: #6b7280;
--gold: #fbbf24;
--radius: 16px;
--font-head: 'Syne', sans-serif;
--font-body: 'DM Sans', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
min-height: 100vh;
overflow-x: hidden;
}
/* ── SCROLLBAR ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
/* ── GLOBAL BG EFFECT ── */
body::before {
content: '';
position: fixed; inset: 0;
background: radial-gradient(ellipse 80% 60% at 20% 0%, rgba(108,99,255,.12) 0%, transparent 60%),
radial-gradient(ellipse 60% 40% at 80% 100%, rgba(0,212,170,.08) 0%, transparent 60%);
pointer-events: none; z-index: 0;
}
/* ── TOPBAR ── */
#topbar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 14px 28px;
background: rgba(10,11,15,.85);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border);
}
.logo {
font-family: var(--font-head);
font-size: 1.35rem; font-weight: 800;
background: linear-gradient(135deg, var(--accent), var(--accent3));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.topbar-right { display: flex; align-items: center; gap: 12px; }
#date-display {
font-size: .8rem; color: var(--muted);
border: 1px solid var(--border);
padding: 5px 12px; border-radius: 99px;
}
.nav-btn {
background: transparent; border: 1px solid var(--border);
color: var(--text); padding: 7px 16px; border-radius: 99px;
font-family: var(--font-body); font-size: .85rem; cursor: pointer;
transition: all .2s;
}
.nav-btn:hover { border-color: var(--accent); color: var(--accent); }
.nav-btn.primary {
background: var(--accent); border-color: var(--accent); color: #fff;
}
.nav-btn.primary:hover { background: #7c73ff; }
#user-info { display: none; align-items: center; gap: 10px; }
.avatar {
width: 32px; height: 32px; border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent3));
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: .85rem; color: #fff;
font-family: var(--font-head);
}
#username-display { font-size: .9rem; font-weight: 500; }
/* ── PAGES ── */
.page { display: none; padding-top: 70px; min-height: 100vh; position: relative; z-index: 1; }
.page.active { display: block; }
/* ── HOME PAGE ── */
#home-page {
display: none; flex-direction: column; align-items: center;
justify-content: center; min-height: 100vh; padding: 80px 24px 40px;
text-align: center;
}
#home-page.active { display: flex; }
.home-badge {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(108,99,255,.15); border: 1px solid rgba(108,99,255,.3);
color: var(--accent); padding: 6px 16px; border-radius: 99px;
font-size: .8rem; font-weight: 500; margin-bottom: 24px;
animation: fadeInDown .6s ease;
}
.home-badge::before { content: '✦'; }
h1.hero-title {
font-family: var(--font-head);
font-size: clamp(2.4rem, 6vw, 4.5rem);
font-weight: 800; line-height: 1.1;
letter-spacing: -2px;
animation: fadeInDown .7s ease .1s both;
}
h1.hero-title span { color: var(--accent); }
.hero-sub {
font-size: 1.05rem; color: var(--muted); margin: 16px 0 40px;
max-width: 500px; line-height: 1.6;
animation: fadeInDown .7s ease .2s both;
}
.generate-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 24px; padding: 32px;
width: 100%; max-width: 540px;
animation: fadeInUp .7s ease .3s both;
box-shadow: 0 24px 60px rgba(0,0,0,.4);
}
.field-label {
font-size: .8rem; font-weight: 600; color: var(--muted);
text-transform: uppercase; letter-spacing: .8px; margin-bottom: 8px;
text-align: left;
}
.field-group { margin-bottom: 20px; text-align: left; }
.input-field {
width: 100%; background: var(--surface);
border: 1px solid var(--border); border-radius: 12px;
padding: 14px 16px; color: var(--text);
font-family: var(--font-body); font-size: .95rem;
transition: border-color .2s, box-shadow .2s;
outline: none;
}
.input-field:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(108,99,255,.15); }
.input-field::placeholder { color: var(--muted); }
.duration-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
}
.dur-btn {
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 10px 8px; text-align: center;
font-size: .82rem; color: var(--muted); cursor: pointer;
transition: all .2s; font-family: var(--font-body);
}
.dur-btn:hover { border-color: var(--accent); color: var(--text); }
.dur-btn.selected { background: rgba(108,99,255,.15); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.generate-btn {
width: 100%; background: linear-gradient(135deg, var(--accent), #8b5cf6);
border: none; border-radius: 12px; padding: 16px;
color: #fff; font-family: var(--font-head); font-size: 1rem;
font-weight: 700; cursor: pointer; letter-spacing: .5px;
transition: all .3s; position: relative; overflow: hidden;
}
.generate-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(108,99,255,.4); }
.generate-btn:active { transform: translateY(0); }
.generate-btn::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(255,255,255,.15), transparent);
}
/* ── AUTH MODAL ── */
.modal-overlay {
display: none; position: fixed; inset: 0; z-index: 200;
background: rgba(0,0,0,.7); backdrop-filter: blur(8px);
align-items: center; justify-content: center;
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--card); border: 1px solid var(--border);
border-radius: 24px; padding: 36px; width: 100%; max-width: 420px;
animation: fadeInScale .3s ease;
box-shadow: 0 32px 80px rgba(0,0,0,.5);
max-height: 90vh; overflow-y: auto;
}
.modal h2 {
font-family: var(--font-head); font-size: 1.6rem;
font-weight: 800; margin-bottom: 6px;
}
.modal p.sub { color: var(--muted); font-size: .9rem; margin-bottom: 24px; }
.modal-link {
color: var(--accent); cursor: pointer; text-decoration: underline;
font-size: .88rem;
}
.modal-footer { text-align: center; margin-top: 16px; color: var(--muted); font-size: .88rem; }
.btn-full {
width: 100%; background: var(--accent); border: none;
border-radius: 12px; padding: 13px; color: #fff;
font-family: var(--font-head); font-size: .95rem; font-weight: 700;
cursor: pointer; transition: all .2s; margin-top: 4px;
}
.btn-full:hover { background: #7c73ff; transform: translateY(-1px); }
.err-msg { color: var(--accent2); font-size: .85rem; margin-bottom: 12px; text-align: center; }
.input-field.error { border-color: var(--accent2); }
/* ── LOADING OVERLAY ── */
#loading-overlay {
display: none; position: fixed; inset: 0; z-index: 300;
background: rgba(10,11,15,.92); backdrop-filter: blur(12px);
align-items: center; justify-content: center; flex-direction: column; gap: 20px;
}
#loading-overlay.active { display: flex; }
.spinner {
width: 56px; height: 56px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text { font-family: var(--font-head); font-size: 1.1rem; font-weight: 700; }
.loading-sub { color: var(--muted); font-size: .85rem; text-align: center; }
/* ── LEARNING PATH PAGE ── */
#path-page { padding: 80px 0 60px; }
.path-header {
background: var(--surface); border-bottom: 1px solid var(--border);
padding: 20px 28px; position: sticky; top: 64px; z-index: 50;
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.path-title { font-family: var(--font-head); font-size: 1.3rem; font-weight: 800; }
.streak-badge {
display: flex; align-items: center; gap: 6px;
background: rgba(251,191,36,.1); border: 1px solid rgba(251,191,36,.3);
color: var(--gold); padding: 6px 14px; border-radius: 99px;
font-size: .82rem; font-weight: 600;
}
.path-nav { display: flex; gap: 8px; margin-left: auto; }
.path-nav-btn {
background: var(--card); border: 1px solid var(--border);
color: var(--text); padding: 8px 18px; border-radius: 99px;
font-size: .85rem; cursor: pointer; transition: all .2s;
font-family: var(--font-body);
}
.path-nav-btn:hover { border-color: var(--accent); color: var(--accent); }
.path-nav-btn.active-day { background: var(--accent); border-color: var(--accent); color: #fff; }
/* ── DAY CARD ── */
.day-section {
max-width: 720px; margin: 0 auto; padding: 32px 24px;
}
.day-card {
background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 28px;
margin-bottom: 20px;
transition: all .3s;
}
.day-card.current-day {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 8px 32px rgba(108,99,255,.2);
}
.day-card.locked {
opacity: .5; pointer-events: none;
}
.day-card.completed { border-color: var(--accent3); }
.day-header {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 16px;
}
.day-num {
font-family: var(--font-head); font-size: .75rem; font-weight: 700;
color: var(--accent); text-transform: uppercase; letter-spacing: 1px;
}
.day-date { font-size: .8rem; color: var(--muted); margin-top: 2px; }
.day-status {
font-size: .75rem; padding: 4px 12px; border-radius: 99px;
font-weight: 600;
}
.day-status.done { background: rgba(0,212,170,.15); color: var(--accent3); }
.day-status.today { background: rgba(108,99,255,.15); color: var(--accent); }
.day-status.upcoming { background: var(--surface); color: var(--muted); }
.day-status.reset { background: rgba(255,107,107,.15); color: var(--accent2); }
.day-topic {
font-family: var(--font-head); font-size: 1.2rem; font-weight: 700;
margin-bottom: 12px; line-height: 1.3;
}
.day-explanation { font-size: .92rem; color: var(--muted); line-height: 1.7; margin-bottom: 20px; }
/* ── VIDEO THUMB ── */
.video-thumb-wrap {
position: relative; border-radius: 12px; overflow: hidden;
cursor: pointer; group: true;
border: 1px solid var(--border);
}
.video-thumb-wrap img {
width: 100%; display: block;
transition: transform .3s;
}
.video-thumb-wrap:hover img { transform: scale(1.02); }
.play-overlay {
position: absolute; inset: 0; display: flex; align-items: center;
justify-content: center;
background: rgba(0,0,0,.35);
transition: background .2s;
}
.video-thumb-wrap:hover .play-overlay { background: rgba(0,0,0,.5); }
.play-btn {
width: 56px; height: 56px; border-radius: 50%;
background: rgba(255,255,255,.95);
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem;
transform: scale(1); transition: transform .2s;
box-shadow: 0 4px 16px rgba(0,0,0,.3);
}
.video-thumb-wrap:hover .play-btn { transform: scale(1.1); }
.video-meta {
position: absolute; bottom: 0; left: 0; right: 0;
padding: 12px 14px;
background: linear-gradient(transparent, rgba(0,0,0,.8));
font-size: .8rem; color: #fff;
}
.progress-bar-wrap {
margin-top: 14px; background: var(--surface);
border-radius: 99px; height: 6px; overflow: hidden;
}
.progress-bar-fill {
height: 100%; border-radius: 99px;
background: linear-gradient(90deg, var(--accent3), var(--accent));
transition: width .5s ease;
}
/* ── VIDEO MODAL ── */
#video-modal {
display: none; position: fixed; inset: 0; z-index: 400;
background: rgba(0,0,0,.9); backdrop-filter: blur(12px);
align-items: center; justify-content: center; padding: 20px;
}
#video-modal.active { display: flex; }
.video-modal-inner {
background: var(--card); border: 1px solid var(--border);
border-radius: 24px; width: 100%; max-width: 860px;
animation: fadeInScale .3s ease;
overflow: hidden;
}
.video-modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 22px; border-bottom: 1px solid var(--border);
}
.video-modal-title { font-family: var(--font-head); font-size: 1rem; font-weight: 700; }
.close-modal {
background: var(--surface); border: 1px solid var(--border);
color: var(--text); width: 32px; height: 32px; border-radius: 50%;
cursor: pointer; font-size: 1rem; display: flex; align-items: center; justify-content: center;
}
.close-modal:hover { border-color: var(--accent2); color: var(--accent2); }
#video-iframe-wrap {
position: relative; padding-bottom: 56.25%; height: 0;
background: #000;
}
#video-iframe-wrap iframe {
position: absolute; inset: 0; width: 100%; height: 100%;
border: none;
}
.video-progress-panel {
padding: 20px 22px; border-top: 1px solid var(--border);
}
.progress-stats {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
margin-bottom: 16px;
}
.stat-box {
background: var(--surface); border-radius: 12px; padding: 14px;
}
.stat-label { font-size: .75rem; color: var(--muted); margin-bottom: 6px; }
.stat-value { font-family: var(--font-head); font-size .95rem; font-weight: 700; }
.stat-sub { font-size: .7rem; color: var(--muted); margin-top: 2px; }
.progress-bar-modal { background: var(--border); border-radius: 99px; height: 8px; overflow: hidden; margin-top: 8px; }
.progress-bar-modal-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent3), var(--accent)); transition: width .3s; }
.complete-banner {
display: none; background: rgba(0,212,170,.1); border: 1px solid rgba(0,212,170,.3);
border-radius: 12px; padding: 16px 20px; text-align: center;
color: var(--accent3); font-weight: 600; margin-bottom: 12px;
}
.complete-banner.show { display: block; }
#next-day-btn {
display: none; width: 100%;
background: linear-gradient(135deg, var(--accent3), #00b894);
border: none; border-radius: 12px; padding: 14px;
color: #fff; font-family: var(--font-head); font-size: .95rem;
font-weight: 700; cursor: pointer; transition: all .2s;
}
#next-day-btn.show { display: block; }
#next-day-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,212,170,.3); }
/* ── PATHS LIST PAGE ── */
#paths-page { padding: 80px 24px 60px; max-width: 780px; margin: 0 auto; }
.page-title {
font-family: var(--font-head); font-size: 2rem; font-weight: 800;
margin-bottom: 24px; margin-top: 20px;
}
.path-list-card {
background: var(--card); border: 1px solid var(--border);
border-radius: var(--radius); padding: 20px 24px;
margin-bottom: 12px; cursor: pointer; transition: all .2s;
display: flex; align-items: center; justify-content: space-between;
}
.path-list-card:hover { border-color: var(--accent); transform: translateX(4px); }
.path-list-topic { font-family: var(--font-head); font-size: 1.05rem; font-weight: 700; }
.path-list-meta { font-size: .8rem; color: var(--muted); margin-top: 4px; }
.path-list-arrow { color: var(--accent); font-size: 1.2rem; }
/* ── RESET NOTICE ── */
.reset-notice {
background: rgba(255,107,107,.08); border: 1px solid rgba(255,107,107,.25);
border-radius: 12px; padding: 16px 20px; margin-bottom: 20px;
font-size: .88rem; color: var(--accent2);
}
.reset-notice strong { font-weight: 700; }
/* ── ANIMATIONS ── */
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInScale {
from { opacity: 0; transform: scale(.94); }
to { opacity: 1; transform: scale(1); }
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── TOAST ── */
#toast {
position: fixed; bottom: 24px; right: 24px; z-index: 500;
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 14px 20px;
font-size: .88rem; opacity: 0;
transform: translateY(8px);
transition: all .3s;
pointer-events: none;
}
#toast.show { opacity: 1; transform: translateY(0); }
#toast.success { border-color: var(--accent3); color: var(--accent3); }
#toast.error { border-color: var(--accent2); color: var(--accent2); }
/* ── DAY TIMELINE ── */
.day-timeline {
display: flex; gap: 4px; overflow-x: auto;
padding: 0 28px 12px; margin-top: 0;
scrollbar-width: thin;
}
.timeline-dot {
flex-shrink: 0; width: 28px; height: 28px;
border-radius: 50%; background: var(--surface);
border: 2px solid var(--border);
display: flex; align-items: center; justify-content: center;
font-size: .62rem; font-weight: 700; color: var(--muted);
cursor: pointer; transition: all .2s;
}
.timeline-dot.done { background: rgba(0,212,170,.15); border-color: var(--accent3); color: var(--accent3); }
.timeline-dot.today { background: rgba(108,99,255,.2); border-color: var(--accent); color: var(--accent); }
.timeline-dot.reset-start { border-color: var(--accent2); }
.timeline-dot:hover { transform: scale(1.15); }
/* ── RESPONSIVE ── */
@media(max-width: 600px) {
.generate-card { padding: 22px 18px; }
.duration-grid { grid-template-columns: repeat(2, 1fr); }
.day-card { padding: 18px; }
.path-header { padding: 14px 16px; }
.video-modal-inner { border-radius: 16px; }
}
</style>
</head>
<body>
<!-- TOPBAR -->
<div id="topbar">
<div class="logo">LearnPath ✦</div>
<div class="topbar-right">
<div id="date-display"></div>
<div id="guest-nav">
<button class="nav-btn" onclick="showModal('login-modal')">Login</button>
</div>
<div id="user-info">
<div class="avatar" id="user-avatar">?</div>
<span id="username-display"></span>
<button class="nav-btn" onclick="goPage('paths-page')">My Paths</button>
<button class="nav-btn" onclick="logout()">Logout</button>
</div>
</div>
</div>
<!-- ═══ HOME PAGE ═══ -->
<div id="home-page" class="page">
<div class="home-badge">AI-Powered Learning Generator</div>
<h1 class="hero-title">Learn Anything.<br><span>Structured.</span></h1>
<p class="hero-sub">AI builds your personalized day-by-day learning path with curated videos for any skill.</p>
<div class="generate-card">
<div class="field-group">
<div class="field-label">What do you want to learn?</div>
<input type="text" id="topic-input" class="input-field"
placeholder="e.g. Python, Machine Learning, Guitar, Spanish…"
onkeydown="if(event.key==='Enter')generate()">
</div>
<div class="field-group">
<div class="field-label">Starting Date</div>
<input type="date" id="start-date-input" class="input-field">
</div>
<div class="field-group">
<div class="field-label">Duration</div>
<div class="duration-grid">
<button class="dur-btn selected" onclick="selectDuration(this,'7 days')">7 Days</button>
<button class="dur-btn" onclick="selectDuration(this,'2 weeks')">2 Weeks</button>
<button class="dur-btn" onclick="selectDuration(this,'1 month')">1 Month</button>
<button class="dur-btn" onclick="selectDuration(this,'2 months')">2 Months</button>
<button class="dur-btn" onclick="selectDuration(this,'6 months')">6 Months</button>
</div>
</div>
<button class="generate-btn" onclick="generate()">β–Ά GENERATE LEARNING PATH</button>
</div>
</div>
<!-- ═══ PATH VIEW PAGE ═══ -->
<div id="path-page" class="page">
<div class="path-header">
<div>
<div class="path-title" id="path-title">Learning Path</div>
<div style="font-size:.8rem;color:var(--muted)" id="path-meta"></div>
</div>
<div class="streak-badge" id="streak-badge">πŸ”₯ 0 Day Streak</div>
<div class="path-nav">
<button class="path-nav-btn" id="prev-btn" onclick="navDay(-1)">← Prev</button>
<button class="path-nav-btn active-day" onclick="goToday()">Today</button>
<button class="path-nav-btn" id="next-btn" onclick="navDay(1)">Next β†’</button>
</div>
</div>
<div class="day-timeline" id="day-timeline"></div>
<div class="day-section" id="day-section"></div>
</div>
<!-- ═══ MY PATHS PAGE ═══ -->
<div id="paths-page" class="page">
<div class="page-title">My Learning Paths</div>
<div id="paths-list"></div>
</div>
<!-- ═══ LOGIN MODAL ═══ -->
<div class="modal-overlay" id="login-modal">
<div class="modal">
<h2>Welcome back πŸ‘‹</h2>
<p class="sub">Sign in to continue your learning journey</p>
<div id="login-err" class="err-msg" style="display:none"></div>
<div class="field-group">
<div class="field-label">Username or Email</div>
<input type="text" id="login-username" class="input-field" placeholder="Enter username or email">
</div>
<div class="field-group">
<div class="field-label">Password</div>
<input type="password" id="login-password" class="input-field" placeholder="Enter your password"
onkeydown="if(event.key==='Enter')doLogin()">
</div>
<button class="btn-full" onclick="doLogin()">Login β†’</button>
<div class="modal-footer">
Don't have an account? <span class="modal-link" onclick="switchModal('login-modal','signup-modal')">Create one.</span>
</div>
</div>
</div>
<!-- ═══ SIGNUP MODAL ═══ -->
<div class="modal-overlay" id="signup-modal">
<div class="modal">
<h2>Create Account ✨</h2>
<p class="sub">Join thousands of students building skills daily</p>
<div id="signup-err" class="err-msg" style="display:none"></div>
<div class="field-group">
<div class="field-label">Full Name</div>
<input type="text" id="su-name" class="input-field" placeholder="Enter your full name">
</div>
<div class="field-group">
<div class="field-label">Student Number</div>
<input type="text" id="su-student-no" class="input-field" placeholder="Enter Student Number">
</div>
<div class="field-group">
<div class="field-label">Parent Mobile Number</div>
<input type="text" id="su-parent-no" class="input-field" placeholder="Enter Parent Mobile Number">
</div>
<div class="field-group">
<div class="field-label">Parent Email</div>
<input type="email" id="su-parent-email" class="input-field" placeholder="Enter Parent Email ID">
</div>
<div class="field-group">
<div class="field-label">Email Address</div>
<input type="email" id="su-email" class="input-field" placeholder="Enter your email address">
</div>
<div class="field-group">
<div class="field-label">Username</div>
<input type="text" id="su-username" class="input-field" placeholder="Choose a username">
</div>
<div class="field-group">
<div class="field-label">Password</div>
<input type="password" id="su-pass" class="input-field" placeholder="Choose a password">
</div>
<div class="field-group">
<div class="field-label">Confirm Password</div>
<input type="password" id="su-confirm" class="input-field" placeholder="Confirm your password">
</div>
<button class="btn-full" onclick="doSignup()">Create Account β†’</button>
<div class="modal-footer">
Already have an account? <span class="modal-link" onclick="switchModal('signup-modal','login-modal')">Login</span>
</div>
</div>
</div>
<!-- ═══ VIDEO MODAL ═══ -->
<div id="video-modal">
<div class="video-modal-inner">
<div class="video-modal-header">
<div class="video-modal-title" id="vm-title">Video</div>
<button class="close-modal" onclick="closeVideoModal()">βœ•</button>
</div>
<div id="video-iframe-wrap">
<iframe id="yt-iframe" allowfullscreen allow="autoplay"></iframe>
</div>
<div class="video-progress-panel">
<div class="progress-stats">
<div class="stat-box">
<div class="stat-label">πŸ“Ί Video Progress</div>
<div class="stat-value" id="vp-time">0:00 / 0:00</div>
<div class="stat-sub" id="vp-pct">(0.0%)</div>
<div class="progress-bar-modal"><div class="progress-bar-modal-fill" id="vp-bar" style="width:0%"></div></div>
</div>
<div class="stat-box">
<div class="stat-label">βŒ› Watch Time Progress</div>
<div class="stat-value" id="wt-time">0:00 / 0:00</div>
<div class="stat-sub" id="wt-pct">(0.0%)</div>
<div class="progress-bar-modal"><div class="progress-bar-modal-fill" id="wt-bar" style="width:0%"></div></div>
</div>
</div>
<div class="complete-banner" id="complete-banner">
πŸŽ‰ Watch time complete! You can proceed to the next day!
</div>
<button id="next-day-btn" onclick="goNextDayFromModal()">Continue to Next Day β†’</button>
</div>
</div>
</div>
<!-- ═══ LOADING OVERLAY ═══ -->
<div id="loading-overlay">
<div class="spinner"></div>
<div class="loading-text">Generating your learning path…</div>
<div class="loading-sub" id="loading-sub">Using AI to build personalized day-by-day curriculum</div>
</div>
<!-- TOAST -->
<div id="toast"></div>
<!-- ═══════════════ JAVASCRIPT ═══════════════ -->
<script>
// ─── State ────────────────────────────────────────────────────────────────────
let currentUser = null;
let currentPathId = null;
let currentPathData = null;
let currentDayIndex = 0;
let selectedDuration = '7 days';
let pendingGenerate = false;
// Video progress tracking
let ytPlayer = null;
let videoWatchTime = 0;
let videoTotalDuration = 0;
let videoWatchInterval = null;
let currentModalDayNum = null;
let videoCompleted = false;
let videoStartTime = 0; // Saved position
let progressData = {};
// ─── Init ─────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
updateDateDisplay();
setInterval(updateDateDisplay, 60000);
// Set default start date to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('start-date-input').value = today;
document.getElementById('start-date-input').min = today;
// Check auth
const res = await api('/api/auth/me');
if (res.authenticated) {
setUser(res.user);
goPage('home-page');
} else {
goPage('home-page');
}
});
function updateDateDisplay() {
const now = new Date();
const opts = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' };
document.getElementById('date-display').textContent = now.toLocaleDateString('en-IN', opts);
}
// ─── API Helper ───────────────────────────────────────────────────────────────
async function api(url, method='GET', body=null) {
const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include' };
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
return res.json();
}
// ─── Auth ─────────────────────────────────────────────────────────────────────
function setUser(user) {
currentUser = user;
document.getElementById('user-info').style.display = 'flex';
document.getElementById('guest-nav').style.display = 'none';
document.getElementById('username-display').textContent = user.full_name || user.username;
document.getElementById('user-avatar').textContent = (user.full_name || user.username)[0].toUpperCase();
}
function clearUser() {
currentUser = null;
document.getElementById('user-info').style.display = 'none';
document.getElementById('guest-nav').style.display = '';
}
async function doLogin() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
const errEl = document.getElementById('login-err');
errEl.style.display = 'none';
if (!username || !password) {
errEl.textContent = 'Please fill in all fields.'; errEl.style.display = 'block'; return;
}
const res = await api('/api/auth/login', 'POST', { username, password });
if (res.success) {
setUser(res.user);
closeAllModals();
toast('Welcome back, ' + res.user.full_name + '!', 'success');
if (pendingGenerate) { pendingGenerate = false; generate(); }
else goPage('home-page');
} else {
errEl.textContent = res.error || 'Login failed.'; errEl.style.display = 'block';
}
}
async function doSignup() {
const data = {
full_name: document.getElementById('su-name').value.trim(),
student_number: document.getElementById('su-student-no').value.trim(),
parent_number: document.getElementById('su-parent-no').value.trim(),
parent_email: document.getElementById('su-parent-email').value.trim(),
email: document.getElementById('su-email').value.trim(),
username: document.getElementById('su-username').value.trim(),
password: document.getElementById('su-pass').value,
confirm_password: document.getElementById('su-confirm').value
};
const errEl = document.getElementById('signup-err');
errEl.style.display = 'none';
if (!data.full_name || !data.email || !data.username || !data.password) {
errEl.textContent = 'Please fill in all required fields.'; errEl.style.display = 'block'; return;
}
if (data.password !== data.confirm_password) {
errEl.textContent = 'Passwords do not match.'; errEl.style.display = 'block'; return;
}
const res = await api('/api/auth/register', 'POST', data);
if (res.success) {
toast('Account created! Please login.', 'success');
switchModal('signup-modal', 'login-modal');
} else {
errEl.textContent = res.error || 'Registration failed.'; errEl.style.display = 'block';
}
}
async function logout() {
await api('/api/auth/logout', 'POST');
clearUser();
currentPathData = null; currentPathId = null;
goPage('home-page');
toast('Logged out.', 'success');
}
// ─── Generate ─────────────────────────────────────────────────────────────────
async function generate() {
const topic = document.getElementById('topic-input').value.trim();
const startDate = document.getElementById('start-date-input').value;
if (!topic) {
document.getElementById('topic-input').classList.add('error');
setTimeout(() => document.getElementById('topic-input').classList.remove('error'), 1500);
return;
}
if (!currentUser) {
pendingGenerate = true;
showModal('login-modal');
return;
}
showLoading(true, 'Building your ' + selectedDuration + ' learning path for "' + topic + '"…');
const res = await api('/api/generate', 'POST', {
topic, start_date: startDate, duration: selectedDuration
});
showLoading(false);
if (res.success) {
currentPathId = res.path_id;
currentPathData = res.path_data;
progressData = {};
renderPathPage(res.path_data, {}, 0, null);
goPage('path-page');
toast('Learning path created! πŸŽ‰', 'success');
} else {
toast(res.error || 'Generation failed. Check your API keys.', 'error');
}
}
// ─── Path Rendering ───────────────────────────────────────────────────────────
async function renderPathPage(pathData, progress, focusDayIndex, resetTo) {
const days = pathData.days;
const today = new Date().toISOString().split('T')[0];
document.getElementById('path-title').textContent = pathData.topic;
document.getElementById('path-meta').textContent =
days.length + ' days Β· Starts ' + formatDateWords(days[0].date);
// Timeline
const tl = document.getElementById('day-timeline');
tl.innerHTML = '';
days.forEach((day, i) => {
const dot = document.createElement('div');
dot.className = 'timeline-dot';
dot.textContent = i + 1;
dot.title = 'Day ' + (i+1) + ' β€” ' + day.date;
const isDone = progress[day.day_number]?.completed;
const isToday = day.date === today;
if (isDone) dot.classList.add('done');
else if (isToday) dot.classList.add('today');
if (resetTo && i+1 === resetTo) dot.classList.add('reset-start');
dot.onclick = () => { currentDayIndex = i; renderDay(pathData, progress, i, resetTo); };
tl.appendChild(dot);
});
currentDayIndex = focusDayIndex;
renderDay(pathData, progress, focusDayIndex, resetTo);
}
function renderDay(pathData, progress, dayIndex, resetTo) {
const days = pathData.days;
const day = days[dayIndex];
const today = new Date().toISOString().split('T')[0];
const dayProg = progress[day.day_number] || {};
const section = document.getElementById('day-section');
section.innerHTML = '';
// Reset notice
if (resetTo && dayIndex + 1 === resetTo) {
const notice = document.createElement('div');
notice.className = 'reset-notice';
notice.innerHTML = `<strong>⚑ Streak Reset!</strong> You missed 2 days. Starting again from Day ${resetTo}. Complete this and the next day to restore your streak.`;
section.appendChild(notice);
}
const card = document.createElement('div');
card.className = 'day-card';
const isToday = day.date === today;
const isDone = dayProg.completed;
const isFuture = day.date > today;
const isLocked = isFuture;
if (isToday) card.classList.add('current-day');
if (isDone) card.classList.add('completed');
if (isLocked) card.classList.add('locked');
const statusLabel = isDone ? 'done' : isToday ? 'today' : isFuture ? 'upcoming' : 'upcoming';
const statusText = isDone ? 'βœ“ Completed' : isToday ? '● Today' : formatDateWords(day.date);
// Watch progress percent
const watchPct = dayProg.total_duration_seconds > 0
? Math.round((dayProg.watch_time_seconds / dayProg.total_duration_seconds) * 100) : 0;
let videoHTML = '';
if (day.video) {
const v = day.video;
videoHTML = `
<div class="video-thumb-wrap" onclick="openVideo(${JSON.stringify(v).replace(/"/g,"'")}, ${day.day_number}, ${JSON.stringify(day.topic).replace(/"/g,"'")})">
<img src="${v.thumbnail}" alt="${v.title}" loading="lazy">
<div class="play-overlay">
<div class="play-btn">β–Ά</div>
</div>
<div class="video-meta">${v.title}</div>
</div>
<div style="font-size:.75rem;color:var(--muted);margin-top:8px">πŸ“Ί ${v.channel}</div>
${watchPct > 0 ? `
<div class="progress-bar-wrap">
<div class="progress-bar-fill" style="width:${watchPct}%"></div>
</div>
<div style="font-size:.72rem;color:var(--muted);margin-top:4px">Watch progress: ${watchPct}%${isDone ? ' βœ“' : ''}</div>
` : ''}
`;
} else {
videoHTML = `<div style="color:var(--muted);font-size:.85rem;padding:12px 0">No video found for this topic.</div>`;
}
card.innerHTML = `
<div class="day-header">
<div>
<div class="day-num">Day ${day.day_number}</div>
<div class="day-date">${formatDateWords(day.date)}</div>
</div>
<div class="day-status ${statusLabel}">${statusText}</div>
</div>
<div class="day-topic">${day.topic}</div>
<div class="day-explanation">${day.explanation}</div>
${videoHTML}
${isFuture ? '<div style="margin-top:14px;font-size:.82rem;color:var(--muted)">πŸ“… Available on ' + formatDateWords(day.date) + '</div>' : ''}
`;
section.appendChild(card);
// Update nav
document.getElementById('prev-btn').style.opacity = dayIndex === 0 ? '.3' : '1';
document.getElementById('next-btn').style.opacity = dayIndex === days.length - 1 ? '.3' : '1';
}
function navDay(dir) {
const days = currentPathData?.days || [];
const newIdx = currentDayIndex + dir;
if (newIdx < 0 || newIdx >= days.length) return;
currentDayIndex = newIdx;
renderDay(currentPathData, progressData, currentDayIndex, null);
// Scroll timeline dot into view
const dots = document.querySelectorAll('.timeline-dot');
if (dots[currentDayIndex]) dots[currentDayIndex].scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' });
}
function goToday() {
if (!currentPathData) return;
const today = new Date().toISOString().split('T')[0];
const idx = currentPathData.days.findIndex(d => d.date === today);
if (idx >= 0) { currentDayIndex = idx; renderDay(currentPathData, progressData, idx, null); }
}
// ─── Open Existing Path ───────────────────────────────────────────────────────
async function openPath(pathId) {
showLoading(true, 'Loading your learning path…');
const res = await api('/api/paths/' + pathId);
showLoading(false);
if (res.path) {
currentPathId = pathId;
currentPathData = res.path_data;
progressData = res.progress || {};
// Calculate starting day index
const today = new Date().toISOString().split('T')[0];
let startIdx = 0;
if (res.reset_to) {
startIdx = res.reset_to - 1;
} else {
const todayIdx = currentPathData.days.findIndex(d => d.date === today);
if (todayIdx >= 0) startIdx = todayIdx;
}
// Update streak badge
document.getElementById('streak-badge').textContent = 'πŸ”₯ ' + (res.streak || 0) + ' Day Streak';
renderPathPage(currentPathData, progressData, startIdx, res.reset_to);
goPage('path-page');
}
}
// ─── My Paths Page ────────────────────────────────────────────────────────────
async function loadPaths() {
const res = await api('/api/paths');
const list = document.getElementById('paths-list');
if (!res.paths || res.paths.length === 0) {
list.innerHTML = '<div style="color:var(--muted);text-align:center;padding:40px">No learning paths yet. Create your first one!</div>';
return;
}
list.innerHTML = '';
res.paths.forEach(p => {
const card = document.createElement('div');
card.className = 'path-list-card';
card.innerHTML = `
<div>
<div class="path-list-topic">${p.topic}</div>
<div class="path-list-meta">${p.duration_days} days Β· Started ${formatDateWords(p.start_date)}</div>
</div>
<div class="path-list-arrow">β†’</div>
`;
card.onclick = () => openPath(p.id);
list.appendChild(card);
});
}
// ─── Video Modal ──────────────────────────────────────────────────────────────
function openVideo(video, dayNum, dayTopic) {
currentModalDayNum = dayNum;
videoCompleted = false;
clearInterval(videoWatchInterval);
// Get saved progress
const saved = progressData[dayNum] || {};
videoWatchTime = saved.watch_time_seconds || 0;
videoTotalDuration = saved.total_duration_seconds || 0;
videoStartTime = videoWatchTime;
document.getElementById('vm-title').textContent = dayTopic;
document.getElementById('complete-banner').classList.remove('show');
document.getElementById('next-day-btn').classList.remove('show');
// Build iframe with YouTube embed
// Using postMessage API for progress tracking via YouTube IFrame API
const startSeconds = Math.floor(videoWatchTime);
const iframeSrc = `https://www.youtube.com/embed/${video.video_id}?enablejsapi=1&autoplay=1&start=${startSeconds}&rel=0&modestbranding=1`;
document.getElementById('yt-iframe').src = iframeSrc;
document.getElementById('video-modal').classList.add('active');
document.body.style.overflow = 'hidden';
// Start simulated progress tracking (fallback since YT API needs postMessage)
startProgressTracking(dayNum);
}
function startProgressTracking(dayNum) {
clearInterval(videoWatchInterval);
let elapsed = videoWatchTime;
const estimatedDuration = videoTotalDuration || 600; // fallback 10 min
videoWatchInterval = setInterval(async () => {
elapsed += 1;
videoWatchTime = elapsed;
// Try to get real data from iframe via postMessage
const iframe = document.getElementById('yt-iframe');
if (iframe.contentWindow) {
try {
iframe.contentWindow.postMessage(JSON.stringify({event:'command', func:'getDuration', args:[]}), '*');
iframe.contentWindow.postMessage(JSON.stringify({event:'command', func:'getCurrentTime', args:[]}), '*');
} catch(e) {}
}
const pct = Math.min(100, (elapsed / estimatedDuration) * 100);
updateProgressUI(elapsed, estimatedDuration, pct);
// Save progress every 5 seconds
if (elapsed % 5 === 0) {
await saveProgress(dayNum, elapsed, estimatedDuration, pct >= 90 ? 1 : 0);
}
// Check completion
if (pct >= 90 && !videoCompleted) {
videoCompleted = true;
document.getElementById('complete-banner').classList.add('show');
document.getElementById('next-day-btn').classList.add('show');
await saveProgress(dayNum, elapsed, estimatedDuration, 1);
// Update local progress
if (!progressData[dayNum]) progressData[dayNum] = {};
progressData[dayNum].completed = 1;
progressData[dayNum].watch_time_seconds = elapsed;
progressData[dayNum].total_duration_seconds = estimatedDuration;
// Refresh streak
refreshStreak();
}
}, 1000);
}
// Listen for YouTube API messages
window.addEventListener('message', (e) => {
if (!e.data) return;
try {
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
if (data.event === 'infoDelivery' && data.info) {
if (data.info.duration) videoTotalDuration = data.info.duration;
if (data.info.currentTime) {
const ct = data.info.currentTime;
const dur = videoTotalDuration || 600;
updateProgressUI(ct, dur, (ct/dur)*100);
}
}
} catch(e) {}
});
function updateProgressUI(current, total, pct) {
const fmt = s => { const m=Math.floor(s/60),sec=Math.floor(s%60); return m+':'+String(sec).padStart(2,'0'); };
const safeTotal = total || 1;
const safePct = Math.min(100, (current / safeTotal) * 100);
document.getElementById('vp-time').textContent = fmt(current) + ' / ' + fmt(safeTotal);
document.getElementById('vp-pct').textContent = '(' + safePct.toFixed(1) + '%)';
document.getElementById('vp-bar').style.width = safePct + '%';
document.getElementById('wt-time').textContent = fmt(current) + ' / ' + fmt(safeTotal);
document.getElementById('wt-pct').textContent = '(' + safePct.toFixed(1) + '%)';
document.getElementById('wt-bar').style.width = safePct + '%';
}
async function saveProgress(dayNum, watchTime, totalDuration, completed) {
if (!currentPathId) return;
await api('/api/progress', 'POST', {
path_id: currentPathId, day_number: dayNum,
watch_time_seconds: watchTime, total_duration_seconds: totalDuration, completed
});
}
async function refreshStreak() {
if (!currentPathId) return;
const res = await api('/api/paths/' + currentPathId);
if (res.streak !== undefined) {
document.getElementById('streak-badge').textContent = 'πŸ”₯ ' + res.streak + ' Day Streak';
progressData = res.progress || {};
}
}
function closeVideoModal() {
clearInterval(videoWatchInterval);
document.getElementById('yt-iframe').src = '';
document.getElementById('video-modal').classList.remove('active');
document.body.style.overflow = '';
// Refresh day view
if (currentPathData) renderDay(currentPathData, progressData, currentDayIndex, null);
}
function goNextDayFromModal() {
closeVideoModal();
navDay(1);
}
// ─── UI Helpers ───────────────────────────────────────────────────────────────
function selectDuration(btn, val) {
document.querySelectorAll('.dur-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
selectedDuration = val;
}
function goPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
const pg = document.getElementById(pageId);
if (pg) pg.classList.add('active');
window.scrollTo(0, 0);
if (pageId === 'paths-page' && currentUser) loadPaths();
}
function showModal(id) {
document.getElementById(id).classList.add('active');
}
function closeAllModals() {
document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('active'));
}
function switchModal(from, to) {
document.getElementById(from).classList.remove('active');
document.getElementById(to).classList.add('active');
}
// Close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.classList.remove('active');
});
});
function showLoading(show, msg='') {
const el = document.getElementById('loading-overlay');
if (show) { el.classList.add('active'); if (msg) document.getElementById('loading-sub').textContent = msg; }
else el.classList.remove('active');
}
function toast(msg, type='success') {
const t = document.getElementById('toast');
t.textContent = msg; t.className = 'show ' + type;
setTimeout(() => t.className = '', 3000);
}
function formatDateWords(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('en-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}
</script>
</body>
</html>