Spaces:
Sleeping
Sleeping
| <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> |