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`
`;
}