Add deployment reading windows and progress-aware quiz unlock
This commit is contained in:
1 parent
edbc9c6056
commit
d8eba9169f
15 files changed
+471
-5
No files matched your search
@@ -389,6 +389,14 @@ body.reader { position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: h
|
||||
#stage.pan { cursor: grab; }
|
||||
#stage.drag { cursor: grabbing; }
|
||||
#canvas { position: relative; }
|
||||
.reading-gate { box-sizing: border-box; max-width: 620px; margin: min(13vh, 100px) auto 100px; padding: 24px;
|
||||
border: 1px solid var(--line); border-radius: 14px; background: var(--card); color: var(--fg); line-height: 1.5; }
|
||||
.reading-gate h1 { margin-top: 0; }
|
||||
.reading-gate-question { margin: 20px 0; }
|
||||
.reading-gate-question input { display: block; box-sizing: border-box; width: 100%; margin: 14px 0; padding: 10px; }
|
||||
.reading-gate-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.reading-gate-message { color: var(--accent); min-height: 1.5em; }
|
||||
@media (max-width: 660px) { .reading-gate { margin: 28px 12px 100px; padding: 18px; } }
|
||||
.pg { position: absolute; overflow: hidden; background: #19191c; }
|
||||
.pg img { display: block; width: 100%; height: 100%; -webkit-user-drag: none; }
|
||||
#canvas[data-page-tone="inverted"] .pg img { filter: invert(1); }
|
||||
|
||||
+57
-1
@@ -12,9 +12,65 @@
|
||||
const P = new URLSearchParams(location.search);
|
||||
const stage = $('#stage'), canvas = $('#canvas'), slider = $('#slider'), ver = $('#ver');
|
||||
let me, D;
|
||||
try { [me, D] = await Promise.all([api('api/me'), api('api/chapter?' + qsa({ m: P.get('m'), c: P.get('c'), v: P.get('v') }))]); }
|
||||
try {
|
||||
me = await api('api/me');
|
||||
const response = await fetch('api/chapter?' + qsa({ m: P.get('m'), c: P.get('c'), v: P.get('v') }));
|
||||
if (response.status === 423) {
|
||||
const access = await api('api/reading-access?' + qsa({ m: P.get('m') }));
|
||||
canvas.textContent = '';
|
||||
const card = h('section', { class: 'reading-gate' },
|
||||
h('h1', {}, 'Reading is closed right now'),
|
||||
h('p', {}, `The reading hours are set by this server's deployment (${access.timezone}). The library and your progress remain available.`),
|
||||
h('p', {}, `Answer five questions drawn from manga you have already read to unlock ${access.unlockMinutes} minutes of reading. There is no answer timer or retry limit.`));
|
||||
const message = h('p', { class: 'reading-gate-message', role: 'status' });
|
||||
const question = h('div', { class: 'reading-gate-question' });
|
||||
const start = h('button', { class: 'btn', type: 'button' }, 'Start five-question challenge');
|
||||
const back = h('a', { class: 'btn', href: mangaUrl(P.get('m')) }, 'Back to manga');
|
||||
card.append(message, question, h('div', { class: 'reading-gate-actions' }, start, back));
|
||||
canvas.append(card);
|
||||
let challenge = null;
|
||||
const showQuestion = data => {
|
||||
challenge = data.challenge;
|
||||
question.textContent = '';
|
||||
const input = h('input', { type: 'text', autocomplete: 'off', 'aria-label': 'Your answer', maxlength: '120', required: true });
|
||||
const form = h('form', {}, h('p', {}, `Question ${data.number} of ${data.total}`),
|
||||
h('p', { class: 'muted' }, data.mangaTitle), h('h2', {}, data.prompt), input, h('button', { class: 'btn', type: 'submit' }, 'Check answer'));
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const result = await api('api/reading-quiz/answer', { m: P.get('m'), challenge, answer: input.value });
|
||||
if (!result.correct) { message.textContent = 'Not quite. Try another spelling or answer—there is no attempt limit.'; input.select(); return; }
|
||||
if (result.complete) { message.textContent = 'Unlocked. Opening your chapter…'; location.reload(); return; }
|
||||
message.textContent = 'Correct.';
|
||||
showQuestion(result);
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
});
|
||||
question.append(form); input.focus();
|
||||
};
|
||||
start.addEventListener('click', async () => {
|
||||
try { showQuestion(await api('api/reading-quiz/start', { m: P.get('m') })); start.hidden = true; message.textContent = ''; }
|
||||
catch (error) { message.textContent = error.message; }
|
||||
});
|
||||
if (!access.quizAvailable) { start.hidden = true; message.textContent = 'Fewer than five questions match your completed chapters. Please return during reading hours.'; }
|
||||
window.addEventListener('pagehide', () => {
|
||||
if (challenge) navigator.sendBeacon('api/reading-quiz/abandon', new Blob([JSON.stringify({ challenge })], { type: 'application/json' }));
|
||||
});
|
||||
setInterval(async () => {
|
||||
try { if ((await api('api/reading-access?' + qsa({ m: P.get('m') }))).allowed) location.reload(); } catch { /* keep the gate visible */ }
|
||||
}, 15000);
|
||||
return;
|
||||
}
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.error || response.statusText);
|
||||
D = payload;
|
||||
}
|
||||
catch (e) { canvas.textContent = ''; canvas.append(h('p', { class: 'loading' }, e.message + ' · ', h('a', { href: './' }, 'Library'))); return; }
|
||||
|
||||
// An already-open chapter also closes when a window ends or an unlock expires.
|
||||
setInterval(async () => {
|
||||
try { if (!(await api('api/reading-access?' + qsa({ m: P.get('m') }))).allowed) location.reload(); } catch { /* transient check failure */ }
|
||||
}, 15000);
|
||||
|
||||
const S = me.settings, rtl = D.direction === 'rtl', ttb = D.direction === 'ttb';
|
||||
const GAP = ttb || S.flowPageGap === false ? 0 : 8, END_H = 280;
|
||||
document.title = `${D.chapter.label} · ${D.manga.title}`;
|
||||
|
||||
Reference in new issue
Block a user