Compare commits
2 Commits
release-38
...
release-40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da12e8de79 | ||
|
|
ea40f17d3c |
@@ -916,7 +916,7 @@ const specialUsersData = {
|
||||
verifiedLink: 'https://kycnot.me',
|
||||
totalKarma: 1001,
|
||||
link: 'https://kycnot.me',
|
||||
picture: 'https://comments.kycnot.me/api/users/549f290e-0542-4c18-b437-5b64b35758f0/avatar?size=L',
|
||||
picture: 'https://kycnot.me/files/users/pictures/c277dc0f2f.png',
|
||||
},
|
||||
moderator: {
|
||||
name: 'moderator_dev',
|
||||
@@ -928,7 +928,7 @@ const specialUsersData = {
|
||||
verifiedLink: 'https://kycnot.me',
|
||||
totalKarma: 1001,
|
||||
link: 'https://kycnot.me',
|
||||
picture: 'https://comments.kycnot.me/api/users/549f290e-0542-4c18-b437-5b64b35758f0/avatar?size=L',
|
||||
picture: 'https://kycnot.me/files/users/pictures/c277dc0f2f.png',
|
||||
},
|
||||
verified: {
|
||||
name: 'verified_dev',
|
||||
|
||||
@@ -6,12 +6,8 @@ import slugify from 'slugify'
|
||||
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
||||
import { saveFileLocally } from '../../lib/fileStorage'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import {
|
||||
imageFileSchema,
|
||||
stringListOfUrlsSchema,
|
||||
stringListOfUrlsSchemaRequired,
|
||||
zodCohercedNumber,
|
||||
} from '../../lib/zodUtils'
|
||||
import { separateServiceUrlsByType } from '../../lib/urls'
|
||||
import { imageFileSchema, stringListOfUrlsSchemaRequired, zodCohercedNumber } from '../../lib/zodUtils'
|
||||
|
||||
const serviceSchemaBase = z.object({
|
||||
id: z.number().int().positive(),
|
||||
@@ -19,11 +15,10 @@ const serviceSchemaBase = z.object({
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]+$/, 'Allowed characters: lowercase letters, numbers, and hyphens')
|
||||
.optional(),
|
||||
name: z.string().min(1).max(20),
|
||||
name: z.string().min(1).max(40),
|
||||
description: z.string().min(1),
|
||||
serviceUrls: stringListOfUrlsSchemaRequired,
|
||||
allServiceUrls: stringListOfUrlsSchemaRequired,
|
||||
tosUrls: stringListOfUrlsSchemaRequired,
|
||||
onionUrls: stringListOfUrlsSchema,
|
||||
kycLevel: z.coerce.number().int().min(0).max(4),
|
||||
attributes: z.array(z.coerce.number().int().positive()),
|
||||
categories: z.array(z.coerce.number().int().positive()).min(1),
|
||||
@@ -85,13 +80,20 @@ export const adminServiceActions = {
|
||||
? await saveFileLocally(input.imageFile, input.imageFile.name)
|
||||
: undefined
|
||||
|
||||
const {
|
||||
web: serviceUrls,
|
||||
onion: onionUrls,
|
||||
i2p: i2pUrls,
|
||||
} = separateServiceUrlsByType(input.allServiceUrls)
|
||||
|
||||
const service = await prisma.service.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
serviceUrls: input.serviceUrls,
|
||||
serviceUrls,
|
||||
tosUrls: input.tosUrls,
|
||||
onionUrls: input.onionUrls,
|
||||
onionUrls,
|
||||
i2pUrls,
|
||||
kycLevel: input.kycLevel,
|
||||
verificationStatus: input.verificationStatus,
|
||||
verificationSummary: input.verificationSummary,
|
||||
@@ -187,14 +189,21 @@ export const adminServiceActions = {
|
||||
const attributesToAdd = input.attributes.filter((aId) => !existingAttributeIds.includes(aId))
|
||||
const attributesToRemove = existingAttributeIds.filter((aId) => !input.attributes.includes(aId))
|
||||
|
||||
const {
|
||||
web: serviceUrls,
|
||||
onion: onionUrls,
|
||||
i2p: i2pUrls,
|
||||
} = separateServiceUrlsByType(input.allServiceUrls)
|
||||
|
||||
const service = await prisma.service.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
serviceUrls: input.serviceUrls,
|
||||
serviceUrls,
|
||||
tosUrls: input.tosUrls,
|
||||
onionUrls: input.onionUrls,
|
||||
onionUrls,
|
||||
i2pUrls,
|
||||
kycLevel: input.kycLevel,
|
||||
verificationStatus: input.verificationStatus,
|
||||
verificationSummary: input.verificationSummary,
|
||||
|
||||
@@ -14,12 +14,8 @@ import { defineProtectedAction } from '../lib/defineProtectedAction'
|
||||
import { saveFileLocally } from '../lib/fileStorage'
|
||||
import { handleHoneypotTrap } from '../lib/honeypot'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import {
|
||||
imageFileSchemaRequired,
|
||||
stringListOfUrlsSchema,
|
||||
stringListOfUrlsSchemaRequired,
|
||||
zodCohercedNumber,
|
||||
} from '../lib/zodUtils'
|
||||
import { separateServiceUrlsByType } from '../lib/urls'
|
||||
import { imageFileSchemaRequired, stringListOfUrlsSchemaRequired, zodCohercedNumber } from '../lib/zodUtils'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
@@ -161,9 +157,8 @@ export const serviceSuggestionActions = {
|
||||
{ message: 'Slug must be unique, try a different one' }
|
||||
),
|
||||
description: z.string().min(1).max(SUGGESTION_DESCRIPTION_MAX_LENGTH),
|
||||
serviceUrls: stringListOfUrlsSchemaRequired,
|
||||
allServiceUrls: stringListOfUrlsSchemaRequired,
|
||||
tosUrls: stringListOfUrlsSchemaRequired,
|
||||
onionUrls: stringListOfUrlsSchema,
|
||||
kycLevel: zodCohercedNumber(z.coerce.number().int().min(0).max(4)),
|
||||
attributes: z.array(z.coerce.number().int().positive()),
|
||||
categories: z.array(z.coerce.number().int().positive()).min(1),
|
||||
@@ -210,6 +205,12 @@ export const serviceSuggestionActions = {
|
||||
|
||||
const imageUrl = await saveFileLocally(input.imageFile, input.imageFile.name)
|
||||
|
||||
const {
|
||||
web: serviceUrls,
|
||||
onion: onionUrls,
|
||||
i2p: i2pUrls,
|
||||
} = separateServiceUrlsByType(input.allServiceUrls)
|
||||
|
||||
const { serviceSuggestion, service } = await prisma.$transaction(async (tx) => {
|
||||
const serviceSelect = {
|
||||
id: true,
|
||||
@@ -221,9 +222,10 @@ export const serviceSuggestionActions = {
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
serviceUrls: input.serviceUrls,
|
||||
serviceUrls,
|
||||
tosUrls: input.tosUrls,
|
||||
onionUrls: input.onionUrls,
|
||||
onionUrls,
|
||||
i2pUrls,
|
||||
kycLevel: input.kycLevel,
|
||||
acceptedCurrencies: input.acceptedCurrencies,
|
||||
imageUrl,
|
||||
|
||||
@@ -21,7 +21,6 @@ type Props = HTMLAttributes<'div'> & {
|
||||
pageSize: number
|
||||
sortSeed?: string
|
||||
filters: ServicesFiltersObject
|
||||
includeScams: boolean
|
||||
countCommunityOnly: number | null
|
||||
inlineIcons?: boolean
|
||||
}
|
||||
@@ -35,15 +34,12 @@ const {
|
||||
sortSeed,
|
||||
class: className,
|
||||
filters,
|
||||
includeScams,
|
||||
countCommunityOnly,
|
||||
inlineIcons,
|
||||
...divProps
|
||||
} = Astro.props
|
||||
|
||||
const hasScams =
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
filters.verification.includes('VERIFICATION_FAILED') || includeScams
|
||||
const hasScams = filters.verification.includes('VERIFICATION_FAILED')
|
||||
const hasSomeScam = !!services?.some((service) => service.verificationStatus.includes('VERIFICATION_FAILED'))
|
||||
|
||||
const hasCommunityContributed = filters.verification.includes('COMMUNITY_CONTRIBUTED')
|
||||
|
||||
@@ -113,3 +113,28 @@ export function urlDomain(url: URL | string) {
|
||||
}
|
||||
return url.origin
|
||||
}
|
||||
|
||||
export function separateServiceUrlsByType(allServiceUrls: string[]) {
|
||||
const result: {
|
||||
web: string[]
|
||||
onion: string[]
|
||||
i2p: string[]
|
||||
} = {
|
||||
web: [],
|
||||
onion: [],
|
||||
i2p: [],
|
||||
}
|
||||
|
||||
for (const url of allServiceUrls) {
|
||||
const parsedUrl = new URL(url)
|
||||
if (parsedUrl.origin.endsWith('.onion')) {
|
||||
result.onion.push(url)
|
||||
} else if (parsedUrl.origin.endsWith('.b32.i2p')) {
|
||||
result.i2p.push(url)
|
||||
} else {
|
||||
result.web.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const zodUrlOptionalProtocol = z.preprocess(
|
||||
const cleanInput = input.trim().replace(/\/$/, '')
|
||||
return !/^\w+:\/\//i.test(cleanInput) ? `https://${cleanInput}` : cleanInput
|
||||
},
|
||||
z.string().refine((value) => /^(https?):\/\/(?=.*\.[a-z]{2,})[^\s$.?#].[^\s]*$/i.test(value), {
|
||||
z.string().refine((value) => /^(https?):\/\/(?=.*\.[a-z0-9]{2,})[^\s$.?#].[^\s]*$/i.test(value), {
|
||||
message: 'Invalid URL',
|
||||
})
|
||||
)
|
||||
|
||||
@@ -201,6 +201,41 @@ Some reviews may be spam or fake. Read comments carefully and **always do your o
|
||||
|
||||
To **see comments waiting for moderation**, toggle the switch in the comments section. These comments show up with a yellow background and a "pending" label.
|
||||
|
||||
## API
|
||||
|
||||
Access basic service data via our public API.
|
||||
|
||||
**Attribution:** Please credit **KYCnot.me** if you use data from this API.
|
||||
|
||||
### `GET /api/v1/service/[id]`
|
||||
|
||||
Fetches details for a single service.
|
||||
|
||||
- **`[id]`**: Can be a service ID, slug, name, or any registered URL (including .onion/.i2p).
|
||||
|
||||
**Example Requests:**
|
||||
|
||||
```
|
||||
/api/v1/service/bisq
|
||||
/api/v1/service/https://bisq.network
|
||||
```
|
||||
|
||||
**Example Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Bisq",
|
||||
"description": "Decentralized Bitcoin exchange network.",
|
||||
"kycLevel": 0,
|
||||
"categories": ["exchange"],
|
||||
"serviceUrls": ["https://bisq.network/"],
|
||||
"onionUrls": [],
|
||||
"i2pUrls": [],
|
||||
"tosUrls": ["https://bisq.network/terms-of-service/"],
|
||||
"kycnotmeUrl": "https://kycnot.me/service/bisq"
|
||||
}
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you like this project, you can **support** it through these methods:
|
||||
|
||||
@@ -233,16 +233,30 @@ if (!service) return Astro.rewrite('/404')
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input type="hidden" name="id" value={service.id} />
|
||||
<InputText
|
||||
label="Name"
|
||||
name="name"
|
||||
inputProps={{
|
||||
required: true,
|
||||
value: service.name,
|
||||
}}
|
||||
error={serviceInputErrors.name}
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
|
||||
<InputText
|
||||
label="Name"
|
||||
name="name"
|
||||
inputProps={{
|
||||
required: true,
|
||||
value: service.name,
|
||||
}}
|
||||
error={serviceInputErrors.name}
|
||||
/>
|
||||
|
||||
<InputText
|
||||
label="Slug"
|
||||
description="Auto-generated if empty"
|
||||
name="slug"
|
||||
inputProps={{
|
||||
value: service.slug,
|
||||
class: 'font-title',
|
||||
}}
|
||||
error={serviceInputErrors.slug}
|
||||
class="font-title"
|
||||
/>
|
||||
</div>
|
||||
<InputTextArea
|
||||
label="Description"
|
||||
name="description"
|
||||
@@ -254,76 +268,44 @@ if (!service) return Astro.rewrite('/404')
|
||||
error={serviceInputErrors.description}
|
||||
/>
|
||||
|
||||
<InputText
|
||||
label="Slug"
|
||||
description="Auto-generated if empty"
|
||||
name="slug"
|
||||
inputProps={{
|
||||
value: service.slug,
|
||||
class: 'font-title',
|
||||
}}
|
||||
error={serviceInputErrors.slug}
|
||||
class="font-title"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 gap-x-4 gap-y-6 md:grid-cols-2">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InputTextArea
|
||||
label="Service URLs"
|
||||
description="One per line"
|
||||
name="serviceUrls"
|
||||
description="One per line. Accepts **Web**, **Onion**, and **I2P** URLs."
|
||||
name="allServiceUrls"
|
||||
inputProps={{
|
||||
rows: 3,
|
||||
placeholder: 'https://example1.com\nhttps://example2.com',
|
||||
placeholder: 'https://example1.com\nhttps://example2.onion\nhttps://example3.b32.i2p',
|
||||
class: 'grow min-h-24',
|
||||
required: true,
|
||||
}}
|
||||
value={service.serviceUrls.join('\n')}
|
||||
error={serviceInputErrors.serviceUrls}
|
||||
class="row-span-2 flex flex-col self-stretch"
|
||||
value={[...service.serviceUrls, ...service.onionUrls, ...service.i2pUrls].join('\n\n')}
|
||||
error={serviceInputErrors.allServiceUrls}
|
||||
/>
|
||||
<InputTextArea
|
||||
label="ToS URLs"
|
||||
description="One per line"
|
||||
name="tosUrls"
|
||||
inputProps={{
|
||||
rows: 3,
|
||||
placeholder: 'https://example1.com/tos\nhttps://example2.com/tos',
|
||||
required: true,
|
||||
}}
|
||||
value={service.tosUrls.join('\n')}
|
||||
error={serviceInputErrors.tosUrls}
|
||||
/>
|
||||
<InputTextArea
|
||||
label="Onion URLs"
|
||||
description="One per line"
|
||||
name="onionUrls"
|
||||
<InputText
|
||||
label="Referral link path"
|
||||
name="referral"
|
||||
inputProps={{
|
||||
rows: 3,
|
||||
placeholder: 'http://example1.onion\nhttp://example2.onion',
|
||||
value: service.referral,
|
||||
placeholder: 'e.g. ?ref=123 or /ref/123',
|
||||
}}
|
||||
value={service.onionUrls.join('\n')}
|
||||
error={serviceInputErrors.onionUrls}
|
||||
/>
|
||||
<InputTextArea
|
||||
label="I2P URLs"
|
||||
description="One per line"
|
||||
name="i2pUrls"
|
||||
inputProps={{
|
||||
rows: 3,
|
||||
placeholder: 'http://example1.b32.i2p\nhttp://example2.b32.i2p',
|
||||
}}
|
||||
value={service.i2pUrls.join('\n')}
|
||||
error={serviceInputErrors.referral}
|
||||
class="self-end"
|
||||
description="Will be appended to the service URL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InputText
|
||||
label="Referral link path"
|
||||
name="referral"
|
||||
inputProps={{
|
||||
value: service.referral,
|
||||
placeholder: 'e.g. ?ref=123 or /ref/123',
|
||||
}}
|
||||
error={serviceInputErrors.referral}
|
||||
description="Will be appended to the service URL"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<InputImageFile
|
||||
label="Image"
|
||||
|
||||
@@ -74,19 +74,19 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="serviceUrls" class="font-title mb-2 block text-sm text-green-500">serviceUrls</label>
|
||||
<label for="allServiceUrls" class="font-title mb-2 block text-sm text-green-500">Service URLs</label>
|
||||
<textarea
|
||||
transition:persist
|
||||
class="font-title w-full rounded-md border border-green-500/30 bg-black/50 p-2 text-gray-300 placeholder-gray-500 focus:border-green-500 focus:ring-green-500"
|
||||
name="serviceUrls"
|
||||
id="serviceUrls"
|
||||
name="allServiceUrls"
|
||||
id="allServiceUrls"
|
||||
rows={3}
|
||||
placeholder="https://example1.com https://example2.com"
|
||||
placeholder="https://example1.com\nhttps://example2.onion\nhttps://example3.b32.i2p"
|
||||
set:text=""
|
||||
/>
|
||||
{
|
||||
inputErrors.serviceUrls && (
|
||||
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.serviceUrls.join(', ')}</p>
|
||||
inputErrors.allServiceUrls && (
|
||||
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.allServiceUrls.join(', ')}</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -109,24 +109,6 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="onionUrls" class="font-title mb-2 block text-sm text-green-500">onionUrls</label>
|
||||
<textarea
|
||||
transition:persist
|
||||
class="font-title w-full rounded-md border border-green-500/30 bg-black/50 p-2 text-gray-300 placeholder-gray-500 focus:border-green-500 focus:ring-green-500"
|
||||
name="onionUrls"
|
||||
id="onionUrls"
|
||||
rows={3}
|
||||
placeholder="http://example.onion"
|
||||
set:text=""
|
||||
/>
|
||||
{
|
||||
inputErrors.onionUrls && (
|
||||
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.onionUrls.join(', ')}</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="imageFile" class="font-title mb-2 block text-sm text-green-500">serviceImage</label>
|
||||
<div class="space-y-2">
|
||||
|
||||
121
web/src/pages/api/v1/service/[...id].ts
Normal file
121
web/src/pages/api/v1/service/[...id].ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { prisma } from '../../../../lib/prisma'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
const MAX_ID_LENGTH = 2048
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const { id } = params
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ error: 'ID parameter is missing' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (id.length > MAX_ID_LENGTH) {
|
||||
return new Response(JSON.stringify({ error: 'ID parameter is too long' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const orConditions: Prisma.ServiceWhereInput[] = [
|
||||
{ slug: id },
|
||||
{ name: id },
|
||||
{ serviceUrls: { has: id } },
|
||||
{ onionUrls: { has: id } },
|
||||
{ i2pUrls: { has: id } },
|
||||
]
|
||||
|
||||
// Try direct ID lookup first
|
||||
const numericId = parseInt(id, 10)
|
||||
|
||||
if (!isNaN(numericId)) {
|
||||
orConditions.push({ id: numericId })
|
||||
}
|
||||
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
let alternativeId: string
|
||||
if (id.endsWith('/')) {
|
||||
alternativeId = id.slice(0, -1) // Remove trailing slash
|
||||
} else {
|
||||
alternativeId = id + '/' // Add trailing slash
|
||||
}
|
||||
orConditions.push({ serviceUrls: { has: alternativeId } })
|
||||
orConditions.push({ onionUrls: { has: alternativeId } })
|
||||
orConditions.push({ i2pUrls: { has: alternativeId } })
|
||||
} else {
|
||||
// For non-HTTP/S IDs, check as is (could be a direct onion/i2p address without protocol)
|
||||
orConditions.push({ serviceUrls: { has: id } })
|
||||
orConditions.push({ onionUrls: { has: id } })
|
||||
orConditions.push({ i2pUrls: { has: id } })
|
||||
}
|
||||
|
||||
try {
|
||||
const service = await prisma.service.findFirst({
|
||||
where: {
|
||||
OR: orConditions,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
kycLevel: true,
|
||||
categories: {
|
||||
select: {
|
||||
name: true,
|
||||
slug: true,
|
||||
icon: true,
|
||||
},
|
||||
},
|
||||
serviceUrls: true,
|
||||
onionUrls: true,
|
||||
i2pUrls: true,
|
||||
tosUrls: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!service) {
|
||||
return new Response(JSON.stringify({ error: 'Service not found' }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
kycLevel: service.kycLevel,
|
||||
categories: service.categories.map((category) => category.slug),
|
||||
serviceUrls: service.serviceUrls,
|
||||
onionUrls: service.onionUrls,
|
||||
i2pUrls: service.i2pUrls,
|
||||
tosUrls: service.tosUrls,
|
||||
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(responseData), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching service:', error)
|
||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -712,7 +712,6 @@ const showFiltersId = 'show-filters'
|
||||
pageSize={PAGE_SIZE}
|
||||
sortSeed={filters['sort-seed']}
|
||||
filters={filters}
|
||||
includeScams={includeScams}
|
||||
countCommunityOnly={countCommunityOnly}
|
||||
inlineIcons
|
||||
/>
|
||||
|
||||
@@ -201,34 +201,31 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
|
||||
error={inputErrors.description}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Service URLs"
|
||||
name="serviceUrls"
|
||||
inputProps={{
|
||||
required: true,
|
||||
placeholder: 'https://example1.com\nhttps://example2.org',
|
||||
}}
|
||||
error={inputErrors.serviceUrls}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Terms of Service URLs"
|
||||
name="tosUrls"
|
||||
inputProps={{
|
||||
required: true,
|
||||
placeholder: 'https://example1.com/tos\nhttps://example2.org/terms',
|
||||
}}
|
||||
error={inputErrors.tosUrls}
|
||||
/>
|
||||
|
||||
<InputTextArea
|
||||
label="Onion URLs"
|
||||
name="onionUrls"
|
||||
inputProps={{
|
||||
placeholder: 'http://example1.onion\nhttp://example2.onion',
|
||||
}}
|
||||
error={inputErrors.onionUrls}
|
||||
/>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InputTextArea
|
||||
label="Service URLs"
|
||||
description="One per line. Accepts **Web**, **Onion**, and **I2P** URLs."
|
||||
name="allServiceUrls"
|
||||
inputProps={{
|
||||
placeholder: 'https://example1.com\nhttps://example2.onion\nhttps://example3.b32.i2p',
|
||||
class: 'min-h-24',
|
||||
required: true,
|
||||
}}
|
||||
class="row-span-2 flex flex-col self-stretch"
|
||||
error={inputErrors.allServiceUrls}
|
||||
/>
|
||||
<InputTextArea
|
||||
label="ToS URLs"
|
||||
description="One per line"
|
||||
name="tosUrls"
|
||||
inputProps={{
|
||||
placeholder: 'https://example1.com/tos\nhttps://example2.com/tos',
|
||||
class: 'md:min-h-24',
|
||||
required: true,
|
||||
}}
|
||||
error={inputErrors.tosUrls}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InputCardGroup
|
||||
name="kycLevel"
|
||||
|
||||
Reference in New Issue
Block a user