Files
radicle-explorer/src/App/Header/Search.svelte
T
2023-02-13 11:28:42 +01:00

135 lines
3.1 KiB
Svelte

<script lang="ts" strictEvents>
import debounce from "lodash/debounce";
import { createEventDispatcher } from "svelte";
import * as Search from "@app/lib/search";
import * as modal from "@app/lib/modal";
import * as router from "@app/lib/router";
import { unreachable } from "@app/lib/utils";
import Icon from "@app/components/Icon.svelte";
import SearchResultsModal from "@app/App/Header/SearchResultsModal.svelte";
import TextInput from "@app/components/TextInput.svelte";
const dispatch = createEventDispatcher<{
finished: never;
}>();
export let input = "";
let loading = false;
let shaking = false;
// Clears search input on user navigation.
router.historyStore.subscribe(() => (input = ""));
const collapsedWidth = 13;
const expandedWidth = 20;
function shake() {
shaking = true;
debounce(() => (shaking = false), 500)();
}
async function search() {
if (!valid) {
return;
}
loading = true;
const query = input;
const searchResult = await Search.searchProjectsAndProfiles(input);
if (searchResult.type === "nothing") {
shake();
} else if (searchResult.type === "error") {
// TODO: show some kind of notification to the user.
shake();
} else if (searchResult.type === "projects") {
input = "";
if (searchResult.projects.length === 1) {
router.push({
resource: "projects",
params: {
view: { resource: "tree" },
id: searchResult.projects[0].info.id,
peer: undefined,
seed: searchResult.projects[0].seed.host,
hash: undefined,
search: undefined,
},
});
} else {
modal.show({
component: SearchResultsModal,
props: {
results: searchResult.projects,
query,
},
});
}
dispatch("finished");
searchBarWidth = collapsedWidth;
} else {
unreachable(searchResult);
}
loading = false;
}
$: valid = input !== "";
$: searchBarWidth = collapsedWidth;
</script>
<style>
.search-bar {
display: flex;
}
.shaking {
animation: horizontal-shaking 0.35s;
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
.search {
transition: all 0.2s;
}
@media (max-width: 720px) {
.search {
display: none;
}
}
</style>
<div class="search" style:width={`${searchBarWidth}rem`}>
<div class="search-bar" class:shaking>
<TextInput
valid={input !== ""}
{loading}
disabled={loading}
bind:value={input}
on:focus={() => (searchBarWidth = expandedWidth)}
on:blur={() =>
input === "" ? (searchBarWidth = collapsedWidth) : undefined}
on:submit={search}
placeholder="Search a name…">
<div slot="left">
<Icon name="magnifying-glass" />
</div>
</TextInput>
</div>
</div>