Release 2025-05-19
This commit is contained in:
@@ -1,163 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { actions } from 'astro:actions'
|
||||
|
||||
import AdminOnly from '../../components/AdminOnly.astro'
|
||||
import Chat from '../../components/Chat.astro'
|
||||
import ServiceCard from '../../components/ServiceCard.astro'
|
||||
import { getServiceSuggestionStatusInfo } from '../../constants/serviceSuggestionStatus'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { cn } from '../../lib/cn'
|
||||
import { parseIntWithFallback } from '../../lib/numbers'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||
import { formatDateShort } from '../../lib/timeAgo'
|
||||
|
||||
const user = Astro.locals.user
|
||||
if (!user) {
|
||||
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to view service suggestion' }))
|
||||
}
|
||||
|
||||
const { id: serviceSuggestionIdRaw } = Astro.params
|
||||
const serviceSuggestionId = parseIntWithFallback(serviceSuggestionIdRaw)
|
||||
if (!serviceSuggestionId) {
|
||||
return Astro.rewrite('/404')
|
||||
}
|
||||
|
||||
const serviceSuggestion = await Astro.locals.banners.try('Error fetching service suggestion', async () =>
|
||||
prisma.serviceSuggestion.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
notes: true,
|
||||
createdAt: true,
|
||||
service: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
overallScore: true,
|
||||
kycLevel: true,
|
||||
imageUrl: true,
|
||||
verificationStatus: true,
|
||||
acceptedCurrencies: true,
|
||||
categories: {
|
||||
select: {
|
||||
name: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: serviceSuggestionId,
|
||||
userId: user.id,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (!serviceSuggestion) {
|
||||
if (user.admin) return Astro.redirect(`/admin/service-suggestions/${serviceSuggestionIdRaw}`)
|
||||
return Astro.rewrite('/404')
|
||||
}
|
||||
|
||||
const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
pageTitle={`${serviceSuggestion.service.name} | Service suggestion`}
|
||||
description="View your service suggestion"
|
||||
ogImage={{ template: 'generic', title: serviceSuggestion.service.name }}
|
||||
widthClassName="max-w-screen-md"
|
||||
htmx
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Service suggestions',
|
||||
url: '/service-suggestion',
|
||||
},
|
||||
{
|
||||
name: `${serviceSuggestion.service.name} | Service suggestion`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Edit service</h1>
|
||||
|
||||
<AdminOnly>
|
||||
<a
|
||||
href={`/admin/service-suggestions/${serviceSuggestionIdRaw}`}
|
||||
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
|
||||
>
|
||||
<Icon name="ri:lock-line" class="size-4" />
|
||||
View in admin
|
||||
</a>
|
||||
</AdminOnly>
|
||||
|
||||
<ServiceCard service={serviceSuggestion.service} class="mb-6" />
|
||||
|
||||
<section class="border-night-400 bg-night-600 rounded-lg border p-6">
|
||||
<div class="text-day-200 grid grid-cols-2 gap-6 text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span>Status:</span>
|
||||
<span
|
||||
class={cn(
|
||||
'border-night-500 bg-night-800 box-content inline-flex h-8 items-center justify-center gap-1 rounded-full border px-2',
|
||||
statusInfo.iconClass
|
||||
)}
|
||||
>
|
||||
<Icon name={statusInfo.icon} class="size-4" />
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span>Submitted:</span>
|
||||
<span>
|
||||
{
|
||||
formatDateShort(serviceSuggestion.createdAt, {
|
||||
prefix: false,
|
||||
hourPrecision: true,
|
||||
caseType: 'sentence',
|
||||
})
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="text-day-200 mb-2 text-sm">Notes for moderators:</div>
|
||||
<div class="text-sm whitespace-pre-wrap">
|
||||
{serviceSuggestion.notes ?? <span class="italic">Empty</span>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Chat
|
||||
messages={serviceSuggestion.messages}
|
||||
title="Chat with moderators"
|
||||
userId={user.id}
|
||||
action={actions.serviceSuggestion.message}
|
||||
formData={{
|
||||
suggestionId: serviceSuggestion.id,
|
||||
}}
|
||||
class="mt-12"
|
||||
/>
|
||||
</BaseLayout>
|
||||
@@ -1,102 +0,0 @@
|
||||
---
|
||||
import { actions, isInputError } from 'astro:actions'
|
||||
import { z } from 'astro:content'
|
||||
|
||||
import Captcha from '../../components/Captcha.astro'
|
||||
import InputHoneypotTrap from '../../components/InputHoneypotTrap.astro'
|
||||
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
||||
import InputTextArea from '../../components/InputTextArea.astro'
|
||||
import ServiceCard from '../../components/ServiceCard.astro'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { zodParseQueryParamsStoringErrors } from '../../lib/parseUrlFilters'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||
|
||||
const user = Astro.locals.user
|
||||
if (!user) {
|
||||
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to suggest a new service' }))
|
||||
}
|
||||
|
||||
const result = Astro.getActionResult(actions.serviceSuggestion.editService)
|
||||
if (result && !result.error) {
|
||||
return Astro.redirect(`/service-suggestion/${result.data.serviceSuggestion.id}`)
|
||||
}
|
||||
const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
||||
|
||||
const { data: params } = zodParseQueryParamsStoringErrors(
|
||||
{
|
||||
serviceId: z.coerce.number().int().positive(),
|
||||
notes: z.string().default(''),
|
||||
},
|
||||
Astro
|
||||
)
|
||||
|
||||
if (!params.serviceId) return Astro.rewrite('/404')
|
||||
|
||||
const service = await Astro.locals.banners.try(
|
||||
'Failed to fetch service',
|
||||
async () =>
|
||||
prisma.service.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
overallScore: true,
|
||||
kycLevel: true,
|
||||
imageUrl: true,
|
||||
verificationStatus: true,
|
||||
acceptedCurrencies: true,
|
||||
categories: {
|
||||
select: {
|
||||
name: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { id: params.serviceId },
|
||||
}),
|
||||
null
|
||||
)
|
||||
|
||||
if (!service) return Astro.rewrite('/404')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
pageTitle="Edit service"
|
||||
description="Suggest an edit to service"
|
||||
ogImage={{ template: 'generic', title: 'Edit service' }}
|
||||
widthClassName="max-w-screen-md"
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Service suggestions',
|
||||
url: '/service-suggestion',
|
||||
},
|
||||
{
|
||||
name: 'Edit service',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Edit service</h1>
|
||||
|
||||
<ServiceCard service={service} withoutLink class="mb-6" />
|
||||
|
||||
<form method="POST" action={actions.serviceSuggestion.editService} class="space-y-6">
|
||||
<input type="hidden" name="serviceId" value={params.serviceId} />
|
||||
|
||||
<InputTextArea
|
||||
label="Note for Moderators"
|
||||
name="notes"
|
||||
value={params.notes}
|
||||
rows={10}
|
||||
placeholder="List the changes you want us to make to the service. Example: 'Add X, Y and Z attributes' 'Monero is accepted'. Provide supporting evidence."
|
||||
error={inputErrors.notes}
|
||||
/>
|
||||
|
||||
<Captcha action={actions.serviceSuggestion.createService} />
|
||||
|
||||
<InputHoneypotTrap name="message" />
|
||||
|
||||
<InputSubmitButton />
|
||||
</form>
|
||||
</BaseLayout>
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { actions } from 'astro:actions'
|
||||
import { Picture } from 'astro:assets'
|
||||
import { z } from 'astro:content'
|
||||
|
||||
import defaultServiceImage from '../../assets/fallback-service-image.jpg'
|
||||
import Button from '../../components/Button.astro'
|
||||
import TimeFormatted from '../../components/TimeFormatted.astro'
|
||||
import Tooltip from '../../components/Tooltip.astro'
|
||||
import {
|
||||
getServiceSuggestionStatusInfo,
|
||||
serviceSuggestionStatuses,
|
||||
} from '../../constants/serviceSuggestionStatus'
|
||||
import { getServiceSuggestionTypeInfo } from '../../constants/serviceSuggestionType'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { zodEnumFromConstant } from '../../lib/arrays'
|
||||
import { cn } from '../../lib/cn'
|
||||
import { zodParseQueryParamsStoringErrors } from '../../lib/parseUrlFilters'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||
|
||||
const user = Astro.locals.user
|
||||
if (!user) {
|
||||
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to manage service suggestions' }))
|
||||
}
|
||||
|
||||
const { data: filters } = zodParseQueryParamsStoringErrors(
|
||||
{
|
||||
serviceId: z.array(z.number().int().positive()).default([]),
|
||||
status: z.array(zodEnumFromConstant(serviceSuggestionStatuses, 'value')).default([]),
|
||||
},
|
||||
Astro
|
||||
)
|
||||
|
||||
const serviceSuggestions = await Astro.locals.banners.try('Error fetching service suggestions', async () =>
|
||||
prisma.serviceSuggestion.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
service: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
imageUrl: true,
|
||||
verificationStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: filters.serviceId.length > 0 ? { in: filters.serviceId } : undefined,
|
||||
status: filters.status.length > 0 ? { in: filters.status } : undefined,
|
||||
userId: user.id,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (!serviceSuggestions) {
|
||||
return Astro.rewrite('/404')
|
||||
}
|
||||
|
||||
const createResult = Astro.getActionResult(actions.serviceSuggestion.createService)
|
||||
const success = !!createResult && !createResult.error
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
pageTitle="My service suggestions"
|
||||
description="Manage your service suggestions"
|
||||
ogImage={{ template: 'generic', title: 'Service suggestions' }}
|
||||
widthClassName="max-w-screen-md"
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Service suggestions',
|
||||
url: '/service-suggestion',
|
||||
},
|
||||
{
|
||||
name: 'My suggestions',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div
|
||||
class="xs:flex-row xs:flex-wrap xs:justify-between mb-8 flex flex-col items-center justify-center gap-4 text-center sm:mt-8"
|
||||
>
|
||||
<h1 class="font-title text-day-100 text-3xl">Service suggestions</h1>
|
||||
<Button as="a" href="/service-suggestion/new" label="Create" icon="ri:add-line" />
|
||||
</div>
|
||||
|
||||
{
|
||||
success && (
|
||||
<div class="mb-8 rounded-lg border border-green-500/30 bg-green-950 p-4 text-sm text-green-500">
|
||||
<Icon name="ri:check-line" class="mr-2 inline-block size-4 text-green-500" />
|
||||
Service suggestion submitted successfully!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
serviceSuggestions.length === 0 ? (
|
||||
<p class="text-day-400">No suggestions yet.</p>
|
||||
) : (
|
||||
<div class="-mx-4 overflow-x-auto px-4">
|
||||
<div class="grid w-full min-w-min grid-cols-[1fr_auto_auto_auto_auto] place-content-center place-items-center gap-x-4 gap-y-2">
|
||||
<p class="place-self-start">Service</p>
|
||||
<p>Type</p>
|
||||
<p>Status</p>
|
||||
<p>Created</p>
|
||||
<p>Actions</p>
|
||||
|
||||
{serviceSuggestions.map((suggestion) => {
|
||||
const typeInfo = getServiceSuggestionTypeInfo(suggestion.type)
|
||||
const statusInfo = getServiceSuggestionStatusInfo(suggestion.status)
|
||||
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={`/service/${suggestion.service.slug}`}
|
||||
class="inline-flex w-full min-w-32 items-center gap-2 hover:underline"
|
||||
>
|
||||
<Picture
|
||||
src={suggestion.service.imageUrl ?? (defaultServiceImage as unknown as string)}
|
||||
alt={suggestion.service.name}
|
||||
width={32}
|
||||
height={32}
|
||||
class="inline-block size-8 min-w-8 shrink-0 rounded-md"
|
||||
formats={['jxl', 'avif', 'webp']}
|
||||
/>
|
||||
<span class="shrink truncate">{suggestion.service.name}</span>
|
||||
</a>
|
||||
|
||||
<Tooltip
|
||||
as="span"
|
||||
class="inline-flex items-center gap-1"
|
||||
text={typeInfo.label}
|
||||
classNames={{ tooltip: 'md:hidden!' }}
|
||||
>
|
||||
<Icon name={typeInfo.icon} class="size-4" />
|
||||
<span class="hidden md:inline">{typeInfo.label}</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
as="span"
|
||||
text={statusInfo.label}
|
||||
class={cn(
|
||||
'border-night-500 bg-night-800 box-content inline-flex h-8 items-center justify-center gap-1 rounded-full border px-2',
|
||||
statusInfo.iconClass
|
||||
)}
|
||||
classNames={{ tooltip: 'md:hidden!' }}
|
||||
>
|
||||
<Icon name={statusInfo.icon} class="size-4" />
|
||||
<span class="hidden md:inline">{statusInfo.label}</span>
|
||||
</Tooltip>
|
||||
|
||||
<TimeFormatted date={suggestion.createdAt} caseType="sentence" prefix={false} />
|
||||
|
||||
<Button
|
||||
as="a"
|
||||
href={`/service-suggestion/${suggestion.id}`}
|
||||
label="View"
|
||||
icon="ri:eye-line"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</BaseLayout>
|
||||
@@ -1,308 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { actions, isInputError } from 'astro:actions'
|
||||
|
||||
import {
|
||||
SUGGESTION_DESCRIPTION_MAX_LENGTH,
|
||||
SUGGESTION_NAME_MAX_LENGTH,
|
||||
SUGGESTION_NOTES_MAX_LENGTH,
|
||||
SUGGESTION_SLUG_MAX_LENGTH,
|
||||
} from '../../actions/serviceSuggestion'
|
||||
import Captcha from '../../components/Captcha.astro'
|
||||
import InputCardGroup from '../../components/InputCardGroup.astro'
|
||||
import InputCheckboxGroup from '../../components/InputCheckboxGroup.astro'
|
||||
import InputHoneypotTrap from '../../components/InputHoneypotTrap.astro'
|
||||
import InputImageFile from '../../components/InputImageFile.astro'
|
||||
import InputSubmitButton from '../../components/InputSubmitButton.astro'
|
||||
import InputText from '../../components/InputText.astro'
|
||||
import InputTextArea from '../../components/InputTextArea.astro'
|
||||
import { currencies } from '../../constants/currencies'
|
||||
import { kycLevels } from '../../constants/kycLevels'
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||
|
||||
const user = Astro.locals.user
|
||||
if (!user) {
|
||||
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Login to suggest a new service' }))
|
||||
}
|
||||
|
||||
const result = Astro.getActionResult(actions.serviceSuggestion.createService)
|
||||
if (result && !result.error && !result.data.hasDuplicates) {
|
||||
return Astro.redirect(`/service-suggestion/${result.data.serviceSuggestion.id}`)
|
||||
}
|
||||
const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
||||
|
||||
const [categories, attributes] = await Astro.locals.banners.tryMany([
|
||||
[
|
||||
'Failed to fetch categories',
|
||||
() =>
|
||||
prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
icon: true,
|
||||
},
|
||||
}),
|
||||
[],
|
||||
],
|
||||
[
|
||||
'Failed to fetch attributes',
|
||||
() =>
|
||||
prisma.attribute.findMany({
|
||||
orderBy: { category: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
}),
|
||||
[],
|
||||
],
|
||||
])
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
pageTitle="New service"
|
||||
description="Suggest a new service to be added to KYCnot.me"
|
||||
ogImage={{ template: 'generic', title: 'New service' }}
|
||||
widthClassName="max-w-screen-md"
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: 'Service suggestions',
|
||||
url: '/service-suggestion',
|
||||
},
|
||||
{
|
||||
name: 'New service',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Service suggestion</h1>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action={actions.serviceSuggestion.createService}
|
||||
enctype="multipart/form-data"
|
||||
class="space-y-6"
|
||||
>
|
||||
{
|
||||
result?.data?.hasDuplicates && (
|
||||
<>
|
||||
<div class="sm:border-night-300 sm:bg-night-500 mb-16 sm:rounded-lg sm:border sm:p-2 md:p-4">
|
||||
<input type="hidden" name="skipDuplicateCheck" value="true" />
|
||||
|
||||
<h2 class="font-title flex items-center justify-center gap-2 text-2xl font-semibold text-red-500">
|
||||
Possible duplicates found
|
||||
</h2>
|
||||
|
||||
<p class="text-day-400 text-center">Is your service already listed below?</p>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
{result.data.possibleDuplicates.map((duplicate) => {
|
||||
const editServiceUrl = new URL('/service-suggestion/edit', Astro.url)
|
||||
editServiceUrl.searchParams.set('serviceId', duplicate.id.toString())
|
||||
if (result.data.extraNotes) {
|
||||
editServiceUrl.searchParams.set('notes', result.data.extraNotes)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="border-night-400 bg-night-600 flex gap-4 rounded-lg border p-4 shadow-sm">
|
||||
<div class="grow">
|
||||
<p class="text-day-100 mb-1 text-lg font-medium">{duplicate.name}</p>
|
||||
<p class="text-day-300 mb-3 line-clamp-2 text-sm">{duplicate.description}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-col justify-center gap-2">
|
||||
<a
|
||||
href={`/service/${duplicate.slug}`}
|
||||
target="_blank"
|
||||
class="text-day-300 bg-night-300 hover:bg-night-400 flex items-center gap-1 rounded px-2.5 py-1.5 text-sm font-medium transition-colors"
|
||||
>
|
||||
<Icon name="ri:external-link-line" class="size-4 shrink-0" />
|
||||
View
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={editServiceUrl.toString()}
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 rounded bg-green-600 px-2.5 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-700"
|
||||
>
|
||||
<Icon name="ri:edit-line" class="size-4 shrink-0" />
|
||||
Submit as edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="bg-night-400 mt-4 hidden h-px w-full sm:block" />
|
||||
|
||||
<div class="mt-4 flex items-center gap-2 px-4">
|
||||
<p class="text-day-200 flex-1">None of these match?</p>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-day-700 text-day-100 hover:bg-day-600 flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium"
|
||||
>
|
||||
<Icon name="ri:send-plane-2-line" class="size-4" />
|
||||
Submit anyway
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="font-title mt-12 mb-6 block text-center text-3xl font-bold sm:hidden">
|
||||
Review your suggestion
|
||||
</h1>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<InputText
|
||||
label="Name"
|
||||
name="name"
|
||||
inputProps={{
|
||||
required: true,
|
||||
maxlength: SUGGESTION_NAME_MAX_LENGTH,
|
||||
'data-generate-slug': true,
|
||||
}}
|
||||
error={inputErrors.name}
|
||||
/>
|
||||
<InputText
|
||||
label="Slug"
|
||||
name="slug"
|
||||
description="Auto-generated from name. Only lowercase letters, numbers, and hyphens."
|
||||
error={inputErrors.slug}
|
||||
inputProps={{
|
||||
required: true,
|
||||
pattern: '^[a-z0-9\\-]+$',
|
||||
placeholder: 'my-service-name',
|
||||
minlength: 1,
|
||||
maxlength: SUGGESTION_SLUG_MAX_LENGTH,
|
||||
'data-generate-slug': true,
|
||||
}}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Description"
|
||||
name="description"
|
||||
id="description"
|
||||
required
|
||||
maxlength={SUGGESTION_DESCRIPTION_MAX_LENGTH}
|
||||
error={inputErrors.description}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Service URLs"
|
||||
name="serviceUrls"
|
||||
required
|
||||
placeholder="https://example1.com\nhttps://example2.org"
|
||||
error={inputErrors.serviceUrls}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Terms of Service URLs"
|
||||
name="tosUrls"
|
||||
required
|
||||
placeholder="https://example1.com/tos\nhttps://example2.org/terms"
|
||||
error={inputErrors.tosUrls}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Onion URLs"
|
||||
name="onionUrls"
|
||||
placeholder="http://example1.onion\nhttp://example2.onion"
|
||||
error={inputErrors.onionUrls}
|
||||
/>
|
||||
|
||||
<InputCardGroup
|
||||
name="kycLevel"
|
||||
label="KYC Level"
|
||||
options={kycLevels.map((kycLevel) => ({
|
||||
label: kycLevel.name,
|
||||
value: kycLevel.id.toString(),
|
||||
icon: kycLevel.icon,
|
||||
description: `${kycLevel.description}\n\n_KYC Level ${kycLevel.value}/4_`,
|
||||
}))}
|
||||
iconSize="md"
|
||||
cardSize="md"
|
||||
required
|
||||
error={inputErrors.kycLevel}
|
||||
/>
|
||||
|
||||
<InputCheckboxGroup
|
||||
name="categories"
|
||||
label="Categories"
|
||||
required
|
||||
options={categories.map((category) => ({
|
||||
label: category.name,
|
||||
value: category.id.toString(),
|
||||
icon: category.icon,
|
||||
}))}
|
||||
error={inputErrors.categories}
|
||||
/>
|
||||
|
||||
<InputCheckboxGroup
|
||||
name="attributes"
|
||||
label="Attributes"
|
||||
options={attributes.map((attribute) => ({
|
||||
label: attribute.title,
|
||||
value: attribute.id.toString(),
|
||||
}))}
|
||||
error={inputErrors.attributes}
|
||||
/>
|
||||
|
||||
<InputCardGroup
|
||||
name="acceptedCurrencies"
|
||||
label="Accepted Currencies"
|
||||
options={currencies.map((currency) => ({
|
||||
label: currency.name,
|
||||
value: currency.id,
|
||||
icon: currency.icon,
|
||||
}))}
|
||||
error={inputErrors.acceptedCurrencies}
|
||||
required
|
||||
multiple
|
||||
/>
|
||||
|
||||
<InputImageFile
|
||||
label="Service Image"
|
||||
name="imageFile"
|
||||
description="Square image. At least 192x192px. Transparency supported."
|
||||
error={inputErrors.imageFile}
|
||||
square
|
||||
required
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Note for Moderators"
|
||||
name="notes"
|
||||
id="notes"
|
||||
error={inputErrors.notes}
|
||||
maxlength={SUGGESTION_NOTES_MAX_LENGTH}
|
||||
/>
|
||||
|
||||
<Captcha action={actions.serviceSuggestion.createService} />
|
||||
|
||||
<InputHoneypotTrap name="message" />
|
||||
|
||||
<InputSubmitButton label={result?.data?.hasDuplicates ? 'Submit anyway' : 'Submit'} />
|
||||
</form>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
const triggerInputs = document.querySelectorAll<HTMLInputElement>('[data-generate-slug] input')
|
||||
const slugInputs = document.querySelectorAll<HTMLInputElement>('input[name="slug"]')
|
||||
|
||||
triggerInputs.forEach((triggerInput) => {
|
||||
triggerInput.addEventListener('input', (event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
slugInputs.forEach((slugInput) => {
|
||||
slugInput.value = target.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user