Last active
March 21, 2022 18:59
-
-
Save straversi/33d772d4aad2e2fb63741e96c963c71c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// First, install one dependency: | |
// npm install lit | |
// | |
// Then, include this file on your page: | |
// import './path/to/change-search-bar' | |
// | |
// Then, you can use the <change-search-bar> component like any | |
// HTML element: | |
// <change-search-bar></change-search-bar> | |
// | |
// You can style the component normally with CSS: | |
// change-search-bar { | |
// width: 100%; | |
// } | |
// | |
// This component emits an event whenever a search result is | |
// selected. You can handle it like this: | |
// document.addEventListener('select-nonprofit', e => { | |
// const selectedNonprofit = e.detail; | |
// // Do something with selectedNonprofit | |
// }); | |
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 { | |
/** 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` | |
<!-- Search box --> | |
<div id="search-area"> | |
${searchIcon()} | |
<input | |
type="text" | |
placeholder="Nonprofit name or EIN" | |
name="nonprofit-search" | |
@input=${this.performSearch} | |
class=${classMap({ "search-term": !this.noSearchTerm() })} | |
></input> | |
</div> | |
<!-- Search results --> | |
${ | |
this.noSearchTerm() | |
? nothing | |
: html` <div id="backdrop">${this.renderSearchResults()}</div> ` | |
} | |
`; | |
} | |
renderSearchResults() { | |
return html` | |
<div id="search-results" tabindex="1"> | |
${this.searchResults.length !== 0 | |
? html` | |
${this.searchResults.slice(0, 10).map( | |
(nonprofit) => html` | |
<button | |
class="search-result" | |
@click=${() => this.handleSearchResultClick(nonprofit)} | |
> | |
<img src=${nonprofit.icon_url} /> | |
<div class="name">${nonprofit.name}</div> | |
<div class="category">${nonprofit.category}</div> | |
</button> | |
` | |
)} | |
` | |
: nothing} | |
${this.searchResults.length === 0 && !this.loading | |
? html` | |
<p id="no-results"> | |
Are we missing a nonprofit? Email hello@getchange.io and | |
we'll help! | |
</p> | |
` | |
: nothing} | |
${this.loading | |
? html` | |
<div id="loading-overlay"><span class="spinner"></span></div> | |
` | |
: nothing} | |
</div> | |
`; | |
} | |
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` | |
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M24.163 41.6551C33.8263 41.6551 41.66 33.8221 41.66 24.1596C41.66 14.4971 33.8263 6.66406 24.163 6.66406C14.4997 6.66406 6.66602 14.4971 6.66602 24.1596C6.66602 33.8221 14.4997 41.6551 24.163 41.6551Z" stroke="#8A959E" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> | |
<path d="M36.5332 36.5322L46.6565 46.6549" stroke="#8A959E" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> | |
</svg> | |
`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment