mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-13 18:40:50 +08:00
135 lines
3.1 KiB
Svelte
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>
|