initial
This commit is contained in:
786
family.html
Normal file
786
family.html
Normal file
@@ -0,0 +1,786 @@
|
||||
<!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 Family 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: #4b5f56;
|
||||
--surface: #ffffff;
|
||||
--surface-soft: #f4fbef;
|
||||
--line: #d8e8cf;
|
||||
--good: #3e9f14;
|
||||
--warn: #ba5d17;
|
||||
--deep: #2b6ed4;
|
||||
--deep-bg: #dbe9ff;
|
||||
--safe: #9f3232;
|
||||
--accent: #4cbb17;
|
||||
--accent-strong: #3b9612;
|
||||
--bg-a: #f6fbf2;
|
||||
--bg-b: #deefd1;
|
||||
--hero-a: #1f3e22;
|
||||
--hero-b: #2f642d;
|
||||
--focus-ring: rgba(76, 187, 23, 0.28);
|
||||
--shadow: 0 16px 36px rgba(19, 45, 23, 0.14);
|
||||
}
|
||||
|
||||
body[data-theme="dark"] {
|
||||
--ink: #e7f6df;
|
||||
--ink-soft: #a5c1af;
|
||||
--surface: #132118;
|
||||
--surface-soft: #1a2d21;
|
||||
--line: #264032;
|
||||
--good: #65d82c;
|
||||
--warn: #f2a45b;
|
||||
--deep: #7db7ff;
|
||||
--deep-bg: #1b3157;
|
||||
--safe: #f08181;
|
||||
--accent: #65d82c;
|
||||
--accent-strong: #4cbb17;
|
||||
--bg-a: #0a140d;
|
||||
--bg-b: #12211a;
|
||||
--hero-a: #15341c;
|
||||
--hero-b: #1f4c28;
|
||||
--focus-ring: rgba(101, 216, 44, 0.25);
|
||||
--shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
* { 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 20% 0%, rgba(76, 187, 23, 0.22), transparent 30%),
|
||||
radial-gradient(circle at 95% 95%, rgba(61, 120, 67, 0.2), transparent 28%),
|
||||
linear-gradient(165deg, var(--bg-a), var(--bg-b));
|
||||
background-attachment: fixed;
|
||||
padding: 24px;
|
||||
transition: background 260ms ease, color 260ms ease;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1050px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.top {
|
||||
background:
|
||||
linear-gradient(120deg, rgba(255, 255, 255, 0.05), transparent 42%),
|
||||
linear-gradient(140deg, var(--hero-a), var(--hero-b));
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
color: #efffe8;
|
||||
box-shadow: var(--shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
right: -60px;
|
||||
top: -102px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(151, 255, 109, 0.25), transparent 70%);
|
||||
}
|
||||
|
||||
.top h1 {
|
||||
margin: 0;
|
||||
font-family: "Archivo Black", "Impact", sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: clamp(1.4rem, 2vw + 1rem, 2.2rem);
|
||||
}
|
||||
|
||||
.top p {
|
||||
margin: 8px 0 0;
|
||||
color: #d8f6c8;
|
||||
}
|
||||
|
||||
.top-controls {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.guest-pass-panel {
|
||||
background: rgba(10, 31, 10, 0.22);
|
||||
border: 1px solid rgba(180, 244, 148, 0.35);
|
||||
border-radius: 14px;
|
||||
padding: 10px 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.guest-pass-label {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.guest-pass-label strong {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: #d8f6c8;
|
||||
}
|
||||
|
||||
.guest-pass-value {
|
||||
font-family: "Archivo Black", "Impact", sans-serif;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1;
|
||||
color: #f7ffe9;
|
||||
}
|
||||
|
||||
.guest-pass-actions {
|
||||
margin-left: auto;
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pass-btn {
|
||||
border: 1px solid rgba(196, 250, 166, 0.35);
|
||||
background: rgba(6, 28, 12, 0.34);
|
||||
color: #f0ffe5;
|
||||
border-radius: 9px;
|
||||
min-width: 50px;
|
||||
padding: 8px 10px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pass-btn:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-main {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: var(--surface-soft);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: background 220ms ease, border-color 220ms ease;
|
||||
}
|
||||
|
||||
.rule-banner {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--line);
|
||||
background: var(--surface-soft);
|
||||
color: var(--ink-soft);
|
||||
padding: 10px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.section-meta {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.member-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.member {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, var(--surface), var(--surface-soft));
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
animation: card-in 260ms ease both;
|
||||
align-self: start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.member::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
bottom: -25px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(76, 187, 23, 0.18), rgba(76, 187, 23, 0.02));
|
||||
}
|
||||
|
||||
.member:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
|
||||
box-shadow: 0 12px 24px rgba(19, 45, 79, 0.12);
|
||||
}
|
||||
|
||||
.member-head {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.member-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.member-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
background: linear-gradient(135deg, var(--accent), #2e8e34);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.member-icon svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.member-first,
|
||||
.member-last {
|
||||
display: block;
|
||||
font-size: 1.16rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.member-sub {
|
||||
margin: 4px 0 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.88rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-block;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chip-in {
|
||||
background: color-mix(in srgb, var(--accent) 20%, var(--surface));
|
||||
color: var(--good);
|
||||
}
|
||||
|
||||
.chip-out {
|
||||
background: #fbe8d9;
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.chip-deep {
|
||||
background: var(--deep-bg);
|
||||
color: var(--deep);
|
||||
}
|
||||
|
||||
.chip-shallow {
|
||||
background: #fde7e7;
|
||||
color: var(--safe);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
background: color-mix(in srgb, var(--surface-soft) 85%, transparent);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 3px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.toggle button {
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
padding: 8px 8px;
|
||||
font: inherit;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.toggle button.active {
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 20px rgba(9, 35, 67, 0.2);
|
||||
}
|
||||
|
||||
.state-in.active {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||
}
|
||||
|
||||
.state-out.active {
|
||||
background: linear-gradient(135deg, #ce7a32, #b75f17);
|
||||
}
|
||||
|
||||
.state-pass.active {
|
||||
background: linear-gradient(135deg, #3f87eb, #2b6ed4);
|
||||
}
|
||||
|
||||
.state-fail.active {
|
||||
background: linear-gradient(135deg, #ad3b3b, #912a2a);
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.86rem;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 9px 10px;
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
|
||||
.chip-wrap {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.status {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
background: var(--surface-soft);
|
||||
opacity: 0.86;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body { padding: 14px; }
|
||||
.member-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.member-list { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<section class="top">
|
||||
<h1 id="familyTitle">Family</h1>
|
||||
<p id="familyMeta">Loading family...</p>
|
||||
<div class="top-controls">
|
||||
<div class="guest-pass-panel" aria-live="polite">
|
||||
<div class="guest-pass-label">
|
||||
<strong>Guest Passes</strong>
|
||||
<span id="guestPassCount" class="guest-pass-value">0</span>
|
||||
</div>
|
||||
<div class="guest-pass-actions">
|
||||
<button id="decreasePassBtn" class="pass-btn" type="button" aria-label="Decrease guest passes">-1</button>
|
||||
<button id="increasePassBtn" class="pass-btn" type="button" aria-label="Increase guest passes">+1</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="toolbar">
|
||||
<a class="btn btn-ghost" href="/">Back To Family Search</a>
|
||||
<button id="refreshBtn" class="btn btn-main" type="button">Refresh Family</button>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div id="ruleBanner" class="rule-banner">Swim test required under age 13.</div>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">Adults</h2>
|
||||
<p class="section-meta" id="adultsMeta">0 members</p>
|
||||
</div>
|
||||
<div id="adultsList" class="member-list"></div>
|
||||
<div id="adultsEmpty" class="empty-note">No adults in this family.</div>
|
||||
</section>
|
||||
|
||||
<section class="section-block">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">Children</h2>
|
||||
<p class="section-meta" id="childrenMeta">0 members</p>
|
||||
</div>
|
||||
<div id="childrenList" class="member-list"></div>
|
||||
<div id="childrenEmpty" class="empty-note">No children in this family.</div>
|
||||
</section>
|
||||
|
||||
<div id="status" class="status">Ready.</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 familyTitle = document.getElementById("familyTitle");
|
||||
const familyMeta = document.getElementById("familyMeta");
|
||||
const ruleBanner = document.getElementById("ruleBanner");
|
||||
const adultsList = document.getElementById("adultsList");
|
||||
const childrenList = document.getElementById("childrenList");
|
||||
const adultsMeta = document.getElementById("adultsMeta");
|
||||
const childrenMeta = document.getElementById("childrenMeta");
|
||||
const adultsEmpty = document.getElementById("adultsEmpty");
|
||||
const childrenEmpty = document.getElementById("childrenEmpty");
|
||||
const statusBox = document.getElementById("status");
|
||||
const refreshBtn = document.getElementById("refreshBtn");
|
||||
const guestPassCount = document.getElementById("guestPassCount");
|
||||
const decreasePassBtn = document.getElementById("decreasePassBtn");
|
||||
const increasePassBtn = document.getElementById("increasePassBtn");
|
||||
let currentMinSwimTestAge = 13;
|
||||
let currentGuestPasses = 0;
|
||||
let isUpdatingGuestPasses = false;
|
||||
|
||||
function currentFamilyId() {
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
const maybeId = parts[parts.length - 1];
|
||||
return Number.parseInt(maybeId, 10);
|
||||
}
|
||||
|
||||
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 setStatus(message) {
|
||||
statusBox.textContent = message;
|
||||
}
|
||||
|
||||
function buildToggle(options, activeValue, onChoose) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "toggle";
|
||||
|
||||
options.forEach((opt) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = opt.label;
|
||||
btn.className = `${opt.className} ${opt.value === activeValue ? "active" : ""}`;
|
||||
btn.addEventListener("click", () => onChoose(opt.value));
|
||||
container.append(btn);
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
async function setCheckIn(memberId, checkedIn) {
|
||||
await api(`/api/members/${memberId}/checkin`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ checkedIn }),
|
||||
});
|
||||
}
|
||||
|
||||
async function setSwimTest(memberId, swimTestPassed) {
|
||||
await api(`/api/members/${memberId}/swim-test`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ swimTestPassed }),
|
||||
});
|
||||
}
|
||||
|
||||
function renderGuestPasses() {
|
||||
guestPassCount.textContent = String(currentGuestPasses);
|
||||
decreasePassBtn.disabled = isUpdatingGuestPasses || currentGuestPasses <= 0;
|
||||
increasePassBtn.disabled = isUpdatingGuestPasses;
|
||||
}
|
||||
|
||||
async function updateGuestPasses(nextCount) {
|
||||
if (!Number.isInteger(nextCount) || nextCount < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const familyId = currentFamilyId();
|
||||
if (!familyId || Number.isNaN(familyId)) {
|
||||
setStatus("Invalid family id.");
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdatingGuestPasses = true;
|
||||
renderGuestPasses();
|
||||
try {
|
||||
setStatus("Updating guest passes...");
|
||||
const payload = await api(`/api/families/${familyId}/guest-passes`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ guestPasses: nextCount }),
|
||||
});
|
||||
currentGuestPasses = payload.item.guestPasses;
|
||||
setStatus(`Guest passes set to ${currentGuestPasses}.`);
|
||||
} catch (error) {
|
||||
setStatus(`Error: ${error.message}`);
|
||||
} finally {
|
||||
isUpdatingGuestPasses = false;
|
||||
renderGuestPasses();
|
||||
}
|
||||
}
|
||||
|
||||
function renderMember(member, refreshFamily) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "member";
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "member-head";
|
||||
|
||||
const idWrap = document.createElement("div");
|
||||
idWrap.className = "member-title-wrap";
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "member-icon";
|
||||
icon.innerHTML = "<svg viewBox='0 0 24 24' aria-hidden='true'><path d='M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5zm0 2c-4.42 0-8 2.24-8 5v1h16v-1c0-2.76-3.58-5-8-5z'/></svg>";
|
||||
|
||||
const textWrap = document.createElement("div");
|
||||
const title = document.createElement("h3");
|
||||
title.className = "member-name";
|
||||
title.innerHTML = `<span class="member-first">${member.firstName}</span><span class="member-last">${member.lastName}</span>`;
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "member-sub";
|
||||
subtitle.textContent = `Age ${member.age}`;
|
||||
textWrap.append(title, subtitle);
|
||||
idWrap.append(icon, textWrap);
|
||||
|
||||
head.append(idWrap);
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "row";
|
||||
|
||||
controls.append(
|
||||
buildToggle(
|
||||
[
|
||||
{ label: "Checked Out", value: false, className: "state-out" },
|
||||
{ label: "Checked In", value: true, className: "state-in" },
|
||||
],
|
||||
!!member.checkedIn,
|
||||
async (value) => {
|
||||
try {
|
||||
setStatus(`Updating check-in for ${member.fullName}...`);
|
||||
await setCheckIn(member.id, value);
|
||||
await refreshFamily();
|
||||
setStatus(`Updated check-in for ${member.fullName}.`);
|
||||
} catch (error) {
|
||||
setStatus(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (member.requiresSwimTest) {
|
||||
controls.append(
|
||||
buildToggle(
|
||||
[
|
||||
{ label: "Not Passed", value: false, className: "state-fail" },
|
||||
{ label: "Passed", value: true, className: "state-pass" },
|
||||
],
|
||||
!!member.swimTestPassed,
|
||||
async (value) => {
|
||||
try {
|
||||
setStatus(`Updating swim test for ${member.fullName}...`);
|
||||
await setSwimTest(member.id, value);
|
||||
await refreshFamily();
|
||||
setStatus(`Updated swim test for ${member.fullName}.`);
|
||||
} catch (error) {
|
||||
setStatus(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const notRequired = document.createElement("div");
|
||||
notRequired.className = "toggle";
|
||||
notRequired.innerHTML = `<button type="button" class="state-pass active">Swim Test Not Required</button><button type="button">Age ${member.age}</button>`;
|
||||
controls.append(notRequired);
|
||||
}
|
||||
|
||||
card.append(head, controls);
|
||||
return card;
|
||||
}
|
||||
|
||||
async function loadFamily() {
|
||||
const familyId = currentFamilyId();
|
||||
if (!familyId || Number.isNaN(familyId)) {
|
||||
setStatus("Invalid family id.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await api(`/api/families/${familyId}`);
|
||||
const family = payload.item;
|
||||
currentMinSwimTestAge = family.minSwimTestAge;
|
||||
currentGuestPasses = Number.isInteger(family.guestPasses) ? family.guestPasses : 0;
|
||||
|
||||
familyTitle.textContent = `${family.familyName} Family`;
|
||||
familyMeta.textContent = `${family.members.length} members • ${family.primaryPhone || "No phone number on file"}`;
|
||||
ruleBanner.textContent = `Swim test required under age ${currentMinSwimTestAge}.`;
|
||||
renderGuestPasses();
|
||||
|
||||
const adults = family.members.filter((member) => member.age >= 18);
|
||||
const children = family.members.filter((member) => member.age < 18);
|
||||
|
||||
adultsMeta.textContent = `${adults.length} member${adults.length === 1 ? "" : "s"}`;
|
||||
childrenMeta.textContent = `${children.length} member${children.length === 1 ? "" : "s"}`;
|
||||
|
||||
adultsList.innerHTML = "";
|
||||
childrenList.innerHTML = "";
|
||||
|
||||
adults.forEach((member) => {
|
||||
adultsList.append(renderMember(member, refreshFamily));
|
||||
});
|
||||
|
||||
children.forEach((member) => {
|
||||
childrenList.append(renderMember(member, refreshFamily));
|
||||
});
|
||||
|
||||
adultsEmpty.style.display = adults.length ? "none" : "block";
|
||||
childrenEmpty.style.display = children.length ? "none" : "block";
|
||||
}
|
||||
|
||||
async function refreshFamily() {
|
||||
try {
|
||||
await loadFamily();
|
||||
} catch (error) {
|
||||
setStatus(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener("click", refreshFamily);
|
||||
decreasePassBtn.addEventListener("click", () => updateGuestPasses(Math.max(0, currentGuestPasses - 1)));
|
||||
increasePassBtn.addEventListener("click", () => updateGuestPasses(currentGuestPasses + 1));
|
||||
|
||||
refreshFamily();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user