Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: search #1415

Merged
merged 23 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ PUBLIC_BASE_URL=string
# URL of the Drips Multiplayer API deployment to use. Set to the defaults below to use public Sepolia deployment.
MULTIPLAYER_API_URL=string # Default for sepolia https://multiplayer-sepolia.up.railway.app
MULTIPLAYER_API_ACCESS_TOKEN=string # Default for sepolia 992b2122-9a09-4a97-b2cc-2292d3dd23aa

# Meilisearch host and API key for the search bar. Not needed during dev, but search won't work without it. Prod build will fail without it.
MEILISEARCH_HOST=string
MEILISEARCH_API_KEY=string
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ ARG PUBLIC_BASE_URL
ARG MULTIPLAYER_API_URL
ARG MULTIPLAYER_API_ACCESS_TOKEN

ARG MEILISEARCH_HOST
ARG MEILISEARCH_API_KEY

efstajas marked this conversation as resolved.
Show resolved Hide resolved
# Set environment variables to optimize the container
ENV NODE_ENV production
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ ARG PUBLIC_BASE_URL
ARG MULTIPLAYER_API_URL
ARG MULTIPLAYER_API_ACCESS_TOKEN

ARG MEILISEARCH_HOST
ARG MEILISEARCH_API_KEY

# Set environment variables to optimize the container
ENV NODE_ENV development
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"jimp": "^0.22.12",
"lodash": "^4.17.21",
"mdsvex": "^0.11.2",
"meilisearch": "^0.47.0",
"puppeteer": "^23.6.0",
"redis": "^4.6.14",
"sanitize-html": "^2.13.0",
Expand Down
29 changes: 16 additions & 13 deletions src/lib/components/drip-list-badge/drip-list-badge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,22 @@
{#if showAvatar}
<DripListAvatar size={avatarSize} {disabled} {outline} />
{/if}
{#if showName}
<div class="name typo-text text-foreground flex-1 min-w-0 truncate">
<span
>{#if username}<span class="text-foreground-level-5">{username}/</span
>{/if}{#if !dripList}<span class="animate-pulse">...</span
>{:else}{dripList.name}{/if}</span
>
</div>
{#if !dripList?.isVisible}
<WarningIcon
style="height: 1.25rem; width: 1.25rem; fill: var(--color-foreground-level-4); display:inline"
/>
{/if}
<div class="name typo-text text-foreground flex-1 min-w-0 truncate">
<span>
{#if username}
<span class="text-foreground-level-5">{username}/</span>
{/if}
{#if !dripList}
<span class="animate-pulse">...</span>
{:else if showName}
{dripList.name}
{/if}
</span>
</div>
{#if !dripList?.isVisible}
<WarningIcon
style="height: 1.25rem; width: 1.25rem; fill: var(--color-foreground-level-4); display:inline"
/>
{/if}
</svelte:element>

Expand Down
21 changes: 13 additions & 8 deletions src/lib/components/header/header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import SettingsIcon from '$lib/components/icons/Settings.svelte';
import SearchIcon from '$lib/components/icons/MagnifyingGlass.svelte';
import { fade, fly } from 'svelte/transition';
import { quadInOut, sineInOut } from 'svelte/easing';
import { quadInOut } from 'svelte/easing';
import Spinner from '../spinner/spinner.svelte';
import CollectButton from '../collect-button/collect-button.svelte';
import breakpointsStore from '$lib/stores/breakpoints/breakpoints.store';
Expand Down Expand Up @@ -84,11 +84,9 @@
<!-- ensure nav items are right-aligned on mobile still even though nothing's on the left -->
<div />
{/if}
{#if searchMode}
<div class="search-bar" transition:fly={{ duration: 300, x: 64, easing: sineInOut }}>
<SearchBar on:dismiss={() => (searchMode = false)} />
</div>
{/if}
<div class="search-bar">
<SearchBar bind:searchOpen={searchMode} />
</div>
<div class="right" class:collect-button-peeking={collectButtonPeeking}>
<div class="header-buttons">
{#if !searchMode}
Expand Down Expand Up @@ -151,7 +149,13 @@
</header>

{#if searchMode}
<div class="search-background" transition:fade={{ duration: 300 }} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="search-background"
transition:fade={{ duration: 300 }}
on:click={() => (searchMode = false)}
/>
{/if}

<style>
Expand Down Expand Up @@ -237,8 +241,9 @@
left: 0;
right: 0;
bottom: 0;
height: 100vh;
background-color: var(--color-background);
opacity: 0.75;
opacity: 0.9;
z-index: 50;
}

Expand Down
207 changes: 114 additions & 93 deletions src/lib/components/search-bar/components/result.svelte
Original file line number Diff line number Diff line change
@@ -1,116 +1,137 @@
<script lang="ts">
import TokensIcon from '$lib/components/icons/Coin.svelte';
import UserIcon from '$lib/components/icons/User.svelte';
import sanitize from 'sanitize-html';

import AccountMenuItem from '$lib/components/account-menu/components/account-menu-item.svelte';
import type { DripListResult, ProjectResult, Result as ResultType } from '../types';
import ProjectAvatar from '$lib/components/project-avatar/project-avatar.svelte';
import network from '$lib/stores/wallet/network';
import IdentityBadge from '$lib/components/identity-badge/identity-badge.svelte';
import Token from '$lib/components/token/token.svelte';
import { type Item, SearchItemType } from '../search';
import wallet from '$lib/stores/wallet/wallet.store';
import sanitize from 'sanitize-html';
import DripListBadge from '$lib/components/drip-list-badge/drip-list-badge.svelte';
import unreachable from '$lib/utils/unreachable';
import Folder from '$lib/components/icons/Folder.svelte';

export let item: Item;
export let highlighted: string;
export let item: ResultType;

export let element: HTMLElement;

function splitGitHubUrl(url: string) {
const [owner, repo] = url.split('/').slice(-2);
return { owner, repo };
}

function makeFakeProjectAvatarType(item: ProjectResult) {
if (item.avatarCid && item.color) {
return {
__typename: 'ClaimedProjectData' as const,
chain: network.gqlName,
color: item.color,
avatar: {
__typename: 'ImageAvatar' as const,
cid: item.avatarCid,
},
};
} else if (item.emoji && item.color) {
return {
__typename: 'ClaimedProjectData' as const,
chain: network.gqlName,
color: item.color,
avatar: {
__typename: 'EmojiAvatar' as const,
emoji: item.emoji,
},
};
} else {
return {
__typename: 'UnClaimedProjectData' as const,
};
}
}

function makeFakeDripListBadgeType(item: DripListResult) {
return {
__typename: 'DripList' as const,
chain: network.gqlName,
isVisible: true,
account: {
__typename: 'NftDriverAccount' as const,
accountId: item.id ?? unreachable(),
},
name: item.name ?? '',
owner: {
__typename: 'AddressDriverAccount' as const,
address: item.ownerAddress ?? unreachable(),
},
};
}

$: highlightPlainText = highlighted.replace(/<\/?[^>]+(>|$)/g, '');
function pickLabel(item: DripListResult | ProjectResult) {
return sanitize((item._formatted ?? item).name ?? '', {
allowedTags: ['em'],
allowedAttributes: {},
});
}
</script>

{#if item.type === SearchItemType.TOKEN}
<AccountMenuItem
{#if item.type === 'project'}
{@const { owner, repo } = splitGitHubUrl(item.url)}
{@const avatarConfig = makeFakeProjectAvatarType(item)}
<a
bind:this={element}
class="search-result typo-text"
href={`/app/projects/github/${owner}/${repo}`}
on:click
href={`/app/${$wallet.address ?? unreachable()}/tokens/${item.item.info.address}`}
>
<div class="icon" slot="left">
<Token show="none" size="huge" address={item.item.info.address} />
<div class="badge"><TokensIcon style="height: 1rem; fill: var(--color-foreground)" /></div>
</div>
<svelte:fragment slot="title">
<div class="highlighted">
{@html sanitize(highlighted, {
allowedTags: [],
allowedAttributes: {},
})}
{#if avatarConfig.__typename === 'ClaimedProjectData'}
<div style:margin-right="-1.25rem">
<ProjectAvatar project={{ __typename: 'UnClaimedProjectData' }} />
</div>
{#if highlightPlainText !== item.item.info.name}<div class="typo-text-small">
{item.item.info.name}
</div>{/if}
</svelte:fragment>
</AccountMenuItem>
{:else if item.type === SearchItemType.PROFILE}
<AccountMenuItem
icon={item.item.address ? undefined : UserIcon}
on:click
href={`/app/${item.item.name ?? item.item.address ?? item.item.dripsAccountId}`}
>
<div class="icon" slot="left">
{#if item.item.address}<IdentityBadge
disableLink={true}
size="big"
address={item.item.address}
showIdentity={false}
disableTooltip
/>{/if}
<div class="badge"><UserIcon style="height: 1rem; fill: var(--color-foreground)" /></div>
{/if}
<ProjectAvatar project={avatarConfig} />
<div class="label">
{@html pickLabel(item)}
</div>
<svelte:fragment slot="title">
<div class="highlighted">
<span style="color: var(--color-foreground)">
{#if !item.item.name && !item.item.address && item.item.dripsAccountId}
Jump to account ID:
{/if}
</span>
{@html sanitize(highlighted, {
allowedTags: [],
allowedAttributes: {},
})}
</div>
{#if highlightPlainText !== item.item.name && item.item.name}<div class="typo-text-small">
{item.item.name}
</div>{/if}
</svelte:fragment>
</AccountMenuItem>
{:else if item.type === SearchItemType.REPO}
<AccountMenuItem
</a>
{:else if item.type === 'drip_list'}
<a
bind:this={element}
class="search-result typo-text"
href={`/app/drip-lists/${item.id}`}
on:click
href={`/app/projects/${item.item.forge}/${item.item.username}/${item.item.repoName}`}
icon={Folder}
>
<svelte:fragment slot="title">
<div class="highlighted">
<span style="color: var(--color-foreground)"> Jump to GitHub repo on Drips: </span>
{@html sanitize(highlighted, {
allowedTags: [],
allowedAttributes: {},
})}
<span style:display="flex" style:align-items="center" style:min-width="0">
<DripListBadge showName={false} dripList={makeFakeDripListBadgeType(item)} />
<div class="label">
{@html pickLabel(item)}
</div>
</svelte:fragment>
</AccountMenuItem>
</span>
</a>
{:else if item.type === 'address'}
<a bind:this={element} class="search-result typo-text" href={`/app/${item.address}`} on:click>
<IdentityBadge size="medium" disableTooltip={true} address={item.address} />
</a>
{/if}

<style>
.highlighted {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

.badge {
a {
display: flex;
justify-content: center;
gap: 0.5rem;
padding: 0.25rem;
align-items: center;
position: absolute;
height: 1.5rem;
width: 1.5rem;
background-color: var(--color-background);
box-shadow: var(--elevation-low);
border-radius: 0.75rem;
bottom: -0.25rem;
right: -0.25rem;
border-radius: 1.25rem 0 1.25rem 1.25rem;
}

a:hover,
a:focus-visible {
background-color: var(--color-foreground-level-1);
}

.label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.icon {
position: relative;
.search-result :global(em) {
font-style: normal;
background-color: var(--color-primary-level-2);
color: var(--color-foreground);
border-radius: 0.2rem;
}
</style>
Loading
Loading