ZenithPrep-style build page with difficulty and year filters
Replicates zenithprepare.com/PYQ topic-selection UX: - Subject switcher pills (Maths/Physics/Chemistry) with live selection counters; only the active subject's chapters are shown. - Searchable chapter rows with independent subtopic expand arrows (checking a chapter no longer toggles the tray). - Complete-syllabus one-click button (selects all 15 units, 75 Qs, JEE mode). - Difficulty filter pills (Easy/Medium/Hard, default all) and year filter pills (2018-2025, default 2020-2025) wired through new difficulties/years form fields; build_paper filters both, including the FIFO recycle path. Verified end-to-end: Hard-only paper returns only Hard questions; Medium+Hard across 2022-2025 leaks zero Easy. - Settings bar with count pills, JEE-mode toggle and live pool estimate. Also fixes: brand header badge, raw-string delimiter clash with SVG hex fill, Maths subject-key mismatch between pills and DB.
This commit is contained in:
parent
54ba844f92
commit
ab7486bcc2
4 changed files with 1014 additions and 276 deletions
69
src/exam.rs
69
src/exam.rs
|
|
@ -50,6 +50,12 @@ pub struct BuildForm {
|
||||||
/// When non-empty, only questions from these subtopic ids are drawn.
|
/// When non-empty, only questions from these subtopic ids are drawn.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub subtopics: String,
|
pub subtopics: String,
|
||||||
|
/// Comma-separated difficulties (e.g. "Easy,Medium,Hard").
|
||||||
|
#[serde(default)]
|
||||||
|
pub difficulties: String,
|
||||||
|
/// Comma-separated years (e.g. "2020,2021,2022,2023,2024,2025").
|
||||||
|
#[serde(default)]
|
||||||
|
pub years: String,
|
||||||
pub count: Option<usize>,
|
pub count: Option<usize>,
|
||||||
pub duration: Option<i64>,
|
pub duration: Option<i64>,
|
||||||
pub unseen_only: Option<String>,
|
pub unseen_only: Option<String>,
|
||||||
|
|
@ -65,6 +71,19 @@ impl BuildForm {
|
||||||
pub fn subtopic_ids(&self) -> Vec<i64> {
|
pub fn subtopic_ids(&self) -> Vec<i64> {
|
||||||
self.subtopics.split(',').filter_map(|c| c.trim().parse().ok()).collect()
|
self.subtopics.split(',').filter_map(|c| c.trim().parse().ok()).collect()
|
||||||
}
|
}
|
||||||
|
pub fn difficulty_list(&self) -> Vec<String> {
|
||||||
|
self.difficulties
|
||||||
|
.split(',')
|
||||||
|
.map(|d| d.trim().to_string())
|
||||||
|
.filter(|d| !d.is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
pub fn year_list(&self) -> Vec<i64> {
|
||||||
|
self.years
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|y| y.trim().parse().ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
pub fn is_jee_mode(&self) -> bool {
|
pub fn is_jee_mode(&self) -> bool {
|
||||||
self.mode == "jee"
|
self.mode == "jee"
|
||||||
}
|
}
|
||||||
|
|
@ -93,33 +112,49 @@ pub async fn build_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow:
|
||||||
f.duration.unwrap_or(count as i64 * 2).clamp(5, 180)
|
f.duration.unwrap_or(count as i64 * 2).clamp(5, 180)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let diffs = f.difficulty_list();
|
||||||
|
let years = f.year_list();
|
||||||
|
|
||||||
let mut ids: Vec<i64> = Vec::new();
|
let mut ids: Vec<i64> = Vec::new();
|
||||||
// Chapters × subtopics: one indexed lookup per pair (sqlx needs a static
|
// Chapters × subtopics: indexed lookup per pair, filtering difficulty & year if selected.
|
||||||
// string per branch, hence the unseen/not split inline).
|
|
||||||
for ch in &chapters {
|
for ch in &chapters {
|
||||||
if !subtopics.is_empty() {
|
if !subtopics.is_empty() {
|
||||||
for st in &subtopics {
|
for st in &subtopics {
|
||||||
let rows: Vec<sqlx::sqlite::SqliteRow> = if unseen {
|
let rows: Vec<sqlx::sqlite::SqliteRow> = if unseen {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"SELECT q.id FROM questions q WHERE q.chapter_id=? AND q.subtopic_id=? AND q.id NOT IN (SELECT question_id FROM seen_questions WHERE user_id=?)"
|
"SELECT q.id, q.difficulty, q.year FROM questions q WHERE q.chapter_id=? AND q.subtopic_id=? AND q.id NOT IN (SELECT question_id FROM seen_questions WHERE user_id=?)"
|
||||||
).bind(ch).bind(st).bind(uid).fetch_all(pool).await?
|
).bind(ch).bind(st).bind(uid).fetch_all(pool).await?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"SELECT q.id FROM questions q WHERE q.chapter_id=? AND q.subtopic_id=?"
|
"SELECT q.id, q.difficulty, q.year FROM questions q WHERE q.chapter_id=? AND q.subtopic_id=?"
|
||||||
).bind(ch).bind(st).fetch_all(pool).await?
|
).bind(ch).bind(st).fetch_all(pool).await?
|
||||||
};
|
};
|
||||||
for r in rows { ids.push(r.get::<i64, _>("id")); }
|
for r in rows {
|
||||||
|
let d: String = r.get("difficulty");
|
||||||
|
let y: i64 = r.get("year");
|
||||||
|
if (!diffs.is_empty() && !diffs.iter().any(|x| x.eq_ignore_ascii_case(&d))) || (!years.is_empty() && !years.contains(&y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ids.push(r.get::<i64, _>("id"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let rows: Vec<sqlx::sqlite::SqliteRow> = if unseen {
|
let rows: Vec<sqlx::sqlite::SqliteRow> = if unseen {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"SELECT q.id FROM questions q WHERE q.chapter_id=? AND q.id NOT IN (SELECT question_id FROM seen_questions WHERE user_id=?)"
|
"SELECT q.id, q.difficulty, q.year FROM questions q WHERE q.chapter_id=? AND q.id NOT IN (SELECT question_id FROM seen_questions WHERE user_id=?)"
|
||||||
).bind(ch).bind(uid).fetch_all(pool).await?
|
).bind(ch).bind(uid).fetch_all(pool).await?
|
||||||
} else {
|
} else {
|
||||||
sqlx::query("SELECT q.id FROM questions q WHERE q.chapter_id=?")
|
sqlx::query("SELECT q.id, q.difficulty, q.year FROM questions q WHERE q.chapter_id=?")
|
||||||
.bind(ch).fetch_all(pool).await?
|
.bind(ch).fetch_all(pool).await?
|
||||||
};
|
};
|
||||||
for r in rows { ids.push(r.get::<i64, _>("id")); }
|
for r in rows {
|
||||||
|
let d: String = r.get("difficulty");
|
||||||
|
let y: i64 = r.get("year");
|
||||||
|
if (!diffs.is_empty() && !diffs.iter().any(|x| x.eq_ignore_ascii_case(&d))) || (!years.is_empty() && !years.contains(&y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ids.push(r.get::<i64, _>("id"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deduplicate: a question can match multiple subtopics if the form sent both.
|
// Deduplicate: a question can match multiple subtopics if the form sent both.
|
||||||
|
|
@ -130,12 +165,8 @@ pub async fn build_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow:
|
||||||
// instead of erroring. seen_at='' rows (pre-migration) sort first.
|
// instead of erroring. seen_at='' rows (pre-migration) sort first.
|
||||||
let mut recycled = 0;
|
let mut recycled = 0;
|
||||||
if unseen && (ids.len() as i64) < count as i64 {
|
if unseen && (ids.len() as i64) < count as i64 {
|
||||||
let need = count as usize - ids.len();
|
let seen_rows: Vec<(i64, i64, i64, String, i64)> = sqlx::query_as(
|
||||||
let scope_ch: String = chapters.iter().map(|c| c.to_string()).collect::<Vec<_>>().join(",");
|
"SELECT q.id, q.chapter_id, q.subtopic_id, q.difficulty, q.year FROM questions q
|
||||||
let scope_st: String = subtopics.iter().map(|st| st.to_string()).collect::<Vec<_>>().join(",");
|
|
||||||
// Static query per branch (sqlx); scope filter applied in Rust below.
|
|
||||||
let seen_rows: Vec<(i64, i64, i64)> = sqlx::query_as(
|
|
||||||
"SELECT q.id, q.chapter_id, q.subtopic_id FROM questions q
|
|
||||||
JOIN seen_questions sq ON sq.question_id=q.id AND sq.user_id=?
|
JOIN seen_questions sq ON sq.question_id=q.id AND sq.user_id=?
|
||||||
ORDER BY sq.seen_at ASC",
|
ORDER BY sq.seen_at ASC",
|
||||||
)
|
)
|
||||||
|
|
@ -143,22 +174,22 @@ pub async fn build_paper(pool: &SqlitePool, uid: &str, f: &BuildForm) -> anyhow:
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let in_scope = |ch: i64, st: i64| {
|
let in_scope = |ch: i64, st: i64, d: &str, y: i64| {
|
||||||
chapters.contains(&ch)
|
chapters.contains(&ch)
|
||||||
&& (subtopics.is_empty() || subtopics.contains(&st))
|
&& (subtopics.is_empty() || subtopics.contains(&st))
|
||||||
|
&& (diffs.is_empty() || diffs.iter().any(|x| x.eq_ignore_ascii_case(d)))
|
||||||
|
&& (years.is_empty() || years.contains(&y))
|
||||||
};
|
};
|
||||||
let _ = (scope_ch, scope_st);
|
for (qid, ch, st, d, y) in seen_rows {
|
||||||
for (qid, ch, st) in seen_rows {
|
|
||||||
if ids.len() >= count as usize {
|
if ids.len() >= count as usize {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if !in_scope(ch, st) || ids.contains(&qid) {
|
if !in_scope(ch, st, &d, y) || ids.contains(&qid) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ids.push(qid);
|
ids.push(qid);
|
||||||
recycled += 1;
|
recycled += 1;
|
||||||
}
|
}
|
||||||
let _ = need;
|
|
||||||
}
|
}
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
anyhow::bail!("No questions left for that selection. Turn off 'unseen only' to reuse questions.");
|
anyhow::bail!("No questions left for that selection. Turn off 'unseen only' to reuse questions.");
|
||||||
|
|
|
||||||
11
src/main.rs
11
src/main.rs
|
|
@ -306,7 +306,16 @@ async fn build_page(State(ctx): State<CtxArc>, h: HeaderMap) -> axum::response::
|
||||||
};
|
};
|
||||||
let blocks = load_chapter_blocks(&ctx.pool).await;
|
let blocks = load_chapter_blocks(&ctx.pool).await;
|
||||||
let available: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM questions").fetch_one(&ctx.pool).await.unwrap_or(0);
|
let available: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM questions").fetch_one(&ctx.pool).await.unwrap_or(0);
|
||||||
let form = BuildForm { chapters: String::new(), subtopics: String::new(), count: Some(12), duration: Some(24), unseen_only: Some("on".into()), mode: String::new() };
|
let form = BuildForm {
|
||||||
|
chapters: String::new(),
|
||||||
|
subtopics: String::new(),
|
||||||
|
difficulties: String::new(),
|
||||||
|
years: String::new(),
|
||||||
|
count: Some(15),
|
||||||
|
duration: Some(30),
|
||||||
|
unseen_only: Some("on".into()),
|
||||||
|
mode: String::new(),
|
||||||
|
};
|
||||||
hx(views::build(&ctx, &name, blocks, &form, available, None), StatusCode::OK).into_response()
|
hx(views::build(&ctx, &name, blocks, &form, available, None), StatusCode::OK).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
579
src/views.rs
579
src/views.rs
|
|
@ -110,28 +110,27 @@ fn head(ctx: &Ctx, title: &str) -> String {
|
||||||
|
|
||||||
fn nav(ctx: &Ctx, user: Option<&str>) -> String {
|
fn nav(ctx: &Ctx, user: Option<&str>) -> String {
|
||||||
let brand = format!(
|
let brand = format!(
|
||||||
r#"<header class="topbar"><a class="brand" href="{b}/">WhalePrep<span>jee main practice</span></a><nav class="nav">"#,
|
r##"<header class="topbar"><div class="brand-group"><a class="brand" href="{b}/"><svg class="brand-logo" viewBox="0 0 24 24" width="22" height="22"><path fill="#60a5fa" d="M12 2L1 21h22L12 2zm0 4.5l7.5 13H4.5L12 6.5z"/></svg> WhalePrep</a><span class="badge-syll">2025 Syllabus</span></div><nav class="nav">"##,
|
||||||
b = ctx.base
|
b = ctx.base
|
||||||
);
|
);
|
||||||
let links = match user {
|
let links = match user {
|
||||||
Some(_) => format!(
|
Some(u) => format!(
|
||||||
r#"<a class="hidem" href="{b}/build">build</a><a class="hidem" href="{b}/dashboard">dashboard</a><a class="hidem" href="{b}/history">history</a><a class="btn" href="{b}/logout">sign out</a>"#,
|
r#"<a class="hidem" href="{b}/build">New paper</a><a class="hidem" href="{b}/history">My papers</a><a class="hidem" href="{b}/dashboard">Dashboard</a><span class="user-greeting">{name}</span><a class="btn-logout" href="{b}/logout">Log out</a>"#,
|
||||||
b = ctx.base
|
b = ctx.base,
|
||||||
|
name = esc(u)
|
||||||
),
|
),
|
||||||
None => format!(
|
None => format!(
|
||||||
r#"<a class="hidem" href="{b}/terms">terms</a><a class="hidem" href="{b}/privacy">privacy</a><a class="btn primary" href="{b}/build">start a paper</a>"#,
|
r#"<a class="hidem" href="{b}/build">New paper</a><a class="hidem" href="{b}/terms">Terms</a><a class="btn primary" href="{b}/student">Sign in</a>"#,
|
||||||
b = ctx.base
|
b = ctx.base
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
// Below 700px the row of links has nowhere to go, so the primary destinations
|
|
||||||
// move to a bottom bar rather than disappearing behind a hidden class.
|
|
||||||
let tabbar = match user {
|
let tabbar = match user {
|
||||||
Some(_) => format!(
|
Some(_) => format!(
|
||||||
r#"<nav class="tabbar" aria-label="primary"><a href="{b}/build">build</a><a href="{b}/history">history</a><a href="{b}/dashboard">dashboard</a><a href="{b}/logout">sign out</a></nav>"#,
|
r#"<nav class="tabbar" aria-label="primary"><a href="{b}/build">New paper</a><a href="{b}/history">My papers</a><a href="{b}/dashboard">Dashboard</a><a href="{b}/logout">Log out</a></nav>"#,
|
||||||
b = ctx.base
|
b = ctx.base
|
||||||
),
|
),
|
||||||
None => format!(
|
None => format!(
|
||||||
r#"<nav class="tabbar" aria-label="primary"><a href="{b}/">home</a><a href="{b}/build">start a paper</a><a href="{b}/terms">terms</a></nav>"#,
|
r#"<nav class="tabbar" aria-label="primary"><a href="{b}/">Home</a><a href="{b}/build">New paper</a><a href="{b}/student">Sign in</a></nav>"#,
|
||||||
b = ctx.base
|
b = ctx.base
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -343,368 +342,436 @@ pub fn error(ctx: &Ctx, code: u16, detail: &str) -> String {
|
||||||
pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm, available: i64, err: Option<&str>) -> String {
|
pub fn build(ctx: &Ctx, user: &str, blocks: Vec<SubjectBlock>, _form: &BuildForm, available: i64, err: Option<&str>) -> String {
|
||||||
let mut picker = String::new();
|
let mut picker = String::new();
|
||||||
for sb in &blocks {
|
for sb in &blocks {
|
||||||
let _subj_lower = sb.subject.to_lowercase();
|
let subj = &sb.subject;
|
||||||
picker.push_str(&format!(
|
picker.push_str(&format!(
|
||||||
r#"<div class="subject-section" data-subject="{subj}">
|
r#"<div class="zp-subject-group" data-subject="{subj}">"#,
|
||||||
<div class="subject-header-row">
|
subj = esc(subj)
|
||||||
<span class="subject-title-badge">{subj}</span>
|
|
||||||
<button type="button" class="btn-select-all-subject" data-subj="{subj}">Select all {subj}</button>
|
|
||||||
</div>"#,
|
|
||||||
subj = esc(&sb.subject)
|
|
||||||
));
|
));
|
||||||
for ch in &sb.chapters {
|
for ch in &sb.chapters {
|
||||||
let sub_count = ch.subtopics.len();
|
let sub_count = ch.subtopics.len();
|
||||||
let subcheck: String = ch.subtopic_ids.iter().zip(ch.subtopics.iter()).map(|(sid, sname)| {
|
let subcheck: String = ch.subtopic_ids.iter().zip(ch.subtopics.iter()).map(|(sid, sname)| {
|
||||||
format!(
|
format!(
|
||||||
r#"<label class="subtopic-chip">
|
r#"<label class="zp-subtopic-row">
|
||||||
<input type="checkbox" class="subtopic" value="{sid}" data-chap="{cid}" data-subj="{subj}">
|
<input type="checkbox" class="subtopic" value="{sid}" data-chap="{cid}" data-subj="{subj}">
|
||||||
<span>{sn}</span>
|
<span class="zp-subtopic-title">{sn}</span>
|
||||||
</label>"#,
|
</label>"#,
|
||||||
sid = sid, cid = ch.id, subj = esc(&sb.subject), sn = esc(sname)
|
sid = sid, cid = ch.id, subj = esc(subj), sn = esc(sname)
|
||||||
)
|
)
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
picker.push_str(&format!(
|
picker.push_str(&format!(
|
||||||
r#"<div class="chapter-card" data-subj="{subj}" data-chap="{cid}">
|
r#"<div class="zp-chapter-item" data-subj="{subj}" data-name="{cname_lower}">
|
||||||
<div class="chapter-card-header">
|
<div class="zp-chapter-row">
|
||||||
<label class="chapter-select-wrap">
|
<label class="zp-chk-wrap">
|
||||||
<input type="checkbox" class="chap" value="{cid}" data-subj="{subj}" data-count="{count}">
|
<input type="checkbox" class="chap" value="{cid}" data-subj="{subj}" data-count="{count}">
|
||||||
<span class="chapter-name">{cname}</span>
|
<span class="zp-chapter-title">{cname}</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="chapter-actions">
|
<button type="button" class="zp-expand-btn" data-chap="{cid}" title="Toggle subtopics ({sub_count})">
|
||||||
<span class="chapter-q-badge">{count} Qs</span>
|
<svg class="zp-caret-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
||||||
<button type="button" class="btn-expand-subtopics" data-chap="{cid}" title="Toggle subtopics list">
|
</button>
|
||||||
<span>{sub_count} Subtopics</span>
|
|
||||||
<svg class="chevron-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="none" stroke="currentColor" stroke-width="2" d="M6 9l6 6 6-6"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="subtopics-tray" id="tray-{cid}">
|
<div class="zp-subtopics-tray" id="tray-{cid}">
|
||||||
<div class="subtopics-tray-head">
|
<div class="zp-subtopics-tray-head">
|
||||||
<span class="dim">Subtopics in {cname}</span>
|
<span class="dim">{sub_count} subtopics in {cname}</span>
|
||||||
<div style="display:flex;gap:12px">
|
<div style="display:flex;gap:12px">
|
||||||
<button type="button" class="btn-sub-link select-all-tray" data-chap="{cid}">Select all</button>
|
<button type="button" class="zp-link-btn select-all-tray" data-chap="{cid}">Select all</button>
|
||||||
<button type="button" class="btn-sub-link clear-tray" data-chap="{cid}">Deselect all</button>
|
<button type="button" class="zp-link-btn clear-tray" data-chap="{cid}">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subtopics-grid">{subcheck}</div>
|
<div class="zp-subtopics-list">{subcheck}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>"#,
|
</div>"#,
|
||||||
cid = ch.id,
|
cid = ch.id,
|
||||||
cname = esc(&ch.name),
|
cname = esc(&ch.name),
|
||||||
|
cname_lower = esc(&ch.name.to_lowercase()),
|
||||||
count = ch.count,
|
count = ch.count,
|
||||||
sub_count = sub_count,
|
sub_count = sub_count,
|
||||||
subj = esc(&sb.subject),
|
subj = esc(subj),
|
||||||
subcheck = subcheck
|
subcheck = subcheck
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
picker.push_str("</div>");
|
picker.push_str("</div>");
|
||||||
}
|
}
|
||||||
|
|
||||||
let e = err.map(|m| format!(r#"<p class="bad-t" role="alert">{m}</p>"#)).unwrap_or_default();
|
let e = err.map(|m| format!(r#"<p class="bad-t" role="alert" style="margin:12px 0">{m}</p>"#)).unwrap_or_default();
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"<div class="build-header">
|
r#"<div class="zp-builder-page">
|
||||||
<h1>Build Practice Paper</h1>
|
<h1 class="zp-page-title">Prepare a JEE Main test</h1>
|
||||||
<p class="meta">Select specific syllabus units or choose a preset from <b>{available}</b> authentic JEE Main questions.</p>
|
<p class="zp-page-subtitle">Build your own paper from the chapters you choose (or the complete syllabus), or take an actual JEE Main paper. {available} authentic PYQs in the bank.</p>
|
||||||
|
|
||||||
<div class="preset-bar">
|
<!-- Mode Selection Cards (ZenithPrep Style) -->
|
||||||
<span class="preset-label">Quick Presets:</span>
|
<div class="zp-mode-cards">
|
||||||
<button type="button" class="preset-btn" data-preset="full">⚡ Full Mock (75 Qs · All 3 Subjects)</button>
|
<div class="zp-mode-card active" id="mode-custom">
|
||||||
<button type="button" class="preset-btn" data-preset="phy">⚛ Physics Focus</button>
|
<b class="zp-mode-title">Build your own paper</b>
|
||||||
<button type="button" class="preset-btn" data-preset="chem">🧪 Chemistry Focus</button>
|
<span class="zp-mode-desc">Chapters you choose, or the complete syllabus</span>
|
||||||
<button type="button" class="preset-btn" data-preset="math">📐 Mathematics Focus</button>
|
</div>
|
||||||
<button type="button" class="preset-btn secondary" data-preset="clear">✕ Clear Selection</button>
|
<div class="zp-mode-card" id="mode-pyq">
|
||||||
|
<b class="zp-mode-title">Previous year paper</b>
|
||||||
|
<span class="zp-mode-desc">An actual JEE Main paper: pick the year and shift</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subject-tabs" role="tablist">
|
<!-- Complete Syllabus Button -->
|
||||||
<button type="button" class="sub-tab active" data-tab="all">All Subjects <span class="tab-badge">15 Units</span></button>
|
<div class="zp-syllabus-section">
|
||||||
<button type="button" class="sub-tab" data-tab="Physics">Physics <span class="tab-badge">6</span></button>
|
<button type="button" class="zp-btn-syllabus" id="btn-complete-syllabus">
|
||||||
<button type="button" class="sub-tab" data-tab="Chemistry">Chemistry <span class="tab-badge">3</span></button>
|
<span class="zp-book-icon">📚</span>
|
||||||
<button type="button" class="sub-tab" data-tab="Maths">Mathematics <span class="tab-badge">6</span></button>
|
<span>Complete syllabus</span>
|
||||||
</div>
|
</button>
|
||||||
|
<p class="zp-syllabus-desc">Selects every chapter in all three subjects (2025 syllabus). You can still remove any you don't want.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{b}/build" id="build-form">
|
<!-- Subject Filter Pills -->
|
||||||
<input type="hidden" name="chapters" id="chapters" value="">
|
<div class="zp-subject-pills-bar">
|
||||||
<input type="hidden" name="subtopics" id="subtopics" value="">
|
<button type="button" class="zp-subj-pill active" data-subj="Maths">
|
||||||
<input type="hidden" name="mode" id="mode" value="">
|
<span class="zp-dot dot-math"></span>
|
||||||
|
<span class="zp-subj-name">Mathematics</span>
|
||||||
|
<span class="zp-subj-badge" id="badge-math">0</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="zp-subj-pill" data-subj="Physics">
|
||||||
|
<span class="zp-dot dot-phy"></span>
|
||||||
|
<span class="zp-subj-name">Physics</span>
|
||||||
|
<span class="zp-subj-badge" id="badge-phy">0</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="zp-subj-pill" data-subj="Chemistry">
|
||||||
|
<span class="zp-dot dot-chem"></span>
|
||||||
|
<span class="zp-subj-name">Chemistry</span>
|
||||||
|
<span class="zp-subj-badge" id="badge-chem">0</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="build-layout">
|
<!-- Search and Actions Bar -->
|
||||||
<div class="picker-col">
|
<div class="zp-search-row">
|
||||||
|
<div class="zp-search-input-wrap">
|
||||||
|
<svg class="zp-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="none" stroke="currentColor" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
|
<input type="text" id="zp-search-input" placeholder="Search chapters or subtopics." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="zp-search-actions">
|
||||||
|
<button type="button" class="zp-btn-action" id="btn-select-all">Select all</button>
|
||||||
|
<button type="button" class="zp-btn-action" id="btn-clear-all">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{b}/build" id="build-form">
|
||||||
|
<input type="hidden" name="chapters" id="chapters" value="">
|
||||||
|
<input type="hidden" name="subtopics" id="subtopics" value="">
|
||||||
|
<input type="hidden" name="difficulties" id="difficulties" value="Easy,Medium,Hard">
|
||||||
|
<input type="hidden" name="years" id="years" value="2020,2021,2022,2023,2024,2025">
|
||||||
|
<input type="hidden" name="mode" id="mode" value="jee">
|
||||||
|
<input type="hidden" name="count" id="count" value="30">
|
||||||
|
|
||||||
|
<!-- Chapter Rows Container -->
|
||||||
|
<div class="zp-chapters-container">
|
||||||
{picker}
|
{picker}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-col">
|
<!-- Bottom Filter Box (DIFFICULTY & YEARS) -->
|
||||||
<div class="settings-sticky">
|
<div class="zp-filter-box">
|
||||||
<div class="settings-card">
|
<div class="zp-filter-sec">
|
||||||
<h2 class="settings-title">Exam Settings</h2>
|
<span class="zp-filter-heading">DIFFICULTY</span>
|
||||||
|
<div class="zp-pill-row" id="diff-row">
|
||||||
|
<label class="zp-filter-pill active">
|
||||||
|
<input type="checkbox" class="diff-chk" value="Easy" checked>
|
||||||
|
<span class="zp-pill-tick">✔</span>
|
||||||
|
<span>Easy</span>
|
||||||
|
</label>
|
||||||
|
<label class="zp-filter-pill active">
|
||||||
|
<input type="checkbox" class="diff-chk" value="Medium" checked>
|
||||||
|
<span class="zp-pill-tick">✔</span>
|
||||||
|
<span>Medium</span>
|
||||||
|
</label>
|
||||||
|
<label class="zp-filter-pill active">
|
||||||
|
<input type="checkbox" class="diff-chk" value="Hard" checked>
|
||||||
|
<span class="zp-pill-tick">✔</span>
|
||||||
|
<span>Hard</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="setting-group">
|
<div class="zp-filter-sec" style="margin-top:20px">
|
||||||
<label class="setting-lbl">Questions Count</label>
|
<span class="zp-filter-heading">YEARS</span>
|
||||||
<div class="count-pills">
|
<div class="zp-pill-row" id="year-row">
|
||||||
<button type="button" class="count-pill" data-count="9">9</button>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2020" checked><span class="zp-pill-tick">✔</span><span>2020</span></label>
|
||||||
<button type="button" class="count-pill active" data-count="15">15</button>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2021" checked><span class="zp-pill-tick">✔</span><span>2021</span></label>
|
||||||
<button type="button" class="count-pill" data-count="30">30</button>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2022" checked><span class="zp-pill-tick">✔</span><span>2022</span></label>
|
||||||
<button type="button" class="count-pill" data-count="45">45</button>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2023" checked><span class="zp-pill-tick">✔</span><span>2023</span></label>
|
||||||
<button type="button" class="count-pill" data-count="75">75</button>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2024" checked><span class="zp-pill-tick">✔</span><span>2024</span></label>
|
||||||
</div>
|
<label class="zp-filter-pill active"><input type="checkbox" class="year-chk" value="2025" checked><span class="zp-pill-tick">✔</span><span>2025</span></label>
|
||||||
<input type="hidden" id="count" name="count" value="15">
|
<label class="zp-filter-pill"><input type="checkbox" class="year-chk" value="2019"><span class="zp-pill-tick">✔</span><span>2019</span></label>
|
||||||
</div>
|
<label class="zp-filter-pill"><input type="checkbox" class="year-chk" value="2018"><span class="zp-pill-tick">✔</span><span>2018</span></label>
|
||||||
|
|
||||||
<div class="setting-group">
|
|
||||||
<label class="toggle-card">
|
|
||||||
<input type="checkbox" id="jee-mode" name="_jee">
|
|
||||||
<div class="toggle-card-content">
|
|
||||||
<div class="toggle-card-title">
|
|
||||||
<b>JEE Main Mode</b>
|
|
||||||
<span class="badge-mode">Official</span>
|
|
||||||
</div>
|
|
||||||
<p class="dim">Independent 60-min timer per subject, auto-switching.</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-group" id="dur-row">
|
|
||||||
<label class="setting-lbl" for="duration">Custom Timer (Minutes)</label>
|
|
||||||
<input id="duration" name="duration" type="number" value="30" min="5" max="180">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-group">
|
|
||||||
<label class="toggle-card">
|
|
||||||
<input type="checkbox" name="unseen_only" checked>
|
|
||||||
<div class="toggle-card-content">
|
|
||||||
<b>Unseen Questions Only</b>
|
|
||||||
<p class="dim">Skips previously attempted questions until topics deplete.</p>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="selection-summary">
|
|
||||||
<div class="summary-stat">
|
|
||||||
<span class="dim">Selected</span>
|
|
||||||
<b id="pick-summary">0 Topics</b>
|
|
||||||
</div>
|
|
||||||
<div class="summary-stat">
|
|
||||||
<span class="dim">Question Pool</span>
|
|
||||||
<b id="pool-count">0 available</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{e}
|
|
||||||
|
|
||||||
<button class="btn-create-paper" id="submit-btn" type="submit" disabled>
|
|
||||||
<span>Start Practice Paper</span>
|
|
||||||
<span class="btn-subtext" id="btn-subtext">Pick topics to start</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
<!-- Paper Settings & Start Bar -->
|
||||||
|
<div class="zp-settings-bar">
|
||||||
|
<div class="zp-settings-row">
|
||||||
|
<div class="zp-setting-item">
|
||||||
|
<label class="zp-setting-label">Questions</label>
|
||||||
|
<div class="zp-count-pills">
|
||||||
|
<button type="button" class="zp-cnt-btn" data-count="9">9</button>
|
||||||
|
<button type="button" class="zp-cnt-btn" data-count="15">15</button>
|
||||||
|
<button type="button" class="zp-cnt-btn active" data-count="30">30</button>
|
||||||
|
<button type="button" class="zp-cnt-btn" data-count="45">45</button>
|
||||||
|
<button type="button" class="zp-cnt-btn" data-count="75">75</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="zp-setting-item">
|
||||||
|
<label class="zp-toggle-label">
|
||||||
|
<input type="checkbox" id="jee-mode" checked>
|
||||||
|
<span>JEE Main 60-min timer per subject</span>
|
||||||
|
</label>
|
||||||
|
<label class="zp-toggle-label" style="margin-top:6px">
|
||||||
|
<input type="checkbox" name="unseen_only" checked>
|
||||||
|
<span>Unseen questions only</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="zp-setting-item" id="dur-box" hidden>
|
||||||
|
<label class="zp-setting-label" for="duration">Duration (Minutes)</label>
|
||||||
|
<input type="number" id="duration" name="duration" value="60" min="5" max="180">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{e}
|
||||||
|
|
||||||
|
<div class="zp-submit-row">
|
||||||
|
<div class="zp-summary-text" id="zp-summary">
|
||||||
|
<b>0 chapters selected</b>
|
||||||
|
<span class="dim" id="zp-pool-estimate">Pick chapters above</span>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="zp-btn-start" id="submit-btn" disabled>
|
||||||
|
<span>Start Practice Paper</span>
|
||||||
|
<span class="zp-btn-desc" id="btn-desc">Select topics to begin</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(() => {{
|
(() => {{
|
||||||
const form = document.getElementById('build-form');
|
const form = document.getElementById('build-form');
|
||||||
const chapField = document.getElementById('chapters');
|
const chapField = document.getElementById('chapters');
|
||||||
const stField = document.getElementById('subtopics');
|
const stField = document.getElementById('subtopics');
|
||||||
|
const diffField = document.getElementById('difficulties');
|
||||||
|
const yearField = document.getElementById('years');
|
||||||
const modeField = document.getElementById('mode');
|
const modeField = document.getElementById('mode');
|
||||||
const countField = document.getElementById('count');
|
const countField = document.getElementById('count');
|
||||||
const jeeBox = document.getElementById('jee-mode');
|
const jeeBox = document.getElementById('jee-mode');
|
||||||
const durRow = document.getElementById('dur-row');
|
const durBox = document.getElementById('dur-box');
|
||||||
const durInput = document.getElementById('duration');
|
const durInput = document.getElementById('duration');
|
||||||
const pickSummary = document.getElementById('pick-summary');
|
|
||||||
const poolCount = document.getElementById('pool-count');
|
|
||||||
const submitBtn = document.getElementById('submit-btn');
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
const btnSubtext = document.getElementById('btn-subtext');
|
const summaryText = document.getElementById('zp-summary');
|
||||||
|
const poolEstimate = document.getElementById('zp-pool-estimate');
|
||||||
|
const btnDesc = document.getElementById('btn-desc');
|
||||||
|
const searchInput = document.getElementById('zp-search-input');
|
||||||
|
|
||||||
// 1. Subject Tabs Filter
|
// 1. Subject Switcher Pills (Mathematics, Physics, Chemistry)
|
||||||
document.querySelectorAll('.sub-tab').forEach(tab => {{
|
document.querySelectorAll('.zp-subj-pill').forEach(pill => {{
|
||||||
tab.addEventListener('click', () => {{
|
pill.addEventListener('click', () => {{
|
||||||
document.querySelectorAll('.sub-tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.zp-subj-pill').forEach(p => p.classList.remove('active'));
|
||||||
tab.classList.add('active');
|
pill.classList.add('active');
|
||||||
const target = tab.dataset.tab;
|
const target = pill.dataset.subj;
|
||||||
document.querySelectorAll('.subject-section').forEach(sec => {{
|
document.querySelectorAll('.zp-subject-group').forEach(group => {{
|
||||||
if (target === 'all' || sec.dataset.subject === target) {{
|
if (group.dataset.subject === target) {{
|
||||||
sec.style.display = '';
|
group.style.display = 'block';
|
||||||
}} else {{
|
}} else {{
|
||||||
sec.style.display = 'none';
|
group.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
}});
|
}});
|
||||||
|
filterBySearch();
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 2. Expand/Collapse subtopics chevron button
|
// Initially show only Mathematics (ZenithPrep default)
|
||||||
document.querySelectorAll('.btn-expand-subtopics').forEach(btn => {{
|
document.querySelectorAll('.zp-subject-group').forEach(g => {{
|
||||||
|
g.style.display = g.dataset.subject === 'Maths' ? 'block' : 'none';
|
||||||
|
}});
|
||||||
|
|
||||||
|
// 2. Expand/Collapse Arrow buttons (▶)
|
||||||
|
document.querySelectorAll('.zp-expand-btn').forEach(btn => {{
|
||||||
btn.addEventListener('click', (e) => {{
|
btn.addEventListener('click', (e) => {{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const card = btn.closest('.chapter-card');
|
const item = btn.closest('.zp-chapter-item');
|
||||||
card.classList.toggle('expanded');
|
item.classList.toggle('expanded');
|
||||||
btn.setAttribute('aria-expanded', card.classList.contains('expanded'));
|
btn.setAttribute('aria-expanded', item.classList.contains('expanded'));
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 3. Question Count Pills
|
// 3. Search Filter
|
||||||
document.querySelectorAll('.count-pill').forEach(pill => {{
|
function filterBySearch() {{
|
||||||
pill.addEventListener('click', () => {{
|
const q = searchInput.value.trim().toLowerCase();
|
||||||
document.querySelectorAll('.count-pill').forEach(p => p.classList.remove('active'));
|
const activeSubj = document.querySelector('.zp-subj-pill.active')?.dataset.subj || 'Maths';
|
||||||
pill.classList.add('active');
|
const activeGroup = document.querySelector(`.zp-subject-group[data-subject="${{activeSubj}}"]`);
|
||||||
countField.value = pill.dataset.count;
|
if (!activeGroup) return;
|
||||||
// Default duration: 2 mins per question
|
|
||||||
if (!jeeBox.checked) {{
|
activeGroup.querySelectorAll('.zp-chapter-item').forEach(item => {{
|
||||||
durInput.value = Math.max(10, Math.min(180, Number(pill.dataset.count) * 2));
|
const name = item.dataset.name || '';
|
||||||
|
const tray = item.querySelector('.zp-subtopics-tray');
|
||||||
|
const trayText = tray ? tray.textContent.toLowerCase() : '';
|
||||||
|
if (!q || name.includes(q) || trayText.includes(q)) {{
|
||||||
|
item.style.display = '';
|
||||||
|
if (q && trayText.includes(q)) {{
|
||||||
|
item.classList.add('expanded');
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
item.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
sync();
|
|
||||||
}});
|
}});
|
||||||
}});
|
}}
|
||||||
|
searchInput.addEventListener('input', filterBySearch);
|
||||||
|
|
||||||
// 4. JEE Mode Toggle
|
// 4. Select All / Clear for active subject
|
||||||
jeeBox.addEventListener('change', () => {{
|
document.getElementById('btn-select-all').addEventListener('click', () => {{
|
||||||
durRow.hidden = jeeBox.checked;
|
const activeSubj = document.querySelector('.zp-subj-pill.active')?.dataset.subj || 'Maths';
|
||||||
modeField.value = jeeBox.checked ? 'jee' : '';
|
const activeGroup = document.querySelector(`.zp-subject-group[data-subject="${{activeSubj}}"]`);
|
||||||
|
if (activeGroup) {{
|
||||||
|
activeGroup.querySelectorAll('.chap, .subtopic').forEach(c => c.checked = true);
|
||||||
|
}}
|
||||||
sync();
|
sync();
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 5. Chapter Checkbox Changes
|
document.getElementById('btn-clear-all').addEventListener('click', () => {{
|
||||||
document.querySelectorAll('.chap').forEach(ch => {{
|
const activeSubj = document.querySelector('.zp-subj-pill.active')?.dataset.subj || 'Maths';
|
||||||
ch.addEventListener('change', () => {{
|
const activeGroup = document.querySelector(`.zp-subject-group[data-subject="${{activeSubj}}"]`);
|
||||||
const card = ch.closest('.chapter-card');
|
if (activeGroup) {{
|
||||||
const chapId = ch.value;
|
activeGroup.querySelectorAll('.chap, .subtopic').forEach(c => c.checked = false);
|
||||||
card.querySelectorAll('.subtopic').forEach(st => {{
|
}}
|
||||||
st.checked = ch.checked;
|
sync();
|
||||||
}});
|
|
||||||
ch.indeterminate = false;
|
|
||||||
sync();
|
|
||||||
}});
|
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 6. Subtopic Checkbox Changes
|
// 5. Complete Syllabus Button
|
||||||
document.querySelectorAll('.subtopic').forEach(st => {{
|
document.getElementById('btn-complete-syllabus').addEventListener('click', () => {{
|
||||||
st.addEventListener('change', () => {{
|
document.querySelectorAll('.chap, .subtopic').forEach(c => c.checked = true);
|
||||||
const card = st.closest('.chapter-card');
|
setCount('75');
|
||||||
const chapBox = card.querySelector('.chap');
|
jeeBox.checked = true;
|
||||||
const allSubs = [...card.querySelectorAll('.subtopic')];
|
modeField.value = 'jee';
|
||||||
const anyChecked = allSubs.some(s => s.checked);
|
durBox.hidden = true;
|
||||||
const allChecked = allSubs.every(s => s.checked);
|
sync();
|
||||||
chapBox.checked = anyChecked;
|
|
||||||
chapBox.indeterminate = anyChecked && !allChecked;
|
|
||||||
sync();
|
|
||||||
}});
|
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 7. Subtopic Tray Select/Clear Helpers
|
// 6. Subtopic Tray Select/Clear
|
||||||
document.querySelectorAll('.select-all-tray').forEach(btn => {{
|
document.querySelectorAll('.select-all-tray').forEach(btn => {{
|
||||||
btn.addEventListener('click', () => {{
|
btn.addEventListener('click', () => {{
|
||||||
const card = btn.closest('.chapter-card');
|
const item = btn.closest('.zp-chapter-item');
|
||||||
card.querySelectorAll('.subtopic').forEach(st => st.checked = true);
|
item.querySelectorAll('.subtopic').forEach(st => st.checked = true);
|
||||||
const chapBox = card.querySelector('.chap');
|
const chap = item.querySelector('.chap');
|
||||||
chapBox.checked = true;
|
chap.checked = true;
|
||||||
chapBox.indeterminate = false;
|
chap.indeterminate = false;
|
||||||
sync();
|
sync();
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
document.querySelectorAll('.clear-tray').forEach(btn => {{
|
document.querySelectorAll('.clear-tray').forEach(btn => {{
|
||||||
btn.addEventListener('click', () => {{
|
btn.addEventListener('click', () => {{
|
||||||
const card = btn.closest('.chapter-card');
|
const item = btn.closest('.zp-chapter-item');
|
||||||
card.querySelectorAll('.subtopic').forEach(st => st.checked = false);
|
item.querySelectorAll('.subtopic').forEach(st => st.checked = false);
|
||||||
const chapBox = card.querySelector('.chap');
|
const chap = item.querySelector('.chap');
|
||||||
chapBox.checked = false;
|
chap.checked = false;
|
||||||
chapBox.indeterminate = false;
|
chap.indeterminate = false;
|
||||||
sync();
|
sync();
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 8. Select All Subject Button
|
// 7. Chapter and Subtopic changes
|
||||||
document.querySelectorAll('.btn-select-all-subject').forEach(btn => {{
|
document.querySelectorAll('.chap').forEach(ch => {{
|
||||||
|
ch.addEventListener('change', () => {{
|
||||||
|
const item = ch.closest('.zp-chapter-item');
|
||||||
|
item.querySelectorAll('.subtopic').forEach(st => st.checked = ch.checked);
|
||||||
|
ch.indeterminate = false;
|
||||||
|
sync();
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
|
document.querySelectorAll('.subtopic').forEach(st => {{
|
||||||
|
st.addEventListener('change', () => {{
|
||||||
|
const item = st.closest('.zp-chapter-item');
|
||||||
|
const chap = item.querySelector('.chap');
|
||||||
|
const allSubs = [...item.querySelectorAll('.subtopic')];
|
||||||
|
const anyChecked = allSubs.some(s => s.checked);
|
||||||
|
const allChecked = allSubs.every(s => s.checked);
|
||||||
|
chap.checked = anyChecked;
|
||||||
|
chap.indeterminate = anyChecked && !allChecked;
|
||||||
|
sync();
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
|
// 8. Difficulty and Year Pills Toggle
|
||||||
|
document.querySelectorAll('.diff-chk, .year-chk').forEach(chk => {{
|
||||||
|
chk.addEventListener('change', () => {{
|
||||||
|
const pill = chk.closest('.zp-filter-pill');
|
||||||
|
pill.classList.toggle('active', chk.checked);
|
||||||
|
sync();
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
|
// 9. Question Count Pills
|
||||||
|
document.querySelectorAll('.zp-cnt-btn').forEach(btn => {{
|
||||||
btn.addEventListener('click', () => {{
|
btn.addEventListener('click', () => {{
|
||||||
const subj = btn.dataset.subj;
|
setCount(btn.dataset.count);
|
||||||
const sec = btn.closest('.subject-section');
|
|
||||||
const allChaps = [...sec.querySelectorAll('.chap')];
|
|
||||||
const areAllSelected = allChaps.every(c => c.checked);
|
|
||||||
const newState = !areAllSelected;
|
|
||||||
sec.querySelectorAll('.chap, .subtopic').forEach(chk => {{
|
|
||||||
chk.checked = newState;
|
|
||||||
chk.indeterminate = false;
|
|
||||||
}});
|
|
||||||
btn.textContent = newState ? `Clear ${{subj}}` : `Select all ${{subj}}`;
|
|
||||||
sync();
|
sync();
|
||||||
}});
|
}});
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// 9. Quick Presets
|
function setCount(n) {{
|
||||||
document.querySelectorAll('.preset-btn').forEach(btn => {{
|
document.querySelectorAll('.zp-cnt-btn').forEach(b => {{
|
||||||
btn.addEventListener('click', () => {{
|
b.classList.toggle('active', b.dataset.count === n);
|
||||||
const preset = btn.dataset.preset;
|
|
||||||
if (preset === 'clear') {{
|
|
||||||
document.querySelectorAll('.chap, .subtopic').forEach(c => {{
|
|
||||||
c.checked = false;
|
|
||||||
c.indeterminate = false;
|
|
||||||
}});
|
|
||||||
}} else if (preset === 'full') {{
|
|
||||||
document.querySelectorAll('.chap, .subtopic').forEach(c => {{
|
|
||||||
c.checked = true;
|
|
||||||
c.indeterminate = false;
|
|
||||||
}});
|
|
||||||
setCountPill('75');
|
|
||||||
jeeBox.checked = true;
|
|
||||||
durRow.hidden = true;
|
|
||||||
modeField.value = 'jee';
|
|
||||||
}} else if (preset === 'phy' || preset === 'chem' || preset === 'math') {{
|
|
||||||
const targetSubj = preset === 'phy' ? 'Physics' : preset === 'chem' ? 'Chemistry' : 'Maths';
|
|
||||||
document.querySelectorAll('.chap, .subtopic').forEach(c => {{
|
|
||||||
const isTarget = c.dataset.subj === targetSubj;
|
|
||||||
c.checked = isTarget;
|
|
||||||
c.indeterminate = false;
|
|
||||||
}});
|
|
||||||
setCountPill('30');
|
|
||||||
}}
|
|
||||||
sync();
|
|
||||||
}});
|
|
||||||
}});
|
|
||||||
|
|
||||||
function setCountPill(num) {{
|
|
||||||
document.querySelectorAll('.count-pill').forEach(p => {{
|
|
||||||
if (p.dataset.count === num) {{
|
|
||||||
p.classList.add('active');
|
|
||||||
countField.value = num;
|
|
||||||
}} else {{
|
|
||||||
p.classList.remove('active');
|
|
||||||
}}
|
|
||||||
}});
|
}});
|
||||||
|
countField.value = n;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 10. Sync state, counter and button
|
// 10. JEE Mode
|
||||||
|
jeeBox.addEventListener('change', () => {{
|
||||||
|
modeField.value = jeeBox.checked ? 'jee' : '';
|
||||||
|
durBox.hidden = jeeBox.checked;
|
||||||
|
sync();
|
||||||
|
}});
|
||||||
|
|
||||||
|
// 11. Sync state, pill badges, and submit button
|
||||||
function sync() {{
|
function sync() {{
|
||||||
const checkedChaps = [...document.querySelectorAll('.chap:checked')];
|
const checkedChaps = [...document.querySelectorAll('.chap:checked')];
|
||||||
const checkedSubs = [...document.querySelectorAll('.subtopic:checked')];
|
const checkedSubs = [...document.querySelectorAll('.subtopic:checked')];
|
||||||
chapField.value = checkedChaps.map(c => c.value).join(',');
|
chapField.value = checkedChaps.map(c => c.value).join(',');
|
||||||
stField.value = checkedSubs.map(s => s.value).join(',');
|
stField.value = checkedSubs.map(s => s.value).join(',');
|
||||||
|
|
||||||
|
// Difficulty & Year list
|
||||||
|
const diffs = [...document.querySelectorAll('.diff-chk:checked')].map(c => c.value);
|
||||||
|
const yrs = [...document.querySelectorAll('.year-chk:checked')].map(c => c.value);
|
||||||
|
diffField.value = diffs.join(',');
|
||||||
|
yearField.value = yrs.join(',');
|
||||||
|
|
||||||
|
// Update subject pill badges
|
||||||
|
['Maths', 'Physics', 'Chemistry'].forEach(subj => {{
|
||||||
|
const key = subj === 'Maths' ? 'math' : subj === 'Physics' ? 'phy' : 'chem';
|
||||||
|
const badge = document.getElementById(`badge-${{key}}`);
|
||||||
|
const cnt = document.querySelectorAll(`.chap:checked[data-subj="${{subj}}"]`).length;
|
||||||
|
if (badge) badge.textContent = cnt;
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Calculate pool
|
||||||
let pool = 0;
|
let pool = 0;
|
||||||
checkedChaps.forEach(c => {{
|
checkedChaps.forEach(c => {{
|
||||||
pool += Number(c.dataset.count || 0);
|
pool += Number(c.dataset.count || 0);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
const nChapters = checkedChaps.length;
|
const nChapters = checkedChaps.length;
|
||||||
const nSubs = checkedSubs.length;
|
const qCount = countField.value || 30;
|
||||||
const qCount = countField.value || 15;
|
|
||||||
|
|
||||||
if (nChapters === 0 && nSubs === 0) {{
|
if (nChapters === 0 && checkedSubs.length === 0) {{
|
||||||
pickSummary.textContent = '0 Topics';
|
summaryText.innerHTML = '<b>0 chapters selected</b><span class="dim">Pick chapters above</span>';
|
||||||
poolCount.textContent = '0 available';
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
btnSubtext.textContent = 'Select topics above to start';
|
btnDesc.textContent = 'Select topics to begin';
|
||||||
}} else {{
|
}} else {{
|
||||||
pickSummary.textContent = `${{nChapters}} Units (${{nSubs}} Subtopics)`;
|
summaryText.innerHTML = `<b>${{nChapters}} chapters (${{checkedSubs.length}} subtopics)</b><span class="dim">~${{pool.toLocaleString()}} questions in pool</span>`;
|
||||||
poolCount.textContent = `~${{pool.toLocaleString()}} in pool`;
|
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
const durationMins = jeeBox.checked ? `${{Math.min(3, Math.max(1, new Set(checkedChaps.map(c => c.dataset.subj)).size)) * 60}} Mins` : `${{durInput.value}} Mins`;
|
const subjsCount = new Set(checkedChaps.map(c => c.dataset.subj)).size;
|
||||||
btnSubtext.textContent = `${{qCount}} Questions · ${{durationMins}}`;
|
const duration = jeeBox.checked ? `${{Math.max(1, subjsCount) * 60}} Mins (JEE Mode)` : `${{durInput.value}} Mins`;
|
||||||
|
btnDesc.textContent = `${{qCount}} Questions · ${{duration}}`;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Initialize
|
|
||||||
sync();
|
sync();
|
||||||
}})();
|
}})();
|
||||||
</script>"#,
|
</script>"#,
|
||||||
|
|
|
||||||
631
static/style.css
631
static/style.css
|
|
@ -1573,3 +1573,634 @@ button.pal-fab {
|
||||||
margin: 15mm;
|
margin: 15mm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
ZENITHPREP UI REPLICA STYLES
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.zp-builder-page {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-page-title {
|
||||||
|
font-size: 2.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-page-subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Mode Cards */
|
||||||
|
.zp-mode-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-card {
|
||||||
|
background: #131b32;
|
||||||
|
border: 1px solid #24355a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-card:hover {
|
||||||
|
background: #182342;
|
||||||
|
border-color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-card.active {
|
||||||
|
background: #16254a;
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-card.active .zp-mode-title {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-mode-desc {
|
||||||
|
font-size: 0.825rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Complete Syllabus Section */
|
||||||
|
.zp-syllabus-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-syllabus {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #172344;
|
||||||
|
border: 1px solid #2e4478;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
color: #93c5fd;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-syllabus:hover {
|
||||||
|
background: #1d2c54;
|
||||||
|
border-color: #60a5fa;
|
||||||
|
color: #ffffff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-book-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-syllabus-desc {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subject Filter Pills Bar */
|
||||||
|
.zp-subject-pills-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subj-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #131b32;
|
||||||
|
border: 1px solid #233357;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 9px 16px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subj-pill:hover {
|
||||||
|
background: #182342;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subj-pill.active {
|
||||||
|
background: #1d2b52;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-dot {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.dot-math { background: #c084fc; box-shadow: 0 0 8px rgba(192, 132, 252, 0.6); }
|
||||||
|
.dot-phy { background: #22d3ee; box-shadow: 0 0 8px rgba(34, 211, 238, 0.6); }
|
||||||
|
.dot-chem { background: #fb923c; box-shadow: 0 0 8px rgba(251, 146, 60, 0.6); }
|
||||||
|
|
||||||
|
.zp-subj-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search and Action Bar */
|
||||||
|
.zp-search-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-search-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #64748b;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zp-search-input {
|
||||||
|
background: #10172c;
|
||||||
|
border: 1px solid #202d4b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px 10px 38px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #ffffff;
|
||||||
|
min-height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#zp-search-input:focus {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-action {
|
||||||
|
background: #141c33;
|
||||||
|
border: 1px solid #24355a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-action:hover {
|
||||||
|
background: #1a2544;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chapter List Container (ZenithPrep Table Style) */
|
||||||
|
.zp-chapters-container {
|
||||||
|
background: #11182d;
|
||||||
|
border: 1px solid #1f2c4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-item {
|
||||||
|
border-bottom: 1px solid #1a243f;
|
||||||
|
}
|
||||||
|
.zp-chapter-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-row:hover {
|
||||||
|
background: #151f38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chk-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chk-wrap input[type=checkbox] {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
accent-color: #2563eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8fafc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-expand-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-expand-btn:hover {
|
||||||
|
color: #38bdf8;
|
||||||
|
background: #17223e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-caret-icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-item.expanded .zp-caret-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtopics Drawer */
|
||||||
|
.zp-subtopics-tray {
|
||||||
|
display: none;
|
||||||
|
background: #0d1324;
|
||||||
|
border-top: 1px solid #19243e;
|
||||||
|
padding: 12px 18px 16px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-chapter-item.expanded .zp-subtopics-tray {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subtopics-tray-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 0.775rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-link-btn {
|
||||||
|
color: #38bdf8;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.zp-link-btn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subtopics-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subtopic-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subtopic-row:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-subtopic-row input[type=checkbox] {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
accent-color: #2563eb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
BOTTOM FILTER CARD (DIFFICULTY & YEARS) — Exact Zenith Replica
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.zp-filter-box {
|
||||||
|
background: #11182d;
|
||||||
|
border: 1px solid #202e4d;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-sec {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-heading {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-pill-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #141c33;
|
||||||
|
border: 1px solid #25365c;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-pill:hover {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-pill input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-filter-pill.active {
|
||||||
|
background: #1c2c54;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-pill-tick {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Bar */
|
||||||
|
.zp-settings-bar {
|
||||||
|
background: #11182d;
|
||||||
|
border: 1px solid #202e4d;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-settings-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
border-bottom: 1px solid #1a2542;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-setting-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-count-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-cnt-btn {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #141c33;
|
||||||
|
border: 1px solid #24355a;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-cnt-btn:hover {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-cnt-btn.active {
|
||||||
|
background: #2563eb;
|
||||||
|
border-color: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-toggle-label input[type=checkbox] {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
accent-color: #2563eb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-submit-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-summary-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-summary-text b {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-start {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #2563eb;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 14px rgba(37, 99, 235, 0.45);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-start:hover:not(:disabled) {
|
||||||
|
background: #1d4ed8;
|
||||||
|
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.6);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-start:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
background: #182342;
|
||||||
|
border: 1px solid #24355a;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zp-btn-desc {
|
||||||
|
font-size: 0.725rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand Group in Header */
|
||||||
|
.brand-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-syll {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(37, 99, 235, 0.2);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.4);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-greeting {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.btn-logout:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.zp-mode-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.zp-settings-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.zp-submit-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.zp-btn-start {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue