Skip to content
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
8 changes: 8 additions & 0 deletions frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,15 @@ export default withNuxt({
.append(
...eslintPluginJsonc.configs['flat/recommended-with-json'],
{
files: [ '**/*.json' ],
rules: {
'@stylistic/no-multiple-empty-lines': [
'error',
{
max: 0,
},
],
'@stylistic/no-trailing-spaces': 'error',
'jsonc/indent': [
'error',
4,
Expand Down
15 changes: 11 additions & 4 deletions frontend/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
},
"beaconchain_homepage": "Beaconchain Homepage",
"common": {
"action": {
"try_again": "Try again"
},
"all": "All",
"close": "Close",
"error_retry": "An error occurred. Please try again.",
"ethereum": "Ethereum",
"log_in": "Log in",
"no_results": "No results found.",
"open_navigation": "Open Navigation",
"side_navigation": "Side Navigation"
"side_navigation": "Side Navigation",
"something_went_wrong": "Something went wrong."
},
"footer": {
"color_mode": {
Expand Down Expand Up @@ -776,7 +779,6 @@
"validator_offline": "Validator offline",
"validator_offline_reminder": "Validator offline reminder",
"withdrawal": "Withdrawal"

},
"heading_webhook": "Edit Webhook",
"info_send_via_discord": {
Expand Down Expand Up @@ -950,7 +952,6 @@
"erc20_token_transfers": {
"label": "ERC20 token transfers"
},

"erc721_token_transfers": {
"label": "ERC721 token transfers"
},
Expand Down Expand Up @@ -1230,6 +1231,12 @@
},
"search": {
"filter_aria_label": "Filter by type",
"history": {
"action": {
"toggle_history": "Toggle Search History"
},
"recent": "Search History"
},
"input_label": "Search by Address / Tx hash / Block / Token / ENS",
"input_placeholder": "Search anything...",
"loading_message": "Loading search results...",
Expand Down
6 changes: 6 additions & 0 deletions frontend/layers/base/app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@
}
}

@layer reset {
input::-webkit-search-cancel-button {
display: none;
}
}

html {
transition-behavior: allow-discrete;
interpolate-size: allow-keywords;
Expand Down
8 changes: 7 additions & 1 deletion frontend/layers/base/app/components/BaseButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ const {
:name="leadingIcon"
class=""
/>
<span class="px-lg">
<span
:class="[
size === 'xl' && 'px-lg',
size === 'lg' && 'px-md',
size === 'md' && 'px-xs',
]"
>
<slot />
</span>
<LazyBaseIcon
Expand Down
7 changes: 6 additions & 1 deletion frontend/layers/base/app/components/BaseButtonIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { IconName } from '~/layers/base/app/components/BaseIcon.vue'

const {
size = 'md',
to,
} = defineProps<(
{
// eslint-disable-next-line vue/prop-name-casing -- conditional props for props like `ariaLabel` do not work
Expand All @@ -19,17 +20,21 @@ const {
}
)
& {
isDisabled?: boolean,
name: IconName,
size?: 'lg' | 'md',
to?: NuxtLinkProps['to'],
variant: 'secondary' | 'tertiary',
}
>()
const isButton = computed(() => !to)
</script>

<template>
<component
:is="to ? NuxtLink : 'button'"
:is="isButton ? 'button' : NuxtLink"
:type="isButton ? 'button' : undefined"
:disabled="isDisabled"
:to
class="border flex rounded-full bg-linear-to-b disabled:opacity-40 aria-disabled:opacity-40 active:opacity-80 size-fit"
:class="[
Expand Down
2 changes: 1 addition & 1 deletion frontend/layers/base/app/components/BaseChipGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const handleSelectFilter = (value: ChipItem['value']) => {

<template>
<ul
class="flex gap-md px-2xl py-lg"
class="flex gap-md"
role="group"
:aria-label
>
Expand Down
52 changes: 29 additions & 23 deletions frontend/layers/base/app/components/BaseSearchInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,13 @@ const input = defineModel<string>({
watchDebounced(
input,
async () => {
if (input.value.length) {
emit('search', input.value)
hasSearched.value = true
}
emit('search', input.value)
},
{
immediate: false,
},
)

const showDropdown = computed(() => isLoading || hasSearched.value)
const groupedResults = computed(() => {
if (!results?.length) return
if (!groupBy) return
Expand All @@ -58,6 +54,7 @@ const handleClickOutside = (e: PointerDownOutsideEvent) => {
input.value = ''
hasSearched.value = false
}
const idSearchInput = useId()
</script>

<template>
Expand All @@ -66,36 +63,39 @@ const handleClickOutside = (e: PointerDownOutsideEvent) => {
class="base-search-input__form p-2xl isolate"
>
<RkComboboxRoot
v-model:open="showDropdown"
:open-on-focus="!!results?.length"
class="relative"
ignore-filter
:reset-search-term-on-blur="false"
>
<RkLabel
for="search-input"
:for="idSearchInput"
class="absolute bottom-2xl left-2xl dark:text-gray-400 text-sm-tight"
>
{{ label }}
</RkLabel>
<RkComboboxInput
id="search-input"
:id="idSearchInput"
ref="search-input"
v-model.trim="input"
type="search"
:aria-busy="isLoading"
:placeholder
class="search-input w-full text-2xl font-semibold rounded-3xl pt-3xl pr-5xl pb-6xl pl-2xl dark:bg-gray-950
class="w-full text-2xl font-semibold rounded-3xl pt-3xl pr-5xl pb-6xl pl-2xl dark:bg-gray-950
bg-white dark:focus:bg-black placeholder:dark:text-gray-500 dark:text-white border border-gray-500 dark:border-charcoal-500
dark:focus:border-charcoal-50 dark:focus-within:outline-0"
@update:model-value="(value) => { if (!value) hasSearched = false }"
/>

<RkComboboxContent
v-if="results !== undefined || isLoading || hasError"
class="absolute z-10 bg-gray-50 dark:bg-gray-950 mt-xl rounded-xl w-full max-h-[400px]"
@pointer-down-outside="handleClickOutside"
@focus-outside.prevent
>
<slot name="dropdown-fixed-header" />

<slot
name="dropdown-fixed-header"
:id-search-input
/>
<div
role="presentation"
class="overflow-y-auto overscroll-contain"
Expand All @@ -113,17 +113,27 @@ const handleClickOutside = (e: PointerDownOutsideEvent) => {
>
<div
role="alert"
class="px-2xl py-md dark:text-gray-400 "
class="px-2xl py-md dark:text-gray-400 flex items-center"
>
{{ $t('base.common.error_retry') }}
<div>
{{ $t('base.common.something_went_wrong') }}
</div>
<BaseButton
trailing-icon="rotate"
variant="quaternary"
@click="$emit('search', input)"
>
{{ $t('base.common.action.try_again') }}
</BaseButton>
</div>
</slot>

<slot v-else-if="!results?.length">
<div class="dark:text-gray-400 px-2xl py-md font-semibold">
{{ $t('base.common.no_results') }}
</div>
</slot>
<div
v-else-if="!results?.length"
class="dark:text-gray-400 px-2xl py-md font-semibold"
>
{{ $t('base.common.no_results') }}
</div>

<template v-else-if="results?.length && groupBy">
<RkComboboxGroup
Expand Down Expand Up @@ -176,10 +186,6 @@ const handleClickOutside = (e: PointerDownOutsideEvent) => {
</template>

<style lang="scss" scoped>
.search-input::-webkit-search-cancel-button {
display: none;
}

form {
position: relative;

Expand Down
10 changes: 10 additions & 0 deletions frontend/layers/base/app/composables/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useStorage } from '@vueuse/core'

type LocalStorageKey = 'bc-search-history-product-landing'

export const useLocalStorage
= <T extends MaybeRefOrGetter<boolean | null | number | Record<PropertyKey, any> | string>>
(key: LocalStorageKey, value: T) => {
const state = useStorage<T>(key, value)
return state
}
87 changes: 71 additions & 16 deletions frontend/layers/products/app/components/BlockchainSearchInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const {
const { t: $t } = useTranslation()

const emit = defineEmits<{
(e: 'search'): void,
(e: 'search', input: string): void,
}>()

const searchParams = defineModel<BlockchainSearchParams>({
Expand Down Expand Up @@ -50,8 +50,9 @@ const chips: { label: string, value: BlockchainSearchParams['types'][number] }[]
},
]

const handleSearch = () => {
emit('search')
const handleSearch = (input: string) => {
isHistoryVisible.value = false
emit('search', input)
}

const handleTypeFilterChange = () => {
Expand All @@ -61,29 +62,80 @@ const handleTypeFilterChange = () => {
searchParams.value.types = typeFilters
}

handleSearch()
handleSearch(searchParams.value.input)
}
const history = useLocalStorage<string[]>('bc-search-history-product-landing', [])
// using localHistory instead of history directly to avoid
// that the search history in the UI is updated before navigating away
const localHistory = ref<InternalPostSearchResponseWithChainId['data']>(history.value.map(item => JSON.parse(item)))
const hasHistory = computed(() => !!localHistory.value.length)
const hasResults = computed(() => results !== undefined)

const isHistoryVisible = ref<boolean>(!hasResults.value && hasHistory.value)
const resultsOrHistory = computed(() => {
if ((!hasResults.value && hasHistory.value) || isHistoryVisible.value) {
return localHistory.value
}
return results
})
const toggleHistory = () => {
isHistoryVisible.value = !isHistoryVisible.value
localHistory.value = history.value.map(item => JSON.parse(item))
}
const handleClick = (searchResult: InternalPostSearchResponseWithChainId['data'][number]) => {
const currentEntry = JSON.stringify(searchResult)
if (history.value.length >= 10) {
history.value.pop()
}
history.value = history.value.filter(entry => entry !== currentEntry)
history.value.unshift(currentEntry)
}
watch(hasResults, () => {
if (!hasHistory.value) return
if (hasResults.value) return
isHistoryVisible.value = true
})
</script>

<template>
<BaseSearchInput
v-model="searchParams.input"
:is-loading
:has-error
:is-loading="isHistoryVisible ? false : isLoading"
:has-error="isHistoryVisible ? false : hasError"
:label="$t('products.landing_page.search.input_label')"
:placeholder="$t('products.landing_page.search.input_placeholder')"
:group-by="'type'"
:results
:results="resultsOrHistory"
@search="handleSearch"
>
<template #dropdown-fixed-header>
<BaseChipGroup
v-model="searchParams.types"
:items="chips"
class="overflow-x-auto overscroll-contain min-h-fit"
:aria-label="$t('products.landing_page.search.filter_aria_label')"
@update:model-value="handleTypeFilterChange"
/>
<template #dropdown-fixed-header="{ idSearchInput }">
<div
class="min-h-fit overflow-x-auto overscroll-contain flex gap-md items-center px-2xl py-lg"
@keydown.enter.stop
>
<BaseButtonIcon
v-if="hasHistory"
:aria-controls="idSearchInput"
:is-disabled="!hasResults"
role="switch"
screenreader-text="products.landing_page.search.history.action.toggle_history"
name="history"
:aria-checked="`${isHistoryVisible}`"
variant="secondary"
@click="toggleHistory"
/>
<BaseChipGroup
v-if="!isHistoryVisible"
v-model="searchParams.types"
:aria-controls="idSearchInput"
:items="chips"
:aria-label="$t('products.landing_page.search.filter_aria_label')"
@update:model-value="handleTypeFilterChange"
/>
<span v-else>
{{ $t('products.landing_page.search.history.recent') }}
</span>
</div>
<hr class="mx-2xl text-gray-600">
</template>

Expand All @@ -107,7 +159,10 @@ const handleTypeFilterChange = () => {
</template>

<template #result-item="{ result }">
<BlockchainSearchResultItem :result />
<BlockchainSearchResultItem
:result
@click="handleClick(result)"
/>
</template>

<template #loading-content>
Expand Down
Loading