Compare commits
5 Commits
release-99
...
release-10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e0193fc3c | ||
|
|
a68523fc73 | ||
|
|
a465849a76 | ||
|
|
25f6dba3eb | ||
|
|
7e7046e7d2 |
@@ -1 +1 @@
|
||||
23
|
||||
24
|
||||
|
||||
@@ -107,7 +107,8 @@ export default defineConfig({
|
||||
SITE_URL === 'http://localhost:4321'
|
||||
? "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: {
|
||||
|
||||
558
web/package-lock.json
generated
558
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,12 +33,12 @@
|
||||
"@fontsource-variable/space-grotesk": "5.2.8",
|
||||
"@fontsource/inter": "5.2.6",
|
||||
"@fontsource/space-grotesk": "5.2.8",
|
||||
"@prisma/client": "6.10.1",
|
||||
"@prisma/client": "6.11.1",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/pg": "8.15.4",
|
||||
"@vercel/og": "0.6.8",
|
||||
"astro": "5.11.0",
|
||||
"@vercel/og": "0.7.2",
|
||||
"astro": "5.9.0",
|
||||
"astro-loading-indicator": "0.7.0",
|
||||
"astro-remote": "0.3.4",
|
||||
"astro-seo-schema": "5.0.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"pg": "8.16.3",
|
||||
"qrcode": "1.5.4",
|
||||
"react": "19.1.0",
|
||||
"redis": "5.5.6",
|
||||
"redis": "5.6.0",
|
||||
"schema-dts": "1.1.5",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.34.2",
|
||||
@@ -69,7 +69,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.30.1",
|
||||
"@faker-js/faker": "9.9.0",
|
||||
"@iconify-json/material-symbols": "1.2.28",
|
||||
"@iconify-json/material-symbols": "1.2.29",
|
||||
"@iconify-json/mdi": "1.2.3",
|
||||
"@iconify-json/ri": "1.2.5",
|
||||
"@stylistic/eslint-plugin": "5.1.0",
|
||||
@@ -82,12 +82,12 @@
|
||||
"@types/react": "19.1.8",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"@typescript-eslint/parser": "8.36.0",
|
||||
"@vite-pwa/assets-generator": "1.0.0",
|
||||
"@vite-pwa/astro": "1.1.0",
|
||||
"astro-icon": "1.1.5",
|
||||
"date-fns": "4.1.0",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild": "0.25.6",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-astro": "1.3.1",
|
||||
@@ -97,13 +97,13 @@
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-astro": "0.14.1",
|
||||
"prettier-plugin-tailwindcss": "0.6.13",
|
||||
"prisma": "6.10.1",
|
||||
"prisma-json-types-generator": "3.5.0",
|
||||
"prisma": "6.11.1",
|
||||
"prisma-json-types-generator": "3.5.1",
|
||||
"tailwind-htmx": "0.1.2",
|
||||
"ts-essentials": "10.1.1",
|
||||
"ts-toolbelt": "9.6.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript-eslint": "8.35.1",
|
||||
"typescript-eslint": "8.36.0",
|
||||
"vite-plugin-devtools-json": "0.2.1",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
|
||||
@@ -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',
|
||||
className
|
||||
)}
|
||||
aria-label="Announcement banner"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -37,6 +37,7 @@ const splashText = showSplashText ? sample(splashTexts) : null
|
||||
}
|
||||
)}
|
||||
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)}>
|
||||
<div class="@container -ml-4 flex max-w-[192px] grow-99999 items-center">
|
||||
|
||||
@@ -13,7 +13,7 @@ import Tooltip from './Tooltip.astro'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import type { HTMLAttributes } from 'astro/types'
|
||||
|
||||
type Props = HTMLAttributes<'a'> & {
|
||||
type Props = HTMLAttributes<'article'> & {
|
||||
inlineIcons?: boolean
|
||||
withoutLink?: boolean
|
||||
service: Prisma.ServiceGetPayload<{
|
||||
@@ -57,7 +57,7 @@ const {
|
||||
},
|
||||
class: className,
|
||||
withoutLink = false,
|
||||
...aProps
|
||||
...htmlProps
|
||||
} = Astro.props
|
||||
|
||||
const statusIcon = {
|
||||
@@ -70,127 +70,129 @@ const Element = withoutLink ? 'div' : 'a'
|
||||
const overallScoreInfo = makeOverallScoreInfo(overallScore)
|
||||
---
|
||||
|
||||
<Element
|
||||
href={Element === 'a' ? `/service/${slug}` : undefined}
|
||||
{...aProps}
|
||||
class={cn(
|
||||
'border-night-600 group/card bg-night-800 flex flex-col gap-(--gap) rounded-xl border p-(--gap) [--gap:calc(var(--spacing)*3)]',
|
||||
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
|
||||
'opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<!-- Header with Icon and Title -->
|
||||
<div class="flex items-center gap-(--gap)">
|
||||
<MyPicture
|
||||
src={imageUrl}
|
||||
fallback="service"
|
||||
alt={name || 'Service logo'}
|
||||
class={cn(
|
||||
'size-12 shrink-0 rounded-sm object-contain text-white',
|
||||
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
|
||||
'grayscale-67 transition-all group-hover/card:grayscale-0 group-focus-visible/card:grayscale-0'
|
||||
)}
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
<article {...htmlProps}>
|
||||
<Element
|
||||
href={Element === 'a' ? `/service/${slug}` : undefined}
|
||||
aria-label={Element === 'a' ? name : undefined}
|
||||
class={cn(
|
||||
'border-night-600 group/card bg-night-800 flex flex-col gap-(--gap) rounded-xl border p-(--gap) [--gap:calc(var(--spacing)*3)]',
|
||||
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
|
||||
'opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<!-- Header with Icon and Title -->
|
||||
<div class="flex items-center gap-(--gap)">
|
||||
<MyPicture
|
||||
src={imageUrl}
|
||||
fallback="service"
|
||||
alt="Logo"
|
||||
class={cn(
|
||||
'size-12 shrink-0 rounded-sm object-contain text-white',
|
||||
(serviceVisibility === 'ARCHIVED' || verificationStatus === 'VERIFICATION_FAILED') &&
|
||||
'grayscale-67 transition-all group-hover/card:grayscale-0 group-focus-visible/card:grayscale-0'
|
||||
)}
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
|
||||
<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">
|
||||
{name}{
|
||||
statusIcon && (
|
||||
<Tooltip
|
||||
text={statusIcon.label}
|
||||
position="right"
|
||||
class="-my-2 shrink-0 whitespace-nowrap"
|
||||
enabled={verificationStatus !== 'VERIFICATION_FAILED'}
|
||||
>
|
||||
{[
|
||||
<div class="flex min-w-0 flex-1 flex-col justify-center self-stretch">
|
||||
<h1 class="font-title text-lg leading-none font-medium tracking-wide text-white">
|
||||
{name}{
|
||||
statusIcon && (
|
||||
<Tooltip
|
||||
text={statusIcon.label}
|
||||
position="right"
|
||||
class="-my-2 shrink-0 whitespace-nowrap"
|
||||
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
|
||||
is:inline={inlineIcons}
|
||||
name={statusIcon.icon}
|
||||
name={serviceVisibilitiesById.ARCHIVED.icon}
|
||||
class={cn(
|
||||
'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]',
|
||||
verificationStatus === 'VERIFICATION_FAILED' && 'pr-0',
|
||||
statusIcon.classNames.icon
|
||||
serviceVisibilitiesById.ARCHIVED.iconClass
|
||||
)}
|
||||
/>,
|
||||
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
|
||||
is:inline={inlineIcons}
|
||||
name={serviceVisibilitiesById.ARCHIVED.icon}
|
||||
class={cn(
|
||||
'inline-block size-6 shrink-0 rounded-lg p-1 align-[-0.37em]',
|
||||
serviceVisibilitiesById.ARCHIVED.iconClass
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</h3>
|
||||
<div class="max-h-2 flex-1"></div>
|
||||
<div class="flex items-center gap-4 overflow-hidden mask-r-from-[calc(100%-var(--spacing)*4)]">
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</h1>
|
||||
<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)]">
|
||||
{
|
||||
categories.map((category) => (
|
||||
<span class="text-day-300 inline-flex shrink-0 items-center gap-1 text-sm leading-none">
|
||||
<Icon name={category.icon} class="size-4" is:inline={inlineIcons} />
|
||||
<span>{category.name}</span>
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 {kycLevel.toLocaleString()}
|
||||
</span>
|
||||
|
||||
<div class="-m-1 ml-auto flex">
|
||||
{
|
||||
categories.map((category) => (
|
||||
<span class="text-day-300 inline-flex shrink-0 items-center gap-1 text-sm leading-none">
|
||||
<Icon name={category.icon} class="size-4" is:inline={inlineIcons} />
|
||||
<span>{category.name}</span>
|
||||
</span>
|
||||
))
|
||||
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>
|
||||
</div>
|
||||
|
||||
<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 {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>
|
||||
</Element>
|
||||
</article>
|
||||
|
||||
@@ -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',
|
||||
className
|
||||
)}
|
||||
aria-label={`Remove filter: ${text}`}
|
||||
>
|
||||
{icon && <Icon name={icon} class={cn('size-4', iconClass)} is:inline={inlineIcons} />}
|
||||
{text}
|
||||
|
||||
@@ -32,6 +32,7 @@ type Props = HTMLAttributes<'div'> & {
|
||||
}
|
||||
}>[]
|
||||
attributeOptions: AttributeOption[]
|
||||
inlineIcons?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -41,6 +42,7 @@ const {
|
||||
categories,
|
||||
attributes,
|
||||
attributeOptions,
|
||||
inlineIcons = true,
|
||||
...divProps
|
||||
} = 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',
|
||||
className
|
||||
)}
|
||||
aria-label="Applied filters"
|
||||
{...divProps}
|
||||
>
|
||||
{
|
||||
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}
|
||||
searchParamName="categories"
|
||||
searchParamValue={categorySlug}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -83,6 +92,7 @@ const {
|
||||
searchParamName="currencies"
|
||||
searchParamValue={currency.slug}
|
||||
icon={currency.icon}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -97,6 +107,7 @@ const {
|
||||
icon={networkOption.icon}
|
||||
searchParamName="networks"
|
||||
searchParamValue={network}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -107,6 +118,7 @@ const {
|
||||
text={`KYC Lvl ≤ ${filters['max-kyc'].toLocaleString()}`}
|
||||
icon="ri:shield-keyhole-line"
|
||||
searchParamName="max-kyc"
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -116,6 +128,7 @@ const {
|
||||
text={`Rating ≥ ${filters['user-rating'].toLocaleString()}★`}
|
||||
icon="ri:star-fill"
|
||||
searchParamName="user-rating"
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -125,6 +138,7 @@ const {
|
||||
text={`Score ≥ ${filters['min-score'].toLocaleString()}`}
|
||||
icon="ri:medal-line"
|
||||
searchParamName="min-score"
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -135,6 +149,7 @@ const {
|
||||
icon="ri:filter-3-line"
|
||||
searchParamName="attribute-mode"
|
||||
searchParamValue="and"
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -152,6 +167,7 @@ const {
|
||||
text={`${prefix}: ${attribute.title}`}
|
||||
searchParamName={`attr-${attributeId}`}
|
||||
searchParamValue={attributeValue}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -176,6 +192,7 @@ const {
|
||||
iconClass={verificationStatusInfo.classNames.icon}
|
||||
searchParamName="verification"
|
||||
searchParamValue={verificationStatusInfo.slug}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -190,6 +190,7 @@ const searchTitle = (() => {
|
||||
categories={categories}
|
||||
attributes={attributes}
|
||||
attributeOptions={attributeOptions}
|
||||
inlineIcons={inlineIcons}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -201,6 +202,7 @@ const searchTitle = (() => {
|
||||
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"
|
||||
aria-hidden="true"
|
||||
is:inline={inlineIcons}
|
||||
/>
|
||||
|
||||
@@ -346,11 +348,9 @@ const searchTitle = (() => {
|
||||
</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) => (
|
||||
<ServiceCard
|
||||
inlineIcons={inlineIcons}
|
||||
service={service}
|
||||
<li
|
||||
data-hx-search-results-card
|
||||
{...(i === services.length - 1 && currentPage < totalPages
|
||||
? {
|
||||
@@ -361,9 +361,11 @@ const searchTitle = (() => {
|
||||
'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="htmx-request:opacity-100 flex items-center gap-2 opacity-0 transition-opacity duration-500">
|
||||
|
||||
@@ -33,6 +33,7 @@ const {
|
||||
enabled && (
|
||||
<span
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class={cn(
|
||||
'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',
|
||||
|
||||
@@ -58,9 +58,13 @@ const ogImageTemplateData = {
|
||||
class="prose prose-invert prose-headings:text-green-400 prose-h1:text-[2.5rem] prose-h1:font-bold prose-h1:my-8 prose-h1:drop-shadow-[0_0_10px_rgba(0,255,0,0.3)] prose-h2:text-green-500 prose-h2:text-[1.8rem] prose-h2:font-semibold prose-h2:my-6 prose-h2:border-b prose-h2:border-green-900 prose-h2:pb-1 prose-h3:text-green-600 prose-h3:text-[1.4rem] prose-h3:font-semibold prose-h3:my-4 prose-h4:text-green-700 prose-h4:text-[1.2rem] prose-h4:font-semibold prose-h4:my-3 prose-strong:font-semibold prose-strong:drop-shadow-[0_0_5px_rgba(0,255,0,0.2)] prose-p:text-gray-300 prose-p:my-4 prose-p:leading-relaxed prose-a:text-green-400 prose-a:no-underline prose-a:transition-all prose-a:border-b prose-a:border-green-900 prose-a:hover:text-green-400 prose-a:hover:drop-shadow-[0_0_8px_rgba(0,255,0,0.4)] prose-a:hover:border-green-400 prose-ul:text-gray-300 prose-ol:text-gray-300 prose-li:my-2 prose-li:leading-relaxed mx-auto"
|
||||
>
|
||||
<h1 class="mb-0!">{frontmatter.title}</h1>
|
||||
<p class="mt-2! opacity-70">
|
||||
Updated {frontmatter.updatedAt && <TimeFormatted date={new Date(frontmatter.updatedAt)} />}
|
||||
</p>
|
||||
{
|
||||
!!frontmatter.updatedAt && (
|
||||
<p class="mt-2! opacity-70">
|
||||
Updated <TimeFormatted date={new Date(frontmatter.updatedAt)} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,15 @@ icon: 'ri:image-line'
|
||||
|
||||
import PressAssets from '../components/PressAssets.astro'
|
||||
|
||||
## How you can use our logo?
|
||||
|
||||
**You are not allowed to list us as partners**. We don't accept partnerships. You should make sure it's clear that you're adding our logo **voluntarily**. Examples of correct use: "Rate us on", "Listed on" sections.
|
||||
|
||||
Please, link back to [KYCnot.me](https://kycnot.me) when possible, and use responsibly.
|
||||
|
||||
<PressAssets />
|
||||
|
||||
Review service link format: `https://kycnot.me/service/[slug]/review`
|
||||
> You can link to a service's review section with: `https://kycnot.me/service/[slug]/review`
|
||||
|
||||
## Brand design
|
||||
|
||||
|
||||
@@ -467,6 +467,7 @@ const showFiltersId = 'show-filters'
|
||||
categories={categories}
|
||||
attributes={attributes}
|
||||
attributeOptions={attributeOptions}
|
||||
inlineIcons={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -492,6 +493,7 @@ const showFiltersId = 'show-filters'
|
||||
/>
|
||||
<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"
|
||||
aria-label="Search filters"
|
||||
>
|
||||
<ServicesFilters
|
||||
searchResultsId={searchResultsId}
|
||||
@@ -519,6 +521,7 @@ const showFiltersId = 'show-filters'
|
||||
filtersOptions={filtersOptions}
|
||||
attributes={attributes}
|
||||
attributeOptions={attributeOptions}
|
||||
aria-label="Search results"
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
|
||||
@@ -426,6 +426,30 @@ const activeAlertOrWarningEvents = service.events
|
||||
.filter((event) => event.typeInfo.showBanner && (event.endedAt === null || event.endedAt >= now))
|
||||
const activeEventToShow =
|
||||
activeAlertOrWarningEvents.find((event) => event.type === EventType.ALERT) ?? activeAlertOrWarningEvents[0]
|
||||
|
||||
// Sort verification steps: failed first, then warnings, then others, newest first within each group
|
||||
const getVerificationStepPriority = (status: VerificationStepStatus) => {
|
||||
switch (status) {
|
||||
case VerificationStepStatus.FAILED:
|
||||
return 0 // Highest priority
|
||||
case VerificationStepStatus.WARNING:
|
||||
return 1
|
||||
case VerificationStepStatus.IN_PROGRESS:
|
||||
return 2
|
||||
case VerificationStepStatus.PENDING:
|
||||
return 3
|
||||
case VerificationStepStatus.PASSED:
|
||||
return 4 // Lowest priority
|
||||
default:
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
const sortedVerificationSteps = orderBy(
|
||||
service.verificationSteps,
|
||||
[(step) => getVerificationStepPriority(step.status), (step) => step.updatedAt],
|
||||
['asc', 'desc'] // Priority ascending (failed first), date descending (newest first)
|
||||
)
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -1476,11 +1500,11 @@ const activeEventToShow =
|
||||
)
|
||||
}
|
||||
{
|
||||
service.verificationSteps.length > 0 && (
|
||||
sortedVerificationSteps.length > 0 && (
|
||||
<>
|
||||
<h3 class="font-title text-md mt-6 mb-2 font-semibold">Verification Steps</h3>
|
||||
<ul class="mb-8 space-y-2">
|
||||
{service.verificationSteps.map((step) => {
|
||||
{sortedVerificationSteps.map((step) => {
|
||||
const statusInfo = getVerificationStepStatusInfo(step.status)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user