727 lines
24 KiB
HTML
727 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Cary Swim Club Check-In</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--ink: #10231a;
|
|
--ink-soft: #4a5e56;
|
|
--surface: #ffffff;
|
|
--surface-soft: #f4fbef;
|
|
--line: #d8e8cf;
|
|
--accent: #4cbb17;
|
|
--accent-strong: #3b9612;
|
|
--accent-soft: #d8f3c6;
|
|
--bg-a: #f6fbf2;
|
|
--bg-b: #deefd1;
|
|
--hero-a: #1f3e22;
|
|
--hero-b: #2f642d;
|
|
--focus-ring: rgba(76, 187, 23, 0.28);
|
|
--shadow: 0 18px 38px rgba(20, 46, 24, 0.14);
|
|
--tile-glow: rgba(76, 187, 23, 0.2);
|
|
}
|
|
|
|
body[data-theme="dark"] {
|
|
--ink: #e7f6df;
|
|
--ink-soft: #a6c2b0;
|
|
--surface: #132118;
|
|
--surface-soft: #1a2d21;
|
|
--line: #264032;
|
|
--accent: #65d82c;
|
|
--accent-strong: #4cbb17;
|
|
--accent-soft: #24411a;
|
|
--bg-a: #0a140d;
|
|
--bg-b: #12211a;
|
|
--hero-a: #15341c;
|
|
--hero-b: #1f4c28;
|
|
--focus-ring: rgba(101, 216, 44, 0.25);
|
|
--shadow: 0 18px 42px rgba(0, 0, 0, 0.45);
|
|
--tile-glow: rgba(101, 216, 44, 0.24);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
|
color: var(--ink);
|
|
background:
|
|
radial-gradient(circle at 82% 12%, var(--tile-glow), transparent 34%),
|
|
radial-gradient(circle at 12% 88%, rgba(24, 93, 142, 0.2), transparent 36%),
|
|
linear-gradient(160deg, var(--bg-a), var(--bg-b));
|
|
background-attachment: fixed;
|
|
padding: 24px;
|
|
transition: background 260ms ease, color 260ms ease;
|
|
}
|
|
|
|
.page {
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
gap: 18px;
|
|
}
|
|
|
|
.hero {
|
|
border-radius: 20px;
|
|
padding: 24px;
|
|
color: #f4ffe8;
|
|
background:
|
|
linear-gradient(120deg, rgba(255, 255, 255, 0.06), transparent 42%),
|
|
linear-gradient(140deg, var(--hero-a), var(--hero-b));
|
|
box-shadow: var(--shadow);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hero::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 230px;
|
|
height: 230px;
|
|
right: -64px;
|
|
top: -112px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(151, 255, 109, 0.28), transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero h1 {
|
|
margin: 0;
|
|
font-family: "Archivo Black", "Impact", sans-serif;
|
|
letter-spacing: 0.03em;
|
|
font-size: clamp(1.4rem, 2vw + 0.9rem, 2.3rem);
|
|
}
|
|
|
|
.hero p {
|
|
margin: 8px 0 0;
|
|
color: #d8f6c8;
|
|
max-width: 52ch;
|
|
}
|
|
|
|
.club-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(196, 250, 166, 0.4);
|
|
background: rgba(13, 31, 14, 0.25);
|
|
padding: 5px 11px;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
|
|
.stat {
|
|
background: var(--surface);
|
|
border: 1px solid var(--line);
|
|
border-radius: 16px;
|
|
padding: 14px;
|
|
box-shadow: var(--shadow);
|
|
transition: transform 180ms ease, border-color 180ms ease, background 220ms ease;
|
|
}
|
|
|
|
.stat:hover {
|
|
transform: translateY(-2px);
|
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
|
|
}
|
|
|
|
.stat .label {
|
|
margin: 0;
|
|
color: var(--ink-soft);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.stat .value {
|
|
margin: 6px 0 0;
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.panel {
|
|
background: var(--surface);
|
|
border: 1px solid var(--line);
|
|
border-radius: 18px;
|
|
padding: 18px;
|
|
box-shadow: var(--shadow);
|
|
backdrop-filter: blur(2px);
|
|
transition: background 220ms ease, border-color 220ms ease;
|
|
}
|
|
|
|
.panel h2 {
|
|
margin: 0;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.panel-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.sub {
|
|
margin: 4px 0 14px;
|
|
color: var(--ink-soft);
|
|
font-size: 0.93rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.search {
|
|
flex: 1;
|
|
min-width: 220px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--line);
|
|
padding: 11px 12px;
|
|
font: inherit;
|
|
color: var(--ink);
|
|
background: var(--surface-soft);
|
|
transition: border-color 200ms ease, box-shadow 200ms ease;
|
|
}
|
|
|
|
.search:focus {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 4px var(--focus-ring);
|
|
outline: none;
|
|
}
|
|
|
|
.btn {
|
|
border: 0;
|
|
border-radius: 12px;
|
|
padding: 10px 14px;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.btn-main {
|
|
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
|
color: #fff;
|
|
transition: transform 160ms ease, filter 160ms ease;
|
|
}
|
|
|
|
.btn-main:hover,
|
|
.btn-main:focus-visible {
|
|
transform: translateY(-1px);
|
|
filter: brightness(1.04);
|
|
outline: none;
|
|
}
|
|
|
|
.btn-ghost {
|
|
background: var(--surface-soft);
|
|
border: 1px solid var(--line);
|
|
color: var(--ink);
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 8px 12px;
|
|
border-radius: 10px;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.checked-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
}
|
|
|
|
.checked-card {
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 10px;
|
|
background: linear-gradient(135deg, var(--surface), var(--surface-soft));
|
|
display: grid;
|
|
gap: 6px;
|
|
animation: fade-up 240ms ease both;
|
|
transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
|
|
}
|
|
|
|
.checked-card.selected {
|
|
border-color: color-mix(in srgb, var(--accent) 60%, var(--line));
|
|
box-shadow: 0 0 0 4px var(--focus-ring);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.checked-name {
|
|
font-size: 1.08rem;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
|
|
.checked-meta {
|
|
color: var(--ink-soft);
|
|
font-size: 0.83rem;
|
|
}
|
|
|
|
.checked-actions {
|
|
display: inline-grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 6px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.mini-btn {
|
|
border: 1px solid var(--line);
|
|
border-radius: 9px;
|
|
padding: 6px 8px;
|
|
font: inherit;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
background: var(--surface-soft);
|
|
color: var(--ink);
|
|
}
|
|
|
|
.mini-btn.active-in {
|
|
background: color-mix(in srgb, var(--accent) 24%, var(--surface));
|
|
color: color-mix(in srgb, var(--accent-strong) 75%, black);
|
|
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
|
|
}
|
|
|
|
.mini-btn.active-out {
|
|
background: #fae8dc;
|
|
color: #ab5816;
|
|
border-color: #e2b390;
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: var(--accent);
|
|
display: inline-block;
|
|
}
|
|
|
|
.list {
|
|
display: grid;
|
|
gap: 12px;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
}
|
|
|
|
.family-tile {
|
|
text-decoration: none;
|
|
color: inherit;
|
|
border: 1px solid var(--line);
|
|
border-radius: 16px;
|
|
padding: 14px;
|
|
display: grid;
|
|
gap: 10px;
|
|
align-content: space-between;
|
|
background: linear-gradient(135deg, var(--surface), var(--surface-soft));
|
|
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
|
box-shadow: 0 8px 24px rgba(14, 43, 73, 0.08);
|
|
aspect-ratio: 1 / 1;
|
|
min-height: 180px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.family-tile::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: -26px;
|
|
bottom: -28px;
|
|
width: 92px;
|
|
height: 92px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, color-mix(in srgb, var(--accent) 30%, transparent), transparent);
|
|
}
|
|
|
|
.family-tile:hover,
|
|
.family-tile:focus-visible {
|
|
transform: translateY(-2px);
|
|
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
|
|
box-shadow: 0 14px 30px rgba(14, 43, 73, 0.14);
|
|
outline: none;
|
|
}
|
|
|
|
.family-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.family-main {
|
|
display: grid;
|
|
gap: 8px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.family-name-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.family-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, var(--accent), #2e8e34);
|
|
color: #fff;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.family-tile h3 {
|
|
margin: 0;
|
|
font-size: 1.16rem;
|
|
line-height: 1.15;
|
|
}
|
|
|
|
.arrow {
|
|
font-size: 1rem;
|
|
color: color-mix(in srgb, var(--accent) 46%, var(--ink-soft));
|
|
font-weight: 700;
|
|
}
|
|
|
|
.meta {
|
|
margin: 2px 0 0;
|
|
color: var(--ink-soft);
|
|
font-size: 0.86rem;
|
|
}
|
|
|
|
.pill-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.pill {
|
|
border-radius: 999px;
|
|
padding: 4px 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
display: inline-flex;
|
|
gap: 4px;
|
|
align-items: center;
|
|
}
|
|
|
|
.pill-in {
|
|
background: color-mix(in srgb, var(--accent) 20%, var(--surface));
|
|
color: color-mix(in srgb, var(--accent-strong) 78%, black);
|
|
}
|
|
|
|
.pill-kids {
|
|
background: #e4edff;
|
|
color: #2456b1;
|
|
}
|
|
|
|
.status-box {
|
|
font-size: 0.9rem;
|
|
color: var(--ink-soft);
|
|
border: 1px dashed var(--line);
|
|
border-radius: 12px;
|
|
padding: 10px 12px;
|
|
background: var(--surface-soft);
|
|
}
|
|
|
|
@keyframes fade-up {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(7px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* {
|
|
animation: none !important;
|
|
transition: none !important;
|
|
}
|
|
}
|
|
|
|
.hidden { display: none; }
|
|
|
|
@media (max-width: 900px) {
|
|
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
body { padding: 14px; }
|
|
.stats { grid-template-columns: 1fr; }
|
|
.list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
.family-tile { min-height: 150px; }
|
|
.checked-list { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
@media (max-width: 420px) {
|
|
.list { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="page">
|
|
<section class="hero">
|
|
<div class="club-tag">Cary Swim Club</div>
|
|
<h1>Family Check-In Lookup</h1>
|
|
<p>Fast and simple member management system for CSC guards and administrators.</p>
|
|
</section>
|
|
|
|
<section class="stats">
|
|
<article class="stat">
|
|
<p class="label">Families</p>
|
|
<p class="value" id="kpiFamilies">0</p>
|
|
</article>
|
|
<article class="stat">
|
|
<p class="label">Total Members</p>
|
|
<p class="value" id="kpiTotal">0</p>
|
|
</article>
|
|
<article class="stat">
|
|
<p class="label">Checked In</p>
|
|
<p class="value" id="kpiInPool">0</p>
|
|
</article>
|
|
<article class="stat">
|
|
<p class="label">Children Swim Passed</p>
|
|
<p class="value" id="kpiSwimKids">0</p>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="panel-head">
|
|
<h2>Checked-In Right Now</h2>
|
|
<a class="btn btn-ghost btn-small" href="/manage">Management</a>
|
|
</div>
|
|
<p class="sub" id="checkedHelp">Search active guests, or type first/family-last names for quick check-in/out lookup.</p>
|
|
<div class="controls">
|
|
<input id="checkedSearch" class="search" type="text" placeholder="Search checked-in members..." autocomplete="off">
|
|
</div>
|
|
<div id="checkedList" class="checked-list"></div>
|
|
<div id="checkedEmpty" class="status-box hidden">No checked-in members found.</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Search Families</h2>
|
|
<p class="sub">Search and tap a family.</p>
|
|
<div class="controls">
|
|
<input id="familySearch" class="search" type="text" placeholder="Search Garcia, Nguyen, Mateo..." autocomplete="off">
|
|
<button id="refreshBtn" class="btn btn-ghost" type="button">Refresh</button>
|
|
</div>
|
|
<div id="familyList" class="list"></div>
|
|
<div id="empty" class="status-box hidden">No matching families found.</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
function getPreferredTheme() {
|
|
const saved = window.localStorage.getItem("csc-theme");
|
|
if (saved === "dark" || saved === "light") {
|
|
return saved;
|
|
}
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
document.body.setAttribute("data-theme", theme);
|
|
}
|
|
|
|
applyTheme(getPreferredTheme());
|
|
|
|
const familySearch = document.getElementById("familySearch");
|
|
const checkedSearch = document.getElementById("checkedSearch");
|
|
const familyList = document.getElementById("familyList");
|
|
const checkedList = document.getElementById("checkedList");
|
|
const refreshBtn = document.getElementById("refreshBtn");
|
|
const emptyBox = document.getElementById("empty");
|
|
const checkedEmpty = document.getElementById("checkedEmpty");
|
|
const checkedHelp = document.getElementById("checkedHelp");
|
|
let latestCheckedItems = [];
|
|
let selectedCheckedIndex = 0;
|
|
|
|
async function api(path, options = {}) {
|
|
const response = await fetch(path, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...options,
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(payload.error || payload.message || "Request failed");
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function renderFamilies(items) {
|
|
familyList.innerHTML = "";
|
|
emptyBox.classList.toggle("hidden", items.length > 0);
|
|
|
|
items.forEach((family) => {
|
|
const checkedInCount = family.members.filter((m) => m.checkedIn).length;
|
|
const childCount = family.members.filter((m) => m.age < 18).length;
|
|
const familyInitial = (family.familyName || "F").slice(0, 1).toUpperCase();
|
|
const node = document.createElement("a");
|
|
node.className = "family-tile";
|
|
node.href = `/family/${family.id}`;
|
|
node.innerHTML = `
|
|
<div class="family-head">
|
|
<div class="family-name-wrap">
|
|
<span class="family-icon">${familyInitial}</span>
|
|
<h3>${family.familyName} Family</h3>
|
|
</div>
|
|
<span class="arrow">View</span>
|
|
</div>
|
|
<div class="family-main">
|
|
<p class="meta">${family.members.length} members • ${family.primaryPhone || "No phone"}</p>
|
|
<div class="pill-row">
|
|
<span class="pill pill-in">In: ${checkedInCount}</span>
|
|
<span class="pill pill-kids">Kids: ${childCount}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
familyList.append(node);
|
|
});
|
|
}
|
|
|
|
async function updateCheckStatus(memberId, checkedIn) {
|
|
try {
|
|
await api(`/api/members/${memberId}/checkin`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ checkedIn }),
|
|
});
|
|
await Promise.all([loadStats(), loadFamilies(), loadCheckedIn()]);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
function renderCheckedIn(items) {
|
|
checkedList.innerHTML = "";
|
|
latestCheckedItems = items;
|
|
if (items.length === 0) {
|
|
selectedCheckedIndex = 0;
|
|
} else {
|
|
selectedCheckedIndex = Math.min(selectedCheckedIndex, items.length - 1);
|
|
}
|
|
checkedEmpty.classList.toggle("hidden", items.length > 0);
|
|
const searching = checkedSearch.value.trim().length > 0;
|
|
checkedHelp.textContent = searching
|
|
? "Search mode: fuzzy first-name and family-last-name matching. Shortcuts: ArrowUp/ArrowDown to select, Enter=In, Shift+Enter=Out."
|
|
: "Showing currently checked-in members. Shortcuts work while search box is focused.";
|
|
|
|
items.forEach((member, index) => {
|
|
const node = document.createElement("article");
|
|
node.className = `checked-card ${(searching && index === selectedCheckedIndex) ? "selected" : ""}`;
|
|
node.innerHTML = `
|
|
<div class="checked-name"><span class="dot"></span>${member.fullName}</div>
|
|
<div class="checked-meta">${member.familyName} • Age ${member.age}</div>
|
|
<div class="checked-actions">
|
|
<button class="mini-btn ${member.checkedIn ? "active-in" : ""}" data-state="in">Checked In</button>
|
|
<button class="mini-btn ${!member.checkedIn ? "active-out" : ""}" data-state="out">Checked Out</button>
|
|
</div>
|
|
`;
|
|
|
|
const inBtn = node.querySelector("[data-state='in']");
|
|
const outBtn = node.querySelector("[data-state='out']");
|
|
inBtn.disabled = member.checkedIn;
|
|
outBtn.disabled = !member.checkedIn;
|
|
|
|
inBtn.addEventListener("click", async () => {
|
|
await updateCheckStatus(member.id, true);
|
|
});
|
|
outBtn.addEventListener("click", async () => {
|
|
await updateCheckStatus(member.id, false);
|
|
});
|
|
|
|
checkedList.append(node);
|
|
});
|
|
}
|
|
|
|
async function loadStats() {
|
|
const stats = await api("/api/dashboard");
|
|
document.getElementById("kpiFamilies").textContent = stats.families;
|
|
document.getElementById("kpiTotal").textContent = stats.totalMembers;
|
|
document.getElementById("kpiInPool").textContent = stats.checkedIn;
|
|
document.getElementById("kpiSwimKids").textContent = stats.childrenSwimPassed;
|
|
}
|
|
|
|
async function loadFamilies() {
|
|
const q = familySearch.value.trim();
|
|
const path = q ? `/api/families?q=${encodeURIComponent(q)}` : "/api/families";
|
|
const payload = await api(path);
|
|
renderFamilies(payload.items || []);
|
|
}
|
|
|
|
async function loadCheckedIn() {
|
|
const q = checkedSearch.value.trim();
|
|
const path = q ? `/api/checked-in?q=${encodeURIComponent(q)}` : "/api/checked-in";
|
|
const payload = await api(path);
|
|
renderCheckedIn(payload.items || []);
|
|
}
|
|
|
|
async function refreshEverything() {
|
|
try {
|
|
await Promise.all([loadStats(), loadFamilies(), loadCheckedIn()]);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
familySearch.addEventListener("input", loadFamilies);
|
|
checkedSearch.addEventListener("input", loadCheckedIn);
|
|
checkedSearch.addEventListener("keydown", async (event) => {
|
|
if (latestCheckedItems.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (event.key === "ArrowDown" || event.key === "ArrowRight") {
|
|
event.preventDefault();
|
|
selectedCheckedIndex = Math.min(selectedCheckedIndex + 1, latestCheckedItems.length - 1);
|
|
renderCheckedIn(latestCheckedItems);
|
|
return;
|
|
}
|
|
|
|
if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
|
|
event.preventDefault();
|
|
selectedCheckedIndex = Math.max(selectedCheckedIndex - 1, 0);
|
|
renderCheckedIn(latestCheckedItems);
|
|
return;
|
|
}
|
|
|
|
if (event.key !== "Enter") {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
const selected = latestCheckedItems[selectedCheckedIndex] || latestCheckedItems[0];
|
|
const targetCheckedIn = !event.shiftKey;
|
|
await updateCheckStatus(selected.id, targetCheckedIn);
|
|
});
|
|
|
|
refreshBtn.addEventListener("click", refreshEverything);
|
|
|
|
refreshEverything();
|
|
</script>
|
|
</body>
|
|
</html> |