Compare commits

...

4 Commits

Author SHA1 Message Date
pluja
a68523fc73 Release 202507090857 2025-07-09 08:57:55 +00:00
pluja
a465849a76 Release 202507080951 2025-07-08 09:51:46 +00:00
pluja
25f6dba3eb Release 202507080939 2025-07-08 09:39:11 +00:00
pluja
7e7046e7d2 Release 202507080931 2025-07-08 09:31:10 +00:00
9 changed files with 153 additions and 124 deletions

View File

@@ -107,7 +107,8 @@ export default defineConfig({
SITE_URL === 'http://localhost:4321' SITE_URL === 'http://localhost:4321'
? "frame-ancestors 'none'; upgrade-insecure-requests" ? "frame-ancestors 'none'; upgrade-insecure-requests"
: "default-src 'self'; img-src 'self' *; frame-ancestors 'none'; upgrade-insecure-requests", : "default-src 'self'; img-src 'self' *; frame-ancestors 'none'; upgrade-insecure-requests",
'Strict-Transport-Security': 'max-age=31536000; includeSubdomains; preload;', 'Strict-Transport-Security':
SITE_URL === 'http://localhost:4321' ? undefined : 'max-age=31536000; includeSubdomains; preload;',
}, },
}, },
image: { image: {

View File

@@ -43,6 +43,7 @@ const Tag = announcement.link ? 'a' : 'div'
'group xs:px-6 2xs:px-4 relative isolate z-50 flex items-center justify-center gap-x-2 overflow-hidden border-b border-zinc-800 bg-black px-2 py-2 focus-visible:outline-none sm:gap-x-6 sm:px-3.5', 'group xs:px-6 2xs:px-4 relative isolate z-50 flex items-center justify-center gap-x-2 overflow-hidden border-b border-zinc-800 bg-black px-2 py-2 focus-visible:outline-none sm:gap-x-6 sm:px-3.5',
className className
)} )}
aria-label="Announcement banner"
{...props} {...props}
> >
<div <div

View File

@@ -37,6 +37,7 @@ const splashText = showSplashText ? sample(splashTexts) : null
} }
)} )}
transition:name="header-container" transition:name="header-container"
aria-label="Header"
> >
<nav class={cn('container mx-auto flex h-full w-full items-stretch justify-between px-4', classNames?.nav)}> <nav class={cn('container mx-auto flex h-full w-full items-stretch justify-between px-4', classNames?.nav)}>
<div class="@container -ml-4 flex max-w-[192px] grow-99999 items-center"> <div class="@container -ml-4 flex max-w-[192px] grow-99999 items-center">

View File

@@ -13,7 +13,7 @@ import Tooltip from './Tooltip.astro'
import type { Prisma } from '@prisma/client' import type { Prisma } from '@prisma/client'
import type { HTMLAttributes } from 'astro/types' import type { HTMLAttributes } from 'astro/types'
type Props = HTMLAttributes<'a'> & { type Props = HTMLAttributes<'article'> & {
inlineIcons?: boolean inlineIcons?: boolean
withoutLink?: boolean withoutLink?: boolean
service: Prisma.ServiceGetPayload<{ service: Prisma.ServiceGetPayload<{
@@ -57,7 +57,7 @@ const {
}, },
class: className, class: className,
withoutLink = false, withoutLink = false,
...aProps ...htmlProps
} = Astro.props } = Astro.props
const statusIcon = { const statusIcon = {
@@ -70,127 +70,129 @@ const Element = withoutLink ? 'div' : 'a'
const overallScoreInfo = makeOverallScoreInfo(overallScore) const overallScoreInfo = makeOverallScoreInfo(overallScore)
--- ---
<Element <article {...htmlProps}>
href={Element === 'a' ? `/service/${slug}` : undefined} <Element
{...aProps} href={Element === 'a' ? `/service/${slug}` : undefined}
class={cn( aria-label={Element === 'a' ? name : undefined}
'border-night-600 group/card bg-night-800 flex flex-col gap-(--gap) rounded-xl border p-(--gap) [--gap:calc(var(--spacing)*3)]', class={cn(
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') && 'border-night-600 group/card bg-night-800 flex flex-col gap-(--gap) rounded-xl border p-(--gap) [--gap:calc(var(--spacing)*3)]',
'opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100', (serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
className 'opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100',
)} className
> )}
<!-- Header with Icon and Title --> >
<div class="flex items-center gap-(--gap)"> <!-- Header with Icon and Title -->
<MyPicture <div class="flex items-center gap-(--gap)">
src={imageUrl} <MyPicture
fallback="service" src={imageUrl}
alt={name || 'Service logo'} fallback="service"
class={cn( alt="Logo"
'size-12 shrink-0 rounded-sm object-contain text-white', class={cn(
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') && 'size-12 shrink-0 rounded-sm object-contain text-white',
'grayscale-67 transition-all group-hover/card:grayscale-0 group-focus-visible/card:grayscale-0' (serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
)} 'grayscale-67 transition-all group-hover/card:grayscale-0 group-focus-visible/card:grayscale-0'
width={48} )}
height={48} width={48}
/> height={48}
/>
<div class="flex min-w-0 flex-1 flex-col justify-center self-stretch"> <div class="flex min-w-0 flex-1 flex-col justify-center self-stretch">
<h3 class="font-title text-lg leading-none font-medium tracking-wide text-white"> <h1 class="font-title text-lg leading-none font-medium tracking-wide text-white">
{name}{ {name}{
statusIcon && ( statusIcon && (
<Tooltip <Tooltip
text={statusIcon.label} text={statusIcon.label}
position="right" position="right"
class="-my-2 shrink-0 whitespace-nowrap" class="-my-2 shrink-0 whitespace-nowrap"
enabled={verificationStatus !== 'VERIFICATION_FAILED'} enabled={verificationStatus !== 'VERIFICATION_FAILED'}
> >
{[ {[
<Icon
is:inline={inlineIcons}
name={statusIcon.icon}
class={cn(
'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]',
verificationStatus === 'VERIFICATION_FAILED' && 'pr-0',
statusIcon.classNames.icon
)}
/>,
verificationStatus === 'VERIFICATION_FAILED' && (
<span class="text-sm font-bold text-red-500">SCAM</span>
),
]}
</Tooltip>
)
}{
serviceVisibility === 'ARCHIVED' && (
<Tooltip
text={serviceVisibilitiesById.ARCHIVED.label}
position="right"
class="-my-2 shrink-0 whitespace-nowrap"
>
<Icon <Icon
is:inline={inlineIcons} is:inline={inlineIcons}
name={statusIcon.icon} name={serviceVisibilitiesById.ARCHIVED.icon}
class={cn( class={cn(
'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]', 'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]',
verificationStatus === 'VERIFICATION_FAILED' && 'pr-0', serviceVisibilitiesById.ARCHIVED.iconClass
statusIcon.classNames.icon
)} )}
/>, />
verificationStatus === 'VERIFICATION_FAILED' && ( </Tooltip>
<span class="text-sm font-bold text-red-500">SCAM</span> )
), }
]} </h1>
</Tooltip> <div class="max-h-2 flex-1" aria-hidden="true"></div>
) <div class="flex items-center gap-4 overflow-hidden mask-r-from-[calc(100%-var(--spacing)*4)]">
}{ {
serviceVisibility === 'ARCHIVED' && ( categories.map((category) => (
<Tooltip <span class="text-day-300 inline-flex shrink-0 items-center gap-1 text-sm leading-none">
text={serviceVisibilitiesById.ARCHIVED.label} <Icon name={category.icon} class="size-4" is:inline={inlineIcons} />
position="right" <span>{category.name}</span>
class="-my-2 shrink-0 whitespace-nowrap" </span>
> ))
<Icon }
is:inline={inlineIcons} </div>
name={serviceVisibilitiesById.ARCHIVED.icon} </div>
class={cn( </div>
'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]',
serviceVisibilitiesById.ARCHIVED.iconClass <div class="flex-1">
)} <p class="text-day-400 line-clamp-3 text-sm leading-tight">
/> {description}
</Tooltip> </p>
) </div>
}
</h3> <div class="flex items-center justify-start">
<div class="max-h-2 flex-1"></div> <Tooltip
<div class="flex items-center gap-4 overflow-hidden mask-r-from-[calc(100%-var(--spacing)*4)]"> class={cn(
'inline-flex size-6 items-center justify-center rounded-sm text-lg font-bold',
overallScoreInfo.classNameBg
)}
text={`${Math.round(privacyScore).toLocaleString()}% Privacy | ${Math.round(trustScore).toLocaleString()}% Trust`}
>
{overallScoreInfo.formattedScore}
</Tooltip>
<span class="text-day-300 ml-3 text-sm font-bold whitespace-nowrap">
KYC &nbsp;{kycLevel.toLocaleString()}
</span>
<div class="-m-1 ml-auto flex">
{ {
categories.map((category) => ( currencies.map((currency) => {
<span class="text-day-300 inline-flex shrink-0 items-center gap-1 text-sm leading-none"> const isAccepted = acceptedCurrencies.includes(currency.id)
<Icon name={category.icon} class="size-4" is:inline={inlineIcons} />
<span>{category.name}</span> return (
</span> <Tooltip text={currency.name}>
)) <Icon
is:inline={inlineIcons}
name={currency.icon}
class={cn('text-day-600 box-content size-4 p-1', { 'text-white': isAccepted })}
/>
</Tooltip>
)
})
} }
</div> </div>
</div> </div>
</div> </Element>
</article>
<div class="flex-1">
<p class="text-day-400 line-clamp-3 text-sm leading-tight">
{description}
</p>
</div>
<div class="flex items-center justify-start">
<Tooltip
class={cn(
'inline-flex size-6 items-center justify-center rounded-sm text-lg font-bold',
overallScoreInfo.classNameBg
)}
text={`${Math.round(privacyScore).toLocaleString()}% Privacy | ${Math.round(trustScore).toLocaleString()}% Trust`}
>
{overallScoreInfo.formattedScore}
</Tooltip>
<span class="text-day-300 ml-3 text-sm font-bold whitespace-nowrap">
KYC &nbsp;{kycLevel.toLocaleString()}
</span>
<div class="-m-1 ml-auto flex">
{
currencies.map((currency) => {
const isAccepted = acceptedCurrencies.includes(currency.id)
return (
<Tooltip text={currency.name}>
<Icon
is:inline={inlineIcons}
name={currency.icon}
class={cn('text-day-600 box-content size-4 p-1', { 'text-white': isAccepted })}
/>
</Tooltip>
)
})
}
</div>
</div>
</Element>

View File

@@ -39,6 +39,7 @@ const makeUrlWithoutFilter = (filter: string, value?: string) => {
'bg-night-800 hover:bg-night-900 border-night-400 flex h-8 shrink-0 items-center gap-2 rounded-full border px-3 text-sm text-white', 'bg-night-800 hover:bg-night-900 border-night-400 flex h-8 shrink-0 items-center gap-2 rounded-full border px-3 text-sm text-white',
className className
)} )}
aria-label={`Remove filter: ${text}`}
> >
{icon && <Icon name={icon} class={cn('size-4', iconClass)} is:inline={inlineIcons} />} {icon && <Icon name={icon} class={cn('size-4', iconClass)} is:inline={inlineIcons} />}
{text} {text}

View File

@@ -32,6 +32,7 @@ type Props = HTMLAttributes<'div'> & {
} }
}>[] }>[]
attributeOptions: AttributeOption[] attributeOptions: AttributeOption[]
inlineIcons?: boolean
} }
const { const {
@@ -41,6 +42,7 @@ const {
categories, categories,
attributes, attributes,
attributeOptions, attributeOptions,
inlineIcons = true,
...divProps ...divProps
} = Astro.props } = Astro.props
--- ---
@@ -50,11 +52,17 @@ const {
'not-pointer-coarse:no-scrollbar -ml-4 flex shrink grow items-center gap-2 overflow-x-auto mask-r-from-[calc(100%-var(--spacing)*16)] pr-12 pl-4', 'not-pointer-coarse:no-scrollbar -ml-4 flex shrink grow items-center gap-2 overflow-x-auto mask-r-from-[calc(100%-var(--spacing)*16)] pr-12 pl-4',
className className
)} )}
aria-label="Applied filters"
{...divProps} {...divProps}
> >
{ {
filters.q && ( filters.q && (
<ServiceFiltersPill text={`"${filters.q}"`} searchParamName="q" searchParamValue={filters.q} /> <ServiceFiltersPill
text={`"${filters.q}"`}
searchParamName="q"
searchParamValue={filters.q}
inlineIcons={inlineIcons}
/>
) )
} }
@@ -69,6 +77,7 @@ const {
icon={category.icon} icon={category.icon}
searchParamName="categories" searchParamName="categories"
searchParamValue={categorySlug} searchParamValue={categorySlug}
inlineIcons={inlineIcons}
/> />
) )
}) })
@@ -83,6 +92,7 @@ const {
searchParamName="currencies" searchParamName="currencies"
searchParamValue={currency.slug} searchParamValue={currency.slug}
icon={currency.icon} icon={currency.icon}
inlineIcons={inlineIcons}
/> />
) )
}) })
@@ -97,6 +107,7 @@ const {
icon={networkOption.icon} icon={networkOption.icon}
searchParamName="networks" searchParamName="networks"
searchParamValue={network} searchParamValue={network}
inlineIcons={inlineIcons}
/> />
) )
}) })
@@ -107,6 +118,7 @@ const {
text={`KYC Lvl ≤ ${filters['max-kyc'].toLocaleString()}`} text={`KYC Lvl ≤ ${filters['max-kyc'].toLocaleString()}`}
icon="ri:shield-keyhole-line" icon="ri:shield-keyhole-line"
searchParamName="max-kyc" searchParamName="max-kyc"
inlineIcons={inlineIcons}
/> />
) )
} }
@@ -116,6 +128,7 @@ const {
text={`Rating ≥ ${filters['user-rating'].toLocaleString()}★`} text={`Rating ≥ ${filters['user-rating'].toLocaleString()}★`}
icon="ri:star-fill" icon="ri:star-fill"
searchParamName="user-rating" searchParamName="user-rating"
inlineIcons={inlineIcons}
/> />
) )
} }
@@ -125,6 +138,7 @@ const {
text={`Score ≥ ${filters['min-score'].toLocaleString()}`} text={`Score ≥ ${filters['min-score'].toLocaleString()}`}
icon="ri:medal-line" icon="ri:medal-line"
searchParamName="min-score" searchParamName="min-score"
inlineIcons={inlineIcons}
/> />
) )
} }
@@ -135,6 +149,7 @@ const {
icon="ri:filter-3-line" icon="ri:filter-3-line"
searchParamName="attribute-mode" searchParamName="attribute-mode"
searchParamValue="and" searchParamValue="and"
inlineIcons={inlineIcons}
/> />
) )
} }
@@ -152,6 +167,7 @@ const {
text={`${prefix}: ${attribute.title}`} text={`${prefix}: ${attribute.title}`}
searchParamName={`attr-${attributeId}`} searchParamName={`attr-${attributeId}`}
searchParamValue={attributeValue} searchParamValue={attributeValue}
inlineIcons={inlineIcons}
/> />
) )
}) })
@@ -176,6 +192,7 @@ const {
iconClass={verificationStatusInfo.classNames.icon} iconClass={verificationStatusInfo.classNames.icon}
searchParamName="verification" searchParamName="verification"
searchParamValue={verificationStatusInfo.slug} searchParamValue={verificationStatusInfo.slug}
inlineIcons={inlineIcons}
/> />
) )
}) })

View File

@@ -190,6 +190,7 @@ const searchTitle = (() => {
categories={categories} categories={categories}
attributes={attributes} attributes={attributes}
attributeOptions={attributeOptions} attributeOptions={attributeOptions}
inlineIcons={inlineIcons}
/> />
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -201,6 +202,7 @@ const searchTitle = (() => {
name="ri:loader-4-line" name="ri:loader-4-line"
id="search-indicator" 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" 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"
aria-hidden="true"
is:inline={inlineIcons} is:inline={inlineIcons}
/> />
@@ -346,11 +348,9 @@ const searchTitle = (() => {
</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))]"> <ol 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) => ( {services.map((service, i) => (
<ServiceCard <li
inlineIcons={inlineIcons}
service={service}
data-hx-search-results-card data-hx-search-results-card
{...(i === services.length - 1 && currentPage < totalPages {...(i === services.length - 1 && currentPage < totalPages
? { ? {
@@ -361,9 +361,11 @@ const searchTitle = (() => {
'hx-indicator': '#infinite-scroll-indicator', 'hx-indicator': '#infinite-scroll-indicator',
} }
: {})} : {})}
/> >
<ServiceCard inlineIcons={inlineIcons} service={service} />
</li>
))} ))}
</div> </ol>
<div class="no-js:hidden mt-8 flex justify-center" id="infinite-scroll-indicator"> <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"> <div class="htmx-request:opacity-100 flex items-center gap-2 opacity-0 transition-opacity duration-500">

View File

@@ -33,6 +33,7 @@ const {
enabled && ( enabled && (
<span <span
tabindex="-1" tabindex="-1"
aria-hidden="true"
class={cn( class={cn(
'pointer-events-none hidden select-none group-hover/tooltip:flex', 'pointer-events-none hidden select-none group-hover/tooltip:flex',
'ease-out-cubic scale-75 opacity-0 transition-all transition-discrete duration-100 group-hover/tooltip:scale-100 group-hover/tooltip:opacity-100 starting:group-hover/tooltip:scale-75 starting:group-hover/tooltip:opacity-0', 'ease-out-cubic scale-75 opacity-0 transition-all transition-discrete duration-100 group-hover/tooltip:scale-100 group-hover/tooltip:opacity-100 starting:group-hover/tooltip:scale-75 starting:group-hover/tooltip:opacity-0',

View File

@@ -467,6 +467,7 @@ const showFiltersId = 'show-filters'
categories={categories} categories={categories}
attributes={attributes} attributes={attributes}
attributeOptions={attributeOptions} attributeOptions={attributeOptions}
inlineIcons={false}
/> />
) )
} }
@@ -492,6 +493,7 @@ const showFiltersId = 'show-filters'
/> />
<div <div
class="bg-night-700 fixed top-0 left-0 z-50 hidden h-dvh w-dvw shrink-0 translate-y-full overflow-y-auto overscroll-contain border-t border-green-500/30 px-8 pt-4 transition-transform peer-checked:translate-y-0 max-sm:peer-checked:block sm:relative sm:z-auto sm:block sm:h-auto sm:w-64 sm:translate-y-0 sm:overflow-visible sm:border-none sm:bg-none sm:p-0" class="bg-night-700 fixed top-0 left-0 z-50 hidden h-dvh w-dvw shrink-0 translate-y-full overflow-y-auto overscroll-contain border-t border-green-500/30 px-8 pt-4 transition-transform peer-checked:translate-y-0 max-sm:peer-checked:block sm:relative sm:z-auto sm:block sm:h-auto sm:w-64 sm:translate-y-0 sm:overflow-visible sm:border-none sm:bg-none sm:p-0"
aria-label="Search filters"
> >
<ServicesFilters <ServicesFilters
searchResultsId={searchResultsId} searchResultsId={searchResultsId}
@@ -519,6 +521,7 @@ const showFiltersId = 'show-filters'
filtersOptions={filtersOptions} filtersOptions={filtersOptions}
attributes={attributes} attributes={attributes}
attributeOptions={attributeOptions} attributeOptions={attributeOptions}
aria-label="Search results"
/> />
</div> </div>
{ {