import { html, LitElement, css, nothing, svg } from "lit"; import { customElement, property, state, query } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; /** * Information about a Nonprofit to show in search results. */ export interface Nonprofit { id: string; name: string; description: string; ein: string; category: string; icon_url: string; crypto: { solana_address: string; ethereum_address: string; }; } /** * A search bar for nonprofits. */ @customElement("change-search-bar") export class ChangeSearchBar extends LitElement { /** Path to an icon to display in the "no results" box. */ @property() noResultsIcon!: string; /** Results for the current search query + filters. */ @state() private searchResults: Nonprofit[] = []; @state() loading = false; /** The search input box. */ @query("input[type=text]") searchInput!: HTMLInputElement; /** Used for cancelling search API requests */ searchTimeout: number | undefined; constructor() { super(); } render() { return html`
${searchIcon()}
${ this.noSearchTerm() ? nothing : html`
${this.renderSearchResults()}
` } `; } renderSearchResults() { return html`
${this.searchResults.length !== 0 ? html` ${this.searchResults.slice(0, 10).map( (nonprofit) => html` ` )} ` : nothing} ${this.searchResults.length === 0 && !this.loading ? html`


No results.
Are we missing a nonprofit? Email hello@getchange.io and we'll help!

` : nothing} ${this.loading ? html`
` : nothing}
`; } clear() { this.searchInput.value = ""; this.requestUpdate(); } private handleSearchResultClick(nonprofit: Nonprofit) { this.dispatchEvent( new CustomEvent("select-nonprofit", { detail: nonprofit, bubbles: true, composed: true, }) ); } /** * Search nonprofits given the current state of the search input box and filters. */ private performSearch() { const name = this.searchInput.value; if (name === "") { this.loading = false; this.searchResults = []; return; } this.loading = true; if (this.searchTimeout) { clearTimeout(this.searchTimeout); } this.searchTimeout = window.setTimeout(() => { const queryParams = new URLSearchParams(); queryParams.append("search_term", name); fetch( `https://api.getchange.io/api/v1/nonprofit_basics?${queryParams.toString()}`, { headers: { "Content-Type": "application/json", }, } ) .then((response) => response.json()) .then((response) => response.nonprofits as Nonprofit[]) .then((nonprofits) => { this.searchResults = nonprofits; }) .catch(() => {}) .finally(() => { this.loading = false; }); }, 200); } private noSearchTerm() { if (!this.searchInput) { return true; } const searchInputEmpty = this.searchInput.value === null || this.searchInput.value === ""; return searchInputEmpty; } static styles = [ css` :host { display: block; position: relative; --spinner-primary-color: white; --spinner-secondary-color: rgba(255, 255, 255, 0.2); } input:focus ~ #icon { opacity: 0.8; } #search-area { display: flex; align-items: center; position: relative; z-index: 2; } #search-area svg { position: absolute; left: 0.7em; width: 1.5em; } input { width: 100%; border-radius: 1em; background: var(--input-background-color, white); padding: 0.8em 1.1em 0.8em 3em; margin: 0; font-family: inherit; border: 1px solid var(--input-border-color, transparent); color: var(--input-color, black); box-shadow: 0px 0px 21px rgba(0, 0, 0, 0.04); } input.search-term { border-bottom-left-radius: 0; border-bottom-right-radius: 0; box-shadow: none; border-bottom: 1px solid var(--input-border-color, #ddd); } input::placeholder { color: var(--input-placeholder-color, #999); } #no-results { text-align: center; font-size: 1.3em; } #search-results { padding: 1.2em; position: relative; top: 0.1em; box-sizing: border-box; min-height: 9em; } .search-result { display: flex; align-items: center; padding: 9px 16px; margin: 0 -16px; width: calc(100% + 32px); border-radius: 6px; border: none; z-index: 1; color: inherit; text-decoration: none; background-color: transparent; transition: background-color 0.1s ease-out; white-space: nowrap; text-overflow: ellipsis; } .search-result img { height: 1.5em; border-radius: 50%; margin-right: 0.7em; } .search-result:hover { background-color: var(--search-result-background-hover, #f6f7fa); } .search-result .name { } @media (max-width: 800px) { .search-result .name { flex: 1; } } .search-result .category { color: var(--color, black); opacity: 0.5; margin-left: 12px; } #loading-overlay { position: absolute; display: flex; justify-content: center; background-color: rgba(0, 0, 0, 0.1); align-items: center; z-index: 10; --inset: 10px; left: var(--inset); right: var(--inset); top: calc(0.5em + var(--inset)); bottom: var(--inset); border-radius: 1em; } #backdrop { position: absolute; top: 0; left: 0; right: 0; background-color: var(--input-background-color, white); padding-top: 2.2em; border-radius: 1em; box-shadow: 0px 0px 21px rgba(0, 0, 0, 0.04); z-index: 1; } .spinner { width: 48px; height: 48px; border: 5px solid var(--spinner-primary-color, rgb(134, 55, 225)); border-bottom-color: var( --spinner-secondary-color, rgba(134, 55, 225, 0.2) ); border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } button, input { font-size: inherit; } button { cursor: pointer; } `, ]; } function searchIcon() { return svg` `; }