Public Access
Visitor sign in kiosk: multi-site, badge printing, WWCC expiry warnings, admin accounts with 2FA
This commit is contained in:
@@ -353,3 +353,19 @@ a:focus-visible {
|
||||
border: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- host dropdown */
|
||||
|
||||
.picker {
|
||||
width: 100%;
|
||||
padding: 15px 14px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
background: var(--card);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 20px;
|
||||
/* Tall enough to be a comfortable touch target on a tablet. */
|
||||
min-height: 58px;
|
||||
}
|
||||
.picker:disabled { color: var(--muted); }
|
||||
|
||||
+10
-2
@@ -63,7 +63,11 @@
|
||||
<div class="rail" data-rail></div>
|
||||
<h2>Who are you here to see?</h2>
|
||||
<label class="field">
|
||||
<span>Start typing a name</span>
|
||||
<span>Pick from the list</span>
|
||||
<select id="host-select" class="picker"></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Or start typing a name</span>
|
||||
<input id="in-host-search" autocomplete="off" enterkeyhint="search" placeholder="e.g. Rogerson">
|
||||
</label>
|
||||
<div class="host-list" id="host-list" role="listbox" aria-label="People you can visit"></div>
|
||||
@@ -162,7 +166,11 @@
|
||||
<section class="screen" id="screen-freq-host">
|
||||
<h2 id="freq-greeting">Who are you here to see?</h2>
|
||||
<label class="field">
|
||||
<span>Start typing a name</span>
|
||||
<span>Pick from the list</span>
|
||||
<select id="freq-host-select" class="picker"></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Or start typing a name</span>
|
||||
<input id="in-freq-host-search" autocomplete="off" enterkeyhint="search">
|
||||
</label>
|
||||
<div class="host-list" id="freq-host-list" role="listbox" aria-label="People you can visit"></div>
|
||||
|
||||
+54
-14
@@ -150,6 +150,9 @@ function resetState() {
|
||||
$$('#app input').forEach((i) => {
|
||||
i.value = '';
|
||||
});
|
||||
$$('#app select').forEach((sel) => {
|
||||
sel.selectedIndex = 0;
|
||||
});
|
||||
$$('.choice').forEach((b) => b.setAttribute('aria-pressed', 'false'));
|
||||
$('#check-number-field').hidden = true;
|
||||
$('#check-continue').hidden = true;
|
||||
@@ -182,16 +185,51 @@ tickClock();
|
||||
|
||||
/* --------------------------------------------------------------- hosts */
|
||||
|
||||
function renderHosts(listEl, searchValue, onPick) {
|
||||
const term = searchValue.trim().toLowerCase();
|
||||
/**
|
||||
* Two ways to say who you are visiting, because neither suits everyone: a dropdown
|
||||
* for someone who wants to browse a short list, and a filter box for someone who
|
||||
* knows the name and would rather type it. The filter narrows both, so the two
|
||||
* never disagree about what is on offer.
|
||||
*/
|
||||
function renderHosts(listEl, searchValue, onPick, selectEl = null) {
|
||||
const term = String(searchValue || '').trim().toLowerCase();
|
||||
const matches = term
|
||||
? hosts.filter((h) => h.name.toLowerCase().includes(term) || (h.area || '').toLowerCase().includes(term))
|
||||
? hosts.filter(
|
||||
(h) => h.name.toLowerCase().includes(term) || (h.area || '').toLowerCase().includes(term)
|
||||
)
|
||||
: hosts;
|
||||
|
||||
const choose = (id, name) => {
|
||||
state.hostId = Number(id);
|
||||
state.hostName = name;
|
||||
onPick();
|
||||
};
|
||||
|
||||
if (selectEl) {
|
||||
const placeholder =
|
||||
matches.length === hosts.length
|
||||
? `${hosts.length} ${hosts.length === 1 ? 'person' : 'people'} — choose a name`
|
||||
: `${matches.length} ${matches.length === 1 ? 'match' : 'matches'}`;
|
||||
selectEl.innerHTML =
|
||||
`<option value="">${escapeHtml(placeholder)}</option>` +
|
||||
matches
|
||||
.map(
|
||||
(h) =>
|
||||
`<option value="${h.id}">${escapeHtml(h.name)}${h.area ? ` — ${escapeHtml(h.area)}` : ''}</option>`
|
||||
)
|
||||
.join('');
|
||||
selectEl.disabled = matches.length === 0;
|
||||
selectEl.onchange = () => {
|
||||
const picked = hosts.find((h) => h.id === Number(selectEl.value));
|
||||
if (picked) choose(picked.id, picked.name);
|
||||
};
|
||||
}
|
||||
|
||||
if (!matches.length) {
|
||||
listEl.innerHTML = `<p class="host-empty">No one matches that. Check the spelling, or ask the front desk.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = matches
|
||||
.slice(0, 60)
|
||||
.map(
|
||||
@@ -202,11 +240,7 @@ function renderHosts(listEl, searchValue, onPick) {
|
||||
)
|
||||
.join('');
|
||||
listEl.querySelectorAll('[data-host-id]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
state.hostId = Number(btn.dataset.hostId);
|
||||
state.hostName = btn.dataset.hostName;
|
||||
onPick();
|
||||
});
|
||||
btn.addEventListener('click', () => choose(btn.dataset.hostId, btn.dataset.hostName));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -413,7 +447,7 @@ async function loadSiteContext() {
|
||||
change.textContent = siteConfig.site ? `Site: ${siteConfig.site.name} — change` : 'Choose site';
|
||||
|
||||
hosts = await api('/api/hosts');
|
||||
renderHosts($('#host-list'), '', () => show('guest-contact'));
|
||||
renderHosts($('#host-list'), '', () => show('guest-contact'), $('#host-select'));
|
||||
}
|
||||
|
||||
$('#change-site').addEventListener('click', chooseSite);
|
||||
@@ -448,7 +482,7 @@ $('[data-next="guest-name"]').addEventListener('click', () => {
|
||||
});
|
||||
|
||||
$('#in-host-search').addEventListener('input', (e) =>
|
||||
renderHosts($('#host-list'), e.target.value, () => show('guest-contact'))
|
||||
renderHosts($('#host-list'), e.target.value, () => show('guest-contact'), $('#host-select'))
|
||||
);
|
||||
|
||||
$('[data-next="guest-contact"]').addEventListener('click', () => {
|
||||
@@ -522,8 +556,11 @@ $('#do-freq-auth').addEventListener('click', async () => {
|
||||
state.hasStoredPhoto = Boolean(person.hasPhoto);
|
||||
$('#freq-greeting').textContent = `Hi ${person.firstName}. Who are you here to see?`;
|
||||
$('#in-freq-host-search').value = '';
|
||||
renderHosts($('#freq-host-list'), '', () =>
|
||||
state.hasStoredPhoto ? submitSignIn() : show('photo')
|
||||
renderHosts(
|
||||
$('#freq-host-list'),
|
||||
'',
|
||||
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
||||
$('#freq-host-select')
|
||||
);
|
||||
show('freq-host');
|
||||
} catch (err) {
|
||||
@@ -535,8 +572,11 @@ $('#do-freq-auth').addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
$('#in-freq-host-search').addEventListener('input', (e) =>
|
||||
renderHosts($('#freq-host-list'), e.target.value, () =>
|
||||
state.hasStoredPhoto ? submitSignIn() : show('photo')
|
||||
renderHosts(
|
||||
$('#freq-host-list'),
|
||||
e.target.value,
|
||||
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
||||
$('#freq-host-select')
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user