diff --git a/apps/web/public/app.js b/apps/web/public/app.js index c910e8f..484c704 100644 --- a/apps/web/public/app.js +++ b/apps/web/public/app.js @@ -2,6 +2,53 @@ form; this file localises times and adds push notifications and passkeys where the browser has them. */ +/* ----------------------------------------------------------- list filter -- */ + +/** + * Filter a list of leagues or teams as you type. + * + * Every word must appear somewhere in the row's `data-filter` text, in any order + * and ignoring case: "city man" keeps Manchester City, "MAN" keeps it too. The + * box is server-rendered `hidden` and only shown here, so with JavaScript off + * nobody sees a control that does nothing. + */ +function listFilterWords(query) { + return String(query || '') + .toLocaleLowerCase() + .split(/\s+/) + .filter(Boolean); +} + +function listFilterMatches(text, words) { + const hay = String(text || '').toLocaleLowerCase(); + return words.every((w) => hay.includes(w)); +} + +function initListFilters(root = document) { + for (const box of root.querySelectorAll('[data-list-filter]')) { + if (box.dataset.ready) continue; + const list = document.getElementById(box.dataset.listFilter); + const input = box.querySelector('input'); + if (!list || !input) continue; + box.dataset.ready = '1'; + box.hidden = false; + const empty = box.querySelector('.list-filter-empty'); + const apply = () => { + const words = listFilterWords(input.value); + let shown = 0; + for (const li of list.children) { + const ok = listFilterMatches(li.dataset.filter ?? li.textContent, words); + li.hidden = !ok; + if (ok) shown++; + } + if (empty) empty.hidden = shown > 0; + }; + input.addEventListener('input', apply); + // A value the browser restored on back/forward must filter too. + if (input.value) apply(); + } +} + /* ------------------------------------------------------------ local time -- */ /** @@ -880,6 +927,7 @@ function initNavigation() { if (push) history.pushState({}, '', url); window.scrollTo(0, 0); localiseTimes(); + initListFilters(); initMarketTabs(); initOwnChannelActions(); initInlinePlayer(); @@ -919,6 +967,7 @@ function initNavigation() { } localiseTimes(); +initListFilters(); reportTimezone(); initMarketTabs(); initOwnChannelActions(); diff --git a/apps/web/public/styles.css b/apps/web/public/styles.css index 9d7930d..43f4983 100644 --- a/apps/web/public/styles.css +++ b/apps/web/public/styles.css @@ -2956,4 +2956,20 @@ ul.group-grid .meta { --live: oklch(0.72 0.19 25); --ok: oklch(0.78 0.16 155); } -} \ No newline at end of file +} +/* ---- league / team list filter (shown by app.js) ---- */ +.list-filter { + margin: 0.75rem 0; +} +.list-filter input[type="search"] { + max-width: 24rem; +} +.list-filter-empty { + margin-top: 0.5rem; +} +ul.sports > li[hidden], +ul.leagues > li[hidden], +ul.teams > li[hidden], +ul.chips > li[hidden] { + display: none; +} diff --git a/apps/web/src/views/components.jsx b/apps/web/src/views/components.jsx index 463704d..bee360f 100644 --- a/apps/web/src/views/components.jsx +++ b/apps/web/src/views/components.jsx @@ -609,9 +609,27 @@ export const EventList = ({ events, emptyText, showBroadcast = false, showOdds = ); +/** + * A filter box for a list of leagues or teams. + * + * Every word typed has to appear in a row, in any order and ignoring case, so + * "city man" finds Manchester City. Rendered `hidden` and switched on by app.js: + * without JavaScript the box would do nothing, and a control that does nothing + * is worse than no control. Rows match on their `data-filter` text, which is the + * name alone, so "upcoming" or "follow" never matches every row. + */ +export const ListFilter = ({ target, label }) => ( + +); + /** A team in the follow picker. */ export const TeamRow = ({ team, user, next }) => ( -
  • +
  • {team.logo_url ? ( ) : ( diff --git a/apps/web/src/views/feeds.jsx b/apps/web/src/views/feeds.jsx index ded6f27..21776a1 100644 --- a/apps/web/src/views/feeds.jsx +++ b/apps/web/src/views/feeds.jsx @@ -1,4 +1,5 @@ import { brand, Word } from '@tipoff/config'; +import { ListFilter } from './components.jsx'; import { Layout } from './Layout.jsx'; /** @@ -31,9 +32,10 @@ export const Feeds = ({ user, sports, leagues }) => (

    By {brand.words.category}

    -