Compare commits
2 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b86a72d1e | ||
|
|
8f2b2c34ff |
@@ -5,6 +5,7 @@ alwaysApply: false
|
|||||||
---
|
---
|
||||||
- We use Prisma as ORM.
|
- We use Prisma as ORM.
|
||||||
- Remember to check the prisma schema [schema.prisma](mdc:web/prisma/schema.prisma) when doing things related to the database.
|
- Remember to check the prisma schema [schema.prisma](mdc:web/prisma/schema.prisma) when doing things related to the database.
|
||||||
|
- After making changes to the [schema.prisma](mdc:web/prisma/schema.prisma) database or [faker.ts](mdc:web/scripts/faker.ts), you can run `npm run db-reset` (from `/web/` folder) [package.json](mdc:web/package.json).
|
||||||
- Import the types from prisma instead of hardcoding duplicates. Specially use the Prisma.___GetPayload type and the enums. Like this:
|
- Import the types from prisma instead of hardcoding duplicates. Specially use the Prisma.___GetPayload type and the enums. Like this:
|
||||||
```ts
|
```ts
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
0
.env.example
Normal file
0
.env.example
Normal file
@@ -12,7 +12,7 @@
|
|||||||
"db-push": "prisma migrate dev",
|
"db-push": "prisma migrate dev",
|
||||||
"db-triggers": "just import-triggers",
|
"db-triggers": "just import-triggers",
|
||||||
"db-update": "prisma migrate dev && just import-triggers",
|
"db-update": "prisma migrate dev && just import-triggers",
|
||||||
"db-reset": "prisma migrate reset && prisma migrate dev && just import-triggers && tsx scripts/faker.ts",
|
"db-reset": "prisma migrate reset -f && prisma migrate dev && just import-triggers && tsx scripts/faker.ts",
|
||||||
"db-fill": "tsx scripts/faker.ts",
|
"db-fill": "tsx scripts/faker.ts",
|
||||||
"db-fill-clean": "tsx scripts/faker.ts --cleanup",
|
"db-fill-clean": "tsx scripts/faker.ts --cleanup",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "ServiceVisibility" ADD VALUE 'ARCHIVED';
|
||||||
@@ -87,6 +87,7 @@ enum ServiceVisibility {
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
UNLISTED
|
UNLISTED
|
||||||
HIDDEN
|
HIDDEN
|
||||||
|
ARCHIVED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Currency {
|
enum Currency {
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import crypto from 'crypto'
|
|||||||
|
|
||||||
import { faker } from '@faker-js/faker'
|
import { faker } from '@faker-js/faker'
|
||||||
import {
|
import {
|
||||||
|
AnnouncementType,
|
||||||
AttributeCategory,
|
AttributeCategory,
|
||||||
AttributeType,
|
AttributeType,
|
||||||
CommentStatus,
|
CommentStatus,
|
||||||
Currency,
|
Currency,
|
||||||
|
EventType,
|
||||||
PrismaClient,
|
PrismaClient,
|
||||||
ServiceSuggestionStatus,
|
ServiceSuggestionStatus,
|
||||||
ServiceSuggestionType,
|
ServiceSuggestionType,
|
||||||
|
ServiceUserRole,
|
||||||
VerificationStatus,
|
VerificationStatus,
|
||||||
type Prisma,
|
type Prisma,
|
||||||
EventType,
|
|
||||||
type User,
|
type User,
|
||||||
ServiceUserRole,
|
type ServiceVisibility,
|
||||||
AnnouncementType,
|
|
||||||
} from '@prisma/client'
|
} from '@prisma/client'
|
||||||
import { uniqBy } from 'lodash-es'
|
import { uniqBy } from 'lodash-es'
|
||||||
import { generateUsername } from 'unique-username-generator'
|
import { generateUsername } from 'unique-username-generator'
|
||||||
@@ -611,7 +612,12 @@ const generateFakeEvent = (serviceId: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generateFakeService = (users: User[]) => {
|
const generateFakeService = (users: User[]) => {
|
||||||
const status = faker.helpers.arrayElement(Object.values(VerificationStatus))
|
const status = faker.helpers.weightedArrayElement<VerificationStatus>([
|
||||||
|
{ weight: 20, value: 'VERIFICATION_SUCCESS' },
|
||||||
|
{ weight: 30, value: 'APPROVED' },
|
||||||
|
{ weight: 40, value: 'COMMUNITY_CONTRIBUTED' },
|
||||||
|
{ weight: 10, value: 'VERIFICATION_FAILED' },
|
||||||
|
])
|
||||||
const name = faker.helpers.arrayElement(serviceNames)
|
const name = faker.helpers.arrayElement(serviceNames)
|
||||||
const slug = `${faker.helpers.slugify(name).toLowerCase()}-${faker.string.alphanumeric({ length: 6, casing: 'lower' })}`
|
const slug = `${faker.helpers.slugify(name).toLowerCase()}-${faker.string.alphanumeric({ length: 6, casing: 'lower' })}`
|
||||||
|
|
||||||
@@ -623,6 +629,12 @@ const generateFakeService = (users: User[]) => {
|
|||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
privacyScore: 0,
|
privacyScore: 0,
|
||||||
trustScore: 0,
|
trustScore: 0,
|
||||||
|
serviceVisibility: faker.helpers.weightedArrayElement<ServiceVisibility>([
|
||||||
|
{ weight: 80, value: 'PUBLIC' },
|
||||||
|
{ weight: 10, value: 'UNLISTED' },
|
||||||
|
{ weight: 5, value: 'HIDDEN' },
|
||||||
|
{ weight: 5, value: 'ARCHIVED' },
|
||||||
|
]),
|
||||||
verificationStatus: status,
|
verificationStatus: status,
|
||||||
verificationSummary:
|
verificationSummary:
|
||||||
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
||||||
@@ -638,22 +650,22 @@ const generateFakeService = (users: User[]) => {
|
|||||||
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraphs() : null,
|
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraphs() : null,
|
||||||
referral: `?ref=${faker.string.alphanumeric(6)}`,
|
referral: `?ref=${faker.string.alphanumeric(6)}`,
|
||||||
acceptedCurrencies: faker.helpers.arrayElements(Object.values(Currency), { min: 1, max: 5 }),
|
acceptedCurrencies: faker.helpers.arrayElements(Object.values(Currency), { min: 1, max: 5 }),
|
||||||
serviceUrls: Array.from({ length: faker.number.int({ min: 1, max: 3 }) }, () => faker.internet.url()),
|
serviceUrls: faker.helpers.multiple(() => faker.internet.url(), { count: { min: 1, max: 3 } }),
|
||||||
tosUrls: Array.from({ length: faker.number.int({ min: 0, max: 2 }) }, () => faker.internet.url()),
|
tosUrls: faker.helpers.multiple(() => faker.internet.url(), { count: { min: 1, max: 2 } }),
|
||||||
onionUrls: Array.from(
|
onionUrls: faker.helpers.multiple(
|
||||||
{ length: faker.number.int({ min: 0, max: 2 }) },
|
() => `http://${faker.string.alphanumeric({ length: 56, casing: 'lower' })}.onion`,
|
||||||
() => `http://${faker.string.alphanumeric({ length: 56, casing: 'lower' })}.onion`
|
{ count: { min: 0, max: 2 } }
|
||||||
),
|
),
|
||||||
i2pUrls: Array.from(
|
i2pUrls: faker.helpers.multiple(
|
||||||
{ length: faker.number.int({ min: 0, max: 2 }) },
|
() => `http://${faker.string.alphanumeric({ length: 52, casing: 'lower' })}.b32.i2p`,
|
||||||
() => `http://${faker.string.alphanumeric({ length: 52, casing: 'lower' })}.b32.i2p`
|
{ count: { min: 0, max: 2 } }
|
||||||
),
|
),
|
||||||
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
||||||
listedAt: faker.date.past(),
|
listedAt: faker.date.past(),
|
||||||
verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null,
|
verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null,
|
||||||
tosReview: faker.helpers.arrayElement(tosReviewExamples),
|
tosReview: faker.helpers.arrayElement(tosReviewExamples),
|
||||||
tosReviewAt: faker.date.past(),
|
tosReviewAt: faker.date.past(),
|
||||||
userSentiment: Math.random() > 0.2 ? generateFakeUserSentiment() : undefined,
|
userSentiment: faker.helpers.maybe(() => generateFakeUserSentiment(), { probability: 0.8 }),
|
||||||
userSentimentAt: faker.date.recent(),
|
userSentimentAt: faker.date.recent(),
|
||||||
} as const satisfies Prisma.ServiceCreateInput
|
} as const satisfies Prisma.ServiceCreateInput
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,9 +106,13 @@ export const adminServiceActions = {
|
|||||||
update: defineProtectedAction({
|
update: defineProtectedAction({
|
||||||
accept: 'form',
|
accept: 'form',
|
||||||
permissions: 'admin',
|
permissions: 'admin',
|
||||||
input: serviceSchemaBase.transform(addSlugIfMissing),
|
input: serviceSchemaBase
|
||||||
|
.extend({
|
||||||
|
removeImage: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.transform(addSlugIfMissing),
|
||||||
handler: async (input) => {
|
handler: async (input) => {
|
||||||
const { id, categories, attributes, imageFile, ...data } = input
|
const { id, categories, attributes, imageFile, removeImage, ...data } = input
|
||||||
|
|
||||||
const existing = await prisma.service.findUnique({
|
const existing = await prisma.service.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -124,7 +128,11 @@ export const adminServiceActions = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = imageFile ? await saveFileLocally(imageFile, imageFile.name) : undefined
|
const imageUrl = removeImage
|
||||||
|
? null
|
||||||
|
: imageFile
|
||||||
|
? await saveFileLocally(imageFile, imageFile.name)
|
||||||
|
: undefined
|
||||||
|
|
||||||
// Get existing attributes and categories to compute differences
|
// Get existing attributes and categories to compute differences
|
||||||
const existingService = await prisma.service.findUnique({
|
const existingService = await prisma.service.findUnique({
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
checked={comment.suspicious}
|
checked={comment.suspicious}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="comment-header flex items-center gap-2 text-sm">
|
<div class="comment-header scrollbar-w-none flex items-center gap-2 overflow-auto text-sm">
|
||||||
<label for={`collapse-${comment.id.toString()}`} class="cursor-pointer text-zinc-500 hover:text-zinc-300">
|
<label for={`collapse-${comment.id.toString()}`} class="cursor-pointer text-zinc-500 hover:text-zinc-300">
|
||||||
<span class="collapse-symbol text-xs"></span>
|
<span class="collapse-symbol text-xs"></span>
|
||||||
<span class="sr-only">Toggle comment visibility</span>
|
<span class="sr-only">Toggle comment visibility</span>
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
{icons.map((icon, index) => (
|
{icons.map((icon, index) => (
|
||||||
<Icon name={icon} class={cn('size-4', iconClassName[index])} />
|
<Icon name={icon} class={cn('size-4 shrink-0', iconClassName[index])} />
|
||||||
))}
|
))}
|
||||||
<span class="text-sm leading-none">{option.label}</span>
|
<span class="truncate text-sm leading-none">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,13 +16,20 @@ type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId'> &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accept, disabled, multiple, removeCheckbox, ...wrapperProps } = Astro.props
|
const { accept, disabled, multiple, removeCheckbox, classNames, ...wrapperProps } = Astro.props
|
||||||
|
|
||||||
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
|
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
|
||||||
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
||||||
---
|
---
|
||||||
|
|
||||||
<InputWrapper inputId={inputId} {...wrapperProps}>
|
<InputWrapper
|
||||||
|
inputId={inputId}
|
||||||
|
classNames={{
|
||||||
|
...classNames,
|
||||||
|
description: cn(classNames?.description, '[&:is(:has([data-remove-checkbox]:checked)_~_*)]:hidden'),
|
||||||
|
}}
|
||||||
|
{...wrapperProps}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
!!removeCheckbox && (
|
!!removeCheckbox && (
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -2,16 +2,24 @@
|
|||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
import { ACCEPTED_IMAGE_TYPES } from '../lib/zodUtils'
|
import { ACCEPTED_IMAGE_TYPES } from '../lib/zodUtils'
|
||||||
|
|
||||||
|
import Button from './Button.astro'
|
||||||
import InputFile from './InputFile.astro'
|
import InputFile from './InputFile.astro'
|
||||||
|
import Tooltip from './Tooltip.astro'
|
||||||
|
|
||||||
import type { ComponentProps } from 'astro/types'
|
import type { ComponentProps } from 'astro/types'
|
||||||
|
|
||||||
type Props = Omit<ComponentProps<typeof InputFile>, 'accept'> & {
|
type Props = Omit<ComponentProps<typeof InputFile>, 'accept'> & {
|
||||||
square?: boolean
|
square?: boolean
|
||||||
value?: string | null
|
value?: string | null
|
||||||
|
downloadButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const { class: className, square, value, ...inputFileProps } = Astro.props
|
const { class: className, square, value, downloadButton, ...inputFileProps } = Astro.props
|
||||||
|
|
||||||
|
function makeDownloadFilename(value: string) {
|
||||||
|
const url = new URL(value, Astro.url.origin)
|
||||||
|
return url.pathname.split('/').pop() ?? 'service-image'
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class={cn('flex flex-wrap items-center justify-center gap-4', className)} data-preview-image>
|
<div class={cn('flex flex-wrap items-center justify-center gap-4', className)} data-preview-image>
|
||||||
@@ -30,6 +38,31 @@ const { class: className, square, value, ...inputFileProps } = Astro.props
|
|||||||
'[&:is(:has([data-remove-checkbox]:checked)_~_*)]:hidden'
|
'[&:is(:has([data-remove-checkbox]:checked)_~_*)]:hidden'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
downloadButton && value && (
|
||||||
|
<Tooltip
|
||||||
|
text="Download"
|
||||||
|
classNames={{
|
||||||
|
tooltip: 'min-2xs:[&:is(:has([data-remove-checkbox]:checked)_~_*_*)]:hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
href={value}
|
||||||
|
download={makeDownloadFilename(value)}
|
||||||
|
icon="ri:download-line"
|
||||||
|
size="sm"
|
||||||
|
label="Download"
|
||||||
|
class={cn(
|
||||||
|
'bg-night-600 border-night-400 text-day-200 2xs:[&:is(:has([data-remove-checkbox]:not(:checked))_~_*_*)]:h-24 2xs:[&:is(:has([data-remove-checkbox]:not(:checked))_~_*_*)]:px-0 2xs:[&:is(:has([data-remove-checkbox]:not(:checked))_~_*_*)]:w-8 shrink-0 rounded-md border'
|
||||||
|
)}
|
||||||
|
classNames={{
|
||||||
|
label: '2xs:[&:is(:has([data-remove-checkbox]:not(:checked))_~_*_*)]:hidden block ',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ type Props = HTMLAttributes<'div'> & {
|
|||||||
icon?: string
|
icon?: string
|
||||||
inputId?: string
|
inputId?: string
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
|
classNames?: {
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -32,13 +35,14 @@ const {
|
|||||||
class: className,
|
class: className,
|
||||||
inputId,
|
inputId,
|
||||||
hideLabel,
|
hideLabel,
|
||||||
|
classNames,
|
||||||
...htmlProps
|
...htmlProps
|
||||||
} = Astro.props
|
} = Astro.props
|
||||||
|
|
||||||
const hasError = !!error && error.length > 0
|
const hasError = !!error && error.length > 0
|
||||||
---
|
---
|
||||||
|
|
||||||
<fieldset class={cn('space-y-1', className)} {...htmlProps}>
|
<fieldset class={cn('min-w-0 space-y-1', className)} {...htmlProps}>
|
||||||
{
|
{
|
||||||
!hideLabel && (
|
!hideLabel && (
|
||||||
<div class={cn('contents', !!descriptionLabel && 'flex flex-wrap items-center gap-x-4')}>
|
<div class={cn('contents', !!descriptionLabel && 'flex flex-wrap items-center gap-x-4')}>
|
||||||
@@ -71,7 +75,12 @@ const hasError = !!error && error.length > 0
|
|||||||
|
|
||||||
{
|
{
|
||||||
!!description && (
|
!!description && (
|
||||||
<div class="prose prose-sm prose-invert prose-a:text-current prose-a:font-normal hover:prose-a:text-day-300 prose-a:transition-colors text-day-400 max-w-none text-xs text-pretty">
|
<div
|
||||||
|
class={cn(
|
||||||
|
'prose prose-sm prose-invert prose-a:text-current prose-a:font-normal hover:prose-a:text-day-300 prose-a:transition-colors text-day-400 max-w-none text-xs text-pretty',
|
||||||
|
classNames?.description
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Markdown content={description} />
|
<Markdown content={description} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
|
|
||||||
import { currencies } from '../constants/currencies'
|
import { currencies } from '../constants/currencies'
|
||||||
|
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||||
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
import { makeOverallScoreInfo } from '../lib/overallScore'
|
import { makeOverallScoreInfo } from '../lib/overallScore'
|
||||||
@@ -25,6 +26,7 @@ type Props = HTMLAttributes<'a'> & {
|
|||||||
kycLevel: true
|
kycLevel: true
|
||||||
imageUrl: true
|
imageUrl: true
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
|
serviceVisibility: true
|
||||||
acceptedCurrencies: true
|
acceptedCurrencies: true
|
||||||
categories: {
|
categories: {
|
||||||
select: {
|
select: {
|
||||||
@@ -43,11 +45,11 @@ const {
|
|||||||
slug,
|
slug,
|
||||||
description,
|
description,
|
||||||
overallScore,
|
overallScore,
|
||||||
|
|
||||||
kycLevel,
|
kycLevel,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
categories,
|
categories,
|
||||||
verificationStatus,
|
verificationStatus,
|
||||||
|
serviceVisibility,
|
||||||
acceptedCurrencies,
|
acceptedCurrencies,
|
||||||
},
|
},
|
||||||
class: className,
|
class: className,
|
||||||
@@ -69,7 +71,9 @@ const overallScoreInfo = makeOverallScoreInfo(overallScore)
|
|||||||
href={Element === 'a' ? `/service/${slug}` : undefined}
|
href={Element === 'a' ? `/service/${slug}` : undefined}
|
||||||
{...aProps}
|
{...aProps}
|
||||||
class={cn(
|
class={cn(
|
||||||
'border-night-600 bg-night-800 flex flex-col gap-(--gap) rounded-xl border p-(--gap) [--gap:calc(var(--spacing)*3)]',
|
'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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -79,7 +83,11 @@ const overallScoreInfo = makeOverallScoreInfo(overallScore)
|
|||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
fallback="service"
|
fallback="service"
|
||||||
alt={name || 'Service logo'}
|
alt={name || 'Service logo'}
|
||||||
class="size-12 shrink-0 rounded-sm object-contain text-white"
|
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}
|
width={48}
|
||||||
height={48}
|
height={48}
|
||||||
/>
|
/>
|
||||||
@@ -110,6 +118,23 @@ const overallScoreInfo = makeOverallScoreInfo(overallScore)
|
|||||||
]}
|
]}
|
||||||
</Tooltip>
|
</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>
|
</h3>
|
||||||
<div class="max-h-2 flex-1"></div>
|
<div class="max-h-2 flex-1"></div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type ServiceVisibilityInfo<T extends string | null | undefined = string> = {
|
|||||||
slug: string
|
slug: string
|
||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
|
longDescription: string
|
||||||
icon: string
|
icon: string
|
||||||
iconClass: string
|
iconClass: string
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ export const {
|
|||||||
slug: value ? value.toLowerCase() : '',
|
slug: value ? value.toLowerCase() : '',
|
||||||
label: value ? transformCase(value, 'title') : String(value),
|
label: value ? transformCase(value, 'title') : String(value),
|
||||||
description: '',
|
description: '',
|
||||||
|
longDescription: '',
|
||||||
icon: 'ri:eye-line',
|
icon: 'ri:eye-line',
|
||||||
iconClass: 'text-current/60',
|
iconClass: 'text-current/60',
|
||||||
}),
|
}),
|
||||||
@@ -37,6 +39,7 @@ export const {
|
|||||||
slug: 'public',
|
slug: 'public',
|
||||||
label: 'Public',
|
label: 'Public',
|
||||||
description: 'Listed in search and browse.',
|
description: 'Listed in search and browse.',
|
||||||
|
longDescription: 'Listed in search and browse.',
|
||||||
icon: 'ri:global-line',
|
icon: 'ri:global-line',
|
||||||
iconClass: 'text-green-500',
|
iconClass: 'text-green-500',
|
||||||
},
|
},
|
||||||
@@ -45,6 +48,7 @@ export const {
|
|||||||
slug: 'unlisted',
|
slug: 'unlisted',
|
||||||
label: 'Unlisted',
|
label: 'Unlisted',
|
||||||
description: 'Only accessible via direct link.',
|
description: 'Only accessible via direct link.',
|
||||||
|
longDescription: "Unlisted service, only accessible via direct link and won't appear in searches.",
|
||||||
icon: 'ri:link',
|
icon: 'ri:link',
|
||||||
iconClass: 'text-yellow-500',
|
iconClass: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
@@ -53,8 +57,19 @@ export const {
|
|||||||
slug: 'hidden',
|
slug: 'hidden',
|
||||||
label: 'Hidden',
|
label: 'Hidden',
|
||||||
description: 'Only visible to moderators.',
|
description: 'Only visible to moderators.',
|
||||||
|
longDescription: 'Hidden service, only visible to moderators.',
|
||||||
icon: 'ri:lock-line',
|
icon: 'ri:lock-line',
|
||||||
iconClass: 'text-red-500',
|
iconClass: 'text-red-500',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'ARCHIVED',
|
||||||
|
slug: 'archived',
|
||||||
|
label: 'Archived',
|
||||||
|
description: 'No ceased operations.',
|
||||||
|
longDescription:
|
||||||
|
'Archived service, no longer exists or ceased operations. Information may be outdated.',
|
||||||
|
icon: 'ri:archive-line',
|
||||||
|
iconClass: 'text-day-100',
|
||||||
|
},
|
||||||
] as const satisfies ServiceVisibilityInfo<ServiceVisibility>[]
|
] as const satisfies ServiceVisibilityInfo<ServiceVisibility>[]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { orderBy } from 'lodash-es'
|
|||||||
|
|
||||||
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
|
||||||
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
import { getAttributeTypeInfo } from '../constants/attributeTypes'
|
||||||
|
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||||
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
|
|
||||||
import { formatDateShort } from './timeAgo'
|
import { formatDateShort } from './timeAgo'
|
||||||
@@ -36,6 +37,7 @@ export function makeNonDbAttributes(
|
|||||||
service: Prisma.ServiceGetPayload<{
|
service: Prisma.ServiceGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
|
serviceVisibility: true
|
||||||
isRecentlyListed: true
|
isRecentlyListed: true
|
||||||
listedAt: true
|
listedAt: true
|
||||||
createdAt: true
|
createdAt: true
|
||||||
@@ -134,6 +136,16 @@ export function makeNonDbAttributes(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: serviceVisibilitiesById.ARCHIVED.label,
|
||||||
|
show: service.serviceVisibility === 'ARCHIVED',
|
||||||
|
type: 'WARNING',
|
||||||
|
category: 'TRUST',
|
||||||
|
description: serviceVisibilitiesById.ARCHIVED.longDescription,
|
||||||
|
privacyPoints: 0,
|
||||||
|
trustPoints: 0,
|
||||||
|
links: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Recently listed',
|
title: 'Recently listed',
|
||||||
show: service.isRecentlyListed,
|
show: service.isRecentlyListed,
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
inputProps={{
|
inputProps={{
|
||||||
rows: 3,
|
rows: 3,
|
||||||
placeholder: 'https://example1.com/tos\nhttps://example2.com/tos',
|
placeholder: 'https://example1.com/tos\nhttps://example2.com/tos',
|
||||||
|
required: true,
|
||||||
}}
|
}}
|
||||||
value={service.tosUrls.join('\n')}
|
value={service.tosUrls.join('\n')}
|
||||||
error={serviceInputErrors.tosUrls}
|
error={serviceInputErrors.tosUrls}
|
||||||
@@ -298,6 +299,7 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
error={serviceInputErrors.referral}
|
error={serviceInputErrors.referral}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
<InputImageFile
|
<InputImageFile
|
||||||
label="Image"
|
label="Image"
|
||||||
name="imageFile"
|
name="imageFile"
|
||||||
@@ -305,9 +307,18 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
error={serviceInputErrors.imageFile}
|
error={serviceInputErrors.imageFile}
|
||||||
square
|
square
|
||||||
value={service.imageUrl}
|
value={service.imageUrl}
|
||||||
|
downloadButton
|
||||||
|
removeCheckbox={service.imageUrl
|
||||||
|
? {
|
||||||
|
name: 'removeImage',
|
||||||
|
label: 'Remove image',
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
|
class="grow"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 items-stretch gap-x-4 gap-y-6 sm:grid-cols-[1fr_2fr]">
|
<div class="xs:grid-cols-[1fr_2fr] grid grid-cols-1 items-stretch gap-x-4 gap-y-6">
|
||||||
<InputCheckboxGroup
|
<InputCheckboxGroup
|
||||||
name="categories"
|
name="categories"
|
||||||
label="Categories"
|
label="Categories"
|
||||||
@@ -320,6 +331,7 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
}))}
|
}))}
|
||||||
selectedValues={service.categories.map((c) => c.id.toString())}
|
selectedValues={service.categories.map((c) => c.id.toString())}
|
||||||
error={serviceInputErrors.categories}
|
error={serviceInputErrors.categories}
|
||||||
|
class="min-w-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputCheckboxGroup
|
<InputCheckboxGroup
|
||||||
@@ -478,28 +490,43 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-1.5">
|
<div class="flex shrink-0 gap-1.5">
|
||||||
|
<Tooltip text={event.visible ? 'Hide' : 'Show'}>
|
||||||
<form method="POST" action={actions.admin.event.toggle} class="contents">
|
<form method="POST" action={actions.admin.event.toggle} class="contents">
|
||||||
<input type="hidden" name="eventId" value={event.id} />
|
<input type="hidden" name="eventId" value={event.id} />
|
||||||
<Tooltip text={event.visible ? 'Hide Event' : 'Show Event'}>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={event.visible ? 'ri:eye-off-line' : 'ri:eye-line'}
|
icon={event.visible ? 'ri:eye-off-line' : 'ri:eye-line'}
|
||||||
|
iconOnly
|
||||||
|
label={event.visible ? 'Hide' : 'Show'}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</form>
|
</form>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Edit">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="ri:pencil-line"
|
icon="ri:pencil-line"
|
||||||
onclick={`document.getElementById('edit-event-${event.id}')?.classList.toggle('hidden')`}
|
onclick={`document.getElementById('edit-event-${event.id}')?.classList.toggle('hidden')`}
|
||||||
|
iconOnly
|
||||||
|
label="Edit"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Delete">
|
||||||
<form method="POST" action={actions.admin.event.delete} class="contents">
|
<form method="POST" action={actions.admin.event.delete} class="contents">
|
||||||
<input type="hidden" name="eventId" value={event.id} />
|
<input type="hidden" name="eventId" value={event.id} />
|
||||||
<Button type="submit" size="sm" variant="faded" icon="ri:delete-bin-line" />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
variant="faded"
|
||||||
|
icon="ri:delete-bin-line"
|
||||||
|
iconOnly
|
||||||
|
label="Delete"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Edit Event Form - Hidden by default */}
|
{/* Edit Event Form - Hidden by default */}
|
||||||
@@ -673,17 +700,30 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-1.5">
|
<div class="flex shrink-0 gap-1.5">
|
||||||
|
<Tooltip text="Edit">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
icon="ri:pencil-line"
|
icon="ri:pencil-line"
|
||||||
onclick={`document.getElementById('edit-verification-step-${step.id}')?.classList.toggle('hidden')`}
|
onclick={`document.getElementById('edit-verification-step-${step.id}')?.classList.toggle('hidden')`}
|
||||||
|
iconOnly
|
||||||
|
label="Edit"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Delete">
|
||||||
<form method="POST" action={actions.admin.verificationStep.delete} class="inline">
|
<form method="POST" action={actions.admin.verificationStep.delete} class="inline">
|
||||||
<input type="hidden" name="id" value={step.id} />
|
<input type="hidden" name="id" value={step.id} />
|
||||||
<Button type="submit" size="sm" variant="faded" icon="ri:delete-bin-line" />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
variant="faded"
|
||||||
|
icon="ri:delete-bin-line"
|
||||||
|
iconOnly
|
||||||
|
label="Delete"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -844,21 +884,34 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
<p class="text-day-400 text-sm text-pretty">{method.value}</p>
|
<p class="text-day-400 text-sm text-pretty">{method.value}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 gap-1.5">
|
<div class="flex shrink-0 gap-1.5">
|
||||||
|
<Tooltip text="Edit">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="ri:pencil-line"
|
icon="ri:pencil-line"
|
||||||
onclick={`document.getElementById('edit-contact-method-${method.id}')?.classList.toggle('hidden')`}
|
onclick={`document.getElementById('edit-contact-method-${method.id}')?.classList.toggle('hidden')`}
|
||||||
|
iconOnly
|
||||||
|
label="Edit"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Delete">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action={actions.admin.service.deleteContactMethod}
|
action={actions.admin.service.deleteContactMethod}
|
||||||
class="contents"
|
class="contents"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={method.id} />
|
<input type="hidden" name="id" value={method.id} />
|
||||||
<Button type="submit" size="sm" variant="faded" icon="ri:delete-bin-line" />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
variant="faded"
|
||||||
|
icon="ri:delete-bin-line"
|
||||||
|
iconOnly
|
||||||
|
label="Delete"
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import MyPicture from '../../../components/MyPicture.astro'
|
|||||||
import SortArrowIcon from '../../../components/SortArrowIcon.astro'
|
import SortArrowIcon from '../../../components/SortArrowIcon.astro'
|
||||||
import Tooltip from '../../../components/Tooltip.astro'
|
import Tooltip from '../../../components/Tooltip.astro'
|
||||||
import { getKycLevelInfo } from '../../../constants/kycLevels'
|
import { getKycLevelInfo } from '../../../constants/kycLevels'
|
||||||
import { getVerificationStatusInfo } from '../../../constants/verificationStatus'
|
import { serviceVisibilities } from '../../../constants/serviceVisibility'
|
||||||
|
import { getVerificationStatusInfo, verificationStatuses } from '../../../constants/verificationStatus'
|
||||||
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||||
import { cn } from '../../../lib/cn'
|
import { cn } from '../../../lib/cn'
|
||||||
import { zodParseQueryParamsStoringErrors } from '../../../lib/parseUrlFilters'
|
import { zodParseQueryParamsStoringErrors } from '../../../lib/parseUrlFilters'
|
||||||
@@ -209,11 +210,11 @@ const truncate = (text: string, length: number) => {
|
|||||||
id="visibility"
|
id="visibility"
|
||||||
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="">All Visibilities</option>
|
<option value="">All</option>
|
||||||
{
|
{
|
||||||
Object.values(ServiceVisibility).map((status) => (
|
serviceVisibilities.map((visibility) => (
|
||||||
<option value={status} selected={filters.visibility === status}>
|
<option value={visibility.value} selected={filters.visibility === visibility.value}>
|
||||||
{status}
|
{visibility.label}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -227,11 +228,11 @@ const truncate = (text: string, length: number) => {
|
|||||||
id="verificationStatus"
|
id="verificationStatus"
|
||||||
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="">All Statuses</option>
|
<option value="">All</option>
|
||||||
{
|
{
|
||||||
Object.values(VerificationStatus).map((status) => (
|
verificationStatuses.map((status) => (
|
||||||
<option value={status} selected={filters.verificationStatus === status}>
|
<option value={status.value} selected={filters.verificationStatus === status.value}>
|
||||||
{status}
|
{status.label}
|
||||||
</option>
|
</option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
import { ServiceVisibility } from '@prisma/client'
|
|
||||||
import { z } from 'astro:schema'
|
import { z } from 'astro:schema'
|
||||||
import { groupBy, omit, orderBy, uniq } from 'lodash-es'
|
import { groupBy, omit, orderBy, uniq } from 'lodash-es'
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
@@ -222,7 +221,9 @@ const where = {
|
|||||||
verificationStatus: {
|
verificationStatus: {
|
||||||
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
||||||
},
|
},
|
||||||
serviceVisibility: ServiceVisibility.PUBLIC,
|
serviceVisibility: {
|
||||||
|
in: ['PUBLIC', 'ARCHIVED'],
|
||||||
|
},
|
||||||
overallScore: { gte: filters['min-score'] },
|
overallScore: { gte: filters['min-score'] },
|
||||||
acceptedCurrencies: filters.currencies.length
|
acceptedCurrencies: filters.currencies.length
|
||||||
? filters['currency-mode'] === 'and'
|
? filters['currency-mode'] === 'and'
|
||||||
@@ -324,7 +325,9 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
|||||||
select: {
|
select: {
|
||||||
services: {
|
services: {
|
||||||
where: {
|
where: {
|
||||||
serviceVisibility: 'PUBLIC',
|
serviceVisibility: {
|
||||||
|
in: ['PUBLIC', 'ARCHIVED'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -372,6 +375,7 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
|||||||
imageUrl: true,
|
imageUrl: true,
|
||||||
verificationStatus: true,
|
verificationStatus: true,
|
||||||
acceptedCurrencies: true,
|
acceptedCurrencies: true,
|
||||||
|
serviceVisibility: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
select: {
|
select: {
|
||||||
attribute: {
|
attribute: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { actions, isInputError } from 'astro:actions'
|
import { actions, isInputError } from 'astro:actions'
|
||||||
|
import { orderBy } from 'lodash-es'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SUGGESTION_DESCRIPTION_MAX_LENGTH,
|
SUGGESTION_DESCRIPTION_MAX_LENGTH,
|
||||||
@@ -16,6 +17,8 @@ import InputImageFile from '../../components/InputImageFile.astro'
|
|||||||
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
||||||
import InputText from '../../components/InputText.astro'
|
import InputText from '../../components/InputText.astro'
|
||||||
import InputTextArea from '../../components/InputTextArea.astro'
|
import InputTextArea from '../../components/InputTextArea.astro'
|
||||||
|
import { getAttributeCategoryInfo } from '../../constants/attributeCategories'
|
||||||
|
import { getAttributeTypeInfo } from '../../constants/attributeTypes'
|
||||||
import { currencies } from '../../constants/currencies'
|
import { currencies } from '../../constants/currencies'
|
||||||
import { kycLevels } from '../../constants/kycLevels'
|
import { kycLevels } from '../../constants/kycLevels'
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||||
@@ -55,6 +58,8 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
category: true,
|
||||||
|
type: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
@@ -241,6 +246,7 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
|
|||||||
error={inputErrors.kycLevel}
|
error={inputErrors.kycLevel}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="xs:grid-cols-[1fr_2fr] grid grid-cols-1 items-stretch gap-x-4 gap-y-6">
|
||||||
<InputCheckboxGroup
|
<InputCheckboxGroup
|
||||||
name="categories"
|
name="categories"
|
||||||
label="Categories"
|
label="Categories"
|
||||||
@@ -250,18 +256,31 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
|
|||||||
value: category.id.toString(),
|
value: category.id.toString(),
|
||||||
icon: category.icon,
|
icon: category.icon,
|
||||||
}))}
|
}))}
|
||||||
|
size="lg"
|
||||||
error={inputErrors.categories}
|
error={inputErrors.categories}
|
||||||
|
class="min-w-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputCheckboxGroup
|
<InputCheckboxGroup
|
||||||
name="attributes"
|
name="attributes"
|
||||||
label="Attributes"
|
label="Attributes"
|
||||||
options={attributes.map((attribute) => ({
|
options={orderBy(
|
||||||
|
attributes.map((attribute) => ({
|
||||||
|
...attribute,
|
||||||
|
categoryInfo: getAttributeCategoryInfo(attribute.category),
|
||||||
|
typeInfo: getAttributeTypeInfo(attribute.type),
|
||||||
|
})),
|
||||||
|
['categoryInfo.order', 'typeInfo.order']
|
||||||
|
).map((attribute) => ({
|
||||||
label: attribute.title,
|
label: attribute.title,
|
||||||
value: attribute.id.toString(),
|
value: attribute.id.toString(),
|
||||||
|
icon: [attribute.categoryInfo.icon, attribute.typeInfo.icon],
|
||||||
|
iconClassName: [attribute.categoryInfo.classNames.icon, attribute.typeInfo.classNames.icon],
|
||||||
}))}
|
}))}
|
||||||
error={inputErrors.attributes}
|
error={inputErrors.attributes}
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InputCardGroup
|
<InputCardGroup
|
||||||
name="acceptedCurrencies"
|
name="acceptedCurrencies"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { formatContactMethod } from '../../constants/contactMethods'
|
|||||||
import { currencies, getCurrencyInfo } from '../../constants/currencies'
|
import { currencies, getCurrencyInfo } from '../../constants/currencies'
|
||||||
import { getEventTypeInfo } from '../../constants/eventTypes'
|
import { getEventTypeInfo } from '../../constants/eventTypes'
|
||||||
import { getKycLevelInfo, kycLevels } from '../../constants/kycLevels'
|
import { getKycLevelInfo, kycLevels } from '../../constants/kycLevels'
|
||||||
import { serviceVisibilitiesById } from '../../constants/serviceVisibility'
|
import { getServiceVisibilityInfo } from '../../constants/serviceVisibility'
|
||||||
import { getTosHighlightRatingInfo } from '../../constants/tosHighlightRating'
|
import { getTosHighlightRatingInfo } from '../../constants/tosHighlightRating'
|
||||||
import { getUserSentimentInfo } from '../../constants/userSentiment'
|
import { getUserSentimentInfo } from '../../constants/userSentiment'
|
||||||
import { getVerificationStatusInfo, verificationStatusesByValue } from '../../constants/verificationStatus'
|
import { getVerificationStatusInfo, verificationStatusesByValue } from '../../constants/verificationStatus'
|
||||||
@@ -240,7 +240,11 @@ const watchingDetails = makeWatchingDetails(dbNotificationPreferences, service?.
|
|||||||
|
|
||||||
if (!service) return Astro.rewrite('/404')
|
if (!service) return Astro.rewrite('/404')
|
||||||
|
|
||||||
if (service.serviceVisibility !== 'PUBLIC' && service.serviceVisibility !== 'UNLISTED') {
|
if (
|
||||||
|
service.serviceVisibility !== 'PUBLIC' &&
|
||||||
|
service.serviceVisibility !== 'UNLISTED' &&
|
||||||
|
service.serviceVisibility !== 'ARCHIVED'
|
||||||
|
) {
|
||||||
return Astro.rewrite('/404')
|
return Astro.rewrite('/404')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,6 +360,8 @@ const ogImageTemplateData = {
|
|||||||
score: service.overallScore,
|
score: service.overallScore,
|
||||||
imageUrl: service.imageUrl,
|
imageUrl: service.imageUrl,
|
||||||
} satisfies OgImageAllTemplatesWithProps
|
} satisfies OgImageAllTemplatesWithProps
|
||||||
|
|
||||||
|
const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility)
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -457,16 +463,17 @@ const ogImageTemplateData = {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
service.serviceVisibility === 'UNLISTED' && (
|
(serviceVisibilityInfo.value === 'UNLISTED' || serviceVisibilityInfo.value === 'ARCHIVED') && (
|
||||||
<div class={cn('mb-4 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400')}>
|
<div class={cn('mb-4 rounded-md bg-yellow-900/50 px-3 py-2 text-sm text-yellow-400')}>
|
||||||
<Icon
|
<Icon
|
||||||
name={serviceVisibilitiesById.UNLISTED.icon}
|
name={serviceVisibilityInfo.icon}
|
||||||
class={cn('me-1.5 inline-block size-4 align-[-0.15em]', serviceVisibilitiesById.UNLISTED.iconClass)}
|
class="me-1.5 inline-block size-4 align-[-0.15em] text-yellow-500"
|
||||||
/>
|
/>
|
||||||
Unlisted service, only accessible via direct link and won't appear in searches.
|
{serviceVisibilityInfo.longDescription}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<VerificationWarningBanner service={service} />
|
<VerificationWarningBanner service={service} />
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@@ -1245,7 +1252,8 @@ const ogImageTemplateData = {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{
|
{
|
||||||
service.verificationStatus !== 'VERIFICATION_SUCCESS' &&
|
service.verificationStatus !== 'VERIFICATION_SUCCESS' &&
|
||||||
service.verificationStatus !== 'VERIFICATION_FAILED' && (
|
service.verificationStatus !== 'VERIFICATION_FAILED' &&
|
||||||
|
service.serviceVisibility !== 'ARCHIVED' && (
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action={actions.service.requestVerification}
|
action={actions.service.requestVerification}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@
|
|||||||
drop-shadow(0 0 4px color-mix(in oklab, currentColor 60%, transparent));
|
drop-shadow(0 0 4px color-mix(in oklab, currentColor 60%, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility scrollbar-w-none {
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
@utility checkbox-force-checked {
|
@utility checkbox-force-checked {
|
||||||
&:not(:checked) {
|
&:not(:checked) {
|
||||||
@apply border-transparent! bg-current/50!;
|
@apply border-transparent! bg-current/50!;
|
||||||
|
|||||||
Reference in New Issue
Block a user