--- import { ServiceVisibility, VerificationStatus, type Prisma } from '@prisma/client' import { z } from 'astro/zod' import { Icon } from 'astro-icon/components' import MyPicture from '../../../components/MyPicture.astro' import SortArrowIcon from '../../../components/SortArrowIcon.astro' import { getKycLevelInfo } from '../../../constants/kycLevels' import { getVerificationStatusInfo } from '../../../constants/verificationStatus' import BaseLayout from '../../../layouts/BaseLayout.astro' import { cn } from '../../../lib/cn' import { zodParseQueryParamsStoringErrors } from '../../../lib/parseUrlFilters' import { prisma } from '../../../lib/prisma' const { data: filters } = zodParseQueryParamsStoringErrors( { search: z.string().optional(), verificationStatus: z.nativeEnum(VerificationStatus).optional(), visibility: z.nativeEnum(ServiceVisibility).optional(), sort: z.enum(['name', 'createdAt', 'overallScore', 'verificationRequests']).default('name'), order: z.enum(['asc', 'desc']).default('asc'), page: z.coerce.number().int().positive().optional().default(1), }, Astro ) const itemsPerPage = 20 const sortProperty = filters.sort const sortDirection = filters.order let orderBy: Prisma.ServiceOrderByWithRelationInput switch (sortProperty) { case 'verificationRequests': orderBy = { verificationRequests: { _count: sortDirection } } break case 'createdAt': // createdAt can be ambiguous without a specific direction case 'name': case 'overallScore': orderBy = { [sortProperty]: sortDirection } break default: orderBy = { name: 'asc' } // Default sort } const whereClause: Prisma.ServiceWhereInput = { ...(filters.search ? { OR: [ { name: { contains: filters.search, mode: 'insensitive' } }, { description: { contains: filters.search, mode: 'insensitive' } }, { serviceUrls: { has: filters.search } }, { tosUrls: { has: filters.search } }, { onionUrls: { has: filters.search } }, { i2pUrls: { has: filters.search } }, ], } : {}), verificationStatus: filters.verificationStatus ?? undefined, serviceVisibility: filters.visibility ?? undefined, } const totalServicesCount = await Astro.locals.banners.try( 'Error counting services', async () => prisma.service.count({ where: whereClause }), 0 ) const totalPages = Math.ceil(totalServicesCount / itemsPerPage) const validPage = Math.max(1, Math.min(filters.page, totalPages || 1)) const skip = (validPage - 1) * itemsPerPage const services = await Astro.locals.banners.try( 'Error fetching services', async () => prisma.service.findMany({ where: whereClause, select: { id: true, name: true, slug: true, description: true, kycLevel: true, overallScore: true, privacyScore: true, trustScore: true, verificationStatus: true, imageUrl: true, serviceUrls: true, tosUrls: true, onionUrls: true, i2pUrls: true, serviceVisibility: true, createdAt: true, categories: { select: { id: true, name: true, icon: true, }, }, attributes: { include: { attribute: true, }, }, _count: { select: { verificationRequests: true, }, }, }, orderBy, take: itemsPerPage, skip, }), [] ) const servicesWithInfo = services.map((service) => { const verificationStatusInfo = getVerificationStatusInfo(service.verificationStatus) const kycLevelInfo = getKycLevelInfo(String(service.kycLevel)) return { ...service, verificationStatusInfo, kycLevelInfo, kycColor: service.kycLevel === 0 ? '22, 163, 74' // green-600 : service.kycLevel === 1 ? '180, 83, 9' // amber-700 : service.kycLevel === 2 ? '220, 38, 38' // red-600 : service.kycLevel === 3 ? '185, 28, 28' // red-700 : service.kycLevel === 4 ? '153, 27, 27' // red-800 : '107, 114, 128', // gray-500 fallback formattedDate: new Date(service.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }), } }) const makeSortUrl = (slug: NonNullable<(typeof filters)['sort']>) => { const newOrder = filters.sort === slug && filters.order === 'asc' ? 'desc' : 'asc' const searchParams = new URLSearchParams(Astro.url.search) searchParams.set('sort', slug) searchParams.set('order', newOrder) return `/admin/services?${searchParams.toString()}` } const getPaginationUrl = (pageNum: number) => { const url = new URL(Astro.url) url.searchParams.set('page', pageNum.toString()) return url.toString() } const truncate = (text: string, length: number) => { if (text.length <= length) return text return text.substring(0, length) + '...' } ---

Service Management

{totalServicesCount} services New Service

Services List

Scroll horizontally to see more →
{ servicesWithInfo.map((service) => ( )) }
Service KYC Score Status Reqs Date Actions
{service.name}
{truncate(service.description, 45)}
{service.categories.slice(0, 2).map((category) => ( {category.name} ))} {service.categories.length > 2 && ( +{service.categories.length - 2} )}
{service.kycLevel}
= 7, 'text-yellow-400': service.overallScore >= 4 && service.overallScore < 7, 'text-red-400': service.overallScore < 4, })} > {service.overallScore}
{service.privacyScore} {service.trustScore}
{service.verificationStatus.substring(0, 4)}
{service.serviceVisibility}
{service._count.verificationRequests} {service.formattedDate}
{/* Pagination controls */} { totalPages > 1 && (
Showing {services.length > 0 ? skip + 1 : 0} to {Math.min(skip + itemsPerPage, totalServicesCount)}{' '} of {totalServicesCount} services
) }