Spaces:
Sleeping
Sleeping
| {% extends "layouts/base.html" %} | |
| {% block title %}HSK {{ level }} · {{ mode|title }} · HanziLearn{% endblock %} | |
| {% block extra_css %} | |
| <link href="{{ url_for('static', filename='css/elearning.css') }}" rel="stylesheet" /> | |
| {% endblock %} | |
| {% block content %} | |
| <!-- LEARN HEADER | |
| NOTE: The CSS variable --lc is set via JavaScript (see LEARN_CONFIG below) | |
| to avoid Jinja2 inline-style linter false positives. --> | |
| <div class="learn-header" id="learnHeader"> | |
| <div class="container"> | |
| <div class="d-flex align-items-center flex-wrap gap-3"> | |
| <div class="learn-badge" id="learnBadge">HSK {{ level }}</div> | |
| <div> | |
| <h1 class="learn-title mb-0">{{ meta.desc }}</h1> | |
| <p class="learn-subtitle mb-0">{{ meta.words }} vocabulary words</p> | |
| </div> | |
| <div class="ms-auto"> | |
| <span class="learn-user-chip"> | |
| <i class="bi bi-person-fill me-1"></i> | |
| <span id="learnUsername">Guest</span> | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Mode Tabs --> | |
| <div class="mode-tabs mt-4"> | |
| {% set mode_icons = { | |
| 'flashcard': 'card-text', | |
| 'quiz': 'patch-question', | |
| 'fillblank': 'pencil-square', | |
| 'leaderboard': 'trophy' | |
| } %} | |
| {% set mode_labels = { | |
| 'flashcard': 'Flashcard', | |
| 'quiz': 'Quiz', | |
| 'fillblank': 'Fill Blank', | |
| 'leaderboard': 'Leaderboard' | |
| } %} | |
| {% for m in valid_modes %} | |
| <a href="/learn/{{ level }}?mode={{ m }}" | |
| class="mode-tab{% if mode == m %} active{% endif %}"> | |
| <i class="bi bi-{{ mode_icons[m] }} me-1"></i>{{ mode_labels[m] }} | |
| </a> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- LEVEL SELECTOR STRIP --> | |
| <div class="level-strip"> | |
| <div class="container"> | |
| <div class="d-flex gap-2 flex-wrap"> | |
| {% for lvl in range(1, 7) %} | |
| <a href="/learn/{{ lvl }}?mode={{ mode }}" | |
| class="level-pill{% if lvl == level %} active{% endif %}"> | |
| HSK {{ lvl }} | |
| </a> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CONTENT AREA --> | |
| <div class="container py-4" id="learnContainer"> | |
| <!-- Loading state --> | |
| <div id="loadingPanel" class="text-center py-5"> | |
| <div class="learn-spinner"></div> | |
| <p class="text-muted mt-3">Loading {{ mode }} dataβ¦</p> | |
| </div> | |
| <!-- ββββββββββββββ FLASHCARD PANEL ββββββββββββββ --> | |
| <div id="flashcardPanel" class="mode-panel d-none"> | |
| <!-- Progress bar row --> | |
| <div class="fc-progress-wrap mb-4"> | |
| <div class="d-flex justify-content-between align-items-center mb-1"> | |
| <span class="small text-muted"> | |
| Card <span id="fcCurrent">1</span> of <span id="fcTotal">0</span> | |
| </span> | |
| <button class="btn btn-sm btn-outline-secondary" id="fcShuffle" type="button"> | |
| <i class="bi bi-shuffle"></i> Shuffle | |
| </button> | |
| </div> | |
| <div class="progress" style="height:6px" role="progressbar" | |
| aria-label="Flashcard progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> | |
| <div class="progress-bar" id="fcProgressBar" style="width:0%"></div> | |
| </div> | |
| </div> | |
| <!-- Card flip scene --> | |
| <div class="fc-scene mx-auto mb-4" id="fcScene" tabindex="0" role="button" | |
| aria-label="Click to flip card"> | |
| <div class="fc-card" id="fcCard"> | |
| <!-- Front face --> | |
| <div class="fc-front"> | |
| <div class="fc-hanzi" id="fcHanziFront"></div> | |
| <div class="fc-tap-hint">Tap to reveal</div> | |
| </div> | |
| <!-- Back face --> | |
| <div class="fc-back"> | |
| <div class="fc-hanzi fc-hanzi--sm" id="fcHanziBack"></div> | |
| <div class="fc-pinyin" id="fcPinyin"></div> | |
| <div class="fc-divider"></div> | |
| <div class="fc-meaning" id="fcEnglish"></div> | |
| <div class="fc-meaning fc-meaning--vi" id="fcVietnamese"></div> | |
| <div class="fc-example" id="fcExample"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Nav controls --> | |
| <div class="d-flex justify-content-center gap-3"> | |
| <button class="btn btn-outline-secondary btn-icon" id="fcPrev" type="button" aria-label="Previous card"> | |
| <i class="bi bi-arrow-left"></i> | |
| </button> | |
| <button class="btn btn-outline-secondary btn-icon" id="fcFlip" type="button" aria-label="Flip card"> | |
| <i class="bi bi-arrow-repeat"></i> Flip | |
| </button> | |
| <button class="btn btn-accent btn-icon" id="fcNext" type="button" aria-label="Next card"> | |
| <i class="bi bi-arrow-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- END flashcardPanel --> | |
| <!-- ββββββββββββββ QUIZ PANEL ββββββββββββββ --> | |
| <div id="quizPanel" class="mode-panel d-none"> | |
| <!-- Quiz in-progress --> | |
| <div id="quizGame"> | |
| <div class="quiz-meta d-flex justify-content-between align-items-center mb-3"> | |
| <span class="small text-muted"> | |
| Q <span id="qNum">1</span> / <span id="qTotal">10</span> | |
| </span> | |
| <div class="quiz-timer" id="quizTimer" aria-live="polite">30</div> | |
| <span class="badge bg-accent" id="qScore">Score: 0</span> | |
| </div> | |
| <div class="progress mb-4" style="height:4px" role="progressbar" | |
| aria-label="Quiz progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> | |
| <div class="progress-bar bg-accent" id="qProgress" style="width:0%"></div> | |
| </div> | |
| <!-- Question card --> | |
| <div class="quiz-question-card mb-4"> | |
| <div class="quiz-hanzi" id="qHanzi"></div> | |
| <div class="quiz-pinyin" id="qPinyin"></div> | |
| </div> | |
| <!-- Options grid --> | |
| <div class="quiz-options" id="qOptions" role="group" aria-label="Answer options"></div> | |
| </div> | |
| <!-- Quiz result screen --> | |
| <div id="quizResult" class="d-none text-center py-4"> | |
| <div class="result-emoji" id="resultEmoji" aria-hidden="true">🎉</div> | |
| <h3 class="mt-3" id="resultTitle">Quiz Complete!</h3> | |
| <div class="result-score-wrap my-4"> | |
| <div class="result-score" id="resultScore">0/10</div> | |
| <div class="result-percent" id="resultPercent">0%</div> | |
| </div> | |
| <p class="text-muted mb-4" id="resultMsg"></p> | |
| <div class="d-flex gap-3 justify-content-center flex-wrap"> | |
| <button class="btn btn-accent" id="quizRetry" type="button"> | |
| <i class="bi bi-arrow-clockwise me-1"></i>Try Again | |
| </button> | |
| <a href="/learn/{{ level }}?mode=flashcard" class="btn btn-outline-secondary"> | |
| <i class="bi bi-card-text me-1"></i>Flashcards | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- END quizPanel --> | |
| <!-- ββββββββββββββ FILL BLANK PANEL ββββββββββββββ --> | |
| <div id="fillblankPanel" class="mode-panel d-none"> | |
| <div class="fb-progress d-flex justify-content-between align-items-center mb-4"> | |
| <span class="small text-muted"> | |
| Exercise <span id="fbCurrent">1</span> / <span id="fbTotal">0</span> | |
| </span> | |
| <span class="badge bg-accent"> | |
| Score: <span id="fbScore">0</span> | |
| </span> | |
| </div> | |
| <div class="fb-exercise-card mb-4" id="fbCard"> | |
| <p class="fb-context text-muted" id="fbContext"></p> | |
| <div class="fb-sentence" id="fbSentence"></div> | |
| <p class="fb-hint" id="fbHint"></p> | |
| </div> | |
| <div class="d-flex gap-2 mb-3" style="max-width:480px"> | |
| <input | |
| type="text" | |
| id="fbInput" | |
| class="form-control fb-input" | |
| placeholder="Type the missing wordβ¦" | |
| autocomplete="off" | |
| spellcheck="false" | |
| /> | |
| <button class="btn btn-accent" id="fbSubmit" type="button">Check</button> | |
| </div> | |
| <div id="fbFeedback" class="fb-feedback d-none" aria-live="polite"></div> | |
| <div id="fbTranslation" class="fb-translation d-none"></div> | |
| <div class="d-flex justify-content-between mt-4" style="max-width:480px"> | |
| <button class="btn btn-outline-secondary" id="fbPrev" type="button"> | |
| <i class="bi bi-arrow-left me-1"></i>Prev | |
| </button> | |
| <button class="btn btn-outline-secondary" id="fbNext" type="button"> | |
| Next<i class="bi bi-arrow-right ms-1"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- END fillblankPanel --> | |
| <!-- ββββββββββββββ LEADERBOARD PANEL ββββββββββββββ --> | |
| <div id="leaderboardPanel" class="mode-panel d-none"> | |
| <div class="row g-4"> | |
| <div class="col-lg-6"> | |
| <div class="lb-card"> | |
| <h5 class="lb-card-title"> | |
| <i class="bi bi-trophy-fill text-warning me-2"></i>Top Scores | |
| </h5> | |
| <div id="topScoresList" class="lb-list" aria-live="polite"></div> | |
| </div> | |
| </div> | |
| <div class="col-lg-6"> | |
| <div class="lb-card"> | |
| <h5 class="lb-card-title"> | |
| <i class="bi bi-fire text-danger me-2"></i>Most Active | |
| </h5> | |
| <div id="mostActiveList" class="lb-list" aria-live="polite"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- My stats (shown when username is set) --> | |
| <div class="lb-my-stats mt-4" id="myStats" style="display:none"> | |
| <h6 class="mb-3"><i class="bi bi-person-fill me-2"></i>Your Stats</h6> | |
| <div class="row g-3" id="myStatsRow"></div> | |
| </div> | |
| </div> | |
| <!-- END leaderboardPanel --> | |
| </div> | |
| <!-- END learnContainer --> | |
| <!-- Config object for elearning.js β NO inline CSS variables here --> | |
| <script> | |
| </script> | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script src="{{ url_for('static', filename='js/elearning.js') }}"></script> | |
| {% endblock %} |