Result page: share box with copy-link button for the paper's public URL

This commit is contained in:
spearzificial 2026-10-17 11:03:35 +05:30
parent 22c6c24e87
commit deaa6d9b3d
3 changed files with 73 additions and 3 deletions

View file

@ -492,6 +492,16 @@ async fn result_page(State(ctx): State<CtxArc>, h: HeaderMap, Path(attempt): Pat
}
};
let a = analyse(&rows, score, max, used);
// Share link for the underlying paper (empty when the paper is gone).
let token: String = sqlx::query_scalar(
"SELECT p.share_token FROM papers p JOIN attempts a ON a.paper_id = p.id WHERE a.id=?",
)
.bind(&attempt)
.fetch_optional(&ctx.pool)
.await
.ok()
.flatten()
.unwrap_or_default();
// Existing tags per question so the form shows current state.
let tag_rows: Vec<(i64, String)> = sqlx::query_as(
"SELECT question_id, tag FROM mistake_tags WHERE attempt_id=?",
@ -519,7 +529,7 @@ async fn result_page(State(ctx): State<CtxArc>, h: HeaderMap, Path(attempt): Pat
}
})
.collect();
hx(views::result(&ctx, &name, &attempt, &a, &rv), StatusCode::OK).into_response()
hx(views::result(&ctx, &name, &attempt, &token, &a, &rv), StatusCode::OK).into_response()
}
#[derive(serde::Deserialize)]

View file

@ -1028,13 +1028,26 @@ fn bucket_rows(items: &[Bucket]) -> String {
.collect()
}
pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, a: &Analysis, rows: &[RowView]) -> String {
pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, share_token: &str, a: &Analysis, rows: &[RowView]) -> String {
let base = ctx.base.clone();
let mistakes_pending = rows
.iter()
.filter(|r| !r.correct && r.tags.is_empty())
.count();
let title = format!("practice paper · {attempt_id}");
// Share box mirrors the paper-preview page: link + copy button.
let share_box = if share_token.is_empty() {
String::new()
} else {
format!(
r##"<div class="card share-box"><h3>share this paper</h3>
<p class="meta">Send the question paper to a friend. Your answers and this score stay private.</p>
<div class="share-row"><code id="share-url">{b}/s/{tok}</code><button class="btn" type="button" data-copy="#share-url">copy link</button></div>
</div>"##,
b = public_origin(),
tok = esc(share_token)
)
};
let detail: String = rows
.iter()
.enumerate()
@ -1114,6 +1127,7 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, a: &Analysis, rows: &[Row
<b>{eh}</b>
<p>{eb}</p>
</div>
{share_box}
<div id="r-subjects" class="grid2">
<div class="card table-wrap"><h3>by subject</h3><table><thead><tr><th>subject</th><th>right</th><th>wrong</th><th>blank</th><th></th><th>%</th><th></th></tr></thead><tbody>{subj}</tbody></table></div>
<div id="r-difficulty" class="card table-wrap"><h3>by difficulty</h3><table><thead><tr><th>difficulty</th><th>right</th><th>wrong</th><th>blank</th><th></th><th>%</th><th></th></tr></thead><tbody>{diff}</tbody></table></div>
@ -1126,7 +1140,25 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, a: &Analysis, rows: &[Row
<button class="btn" type="button" onclick="window.print()">print / PDF</button>
</p>
</div>
</div>"##,
</div>
<script>
(function () {{
var btn = document.querySelector('[data-copy]');
if (!btn) return;
btn.addEventListener('click', function () {{
var el = document.querySelector(btn.dataset.copy);
var url = el ? el.textContent.trim() : '';
var done = function () {{ btn.textContent = 'copied'; setTimeout(function () {{ btn.textContent = 'copy link'; }}, 1500); }};
if (navigator.clipboard) navigator.clipboard.writeText(url).then(done, done);
else {{
var ta = document.createElement('textarea');
ta.value = url; document.body.appendChild(ta); ta.select();
try {{ document.execCommand('copy'); }} catch (e) {{}}
document.body.removeChild(ta); done();
}}
}});
}})();
</script>"##,
score = a.score,
max = a.max_score,
neg = neg,
@ -1134,6 +1166,7 @@ pub fn result(ctx: &Ctx, user: &str, attempt_id: &str, a: &Analysis, rows: &[Row
dur = fmt_dur(a.used_secs),
eh = enc_head,
eb = enc_body,
share_box = share_box,
subj = bucket_rows(&a.by_subject),
diff = bucket_rows(&a.by_difficulty),
topic = bucket_rows(&topic_buckets),

View file

@ -1315,6 +1315,33 @@ footer {
background: #ef4444;
}
/* Share box on the result page */
.card.share-box {
margin: 14px 0;
}
.share-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-top: 10px;
}
.share-row code {
flex: 1 1 240px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 8px 10px;
font-size: 0.8rem;
color: #38bdf8;
}
/* Encouragement card under the overview stats */
.card.encourage {
margin: 14px 0;