266 lines
8.5 KiB
Plaintext
266 lines
8.5 KiB
Plaintext
---
|
|
import { Icon } from 'astro-icon/components'
|
|
import { uniq } from 'lodash-es'
|
|
|
|
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
|
import { cn } from '../lib/cn'
|
|
import { pluralize } from '../lib/pluralize'
|
|
import { createPageUrl, urlWithParams } from '../lib/urls'
|
|
|
|
import Button from './Button.astro'
|
|
import ServiceCard from './ServiceCard.astro'
|
|
|
|
import type { ServicesFiltersObject } from '../pages/index.astro'
|
|
import type { ComponentProps, HTMLAttributes } from 'astro/types'
|
|
|
|
type Props = HTMLAttributes<'div'> & {
|
|
hasDefaultFilters?: boolean
|
|
services: ComponentProps<typeof ServiceCard>['service'][] | undefined
|
|
currentPage?: number
|
|
total: number
|
|
pageSize: number
|
|
sortSeed?: string
|
|
filters: ServicesFiltersObject
|
|
includeScams: boolean
|
|
countCommunityOnly: number | null
|
|
inlineIcons?: boolean
|
|
}
|
|
|
|
const {
|
|
services,
|
|
hasDefaultFilters = false,
|
|
currentPage = 1,
|
|
total,
|
|
pageSize,
|
|
sortSeed,
|
|
class: className,
|
|
filters,
|
|
includeScams,
|
|
countCommunityOnly,
|
|
inlineIcons,
|
|
...divProps
|
|
} = Astro.props
|
|
|
|
const hasScams =
|
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
filters.verification.includes('VERIFICATION_FAILED') || includeScams
|
|
const hasSomeScam = !!services?.some((service) => service.verificationStatus.includes('VERIFICATION_FAILED'))
|
|
|
|
const hasCommunityContributed = filters.verification.includes('COMMUNITY_CONTRIBUTED')
|
|
const hasSomeCommunityContributed = !!services?.some((service) =>
|
|
service.verificationStatus.includes('COMMUNITY_CONTRIBUTED')
|
|
)
|
|
|
|
const totalPages = Math.ceil(total / pageSize) || 1
|
|
|
|
const urlIfIncludingCommunity = urlWithParams(Astro.url, {
|
|
verification: uniq([
|
|
...filters.verification.map((v) => verificationStatusesByValue[v].slug),
|
|
verificationStatusesByValue.COMMUNITY_CONTRIBUTED.slug,
|
|
]),
|
|
})
|
|
---
|
|
|
|
<div {...divProps} class={cn('flex-1', className)}>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-day-500 xs:gap-x-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm sm:gap-x-6">
|
|
{total.toLocaleString()}
|
|
{pluralize('result', total)}
|
|
|
|
<Icon
|
|
name="ri:loader-4-line"
|
|
id="search-indicator"
|
|
class="htmx-request:opacity-100 xs:-mx-1.5 -mx-1 inline-block size-4 animate-spin text-white opacity-0 transition-opacity duration-500 sm:-mx-3"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
|
|
{
|
|
countCommunityOnly && (
|
|
<>
|
|
<Button
|
|
as="a"
|
|
href={urlIfIncludingCommunity}
|
|
label={`Include +${countCommunityOnly.toLocaleString()} community contributed`}
|
|
size="sm"
|
|
class="hidden lg:inline-flex"
|
|
icon="ri:search-line"
|
|
inlineIcon={inlineIcons}
|
|
/>
|
|
<Button
|
|
as="a"
|
|
href={urlIfIncludingCommunity}
|
|
label={`Include +${countCommunityOnly.toLocaleString()}`}
|
|
size="sm"
|
|
class="hidden sm:inline-flex lg:hidden"
|
|
icon="ri:search-line"
|
|
endIcon="ri:question-line"
|
|
classNames={{
|
|
endIcon: 'text-yellow-200/50',
|
|
}}
|
|
inlineIcon={inlineIcons}
|
|
/>
|
|
|
|
<a
|
|
href={urlIfIncludingCommunity}
|
|
class="border-night-500 bg-night-800 flex items-center gap-1 rounded-md border px-2 py-0.5 text-sm sm:hidden"
|
|
>
|
|
<Icon
|
|
name="ri:search-line"
|
|
class="mr-0.5 inline-block size-3.5 shrink-0 align-[-0.15em]"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
Include
|
|
{countCommunityOnly.toLocaleString()}
|
|
<Icon
|
|
name="ri:question-line"
|
|
class="inline-block size-3.5 shrink-0 align-[-0.15em] text-yellow-200/50"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
</a>
|
|
</>
|
|
)
|
|
}
|
|
</span>
|
|
<Button
|
|
as="a"
|
|
href="/service-suggestion/new"
|
|
label="Add service"
|
|
icon="ri:add-line"
|
|
inlineIcon={inlineIcons}
|
|
class="max-xs:w-9 max-xs:px-0"
|
|
classNames={{
|
|
label: 'max-xs:hidden',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{
|
|
hasScams && hasCommunityContributed && (
|
|
<div class="font-title mt-2 rounded-lg bg-red-900/50 px-3 py-2 text-sm text-pretty text-red-400">
|
|
<Icon
|
|
name="ri:alert-fill"
|
|
class="inline-block size-4 shrink-0 align-[-0.2em] text-red-500"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
<Icon
|
|
name="ri:question-line"
|
|
class="mr-1 inline-block size-4 shrink-0 align-[-0.2em] text-yellow-400"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
Results {hasSomeScam || hasSomeCommunityContributed ? 'include' : 'may include'} SCAMs or
|
|
community-contributed services.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
hasScams && !hasCommunityContributed && (
|
|
<div class="font-title mt-2 rounded-lg bg-red-900/50 px-3 py-2 text-sm text-pretty text-red-400">
|
|
<Icon
|
|
name="ri:alert-fill"
|
|
class="mr-1 inline-block size-4 shrink-0 align-[-0.2em] text-red-500"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
Results {hasSomeScam ? 'include' : 'may include'} SCAM services
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
!hasScams && hasCommunityContributed && (
|
|
<div class="font-title mt-2 rounded-lg bg-yellow-600/30 px-3 py-2 text-sm text-pretty text-yellow-200">
|
|
<Icon
|
|
name="ri:question-line"
|
|
class="mr-1 inline-block size-4 shrink-0 align-[-0.2em] text-yellow-400"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
Results {hasSomeCommunityContributed ? 'include' : 'may include'} unverified community-contributed
|
|
services, some might be scams.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
!services || services.length === 0 ? (
|
|
<div class="sticky top-20 mt-6 flex flex-col items-center justify-center py-12 text-center">
|
|
<Icon name="ri:emotion-sad-line" class="mb-4 size-16 text-green-500/50" is:inline={inlineIcons} />
|
|
<h3 class="font-title mb-3 text-xl text-green-500">No services found</h3>
|
|
<p class="text-day-400">Try adjusting your filters to find more services</p>
|
|
<div class="mt-4 flex justify-center gap-2">
|
|
{!hasDefaultFilters && (
|
|
<Button
|
|
as="a"
|
|
href={Astro.url.pathname}
|
|
label="Clear filters"
|
|
icon="ri:close-line"
|
|
inlineIcon={inlineIcons}
|
|
/>
|
|
)}
|
|
{countCommunityOnly && (
|
|
<Button
|
|
as="a"
|
|
href={urlIfIncludingCommunity}
|
|
label={`Show ${countCommunityOnly.toLocaleString()} community contributed`}
|
|
icon="ri:search-line"
|
|
inlineIcon={inlineIcons}
|
|
/>
|
|
)}
|
|
<Button
|
|
as="a"
|
|
href="/service-suggestion/new"
|
|
label="Add service"
|
|
icon="ri:add-line"
|
|
inlineIcon={inlineIcons}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div class="mt-6 grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*80),1fr))]">
|
|
{services.map((service, i) => (
|
|
<ServiceCard
|
|
inlineIcons={inlineIcons}
|
|
service={service}
|
|
data-hx-search-results-card
|
|
{...(i === services.length - 1 && currentPage < totalPages
|
|
? {
|
|
'hx-get': createPageUrl(currentPage + 1, Astro.url, { 'sort-seed': sortSeed }),
|
|
'hx-trigger': 'revealed',
|
|
'hx-swap': 'afterend',
|
|
'hx-select': '[data-hx-search-results-card]',
|
|
'hx-indicator': '#infinite-scroll-indicator',
|
|
}
|
|
: {})}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div class="no-js:hidden mt-8 flex justify-center" id="infinite-scroll-indicator">
|
|
<div class="htmx-request:opacity-100 flex items-center gap-2 opacity-0 transition-opacity duration-500">
|
|
<Icon
|
|
name="ri:loader-4-line"
|
|
class="size-8 animate-spin text-green-500"
|
|
is:inline={inlineIcons}
|
|
/>
|
|
Loading more services...
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
{
|
|
services && services.length > 0 && (
|
|
<div class="mt-4 text-center">
|
|
<Button
|
|
as="a"
|
|
href="/service-suggestion/new"
|
|
label="Add service"
|
|
icon="ri:add-line"
|
|
inlineIcon={inlineIcons}
|
|
class="mx-auto"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|