Rejected Whatcom Staff Page Sort by Team Sort Alphabetical Feature

For demonstration purposes since it is going completely away, there is this video.
<div class="hidden-team-links" style="display:none;">
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Administration%20%26%20Finance">
Administration & Finance
</a><br>
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Climate%20Resilience%20and%20Preparedness">
Climate Resilience and Preparedness
</a><br>
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Conservation%20Planning">
Conservation Planning
</a><br>
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Engineering">
Engineering
</a><br>
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Habitat%20Restoration">
Habitat Restoration
</a><br>
<a href="https://whatcomcd.specialdistrict.org/all-staff-teams?mode=team&team=Outreach%20Education">
Outreach Education
</a>
</div>
<!-- Staff Directory: Expand All | By Team | A–Z -->
<style>
/* ---------- Controls bar (mode buttons) ---------- */
.items .staff-controls {
display: flex;
flex-wrap: wrap;
gap: .5rem;
padding: 0 1rem;
margin: 1rem 0 .25rem;
}
.items .staff-controls button {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
padding: .5rem .9rem;
cursor: pointer;
}
.items .staff-controls button[aria-pressed="true"] {
background: #fff;
border-color: #bbb;
font-weight: 600;
}
/* ---------- Tab bar ---------- */
.items .tablist {
display: flex;
flex-wrap: wrap;
gap: .5rem;
margin: .5rem 0 0;
padding: 0 1rem;
}
.items .tablist [role="tab"] {
background: #f5f5f5;
border: 1px solid transparent;
border-bottom: none;
border-radius: 4px 4px 0 0;
padding: .45rem .8rem;
cursor: pointer;
white-space: nowrap;
}
.items .tablist [role="tab"]:hover { background: #e9e9e9; }
.items .tablist [role="tab"][aria-selected="true"] {
background: #fff;
border-color: #ccc;
font-weight: 600;
}
/* ---------- Panels + cards layout ---------- */
.items .tabpanels { width: 100%; }
.items .tabpanels [role="tabpanel"] {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
width: 100%;
box-sizing: border-box;
}
.items .tabpanels [role="tabpanel"][hidden] { display: none !important; }
.items .tabpanels .poc-instance {
flex: 0 0 calc((100% - 2rem)/3);
max-width: calc((100% - 2rem)/3);
box-sizing: border-box;
}
@media (max-width: 800px) {
.items .tabpanels .poc-instance {
flex: 0 0 calc((100% - 1rem)/2);
max-width: calc((100% - 1rem)/2);
}
}
@media (max-width: 480px) {
.items .tabpanels .poc-instance {
flex: 0 0 100%;
max-width: 100%;
}
}
/* ---------- Team headers for Expand All ---------- */
.items .staff-heading {
display: block;
width: 100%;
margin: 32px 0 12px 6px;
font-size: 28px;
line-height: 1.35;
font-weight: 700;
clear: both;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.items');
if (!container) return;
// ---------- Helpers ----------
const slugify = s => (s || '')
.toString()
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '') // strip accents
.toLowerCase()
.replace(/&/g, ' and ')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const getParams = () => {
const search = new URLSearchParams(location.search);
// Accept hash as either "#team=Foo", "#mode=alpha&letter=A", or "#foo-bar" (team slug)
const rawHash = (location.hash || '').replace(/^#/, '');
const hashAsParams = new URLSearchParams(
rawHash.includes('=') ? rawHash : ''
);
const hashAsTeamSlug = rawHash && !rawHash.includes('=') ? rawHash : '';
// Priority: query params > hash params > bare hash team slug
return {
mode: search.get('mode') || hashAsParams.get('mode') || null,
team: search.get('team') || hashAsParams.get('team') || (hashAsTeamSlug || null),
letter: search.get('letter') || hashAsParams.get('letter') || null
};
};
const setURL = ({ mode, team, letter }, replace = true) => {
const url = new URL(location.href);
url.searchParams.delete('mode');
url.searchParams.delete('team');
url.searchParams.delete('letter');
if (mode) url.searchParams.set('mode', mode);
if (team) url.searchParams.set('team', team);
if (letter) url.searchParams.set('letter', letter);
// For expand+team jump, keep a clean hash to the team header
if (mode === 'expand' && team) {
url.hash = slugify(team);
} else {
url.hash = '';
}
(replace ? history.replaceState : history.pushState).call(history, null, '', url.toString());
};
// ---------- Grab & parse cards ----------
const originals = Array.from(container.querySelectorAll('article.poc-instance'));
if (!originals.length) return;
function parseTitle(card) {
const titleEl = card.querySelector('h3 span, h3');
const raw = titleEl ? titleEl.textContent.trim() : '';
const m = raw.match(/^(.*?)\s*\(([^)]+)\)\s*$/);
const name = m ? m[1].trim() : raw;
const team = m ? m[2].trim() : 'Other';
return { name, team, raw, titleElSelector: titleEl ? (titleEl.matches('h3 span') ? 'h3 span' : 'h3') : 'h3' };
}
const data = originals.map((node, i) => {
const parsed = parseTitle(node);
return { i, node, ...parsed, teamSlug: slugify(parsed.team), firstLetter: (parsed.name.charAt(0) || '#').toUpperCase() };
});
const teams = Array.from(new Set(data.map(d => d.team))).sort((a,b)=>a.localeCompare(b));
const letters = Array.from(new Set(data.map(d => d.firstLetter))).sort((a,b)=>a.localeCompare(b));
// ---------- Build UI skeleton ----------
container.innerHTML = '';
const controls = document.createElement('div');
controls.className = 'staff-controls';
container.appendChild(controls);
const modes = [
{ key: 'expand', label: 'Expand all' },
{ key: 'team', label: 'By team' },
{ key: 'alpha', label: 'A–Z' }
];
let activeMode = 'expand';
const tablist = document.createElement('div');
tablist.className = 'tablist';
tablist.setAttribute('role', 'tablist');
container.appendChild(tablist);
const panels = document.createElement('div');
panels.className = 'tabpanels';
container.appendChild(panels);
function cloneCard(d, { showTeamInTitle }) {
const clone = d.node.cloneNode(true);
const t = clone.querySelector(d.titleElSelector) || clone.querySelector('h3');
if (t) t.textContent = showTeamInTitle ? d.raw : d.name;
return clone;
}
function resetTabs() {
tablist.innerHTML = '';
panels.innerHTML = '';
}
function wireKeyboard(keys) {
keys.forEach((k, idx) => {
const tab = document.getElementById(`tab-${k}`);
tab.addEventListener('keydown', e => {
let next;
if (e.key === 'ArrowRight') next = (idx + 1) % keys.length;
if (e.key === 'ArrowLeft') next = (idx - 1 + keys.length) % keys.length;
if (e.key === 'Home') next = 0;
if (e.key === 'End') next = keys.length - 1;
if (next !== undefined) {
e.preventDefault();
document.getElementById(`tab-${keys[next]}`).focus();
activate(keys[next], keys);
}
});
});
}
function activate(activeKey, keys) {
keys.forEach(k => {
const tab = document.getElementById(`tab-${k}`);
const panel = document.getElementById(`panel-${k}`);
const isActive = k === activeKey;
tab?.setAttribute('aria-selected', isActive);
if (isActive) tab?.removeAttribute('tabindex'); else tab?.setAttribute('tabindex', '-1');
if (panel) panel.hidden = !isActive;
});
}
// ---------- Renderers ----------
function renderExpandAll(scrollToTeamSlug = null) {
resetTabs();
const key = 'All';
const tab = document.createElement('button');
tab.setAttribute('role', 'tab');
tab.id = `tab-${key}`;
tab.textContent = 'All';
tab.setAttribute('aria-selected', 'true');
tablist.appendChild(tab);
const panel = document.createElement('div');
panel.setAttribute('role', 'tabpanel');
panel.id = `panel-${key}`;
panels.appendChild(panel);
const grouped = [...data].sort((a,b) => {
const t = a.team.localeCompare(b.team);
return t !== 0 ? t : a.name.localeCompare(b.name);
});
let lastTeam = null;
let jumpTarget = null;
grouped.forEach(d => {
if (d.team !== lastTeam) {
const h2 = document.createElement('h2');
h2.className = 'staff-heading';
h2.textContent = d.team;
h2.id = d.teamSlug; // <— anchor target for Expand All
panel.appendChild(h2);
lastTeam = d.team;
if (scrollToTeamSlug && d.teamSlug === scrollToTeamSlug) {
jumpTarget = h2;
}
}
panel.appendChild(cloneCard(d, { showTeamInTitle: false }));
});
if (jumpTarget) {
// Small delay to ensure layout is ready
requestAnimationFrame(() => jumpTarget.scrollIntoView({ behavior: 'smooth', block: 'start' }));
}
}
function renderByTeam(activeTeamKey = 'All Teams') {
resetTabs();
const keys = ['All Teams', ...teams.map(t => `team-${slugify(t)}`)];
const labelFromKey = k => k === 'All Teams'
? 'All Teams'
: teams.find(t => `team-${slugify(t)}` === k);
// Build tabs/panels
keys.forEach((k, idx) => {
const human = labelFromKey(k);
const tab = document.createElement('button');
tab.setAttribute('role', 'tab');
tab.id = `tab-${k}`;
const count = k === 'All Teams'
? data.length
: data.filter(d => `team-${d.teamSlug}` === k).length;
tab.textContent = `${human} (${count})`;
tab.setAttribute('aria-selected', idx === 0 ? 'true' : 'false');
if (idx !== 0) tab.setAttribute('tabindex', '-1');
tab.addEventListener('click', () => {
activate(k, keys);
// reflect in URL for shareable link
const teamName = k === 'All Teams' ? null : human;
setURL({ mode: 'team', team: teamName }, false);
});
tablist.appendChild(tab);
const panel = document.createElement('div');
panel.setAttribute('role', 'tabpanel');
panel.id = `panel-${k}`;
if (idx !== 0) panel.hidden = true;
panels.appendChild(panel);
const subset = k === 'All Teams'
? data
: data.filter(d => `team-${d.teamSlug}` === k);
subset
.slice()
.sort((a,b)=>a.name.localeCompare(b.name))
.forEach(d => panel.appendChild(cloneCard(d, { showTeamInTitle: false })));
});
wireKeyboard(keys);
// Activate requested team
if (keys.includes(activeTeamKey)) {
activate(activeTeamKey, keys);
// Scroll the active panel into view
const panelEl = document.getElementById(`panel-${activeTeamKey}`);
if (panelEl) {
const headerOffset = 120; // adjust to match your sticky header height
const elementPosition = panelEl.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
} else {
activate('All Teams', keys);
}
}
function renderAlpha(activeLetter = 'All') {
resetTabs();
const keys = ['All', ...letters];
keys.forEach((letter, idx) => {
const tab = document.createElement('button');
tab.setAttribute('role', 'tab');
tab.id = `tab-${letter}`;
const count = letter === 'All'
? data.length
: data.filter(d => d.firstLetter === letter).length;
tab.textContent = `${letter} (${count})`;
tab.setAttribute('aria-selected', idx === 0 ? 'true' : 'false');
if (idx !== 0) tab.setAttribute('tabindex', '-1');
tab.addEventListener('click', () => {
activate(letter, keys);
setURL({ mode: 'alpha', letter }, false);
});
tablist.appendChild(tab);
const panel = document.createElement('div');
panel.setAttribute('role', 'tabpanel');
panel.id = `panel-${letter}`;
if (idx !== 0) panel.hidden = true;
panels.appendChild(panel);
const subset = letter === 'All'
? data
: data.filter(d => d.firstLetter === letter);
subset
.slice()
.sort((a,b)=>a.name.localeCompare(b.name))
.forEach(d => panel.appendChild(cloneCard(d, { showTeamInTitle: true })));
});
wireKeyboard(keys);
if (keys.includes(activeLetter)) activate(activeLetter, keys); else activate('All', keys);
}
// ---------- Controls ----------
let activeModeBtn = null;
modes.forEach(m => {
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = m.label;
btn.setAttribute('aria-pressed', m.key === 'expand' ? 'true' : 'false');
btn.addEventListener('click', () => {
activeModeBtn?.setAttribute('aria-pressed', 'false');
btn.setAttribute('aria-pressed', 'true');
activeModeBtn = btn;
if (m.key === 'expand') { renderExpandAll(); setURL({ mode: 'expand' }, false); }
if (m.key === 'team') { renderByTeam('All Teams'); setURL({ mode: 'team' }, false); }
if (m.key === 'alpha') { renderAlpha('All'); setURL({ mode: 'alpha' }, false); }
});
controls.appendChild(btn);
if (m.key === 'expand') activeModeBtn = btn; // default pressed
});
// ---------- Initialization with deep links ----------
function bootFromURL() {
const { mode, team, letter } = getParams();
// Default view
if (!mode) {
renderExpandAll();
setURL({ mode: 'expand' }, true);
return;
}
if (mode === 'expand') {
renderExpandAll(team ? slugify(team) : null);
} else if (mode === 'team') {
const targetKey = team ? `team-${slugify(team)}` : 'All Teams';
renderByTeam(targetKey);
} else if (mode === 'alpha') {
const L = (letter || 'All').toUpperCase();
renderAlpha(L);
} else {
renderExpandAll();
}
// Reflect mode button state
controls.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', 'false'));
const btn = Array.from(controls.querySelectorAll('button')).find(b => b.textContent.startsWith(
mode === 'expand' ? 'Expand' : mode === 'team' ? 'By team' : 'A–Z'
));
if (btn) { btn.setAttribute('aria-pressed', 'true'); activeModeBtn = btn; }
// Normalize URL (encode exact params + hash)
setURL({ mode, team, letter }, true);
}
// Rehydrate on back/forward and hash changes
window.addEventListener('popstate', bootFromURL);
window.addEventListener('hashchange', bootFromURL);
bootFromURL();
});
</script>
We could have had it all. - Adele

<!-- Library Services: Category Filter -->
<div class="services-filter" aria-label="Filter library services by category">
<div class="services-filter__tabs" role="tablist" aria-label="Service categories">
<button class="services-filter__btn" role="tab" aria-selected="true" data-cat="All Services">All Services</button>
<button class="services-filter__btn" role="tab" data-cat="Toddlers (0-5)">Toddlers (0-5)</button>
<button class="services-filter__btn" role="tab" data-cat="Youth">Youth</button>
<button class="services-filter__btn" role="tab" data-cat="Circulation">Circulation</button>
<button class="services-filter__btn" role="tab" data-cat="Library Account">Library Account</button>
</div>
</div>
<style>
/* ---------- CTA-like buttons (reuse your gradient + white text) ---------- */
.services-filter { margin: .5rem 0 1rem; padding: 0 1rem; }
.services-filter__tabs {
display: flex; flex-wrap: wrap; gap: .5rem;
}
.services-filter__btn {
display: inline-block;
min-height: 3rem;
padding: .6rem 1rem;
border-radius: .75rem;
border: 2px solid #1E5956;
background: linear-gradient(90deg, #1E5956 0%, #9F5035 100%);
color: #fff !important;
font-weight: 700;
letter-spacing: .2px;
text-align: center;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,.12);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.services-filter__btn:hover { box-shadow: 0 4px 12px rgba(0,0,0,.16); transform: translateY(-1px); color:#fff !important; }
.services-filter__btn:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
.services-filter__btn[aria-selected="true"] { filter: brightness(1.05); }
/* Card grid tweaks (optional, matches your earlier layout) */
.items .poc-instance { transition: opacity 120ms ease; }
.items .poc-instance[hidden] { display: none !important; }
@media (max-width:480px){
.services-filter__btn{ min-height: 2.75rem; padding:.55rem .85rem; font-size:.95rem; }
}
@media (prefers-reduced-motion: reduce){
.services-filter__btn{ transition: none; }
.services-filter__btn:hover{ transform:none; }
}
#app input, #app button, #app select, #app textarea {
color: #fff !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.items');
if (!container) return;
// 1) HOW WE DETECT A CARD'S CATEGORY
// Priority:
// A) data-category on the card (preferred, if you can add it via editor)
// B) Title suffix in parentheses e.g., "Storytime (Toddlers (0-5))"
// C) Fallback keyword rules below (customize as needed)
const keywordRules = [
{ cat: 'Toddlers (0-5)', match: /(toddler|0-5|preschool|story ?time|early literacy)/i },
{ cat: 'Youth', match: /(youth|teen|tween|homework|summer reading|after ?school)/i },
{ cat: 'Circulation', match: /(checkout|check ?out|holds?|renewals?|fines?|loan|return|borrowing)/i },
{ cat: 'Library Account', match: /(account|card|e-?card|pin|password|login|sign ?in|my account)/i },
];
// Helper to read a card's visible title
const getTitle = card => {
const t = card.querySelector('h3 span, h3, .title, [data-title]');
return (t?.textContent || t?.innerText || '').trim();
};
// Determine category for a given card
function getCategory(card) {
// A) data-category (best)
const dataCat = card.getAttribute('data-category') || card.dataset?.category;
if (dataCat) return dataCat.trim();
// B) Title suffix "(Category)"
const title = getTitle(card);
const m = title.match(/\(([^)]+)\)\s*$/);
if (m && m[1]) return m[1].trim();
// C) Keyword fallback
for (const rule of keywordRules) {
if (rule.match.test(title)) return rule.cat;
// also scan brief text if present
const teaser = card.querySelector('.teaser, .summary, p')?.textContent || '';
if (rule.match.test(teaser)) return rule.cat;
}
return 'All Services';
}
// 2) COLLECT CARDS + TAG WITH CATEGORIES
const cards = Array.from(container.querySelectorAll('article.poc-instance'));
const cardData = cards.map(card => {
const cat = getCategory(card);
card.dataset.categoryResolved = cat; // annotate for debugging
return { card, cat };
});
// 3) FILTER LOGIC + BUTTON WIRING
const tabs = document.querySelectorAll('.services-filter__btn');
const categories = Array.from(tabs).map(b => b.dataset.cat);
function applyFilter(cat) {
cardData.forEach(({ card, cat: c }) => {
const show = (cat === 'All Services') || (c === cat);
if (show) { card.hidden = false; card.style.opacity = '1'; }
else { card.hidden = true; card.style.opacity = '0'; }
});
}
function setActive(btn) {
tabs.forEach(b => b.setAttribute('aria-selected', 'false'));
btn.setAttribute('aria-selected', 'true');
}
tabs.forEach((btn, idx) => {
btn.addEventListener('click', () => {
setActive(btn);
applyFilter(btn.dataset.cat);
// reflect in URL for shareable deep-link
const url = new URL(location.href);
url.searchParams.set('category', btn.dataset.cat);
history.replaceState(null, '', url);
});
// Keyboard navigation (Left/Right/Home/End)
btn.addEventListener('keydown', e => {
const k = e.key;
const list = Array.from(tabs);
const i = list.indexOf(btn);
let next = null;
if (k === 'ArrowRight') next = (i + 1) % list.length;
if (k === 'ArrowLeft') next = (i - 1 + list.length) % list.length;
if (k === 'Home') next = 0;
if (k === 'End') next = list.length - 1;
if (next !== null) {
e.preventDefault();
list[next].focus();
}
});
});
// 4) INITIALIZE FROM URL (?category=...) OR DEFAULT TO "All Services"
const startCat = new URLSearchParams(location.search).get('category') || 'All Services';
const startBtn = Array.from(tabs).find(b => b.dataset.cat === startCat) || tabs[0];
setActive(startBtn);
applyFilter(startBtn.dataset.cat);
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.items');
if (!container) return;
const CATS = [
'All Services',
'Toddlers (0-5)',
'Youth',
'Circulation',
'Library Account'
];
const slug = s => (s || '')
.toString()
.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/&/g, ' and ')
.replace(/[\(\)\u2013\u2014]/g, '-') // normalize parentheses and dashes
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
const CANON = new Map(CATS.map(label => [slug(label), label]));
const keywordRules = [
{ cat: 'Toddlers (0-5)', match: /(toddler|0[\s–-]?5|preschool|story ?time|early literacy)/i },
{ cat: 'Youth', match: /(youth|teen|tween|homework|summer reading|after ?school)/i },
{ cat: 'Circulation', match: /(checkout|check ?out|holds?|renewals?|fines?|loan|return|borrowing)/i },
{ cat: 'Library Account', match: /(account|card|e-?card|pin|password|login|sign ?in|my account)/i },
];
const getTitleEl = card => card.querySelector('h3 span, h3, .title, [data-title]');
const getTitleText = card => (getTitleEl(card)?.textContent || '').trim();
const canonicalize = (label) => CANON.get(slug(label)) || label;
function resolveCategoryAndCleanTitle(card) {
let rawCat = card.getAttribute('data-category') || card.dataset?.category;
const titleEl = getTitleEl(card);
const titleText = getTitleText(card);
const m = titleText.match(/\(([^)]+)\)\s*$/);
if (!rawCat && m && m[1]) {
rawCat = m[1].trim();
if (titleEl) titleEl.textContent = titleText.replace(/\s*\([^)]+\)\s*$/, '').trim();
}
if (!rawCat) {
for (const rule of keywordRules) {
if (rule.match.test(titleText)) { rawCat = rule.cat; break; }
const teaser = card.querySelector('.teaser, .summary, p')?.textContent || '';
if (rule.match.test(teaser)) { rawCat = rule.cat; break; }
}
}
return canonicalize(rawCat || 'All Services');
}
const cards = Array.from(container.querySelectorAll('article.poc-instance'));
const cardData = cards.map(card => {
const cat = resolveCategoryAndCleanTitle(card);
card.dataset.categoryResolved = cat;
return { card, cat };
});
const tabs = document.querySelectorAll('.services-filter__btn');
function applyFilter(catLabel) {
const targetSlug = slug(catLabel);
cardData.forEach(({ card, cat }) => {
const show = targetSlug === slug('All Services') || slug(cat) === targetSlug;
card.hidden = !show;
card.style.opacity = show ? '1' : '0';
});
}
function setActive(btn) {
tabs.forEach(b => b.setAttribute('aria-selected', 'false'));
btn.setAttribute('aria-selected', 'true');
}
tabs.forEach(btn => {
btn.addEventListener('click', () => {
setActive(btn);
applyFilter(btn.dataset.cat);
const url = new URL(location.href);
url.searchParams.set('category', btn.dataset.cat);
history.replaceState(null, '', url);
});
btn.addEventListener('keydown', e => {
const k = e.key;
const list = Array.from(tabs);
const i = list.indexOf(btn);
let next = null;
if (k === 'ArrowRight') next = (i + 1) % list.length;
if (k === 'ArrowLeft') next = (i - 1 + list.length) % list.length;
if (k === 'Home') next = 0;
if (k === 'End') next = list.length - 1;
if (next !== null) { e.preventDefault(); list[next].focus(); }
});
});
const startCat = canonicalize(new URLSearchParams(location.search).get('category') || 'All Services');
const startBtn = Array.from(tabs).find(b => slug(b.dataset.cat) === slug(startCat)) || tabs[0];
setActive(startBtn);
applyFilter(startBtn.dataset.cat);
});
</script>