Skip to content

Commit

Permalink
feat: introduce filtering by country
Browse files Browse the repository at this point in the history
  • Loading branch information
isqua committed Jun 5, 2024
1 parent 8ba2444 commit 4c72570
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 1 deletion.
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ <h1 class="hero-title">Showcase your journey</h1>
</div>
<form action="#" class="sidebar-content form">
<div id="countries" role="tabpanel" tabindex="0" aria-labelledby="countries-tab" class="sidebar-tabpanel">
<input
type="search"
id="countries-search"
name="countries-search"
class="countries-search"
placeholder="Type to filter"
/>
<ul class="countries-list">
</ul>
</div>
Expand Down
11 changes: 10 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { countries, getFlagSrc } from "./flags";
import { CanvasManager, ImageGenerator } from "./images";
import { Landing } from "./landing";
import { ColorSelector, CountrySelector, Settings } from "./settings";
import { ColorSelector, CountrySearch, CountrySelector, Settings } from "./settings";
import { getFullfiled, loadImage, querySelectorSafe } from "./shared/utils";
import { Sidebar } from "./sidebar";
import { StateManager } from "./state";
Expand All @@ -22,6 +22,11 @@ const sidebar = new Sidebar(
querySelectorSafe<HTMLButtonElement>(".sidebar-control"),
);

const countrySearch = new CountrySearch(
querySelectorSafe<HTMLInputElement>("#countries-search"),
countries,
);

const countrySelector = new CountrySelector(
querySelectorSafe<HTMLUListElement>(".countries-list"),
querySelectorSafe<HTMLTemplateElement>("#country"),
Expand Down Expand Up @@ -59,6 +64,10 @@ function main() {
return draw(data);
});

countrySearch.onChange(visibleCountries => {
countrySelector.setVisibleCountries(visibleCountries);
});

sidebar.initialize();
setTimeout(() => sidebar.toggle(true), 500);
}
Expand Down
15 changes: 15 additions & 0 deletions src/settings/CountrySearch.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.countries-search {
padding: 5px 7px;
border-radius: 6px;
line-height: 20px;
font-size: inherit;
width: 100%;
border: 1px solid #e2e8f0;
margin: 0 0 4px;
}

.countries-search:focus {
outline: none;
border-color: #0ea5e9;
box-shadow: inset 0 0 0 2px #0ea5e9;
}
36 changes: 36 additions & 0 deletions src/settings/CountrySearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type Country } from "flag-icons";
import { isTextMatch } from "../shared/filter";
import { debounce } from "../shared/utils";

import "./CountrySearch.css";

const INPUT_DELAY_IN_MS = 300;

export class CountrySearch {
constructor(
private input: HTMLInputElement,
private countries: Country[],
) { }

#filterCountries(text: string): Set<string> {
const filteredCountries = new Set<string>();

for (const country of this.countries) {
if (isTextMatch(country.name, text)) {
filteredCountries.add(country.code);
}
}

return filteredCountries;
}

onChange(callback: (codes: Set<string>) => void) {
const debouncedCallback = debounce(() => {
const filteredCountries = this.#filterCountries(this.input.value.trim());

callback(filteredCountries);
}, INPUT_DELAY_IN_MS);

this.input.addEventListener("input", debouncedCallback);
}
}
4 changes: 4 additions & 0 deletions src/settings/CountrySelector.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
margin: 0 0 4px;
}

.country-item.country-hidden {
display: none;
}

.country-label {
display: flex;
padding: 6px 8px;
Expand Down
13 changes: 13 additions & 0 deletions src/settings/CountrySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Country } from "flag-icons";

import "./CountrySelector.css";

const HIDDEN_CLS = "country-hidden";

export class CountrySelector {
constructor(
private container: HTMLElement,
Expand All @@ -15,6 +17,7 @@ export class CountrySelector {
countries.forEach(country => {
const clone = this.template.content.cloneNode(true) as HTMLLIElement;

clone.children[0].setAttribute("data-code", country.code);
querySelectorSafe<HTMLInputElement>("input", clone).value = country.code;
querySelectorSafe<HTMLInputElement>("input", clone).id = country.code;
querySelectorSafe<HTMLLabelElement>("label", clone).setAttribute("for", country.code);
Expand All @@ -24,4 +27,14 @@ export class CountrySelector {
this.container.appendChild(clone);
});
}

setVisibleCountries(codes: Set<string>) {
const countries = this.container.querySelectorAll<HTMLElement>("[data-code]");

for (let country of countries) {
const code = country.dataset.code!;

country.classList.toggle(HIDDEN_CLS, !codes.has(code));
}
}
}
5 changes: 5 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export class Settings {
this.form.addEventListener("change", () => {
callback(this.getData());
});

this.form.addEventListener("submit", (event) => {
event.preventDefault();
callback(this.getData());
});
}

getData(): SettingsData {
Expand Down
1 change: 1 addition & 0 deletions src/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { ColorSelector } from "./ColorSelector";
export { CountrySearch } from "./CountrySearch";
export { CountrySelector } from "./CountrySelector";
export { Settings, type SettingsData } from "./Settings";
29 changes: 29 additions & 0 deletions src/shared/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Rough fuzzy search that finds "omit" in "jOhn sMITh".
*
* @param value value to check, e.g. a page title "jOhn sMITh"
* @param normalizedSearch string to search in the value, e.g. "omit"
* @returns boolean if the value matches the search string
*/
export function isTextMatch(value: string, search: string) {
const normalizedValue = value.toLowerCase();
const normalizedSearch = search.toLowerCase();

let valuePointer = 0;
let searchPointer = 0;

while (valuePointer < normalizedValue.length && searchPointer < normalizedSearch.length) {
if (normalizedSearch[searchPointer] === ' ') {
searchPointer++;
continue;
}

if (normalizedValue[valuePointer] === normalizedSearch[searchPointer]) {
searchPointer++;
}

valuePointer++;
}

return searchPointer === normalizedSearch.length;
}
24 changes: 24 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,27 @@ export async function getFullfiled<T>(promises: Promise<T>[]): Promise<T[]> {
return acc;
}, []);
}

interface debounceCallback<TArgs extends unknown[]> {
(...args: TArgs): void;
}

export function debounce<TArgs extends unknown[]>(
callback: debounceCallback<TArgs>,
delayInMs: number,
): debounceCallback<TArgs> {
let timer: number;

const debouncedFunc: debounceCallback<TArgs> = (...args) => {
if (timer) {
clearTimeout(timer);
}

timer = window.setTimeout(
() => callback(...args),
delayInMs,
);
};

return debouncedFunc;
}

0 comments on commit 4c72570

Please sign in to comment.