Paper preview in ZenithPrep style: stat cards, subject sections

shared() now renders: stat cards (questions, duration, max marks,
+4/-1), per-subject sections with q-ranges and chapter lists, a
'take this paper' card with the save-and-next explainer and privacy
note, then the full question list. New .kv/.grid2/.subj-sec/.ch-list
CSS with mobile single-column fallback.
This commit is contained in:
dev 2026-10-16 14:56:27 +05:30
parent 600ff95315
commit 374e2e2056
2 changed files with 88 additions and 3 deletions

View file

@ -1216,6 +1216,39 @@ pub fn dashboard(ctx: &Ctx, user: &str, tests: &[TestRow], weak: &[(Bucket, Stri
}
pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question], paper_id: &str, token: &str, signed: bool) -> String {
use std::collections::BTreeMap;
// Group questions by subject, preserving first-seen order.
let mut order: Vec<String> = Vec::new();
let mut by_subj: BTreeMap<String, Vec<(usize, &Question)>> = BTreeMap::new();
for (i, q) in questions.iter().enumerate() {
if !by_subj.contains_key(&q.subject) {
order.push(q.subject.clone());
}
by_subj.entry(q.subject.clone()).or_default().push((i, q));
}
let n = questions.len();
let max_marks = n as i64 * 4;
// Subject sections with chapter lists.
let mut sections = String::new();
for subj in &order {
let qs = &by_subj[subj];
let start = qs.first().map(|(i, _)| i + 1).unwrap_or(1);
let end = qs.last().map(|(i, _)| i + 1).unwrap_or(n);
let mut chapters: Vec<String> = Vec::new();
for (_, q) in qs {
if !chapters.iter().any(|c| c == &q.chapter) {
chapters.push(q.chapter.clone());
}
}
let ch_list: String = chapters.iter().map(|c| format!("<li>{}</li>", esc(c))).collect();
sections.push_str(&format!(
r#"<div class="card subj-sec"><h3>{subj} <span class="dim">q{start}–q{end}</span></h3><ul class="ch-list">{chs}</ul></div>"#,
subj = esc(subj),
start = start,
end = end,
chs = ch_list
));
}
let qs: String = questions
.iter()
.enumerate()
@ -1253,12 +1286,23 @@ pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question], pap
)
};
let body = format!(
r#"<h1>{title}</h1><p class="meta">{count} questions · {dur} minutes · no answers or scores on this page.</p>{cta}<p><button class="btn" type="button" onclick="window.print()">print / PDF</button></p><div style="margin-top:16px">{qs}</div>"#,
r#"<h1>Practice paper</h1><p class="meta">{title} · JEE Main pattern · no answers or scores on this page.</p>
<div class="kv">
<div class="card"><span class="dim">questions</span><b>{count}</b></div>
<div class="card"><span class="dim">duration</span><b>{dur} min</b></div>
<div class="card"><span class="dim">maximum marks</span><b>{max}</b></div>
<div class="card"><span class="dim">marking</span><b>+4 / −1</b></div>
</div>
<div class="grid2"><div>{sections}</div><div class="card"><h3>take this paper</h3><p class="meta">The online test works like the real exam: answers count only after save &amp; next.</p>{cta}<p><button class="btn" type="button" onclick="window.print()">print / PDF</button></p><p class="meta">Only the question paper is shared. Your answers and score stay private.</p></div></div>
<h2>every question</h2>
<div style="margin-top:12px">{qs}</div>"#,
title = esc(title),
count = questions.len(),
dur = duration,
qs = qs,
cta = cta
max = max_marks,
sections = sections,
cta = cta,
qs = qs
);
shell(ctx, None, title, body)
}

View file

@ -2229,3 +2229,44 @@ img.brand-logo {
vertical-align: middle;
filter: drop-shadow(0 0 6px rgba(59, 130, 246, 0.3));
}
/* ===== paper preview: stat cards + subject sections ===== */
.kv {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin: 16px 0;
}
.kv .card {
text-align: center;
padding: 16px 12px;
}
.kv .card b {
display: block;
font-size: 1.5rem;
margin-top: 4px;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 320px;
gap: 16px;
margin: 16px 0;
}
@media (max-width: 760px) {
.grid2 { grid-template-columns: 1fr; }
}
.subj-sec { margin-bottom: 12px; }
.subj-sec h3 { margin-bottom: 8px; }
.ch-list {
list-style: none;
margin: 0;
padding: 0;
columns: 2;
font-size: 0.85rem;
color: var(--text-main);
}
.ch-list li { margin-bottom: 4px; }
.ch-list li::before { content: "• "; color: var(--text-dim); }
@media (max-width: 560px) {
.ch-list { columns: 1; }
}