Public Access
Name Picker
This commit is contained in:
1 parent
2f6c475f66
commit
b2e8db5bf4
4 files changed
+100
-27
No files matched your search
@@ -108,6 +108,15 @@ same person can be signed in at two sites at once without the system arguing abo
|
|||||||
An admin account can be limited to a single site. Those admins see only that site's visitors,
|
An admin account can be limited to a single site. Those admins see only that site's visitors,
|
||||||
staff list and log, and cannot touch the others.
|
staff list and log, and cannot touch the others.
|
||||||
|
|
||||||
|
### Choosing who to visit
|
||||||
|
|
||||||
|
The kiosk asks for a few letters of the name rather than presenting a long list. Once the filter
|
||||||
|
narrows to five or fewer people, their names appear as large buttons to tap and the full-list
|
||||||
|
dropdown gets out of the way; above that the dropdown carries everyone, because a page of eighty
|
||||||
|
buttons is worse than a scroll wheel. Pressing enter picks the person when exactly one is left.
|
||||||
|
|
||||||
|
The cut-off is `TAP_LIST_LIMIT` in `public/js/kiosk.js`.
|
||||||
|
|
||||||
## Branding the kiosk
|
## Branding the kiosk
|
||||||
|
|
||||||
Each site can carry its own banner and colours, set under **Sites → Edit**.
|
Each site can carry its own banner and colours, set under **Sites → Edit**.
|
||||||
|
|||||||
@@ -234,6 +234,21 @@ a:focus-visible {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The shortlist is the thing to tap, so make it read as the answer rather than
|
||||||
|
as another field. */
|
||||||
|
#host-list .host-option,
|
||||||
|
#freq-host-list .host-option {
|
||||||
|
border-color: var(--deep);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--deep);
|
||||||
|
min-height: 64px;
|
||||||
|
font-size: 19px;
|
||||||
|
animation: rise 160ms ease-out;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#host-list .host-option,
|
||||||
|
#freq-host-list .host-option { animation: none; }
|
||||||
|
}
|
||||||
.host-option small { display: block; color: var(--muted); font-size: 14px; }
|
.host-option small { display: block; color: var(--muted); font-size: 14px; }
|
||||||
.host-empty {
|
.host-empty {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
+16
-8
@@ -68,12 +68,16 @@
|
|||||||
<section class="screen" id="screen-guest-host" data-step="2">
|
<section class="screen" id="screen-guest-host" data-step="2">
|
||||||
<div class="rail" data-rail></div>
|
<div class="rail" data-rail></div>
|
||||||
<h2>Who are you here to see?</h2>
|
<h2>Who are you here to see?</h2>
|
||||||
|
<p class="hint">Type the first few letters of their name. Their name will appear below —
|
||||||
|
tap it.</p>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Start typing a name to narrow the list</span>
|
<span>Their name</span>
|
||||||
<input id="in-host-search" autocomplete="off" enterkeyhint="search" placeholder="e.g. Rogerson">
|
<input id="in-host-search" autocomplete="off" enterkeyhint="search"
|
||||||
|
placeholder="e.g. Smi for Smith">
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<div class="host-list" id="host-list" role="list" hidden></div>
|
||||||
<span>Then choose a name</span>
|
<label class="field" id="host-select-field">
|
||||||
|
<span id="host-select-label">Or pick from the full list</span>
|
||||||
<select id="host-select" class="picker"></select>
|
<select id="host-select" class="picker"></select>
|
||||||
</label>
|
</label>
|
||||||
<p class="host-empty" id="host-empty" hidden></p>
|
<p class="host-empty" id="host-empty" hidden></p>
|
||||||
@@ -171,12 +175,16 @@
|
|||||||
<!-- --------------------------------------------- recurring: host -->
|
<!-- --------------------------------------------- recurring: host -->
|
||||||
<section class="screen" id="screen-freq-host">
|
<section class="screen" id="screen-freq-host">
|
||||||
<h2 id="freq-greeting">Who are you here to see?</h2>
|
<h2 id="freq-greeting">Who are you here to see?</h2>
|
||||||
|
<p class="hint">Type the first few letters of their name. Their name will appear below —
|
||||||
|
tap it.</p>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Start typing a name to narrow the list</span>
|
<span>Their name</span>
|
||||||
<input id="in-freq-host-search" autocomplete="off" enterkeyhint="search">
|
<input id="in-freq-host-search" autocomplete="off" enterkeyhint="search"
|
||||||
|
placeholder="e.g. Smi for Smith">
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<div class="host-list" id="freq-host-list" role="list" hidden></div>
|
||||||
<span>Then choose a name</span>
|
<label class="field" id="freq-host-select-field">
|
||||||
|
<span id="freq-host-select-label">Or pick from the full list</span>
|
||||||
<select id="freq-host-select" class="picker"></select>
|
<select id="freq-host-select" class="picker"></select>
|
||||||
</label>
|
</label>
|
||||||
<p class="host-empty" id="freq-host-empty" hidden></p>
|
<p class="host-empty" id="freq-host-empty" hidden></p>
|
||||||
|
|||||||
+60
-19
@@ -188,12 +188,16 @@ tickClock();
|
|||||||
/* --------------------------------------------------------------- hosts */
|
/* --------------------------------------------------------------- hosts */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Who you are here to see: type to narrow, then choose from the dropdown. The
|
* Who you are here to see.
|
||||||
* dropdown is a native select, so a tablet gives it a proper full-screen picker
|
*
|
||||||
* with its own scrolling, which handles a long staff list better than a page of
|
* Narrow it down by typing, then tap. Once the list is short enough to show
|
||||||
* buttons ever did.
|
* without scrolling, the names appear as buttons — one tap beats opening a
|
||||||
|
* picker, choosing, and dismissing it. Above that count the dropdown carries the
|
||||||
|
* full list, because a page of eighty buttons is worse than a scroll wheel.
|
||||||
*/
|
*/
|
||||||
function renderHosts(selectEl, searchValue, onPick, emptyEl = null) {
|
const TAP_LIST_LIMIT = 5;
|
||||||
|
|
||||||
|
function renderHosts(selectEl, searchValue, onPick, emptyEl = null, listEl = null) {
|
||||||
const term = String(searchValue || '').trim().toLowerCase();
|
const term = String(searchValue || '').trim().toLowerCase();
|
||||||
const matches = term
|
const matches = term
|
||||||
? hosts.filter(
|
? hosts.filter(
|
||||||
@@ -201,14 +205,47 @@ function renderHosts(selectEl, searchValue, onPick, emptyEl = null) {
|
|||||||
)
|
)
|
||||||
: hosts;
|
: hosts;
|
||||||
|
|
||||||
|
const choose = (host) => {
|
||||||
|
state.hostId = host.id;
|
||||||
|
state.hostName = host.name;
|
||||||
|
onPick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAsButtons = matches.length > 0 && matches.length <= TAP_LIST_LIMIT;
|
||||||
|
|
||||||
|
// The tappable shortlist.
|
||||||
|
if (listEl) {
|
||||||
|
listEl.hidden = !showAsButtons;
|
||||||
|
listEl.innerHTML = showAsButtons
|
||||||
|
? matches
|
||||||
|
.map(
|
||||||
|
(h) =>
|
||||||
|
`<button class="host-option" data-host-id="${h.id}">
|
||||||
|
${escapeHtml(h.name)}${h.area ? `<small>${escapeHtml(h.area)}</small>` : ''}
|
||||||
|
</button>`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
: '';
|
||||||
|
listEl.querySelectorAll('[data-host-id]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const picked = hosts.find((h) => h.id === Number(btn.dataset.hostId));
|
||||||
|
if (picked) choose(picked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The full list, kept out of the way once the shortlist is showing.
|
||||||
|
const field = selectEl.closest('.field');
|
||||||
|
if (field) field.hidden = showAsButtons;
|
||||||
|
|
||||||
const label = !hosts.length
|
const label = !hosts.length
|
||||||
? 'Nobody has been added yet'
|
? 'Nobody has been added yet'
|
||||||
: matches.length === hosts.length
|
: `Or pick from the full list of ${hosts.length}`;
|
||||||
? `Choose one of ${hosts.length}`
|
const labelEl = field?.querySelector('span');
|
||||||
: `${matches.length} ${matches.length === 1 ? 'match' : 'matches'} — choose one`;
|
if (labelEl) labelEl.textContent = label;
|
||||||
|
|
||||||
selectEl.innerHTML =
|
selectEl.innerHTML =
|
||||||
`<option value="">${escapeHtml(label)}</option>` +
|
`<option value="">${escapeHtml(matches.length === hosts.length ? 'Choose a name' : `${matches.length} match${matches.length === 1 ? '' : 'es'}`)}</option>` +
|
||||||
matches
|
matches
|
||||||
.map(
|
.map(
|
||||||
(h) =>
|
(h) =>
|
||||||
@@ -219,20 +256,16 @@ function renderHosts(selectEl, searchValue, onPick, emptyEl = null) {
|
|||||||
|
|
||||||
selectEl.onchange = () => {
|
selectEl.onchange = () => {
|
||||||
const picked = hosts.find((h) => h.id === Number(selectEl.value));
|
const picked = hosts.find((h) => h.id === Number(selectEl.value));
|
||||||
if (!picked) return;
|
if (picked) choose(picked);
|
||||||
state.hostId = picked.id;
|
|
||||||
state.hostName = picked.name;
|
|
||||||
onPick();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (emptyEl) {
|
if (emptyEl) {
|
||||||
emptyEl.hidden = matches.length > 0;
|
emptyEl.hidden = matches.length > 0;
|
||||||
emptyEl.textContent = hosts.length
|
emptyEl.textContent = hosts.length
|
||||||
? 'No one matches that. Check the spelling, or ask the front desk.'
|
? 'No one matches that. Check the spelling, or ask at the front desk.'
|
||||||
: 'No one has been added for this site yet. Please see the front desk.';
|
: 'No one has been added for this site yet. Please see the front desk.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typing a name and pressing enter should just work when only one person is left.
|
|
||||||
selectEl.dataset.only = matches.length === 1 ? String(matches[0].id) : '';
|
selectEl.dataset.only = matches.length === 1 ? String(matches[0].id) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +529,7 @@ async function loadSiteContext() {
|
|||||||
change.textContent = siteConfig.site ? `Site: ${siteConfig.site.name} — change` : 'Choose site';
|
change.textContent = siteConfig.site ? `Site: ${siteConfig.site.name} — change` : 'Choose site';
|
||||||
|
|
||||||
hosts = await api('/api/hosts');
|
hosts = await api('/api/hosts');
|
||||||
renderHosts($('#host-select'), '', () => show('guest-contact'), $('#host-empty'));
|
renderHosts($('#host-select'), '', () => show('guest-contact'), $('#host-empty'), $('#host-list'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#change-site').addEventListener('click', chooseSite);
|
$('#change-site').addEventListener('click', chooseSite);
|
||||||
@@ -532,7 +565,13 @@ $('[data-next="guest-name"]').addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#in-host-search').addEventListener('input', (e) =>
|
$('#in-host-search').addEventListener('input', (e) =>
|
||||||
renderHosts($('#host-select'), e.target.value, () => show('guest-contact'), $('#host-empty'))
|
renderHosts(
|
||||||
|
$('#host-select'),
|
||||||
|
e.target.value,
|
||||||
|
() => show('guest-contact'),
|
||||||
|
$('#host-empty'),
|
||||||
|
$('#host-list')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
$('#in-host-search').addEventListener('keydown', (event) => {
|
$('#in-host-search').addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@@ -616,7 +655,8 @@ $('#do-freq-auth').addEventListener('click', async () => {
|
|||||||
$('#freq-host-select'),
|
$('#freq-host-select'),
|
||||||
'',
|
'',
|
||||||
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
||||||
$('#freq-host-empty')
|
$('#freq-host-empty'),
|
||||||
|
$('#freq-host-list')
|
||||||
);
|
);
|
||||||
show('freq-host');
|
show('freq-host');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -632,7 +672,8 @@ $('#in-freq-host-search').addEventListener('input', (e) =>
|
|||||||
$('#freq-host-select'),
|
$('#freq-host-select'),
|
||||||
e.target.value,
|
e.target.value,
|
||||||
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
|
||||||
$('#freq-host-empty')
|
$('#freq-host-empty'),
|
||||||
|
$('#freq-host-list')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$('#in-freq-host-search').addEventListener('keydown', (event) => {
|
$('#in-freq-host-search').addEventListener('keydown', (event) => {
|
||||||
|
|||||||
Reference in new issue
Block a user