mirror of
https://github.com/tmdinosaurcenter/kiosk-guestbook.git
synced 2026-06-03 22:48:20 -06:00
b20e118def
Intercepts form submit via fetch and stores failed submissions in IndexedDB when offline. Replays queued entries on the online event and on each page load. Shows an offline banner on the form page and a sync-pending message on the thank-you page. Service worker bumped to guestbook-v2 to pre-cache offline-queue.js so the script is available when the kiosk has no network.
218 lines
7.9 KiB
Plaintext
218 lines
7.9 KiB
Plaintext
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<title>${SITE_TITLE}</title>
|
|
|
|
<!-- PWA -->
|
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
<link rel="apple-touch-icon" href="/static/images/logo.png" />
|
|
<meta name="theme-color" content="#3a9cb8" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<meta name="apple-mobile-web-app-title" content="${SITE_TITLE}" />
|
|
|
|
<!-- Bootstrap CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
<!-- Fonts -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Vollkorn:wght@700&family=Open+Sans&display=swap" rel="stylesheet" />
|
|
<style>
|
|
body {
|
|
font-family: 'Open Sans', sans-serif;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6 {
|
|
font-family: 'Vollkorn', serif;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* PWA / iOS kiosk */
|
|
html, body {
|
|
height: 100%;
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
body {
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
/* Prevent iOS auto-zoom on input focus (requires >= 16px) */
|
|
input.form-control,
|
|
textarea.form-control,
|
|
select.form-select {
|
|
font-size: 16px !important;
|
|
}
|
|
|
|
/* Scrolling marquee styles */
|
|
.scrolling-wrapper {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
position: fixed;
|
|
bottom: 0;
|
|
width: 100%;
|
|
background-color: #f8f9fa;
|
|
border-top: 1px solid #dee2e6;
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.scrolling-content {
|
|
display: inline-block;
|
|
padding: 10px;
|
|
font-size: 1.25rem;
|
|
animation: scroll-left linear infinite;
|
|
}
|
|
|
|
@keyframes scroll-left {
|
|
0% {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
100% {
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="container mt-5 mb-5">
|
|
<header class="d-flex align-items-center mb-4">
|
|
<img src="${LOGO_URL}" alt="Logo" class="me-3" style="height: 50px;" />
|
|
<h1 class="h3 mb-0">${SITE_TITLE}</h1>
|
|
</header>
|
|
|
|
<!-- Brief instructions for the form -->
|
|
<div class="alert alert-info" role="alert">
|
|
Please fill in your details below. First name, last name, and location are required.
|
|
Providing your email is optional, but it helps us follow up if needed.
|
|
</div>
|
|
|
|
<div id="offline-indicator" class="alert alert-warning d-none" role="status">
|
|
No internet connection — your entry will be saved and submitted automatically when reconnected.
|
|
</div>
|
|
|
|
{% if error %}
|
|
<div class="alert alert-danger" role="alert">
|
|
{{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="post" action="/" class="mb-4">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
<div class="mb-3">
|
|
<label for="first_name" class="form-label">First Name(s):</label>
|
|
<input type="text" class="form-control" id="first_name" name="first_name" maxlength="100" required />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="last_name" class="form-label">Last Name:</label>
|
|
<input type="text" class="form-control" id="last_name" name="last_name" maxlength="100" required />
|
|
</div>
|
|
|
|
<!-- Email + Newsletter Block (fully fixed) -->
|
|
<div class="mb-3">
|
|
<label for="email" class="form-label">Email (Optional):</label>
|
|
<input type="email" class="form-control" id="email" name="email" maxlength="254" />
|
|
|
|
<div class="form-check mt-2">
|
|
<input class="form-check-input" type="checkbox" name="newsletter_opt_in" id="newsletter_opt_in"
|
|
checked />
|
|
<label class="form-check-label" for="newsletter_opt_in">
|
|
Subscribe our newsletter
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="location" class="form-label">Location:</label>
|
|
<input type="text" class="form-control" id="location" name="location" maxlength="100" required />
|
|
</div>
|
|
|
|
<!-- Comment field hidden by default -->
|
|
<div class="mb-3" id="comment-field" style="display: none;">
|
|
<label for="comment" class="form-label">Comment (Optional):</label>
|
|
<textarea class="form-control" id="comment" name="comment" rows="3" maxlength="2000"></textarea>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">Submit</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Scrolling Guest Entries at the Bottom -->
|
|
<!-- Content is duplicated so the loop is seamless: animate 0 → -50% -->
|
|
<div class="scrolling-wrapper">
|
|
<div class="scrolling-content">
|
|
{% for guest in guests %}
|
|
<span class="me-5">
|
|
<strong>{{ guest[0] }}</strong> from {{ guest[1] }}
|
|
</span>
|
|
{% endfor %}
|
|
{% for guest in guests %}
|
|
<span class="me-5">
|
|
<strong>{{ guest[0] }}</strong> from {{ guest[1] }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Set scrolling speed to a fixed pixels-per-second rate -->
|
|
<script>
|
|
(function () {
|
|
const pixelsPerSecond = 80;
|
|
const content = document.querySelector(".scrolling-content");
|
|
|
|
function updateScrollSpeed() {
|
|
// Travel distance is half the total width (one copy of the list)
|
|
const oneCopyWidth = content.offsetWidth / 2;
|
|
content.style.animationDuration = (oneCopyWidth / pixelsPerSecond) + "s";
|
|
}
|
|
|
|
updateScrollSpeed();
|
|
window.addEventListener("resize", updateScrollSpeed);
|
|
})();
|
|
</script>
|
|
|
|
<!-- JavaScript to reveal the comment field -->
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const firstNameInput = document.getElementById("first_name");
|
|
const lastNameInput = document.getElementById("last_name");
|
|
const locationInput = document.getElementById("location");
|
|
const commentField = document.getElementById("comment-field");
|
|
|
|
function checkFields() {
|
|
if (
|
|
firstNameInput.value.trim().length >= 3 &&
|
|
lastNameInput.value.trim().length >= 3 &&
|
|
locationInput.value.trim().length >= 3
|
|
) {
|
|
commentField.style.display = "block";
|
|
} else {
|
|
commentField.style.display = "none";
|
|
}
|
|
}
|
|
|
|
firstNameInput.addEventListener("input", checkFields);
|
|
lastNameInput.addEventListener("input", checkFields);
|
|
locationInput.addEventListener("input", checkFields);
|
|
});
|
|
</script>
|
|
|
|
<!-- Bootstrap JS (optional) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"></script>
|
|
|
|
<script src="/static/offline-queue.js"></script>
|
|
|
|
<!-- Service worker registration -->
|
|
<script>
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function () {
|
|
navigator.serviceWorker.register('/sw.js', { scope: '/' });
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |