Compare commits
4 Commits
release-47
...
release-51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d065910ff3 | ||
|
|
490433b002 | ||
|
|
e17bc8a521 | ||
|
|
ec1215f2ae |
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "previousSlugs" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_previousSlugs_idx" ON "Service"("previousSlugs");
|
||||||
@@ -336,6 +336,7 @@ model Service {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
slug String @unique
|
slug String @unique
|
||||||
|
previousSlugs String[] @default([])
|
||||||
description String
|
description String
|
||||||
categories Category[] @relation("ServiceToCategory")
|
categories Category[] @relation("ServiceToCategory")
|
||||||
kycLevel Int @default(4)
|
kycLevel Int @default(4)
|
||||||
@@ -396,6 +397,7 @@ model Service {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([updatedAt])
|
@@index([updatedAt])
|
||||||
@@index([slug])
|
@@index([slug])
|
||||||
|
@@index([previousSlugs])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ServiceContactMethod {
|
model ServiceContactMethod {
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ const generateFakeService = (users: User[]) => {
|
|||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
|
previousSlugs: faker.helpers.maybe(() => [`${slug}-old`], { probability: 0.5 }),
|
||||||
description: faker.helpers.arrayElement(serviceDescriptions),
|
description: faker.helpers.arrayElement(serviceDescriptions),
|
||||||
kycLevel: faker.helpers.arrayElement(kycLevels.map((level) => level.value)),
|
kycLevel: faker.helpers.arrayElement(kycLevels.map((level) => level.value)),
|
||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Currency, ServiceVisibility, VerificationStatus } from '@prisma/client'
|
import { Currency, ServiceVisibility, VerificationStatus } from '@prisma/client'
|
||||||
import { z } from 'astro/zod'
|
import { z } from 'astro/zod'
|
||||||
import { ActionError } from 'astro:actions'
|
import { ActionError } from 'astro:actions'
|
||||||
|
import { uniq } from 'lodash-es'
|
||||||
import slugify from 'slugify'
|
import slugify from 'slugify'
|
||||||
|
|
||||||
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
||||||
@@ -156,19 +157,24 @@ export const adminServiceActions = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = input.removeImage
|
|
||||||
? null
|
|
||||||
: input.imageFile
|
|
||||||
? await saveFileLocally(input.imageFile, input.imageFile.name)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const existingService = await prisma.service.findUnique({
|
const existingService = await prisma.service.findUnique({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
include: {
|
select: {
|
||||||
categories: true,
|
slug: true,
|
||||||
|
previousSlugs: true,
|
||||||
|
categories: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
include: {
|
select: {
|
||||||
attribute: true,
|
attributeId: true,
|
||||||
|
attribute: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -189,6 +195,12 @@ export const adminServiceActions = {
|
|||||||
const attributesToAdd = input.attributes.filter((aId) => !existingAttributeIds.includes(aId))
|
const attributesToAdd = input.attributes.filter((aId) => !existingAttributeIds.includes(aId))
|
||||||
const attributesToRemove = existingAttributeIds.filter((aId) => !input.attributes.includes(aId))
|
const attributesToRemove = existingAttributeIds.filter((aId) => !input.attributes.includes(aId))
|
||||||
|
|
||||||
|
const imageUrl = input.removeImage
|
||||||
|
? null
|
||||||
|
: input.imageFile
|
||||||
|
? await saveFileLocally(input.imageFile, input.imageFile.name)
|
||||||
|
: undefined
|
||||||
|
|
||||||
const {
|
const {
|
||||||
web: serviceUrls,
|
web: serviceUrls,
|
||||||
onion: onionUrls,
|
onion: onionUrls,
|
||||||
@@ -213,6 +225,14 @@ export const adminServiceActions = {
|
|||||||
serviceVisibility: input.serviceVisibility,
|
serviceVisibility: input.serviceVisibility,
|
||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
overallScore: input.overallScore,
|
overallScore: input.overallScore,
|
||||||
|
previousSlugs:
|
||||||
|
existingService.slug !== input.slug
|
||||||
|
? {
|
||||||
|
set: uniq([...existingService.previousSlugs, existingService.slug]).filter(
|
||||||
|
(slug) => slug !== input.slug
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
imageUrl,
|
imageUrl,
|
||||||
categories: {
|
categories: {
|
||||||
|
|||||||
@@ -44,7 +44,30 @@ export const apiServiceActions = {
|
|||||||
.flatMap((url) => [url, url.endsWith('/') ? url.slice(0, -1) : `${url}/`])
|
.flatMap((url) => [url, url.endsWith('/') ? url.slice(0, -1) : `${url}/`])
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const service = await prisma.service.findFirst({
|
const select = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
kycLevel: true,
|
||||||
|
verificationStatus: true,
|
||||||
|
categories: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceUrls: true,
|
||||||
|
onionUrls: true,
|
||||||
|
i2pUrls: true,
|
||||||
|
tosUrls: true,
|
||||||
|
referral: true,
|
||||||
|
listedAt: true,
|
||||||
|
verifiedAt: true,
|
||||||
|
serviceVisibility: true,
|
||||||
|
} as const satisfies Prisma.ServiceSelect
|
||||||
|
|
||||||
|
let service = await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
listedAt: { lte: new Date() },
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
@@ -61,30 +84,21 @@ export const apiServiceActions = {
|
|||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
select: {
|
select,
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
description: true,
|
|
||||||
kycLevel: true,
|
|
||||||
verificationStatus: true,
|
|
||||||
categories: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
serviceUrls: true,
|
|
||||||
onionUrls: true,
|
|
||||||
i2pUrls: true,
|
|
||||||
tosUrls: true,
|
|
||||||
referral: true,
|
|
||||||
listedAt: true,
|
|
||||||
verifiedAt: true,
|
|
||||||
serviceVisibility: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!service && input.slug) {
|
||||||
|
service = await prisma.service.findFirst({
|
||||||
|
where: {
|
||||||
|
listedAt: { lte: new Date() },
|
||||||
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
|
|
||||||
|
previousSlugs: { has: input.slug },
|
||||||
|
},
|
||||||
|
select,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!service ||
|
!service ||
|
||||||
(service.serviceVisibility !== 'PUBLIC' &&
|
(service.serviceVisibility !== 'PUBLIC' &&
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
{...htmlProps}
|
{...htmlProps}
|
||||||
id={`comment-${comment.id.toString()}`}
|
id={`comment-${comment.id.toString()}`}
|
||||||
class={cn([
|
class={cn([
|
||||||
'group',
|
'group bg-night-700',
|
||||||
depth > 0 && 'ml-3 border-b-0 border-l border-zinc-800 pt-2 pl-2 sm:ml-4',
|
depth > 0 && 'ml-3 border-b-0 border-l border-zinc-800 pt-2 pl-2 sm:ml-4',
|
||||||
comment.author.serviceAffiliations.some((affiliation) => affiliation.service.slug === serviceSlug) &&
|
comment.author.serviceAffiliations.some((affiliation) => affiliation.service.slug === serviceSlug) &&
|
||||||
'bg-[#182a1f]',
|
'bg-[#182a1f]',
|
||||||
@@ -270,12 +270,6 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
|
|
||||||
{comment.suspicious && <BadgeSmall icon="ri:spam-2-fill" color="red" text="Potential SPAM" inlineIcon />}
|
{comment.suspicious && <BadgeSmall icon="ri:spam-2-fill" color="red" text="Potential SPAM" inlineIcon />}
|
||||||
|
|
||||||
{
|
|
||||||
comment.requiresAdminReview && isAuthorOrPrivileged && (
|
|
||||||
<BadgeSmall icon="ri:alert-fill" color="yellow" text="Reported" inlineIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
comment.rating !== null && !comment.parentId && (
|
comment.rating !== null && !comment.parentId && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -320,6 +314,19 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
color={commentStatusById.REJECTED.color}
|
color={commentStatusById.REJECTED.color}
|
||||||
text={commentStatusById.REJECTED.label}
|
text={commentStatusById.REJECTED.label}
|
||||||
inlineIcon
|
inlineIcon
|
||||||
|
endIcon="ri:lock-line"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
comment.requiresAdminReview && isAuthorOrPrivileged && (
|
||||||
|
<BadgeSmall
|
||||||
|
icon="ri:alert-fill"
|
||||||
|
color="yellow"
|
||||||
|
text="Needs admin review"
|
||||||
|
inlineIcon
|
||||||
|
endIcon="ri:lock-line"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ if (!user || !user.admin || !user.moderator) return null
|
|||||||
data-comment-id={comment.id}
|
data-comment-id={comment.id}
|
||||||
data-user-id={user.id}
|
data-user-id={user.id}
|
||||||
>
|
>
|
||||||
{comment.requiresAdminReview ? 'No Admin Review' : 'Admin Review'}
|
{comment.requiresAdminReview ? 'No Admin Review' : 'Needs Admin Review'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const userCommentsDisabled = user ? user.karmaUnlocks.commentsDisabled : false
|
|||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<InputRating name="rating" label="Rating" />
|
<InputRating name="rating" label="Rating" />
|
||||||
|
|
||||||
<InputWrapper label="Tags" name="tags">
|
<InputWrapper label="I experienced..." name="tags">
|
||||||
<label class="flex cursor-pointer items-center gap-2">
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
<input type="checkbox" name="issueKycRequested" class="text-red-400" />
|
<input type="checkbox" name="issueKycRequested" class="text-red-400" />
|
||||||
<span class="flex items-center gap-1 text-xs text-red-400">
|
<span class="flex items-center gap-1 text-xs text-red-400">
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ function makeLink(url: string, referral: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bitcointalkMatch = /^(?:https?:\/\/)?(?:www\.)?bitcointalk\.org$/.exec(hostname)
|
||||||
|
if (bitcointalkMatch) {
|
||||||
|
return {
|
||||||
|
type: 'clearnet' as const,
|
||||||
|
url: urlWithReferral,
|
||||||
|
textBits: [
|
||||||
|
{
|
||||||
|
style: 'normal',
|
||||||
|
text: 'BitcoinTalk ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
style: 'irrelevant',
|
||||||
|
text: 'thread',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
icon: networksBySlug.clearnet.icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'clearnet' as const,
|
type: 'clearnet' as const,
|
||||||
url: urlWithReferral,
|
url: urlWithReferral,
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
|
noTransitionPersist: false,
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -305,8 +306,8 @@ if (toggleResult?.error) {
|
|||||||
label="Status"
|
label="Status"
|
||||||
error={createInputErrors.isActive}
|
error={createInputErrors.isActive}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true' },
|
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
||||||
{ label: 'Inactive', value: 'false' },
|
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
||||||
]}
|
]}
|
||||||
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
@@ -627,6 +628,7 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -659,8 +661,8 @@ if (toggleResult?.error) {
|
|||||||
name="isActive"
|
name="isActive"
|
||||||
label="Status"
|
label="Status"
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true' },
|
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
||||||
{ label: 'Inactive', value: 'false' },
|
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
||||||
]}
|
]}
|
||||||
selectedValue={announcement.isActive ? 'true' : 'false'}
|
selectedValue={announcement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
verificationStepStatuses,
|
verificationStepStatuses,
|
||||||
} from '../../../../constants/verificationStepStatus'
|
} from '../../../../constants/verificationStepStatus'
|
||||||
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
||||||
|
import { DEPLOYMENT_MODE } from '../../../../lib/envVariables'
|
||||||
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
||||||
import { pluralize } from '../../../../lib/pluralize'
|
import { pluralize } from '../../../../lib/pluralize'
|
||||||
import { prisma } from '../../../../lib/prisma'
|
import { prisma } from '../../../../lib/prisma'
|
||||||
@@ -182,7 +183,21 @@ const [service, categories, attributes] = await Astro.locals.banners.tryMany([
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!service) return Astro.rewrite('/404')
|
if (!service) {
|
||||||
|
try {
|
||||||
|
const serviceWithOldSlug = await prisma.service.findFirst({
|
||||||
|
where: { previousSlugs: { has: slug } },
|
||||||
|
select: { slug: true },
|
||||||
|
})
|
||||||
|
if (serviceWithOldSlug) {
|
||||||
|
return Astro.redirect(`/admin/services/${serviceWithOldSlug.slug}/edit`, 301)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Astro.rewrite('/404')
|
||||||
|
}
|
||||||
|
|
||||||
const apiCalls = await Astro.locals.banners.try(
|
const apiCalls = await Astro.locals.banners.try(
|
||||||
'Error fetching api calls',
|
'Error fetching api calls',
|
||||||
@@ -263,7 +278,9 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
label="Slug"
|
label="Slug"
|
||||||
description="Auto-generated if empty"
|
description={`Auto-generated if empty. ${
|
||||||
|
service.previousSlugs.length > 0 ? `Old slugs: ${service.previousSlugs.join(', ')}` : ''
|
||||||
|
}`}
|
||||||
name="slug"
|
name="slug"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
value: service.slug,
|
value: service.slug,
|
||||||
@@ -389,6 +406,7 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
value: kycLevel.id.toString(),
|
value: kycLevel.id.toString(),
|
||||||
icon: kycLevel.icon,
|
icon: kycLevel.icon,
|
||||||
description: kycLevel.description,
|
description: kycLevel.description,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.kycLevel.toString()}
|
selectedValue={service.kycLevel.toString()}
|
||||||
iconSize="md"
|
iconSize="md"
|
||||||
@@ -406,6 +424,7 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: status.icon,
|
icon: status.icon,
|
||||||
iconClass: status.classNames.icon,
|
iconClass: status.classNames.icon,
|
||||||
description: status.description,
|
description: status.description,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.verificationStatus}
|
selectedValue={service.verificationStatus}
|
||||||
error={serviceInputErrors.verificationStatus}
|
error={serviceInputErrors.verificationStatus}
|
||||||
@@ -421,6 +440,7 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
label: currency.name,
|
label: currency.name,
|
||||||
value: currency.id,
|
value: currency.id,
|
||||||
icon: currency.icon,
|
icon: currency.icon,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.acceptedCurrencies}
|
selectedValue={service.acceptedCurrencies}
|
||||||
error={serviceInputErrors.acceptedCurrencies}
|
error={serviceInputErrors.acceptedCurrencies}
|
||||||
@@ -461,6 +481,7 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: visibility.icon,
|
icon: visibility.icon,
|
||||||
iconClass: visibility.iconClass,
|
iconClass: visibility.iconClass,
|
||||||
description: visibility.description,
|
description: visibility.description,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.serviceVisibility}
|
selectedValue={service.serviceVisibility}
|
||||||
error={serviceInputErrors.serviceVisibility}
|
error={serviceInputErrors.serviceVisibility}
|
||||||
@@ -1118,6 +1139,14 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<FormSection title="API">
|
<FormSection title="API">
|
||||||
|
{
|
||||||
|
DEPLOYMENT_MODE === 'staging' && (
|
||||||
|
<p class="rounded-lg bg-red-900/30 p-4 text-sm text-red-200">
|
||||||
|
<Icon name="ri:alert-line" class="inline-block size-4 align-[-0.2em] text-red-400" />
|
||||||
|
This endpoints section doesn't work in PRE. Use curl commands instead.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
apiCalls.map((call) => (
|
apiCalls.map((call) => (
|
||||||
<FormSubSection title={`${call.method} ${call.path}`}>
|
<FormSubSection title={`${call.method} ${call.path}`}>
|
||||||
|
|||||||
@@ -226,9 +226,24 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
name="type"
|
name="type"
|
||||||
label="Type"
|
label="Type"
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Admin', value: 'admin', icon: 'ri:shield-star-fill' },
|
{
|
||||||
{ label: 'Moderator', value: 'moderator', icon: 'ri:graduation-cap-fill' },
|
label: 'Admin',
|
||||||
{ label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill' },
|
value: 'admin',
|
||||||
|
icon: 'ri:shield-star-fill',
|
||||||
|
noTransitionPersist: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Moderator',
|
||||||
|
value: 'moderator',
|
||||||
|
icon: 'ri:graduation-cap-fill',
|
||||||
|
noTransitionPersist: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Spammer',
|
||||||
|
value: 'spammer',
|
||||||
|
icon: 'ri:alert-fill',
|
||||||
|
noTransitionPersist: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Verified',
|
label: 'Verified',
|
||||||
value: 'verified',
|
value: 'verified',
|
||||||
@@ -419,6 +434,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
label: role.label,
|
label: role.label,
|
||||||
value: role.value,
|
value: role.value,
|
||||||
icon: role.icon,
|
icon: role.icon,
|
||||||
|
noTransitionPersist: true,
|
||||||
}))}
|
}))}
|
||||||
required
|
required
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getEventTypeInfo,
|
getEventTypeInfo,
|
||||||
getEventTypeInfoBySlug,
|
getEventTypeInfoBySlug,
|
||||||
} from '../constants/eventTypes'
|
} from '../constants/eventTypes'
|
||||||
|
import { getServiceVisibilityInfo } from '../constants/serviceVisibility'
|
||||||
import { getVerificationStatusInfo } from '../constants/verificationStatus'
|
import { getVerificationStatusInfo } 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'
|
||||||
@@ -44,6 +45,8 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
async () =>
|
async () =>
|
||||||
prisma.service.findMany({
|
prisma.service.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
listedAt: { lte: new Date() },
|
||||||
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
events: {
|
events: {
|
||||||
some: {
|
some: {
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -72,8 +75,15 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
createdAt: {
|
createdAt: {
|
||||||
lte: params.now,
|
lte: params.now,
|
||||||
},
|
},
|
||||||
...(params.service ? { service: { slug: params.service } } : {}),
|
service: {
|
||||||
...(params.type ? { type: getEventTypeInfoBySlug(params.type).id } : {}),
|
slug: params.service ?? undefined,
|
||||||
|
listedAt: params.service ? undefined : { lte: new Date() },
|
||||||
|
serviceVisibility: {
|
||||||
|
in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: params.type ? getEventTypeInfoBySlug(params.type).id : undefined,
|
||||||
|
|
||||||
...(params.from || params.to
|
...(params.from || params.to
|
||||||
? {
|
? {
|
||||||
OR: [
|
OR: [
|
||||||
@@ -105,6 +115,7 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
name: true,
|
name: true,
|
||||||
imageUrl: true,
|
imageUrl: true,
|
||||||
verificationStatus: true,
|
verificationStatus: true,
|
||||||
|
serviceVisibility: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -126,6 +137,7 @@ const events = orderBy(
|
|||||||
service: {
|
service: {
|
||||||
...event.service,
|
...event.service,
|
||||||
verificationStatusInfo: getVerificationStatusInfo(event.service.verificationStatus),
|
verificationStatusInfo: getVerificationStatusInfo(event.service.verificationStatus),
|
||||||
|
serviceVisibilityInfo: getServiceVisibilityInfo(event.service.serviceVisibility),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
['actualEndedAt', 'startedAt'],
|
['actualEndedAt', 'startedAt'],
|
||||||
@@ -416,6 +428,16 @@ const createUrlWithoutFilter = (paramName: keyof typeof params) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{event.service.serviceVisibility === 'ARCHIVED' && (
|
||||||
|
<Icon
|
||||||
|
name={event.service.serviceVisibilityInfo.icon}
|
||||||
|
class={cn(
|
||||||
|
'ms-1 inline-block size-3 shrink-0',
|
||||||
|
event.service.serviceVisibilityInfo.iconClass
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{event.source && (
|
{event.source && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { z } from 'astro:schema'
|
import { z } from 'astro:schema'
|
||||||
import { groupBy, omit, orderBy, uniq } from 'lodash-es'
|
import { groupBy, omit, orderBy, sortBy, uniq } from 'lodash-es'
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
import Button from '../components/Button.astro'
|
import Button from '../components/Button.astro'
|
||||||
@@ -182,6 +182,7 @@ const {
|
|||||||
'min-score': { if: 'default' },
|
'min-score': { if: 'default' },
|
||||||
'user-rating': { if: 'default' },
|
'user-rating': { if: 'default' },
|
||||||
'max-kyc': { if: 'default' },
|
'max-kyc': { if: 'default' },
|
||||||
|
'sort-seed': { if: 'another-is-unset', prop: 'page' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -349,7 +350,8 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
|||||||
const selectedSort = sortOptions.find((sort) => sort.value === filters.sort) ?? defaultSortOption
|
const selectedSort = sortOptions.find((sort) => sort.value === filters.sort) ?? defaultSortOption
|
||||||
|
|
||||||
const sortedServices = orderBy(
|
const sortedServices = orderBy(
|
||||||
unsortedServices,
|
// NOTE: We do a first sort by id to make the seeded sort deterministic
|
||||||
|
sortBy(unsortedServices, 'id'),
|
||||||
[
|
[
|
||||||
...(filters.q ? (['similarityScore'] as const) : ([] as const)),
|
...(filters.q ? (['similarityScore'] as const) : ([] as const)),
|
||||||
selectedSort.orderBy.key,
|
selectedSort.orderBy.key,
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
'Error fetching service',
|
'Error fetching service',
|
||||||
async () =>
|
async () =>
|
||||||
prisma.service.findUnique({
|
prisma.service.findUnique({
|
||||||
where: { slug },
|
where: {
|
||||||
|
slug,
|
||||||
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
|
listedAt: { lte: new Date() },
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@@ -219,6 +223,34 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
try {
|
||||||
|
const serviceWithOldSlug = await prisma.service.findFirst({
|
||||||
|
where: {
|
||||||
|
previousSlugs: { has: slug },
|
||||||
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
|
listedAt: { lte: new Date() },
|
||||||
|
},
|
||||||
|
select: { slug: true },
|
||||||
|
})
|
||||||
|
if (serviceWithOldSlug) {
|
||||||
|
return Astro.redirect(`/service/${serviceWithOldSlug.slug}`, 301)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Astro.rewrite('/404')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
service.serviceVisibility !== 'PUBLIC' &&
|
||||||
|
service.serviceVisibility !== 'UNLISTED' &&
|
||||||
|
service.serviceVisibility !== 'ARCHIVED'
|
||||||
|
) {
|
||||||
|
return Astro.rewrite('/404')
|
||||||
|
}
|
||||||
|
|
||||||
const makeWatchingDetails = (
|
const makeWatchingDetails = (
|
||||||
dbNotificationPreferences: Prisma.NotificationPreferencesGetPayload<{
|
dbNotificationPreferences: Prisma.NotificationPreferencesGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
@@ -254,17 +286,7 @@ const makeWatchingDetails = (
|
|||||||
} as const
|
} as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchingDetails = makeWatchingDetails(dbNotificationPreferences, service?.id)
|
const watchingDetails = makeWatchingDetails(dbNotificationPreferences, service.id)
|
||||||
|
|
||||||
if (!service) return Astro.rewrite('/404')
|
|
||||||
|
|
||||||
if (
|
|
||||||
service.serviceVisibility !== 'PUBLIC' &&
|
|
||||||
service.serviceVisibility !== 'UNLISTED' &&
|
|
||||||
service.serviceVisibility !== 'ARCHIVED'
|
|
||||||
) {
|
|
||||||
return Astro.rewrite('/404')
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusIcon = {
|
const statusIcon = {
|
||||||
...verificationStatusesByValue,
|
...verificationStatusesByValue,
|
||||||
|
|||||||
Reference in New Issue
Block a user