feat: add kiosk frontend for guided intake flow
This commit is contained in:
parent
7d0bebf16d
commit
7807e32133
4 changed files with 144 additions and 2 deletions
42
bookmark/static/app.js
Normal file
42
bookmark/static/app.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// bookmark/bookmark/static/app.js
|
||||||
|
const statusEl = document.getElementById("status");
|
||||||
|
const hintEl = document.getElementById("hint");
|
||||||
|
const previewEl = document.getElementById("preview");
|
||||||
|
const triggerButton = document.getElementById("trigger-button");
|
||||||
|
|
||||||
|
const STATE_COPY = {
|
||||||
|
waiting_front: { status: "Waiting for book...", hint: "Center the front cover in the guide box." },
|
||||||
|
waiting_back: { status: "Front captured — flip it over", hint: "Center the back cover in the guide box." },
|
||||||
|
book_complete: { status: "Book complete!", hint: "Tap Next Book to continue." },
|
||||||
|
};
|
||||||
|
|
||||||
|
async function refreshStatus() {
|
||||||
|
const response = await fetch("/api/session-state");
|
||||||
|
const body = await response.json();
|
||||||
|
const copy = STATE_COPY[body.state] || { status: body.state, hint: "" };
|
||||||
|
statusEl.textContent = copy.status;
|
||||||
|
hintEl.textContent = copy.hint;
|
||||||
|
triggerButton.textContent = body.state === "book_complete" ? "Next Book" : "Capture";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPreview() {
|
||||||
|
previewEl.src = `/api/preview.jpg?ts=${Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function observe() {
|
||||||
|
await fetch("/api/observe", { method: "POST" });
|
||||||
|
await refreshStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerButton.addEventListener("click", async () => {
|
||||||
|
if (statusEl.textContent === "Book complete!") {
|
||||||
|
await fetch("/api/next-book", { method: "POST" });
|
||||||
|
} else {
|
||||||
|
await fetch("/api/manual-trigger", { method: "POST" });
|
||||||
|
}
|
||||||
|
await refreshStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(observe, 500);
|
||||||
|
setInterval(refreshPreview, 500);
|
||||||
|
refreshStatus();
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
|
<!-- bookmark/bookmark/static/index.html -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head><meta charset="UTF-8" /><title>Books in Hand — Intake</title></head>
|
<head>
|
||||||
<body><h1>Books in Hand — Intake</h1></body>
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Books in Hand — Intake</title>
|
||||||
|
<link rel="stylesheet" href="/theme.css" />
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="kiosk">
|
||||||
|
<div class="kiosk__preview">
|
||||||
|
<img id="preview" alt="Camera preview" />
|
||||||
|
</div>
|
||||||
|
<div class="kiosk__status" id="status">Waiting for book...</div>
|
||||||
|
<div class="kiosk__hint" id="hint">Center the front cover in the guide box.</div>
|
||||||
|
<button class="kiosk__button" id="trigger-button">Capture</button>
|
||||||
|
</main>
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
48
bookmark/static/style.css
Normal file
48
bookmark/static/style.css
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* bookmark/bookmark/static/style.css */
|
||||||
|
.kiosk {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
gap: var(--space-md);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__preview {
|
||||||
|
width: min(90vw, 640px);
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__status {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__hint {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__button {
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiosk__button:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
35
bookmark/static/theme.css
Normal file
35
bookmark/static/theme.css
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* bookmark/bookmark/static/theme.css */
|
||||||
|
:root {
|
||||||
|
--color-bg: #101418;
|
||||||
|
--color-surface: #1b2128;
|
||||||
|
--color-text: #f2f4f6;
|
||||||
|
--color-muted: #9aa5b1;
|
||||||
|
--color-accent: #4fb0ae;
|
||||||
|
--color-success: #5cb85c;
|
||||||
|
--color-warning: #e0a13a;
|
||||||
|
--space-sm: 0.5rem;
|
||||||
|
--space-md: 1rem;
|
||||||
|
--space-lg: 2rem;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--font-base: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--color-bg: #f2f4f6;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-text: #101418;
|
||||||
|
--color-muted: #5a6472;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-base);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue