Release 202506061009

This commit is contained in:
pluja
2025-06-06 10:09:59 +00:00
parent 2489e94b0e
commit 8b90b3eef6
12 changed files with 881 additions and 696 deletions

View File

@@ -18,6 +18,7 @@ services:
pyworker: pyworker:
build: build:
context: ./pyworker context: ./pyworker
image: kycnotme/pyworker:${PYWORKER_IMAGE_TAG:-latest}
restart: always restart: always
env_file: env_file:
- .env - .env

View File

@@ -184,16 +184,16 @@ Be concise but thorough, and make sure your output is properly formatted JSON.
PROMPT_COMMENT_SENTIMENT_SUMMARY = """ PROMPT_COMMENT_SENTIMENT_SUMMARY = """
You will be given a list of user comments to a service. You will be given a list of user comments to a service.
Your task is to summarize the comments in a way that is easy to understand and to the point. Your task is to summarize the comments in a way that is easy to understand and to the point.
The summary should be concise and to the point, no more than 150 words. The summary should be concise and to the point, no more than 100 words. Keep it short and concise.
Use markdown formatting to highlight in bold the most important information. Only bold is allowed. Use markdown formatting to highlight in bold the most important information. Only bold is allowed.
You must format your response as a valid JSON object with the following structure: You must format your response as a valid JSON object with the following structure:
interface CommentSummary { interface CommentSummary {
summary: string; summary: string; // Concise, 100 words max
sentiment: 'positive'|'negative'|'neutral'; sentiment: 'positive'|'negative'|'neutral';
whatUsersLike: string[]; // Concise, 2-3 words, max 4 whatUsersLike: string[]; // Concise, 2-3 words max
whatUsersDislike: string[]; // Concise, 2-3 words, max 4 whatUsersDislike: string[]; // Concise, 2-3 words max
} }
Always avoid repeating information in the list of what users like or dislike. Also, make sure you keep the summary short and concise, no more than 150 words. Ignore irrelevant comments. Make an item for each like/dislike, avoid something like 'No logs / Audited', it should be 'No logs' and 'Audited' as separate items. Always avoid repeating information in the list of what users like or dislike. Also, make sure you keep the summary short and concise, no more than 150 words. Ignore irrelevant comments. Make an item for each like/dislike, avoid something like 'No logs / Audited', it should be 'No logs' and 'Audited' as separate items.

1265
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,19 +23,19 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "0.9.4", "@astrojs/check": "0.9.4",
"@astrojs/db": "0.14.14", "@astrojs/db": "0.15.0",
"@astrojs/mdx": "4.2.6", "@astrojs/mdx": "4.3.0",
"@astrojs/node": "9.2.1", "@astrojs/node": "9.2.2",
"@astrojs/sitemap": "3.4.0", "@astrojs/sitemap": "3.4.1",
"@fontsource-variable/space-grotesk": "5.2.7", "@fontsource-variable/space-grotesk": "5.2.8",
"@fontsource/inter": "5.2.5", "@fontsource/inter": "5.2.5",
"@fontsource/space-grotesk": "5.2.7", "@fontsource/space-grotesk": "5.2.8",
"@prisma/client": "6.8.2", "@prisma/client": "6.9.0",
"@tailwindcss/vite": "4.1.7", "@tailwindcss/vite": "4.1.8",
"@types/mime-types": "2.1.4", "@types/mime-types": "3.0.0",
"@types/pg": "8.15.4", "@types/pg": "8.15.4",
"@vercel/og": "0.6.8", "@vercel/og": "0.6.8",
"astro": "5.7.13", "astro": "5.9.0",
"astro-loading-indicator": "0.7.0", "astro-loading-indicator": "0.7.0",
"astro-remote": "0.3.4", "astro-remote": "0.3.4",
"astro-seo-schema": "5.0.0", "astro-seo-schema": "5.0.0",
@@ -43,59 +43,59 @@
"clsx": "2.1.1", "clsx": "2.1.1",
"htmx.org": "1.9.12", "htmx.org": "1.9.12",
"javascript-time-ago": "2.5.11", "javascript-time-ago": "2.5.11",
"libphonenumber-js": "1.12.8", "libphonenumber-js": "1.12.9",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"mime-types": "3.0.1", "mime-types": "3.0.1",
"object-to-formdata": "4.5.1", "object-to-formdata": "4.5.1",
"pg": "8.16.0", "pg": "8.16.0",
"qrcode": "1.5.4", "qrcode": "1.5.4",
"react": "19.1.0", "react": "19.1.0",
"redis": "5.0.1", "redis": "5.5.6",
"schema-dts": "1.1.5", "schema-dts": "1.1.5",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"sharp": "0.34.1", "sharp": "0.34.2",
"slugify": "1.6.6", "slugify": "1.6.6",
"tailwind-merge": "3.3.0", "tailwind-merge": "3.3.0",
"tailwind-variants": "1.0.0", "tailwind-variants": "1.0.0",
"tailwindcss": "4.1.7", "tailwindcss": "4.1.8",
"typescript": "5.8.3", "typescript": "5.8.3",
"unique-username-generator": "1.4.0", "unique-username-generator": "1.4.0",
"web-push": "3.6.7", "web-push": "3.6.7",
"zod-form-data": "2.0.7" "zod-form-data": "2.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.27.0", "@eslint/js": "9.28.0",
"@faker-js/faker": "9.8.0", "@faker-js/faker": "9.8.0",
"@iconify-json/material-symbols": "1.2.21", "@iconify-json/material-symbols": "1.2.24",
"@iconify-json/mdi": "1.2.3", "@iconify-json/mdi": "1.2.3",
"@iconify-json/ri": "1.2.5", "@iconify-json/ri": "1.2.5",
"@stylistic/eslint-plugin": "4.2.0", "@stylistic/eslint-plugin": "4.4.1",
"@tailwindcss/forms": "0.5.10", "@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16", "@tailwindcss/typography": "0.5.16",
"@types/eslint__js": "9.14.0", "@types/eslint__js": "9.14.0",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/react": "19.1.4", "@types/react": "19.1.6",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@typescript-eslint/parser": "8.32.1", "@typescript-eslint/parser": "8.33.1",
"astro-icon": "1.1.5", "astro-icon": "1.1.5",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"eslint": "9.27.0", "eslint": "9.28.0",
"eslint-import-resolver-typescript": "4.3.5", "eslint-import-resolver-typescript": "4.4.3",
"eslint-plugin-astro": "1.3.1", "eslint-plugin-astro": "1.3.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"globals": "16.1.0", "globals": "16.2.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"prettier-plugin-astro": "0.14.1", "prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "0.6.11", "prettier-plugin-tailwindcss": "0.6.12",
"prisma": "6.8.2", "prisma": "6.9.0",
"prisma-json-types-generator": "3.4.1", "prisma-json-types-generator": "3.4.2",
"tailwind-htmx": "0.1.2", "tailwind-htmx": "0.1.2",
"ts-essentials": "10.0.4", "ts-essentials": "10.0.4",
"ts-toolbelt": "9.6.0", "ts-toolbelt": "9.6.0",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript-eslint": "8.32.1" "typescript-eslint": "8.33.1"
} }
} }

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "OrderIdStatus" ADD VALUE 'WITHDRAWN';

View File

@@ -25,6 +25,7 @@ enum OrderIdStatus {
PENDING PENDING
APPROVED APPROVED
REJECTED REJECTED
WITHDRAWN
} }
model Comment { model Comment {

View File

@@ -1,12 +1,10 @@
import { type Prisma, type ServiceUserRole, type PrismaClient } from '@prisma/client' import { type Prisma, type ServiceUserRole } from '@prisma/client'
import { ActionError } from 'astro:actions' import { ActionError } from 'astro:actions'
import { z } from 'zod' import { z } from 'zod'
import { defineProtectedAction } from '../../lib/defineProtectedAction' import { defineProtectedAction } from '../../lib/defineProtectedAction'
import { saveFileLocally } from '../../lib/fileStorage' import { saveFileLocally } from '../../lib/fileStorage'
import { prisma as prismaInstance } from '../../lib/prisma' import { prisma } from '../../lib/prisma'
const prisma = prismaInstance as PrismaClient
const selectUserReturnFields = { const selectUserReturnFields = {
id: true, id: true,

View File

@@ -345,10 +345,11 @@ export const commentActions = {
'order-id-status', 'order-id-status',
'kyc-requested', 'kyc-requested',
'funds-blocked', 'funds-blocked',
'toggle-rating-active',
]), ]),
value: z.union([ value: z.union([
z.enum(['PENDING', 'APPROVED', 'VERIFIED', 'REJECTED']), z.enum(['PENDING', 'APPROVED', 'VERIFIED', 'REJECTED']),
z.enum(['PENDING', 'APPROVED', 'REJECTED']), z.enum(['PENDING', 'APPROVED', 'REJECTED', 'WITHDRAWN']),
z.boolean(), z.boolean(),
z.string(), z.string(),
]), ]),
@@ -411,7 +412,7 @@ export const commentActions = {
updateData.privateContext = input.value as string updateData.privateContext = input.value as string
break break
case 'order-id-status': case 'order-id-status':
updateData.orderIdStatus = input.value as 'APPROVED' | 'PENDING' | 'REJECTED' updateData.orderIdStatus = input.value as 'APPROVED' | 'PENDING' | 'REJECTED' | 'WITHDRAWN'
break break
case 'kyc-requested': case 'kyc-requested':
updateData.kycRequested = !!input.value updateData.kycRequested = !!input.value
@@ -419,6 +420,9 @@ export const commentActions = {
case 'funds-blocked': case 'funds-blocked':
updateData.fundsBlocked = !!input.value updateData.fundsBlocked = !!input.value
break break
case 'toggle-rating-active':
updateData.ratingActive = !!input.value
break
} }
// Update the comment // Update the comment

View File

@@ -20,6 +20,8 @@ type Props = HTMLAttributes<'div'> & {
privateContext: true privateContext: true
orderId: true orderId: true
orderIdStatus: true orderIdStatus: true
rating: true
ratingActive: true
} }
}> }>
} }
@@ -46,10 +48,10 @@ if (!user || !user.admin || !user.moderator) return null
<div <div
class="bg-night-600 border-night-500 mt-2 hidden overflow-hidden rounded-md border peer-checked:block peer-checked:p-2" class="bg-night-600 border-night-500 mt-2 hidden overflow-hidden rounded-md border peer-checked:block peer-checked:p-2"
> >
<div class="border-night-500 flex flex-wrap gap-1 border-b pb-2"> <div class="border-night-500 flex flex-wrap items-center gap-1 border-b pb-2">
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'REJECTED' comment.status === 'REJECTED'
? 'border border-red-500/30 bg-red-500/20 text-red-400' ? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400' : 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
@@ -59,42 +61,13 @@ 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.status === 'REJECTED' ? 'Unreject' : 'Reject'} <Icon name="ri:close-circle-line" class="h-3.5 w-3.5" />
<span>{comment.status === 'REJECTED' ? 'Unreject' : 'Reject'}</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.suspicious
? 'border border-yellow-500/30 bg-yellow-500/20 text-yellow-400'
: 'bg-night-700 hover:bg-yellow-500/20 hover:text-yellow-400'
)}
data-action="suspicious"
data-value={!comment.suspicious}
data-comment-id={comment.id}
data-user-id={user.id}
>
{comment.suspicious ? 'Not Spam' : 'Spam'}
</button>
<button
class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.requiresAdminReview
? 'border border-purple-500/30 bg-purple-500/20 text-purple-400'
: 'bg-night-700 hover:bg-purple-500/20 hover:text-purple-400'
)}
data-action="requires-admin-review"
data-value={!comment.requiresAdminReview}
data-comment-id={comment.id}
data-user-id={user.id}
>
{comment.requiresAdminReview ? 'No Admin Review' : 'Needs Admin Review'}
</button>
<button
class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'VERIFIED' comment.status === 'VERIFIED'
? 'border border-green-500/30 bg-green-500/20 text-green-400' ? 'border border-green-500/30 bg-green-500/20 text-green-400'
: 'bg-night-700 hover:bg-green-500/20 hover:text-green-400' : 'bg-night-700 hover:bg-green-500/20 hover:text-green-400'
@@ -104,12 +77,13 @@ 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.status === 'VERIFIED' ? 'Unverify' : 'Verify'} <Icon name="ri:verified-badge-line" class="h-3.5 w-3.5" />
<span>{comment.status === 'VERIFIED' ? 'Unverify' : 'Verify'}</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING' comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING'
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400' ? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400' : 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
@@ -121,12 +95,49 @@ 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.status === 'PENDING' || comment.status === 'HUMAN_PENDING' ? 'Approve' : 'Pending'} <Icon name="ri:checkbox-circle-line" class="h-3.5 w-3.5" />
<span>
{comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING' ? 'Approve' : 'Pending'}
</span>
</button>
<div class="bg-night-500 h-5 w-px"></div>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.suspicious
? 'border border-yellow-500/30 bg-yellow-500/20 text-yellow-400'
: 'bg-night-700 hover:bg-yellow-500/20 hover:text-yellow-400'
)}
data-action="suspicious"
data-value={!comment.suspicious}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:spam-2-line" class="h-3.5 w-3.5" />
<span>{comment.suspicious ? 'Not Spam' : 'Spam'}</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.requiresAdminReview
? 'border border-purple-500/30 bg-purple-500/20 text-purple-400'
: 'bg-night-700 hover:bg-purple-500/20 hover:text-purple-400'
)}
data-action="requires-admin-review"
data-value={!comment.requiresAdminReview}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:shield-user-line" class="h-3.5 w-3.5" />
<span>{comment.requiresAdminReview ? 'No Admin Review' : 'Needs Admin Review'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.kycRequested comment.kycRequested
? 'border border-red-500/30 bg-red-500/20 text-red-400' ? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400' : 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
@@ -136,12 +147,13 @@ 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.kycRequested ? 'No KYC Issue' : 'KYC Issue'} <Icon name="ri:bank-card-line" class="h-3.5 w-3.5" />
<span>{comment.kycRequested ? 'No KYC Issue' : 'KYC Issue'}</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.fundsBlocked comment.fundsBlocked
? 'border border-red-500/30 bg-red-500/20 text-red-400' ? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400' : 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
@@ -151,8 +163,31 @@ 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.fundsBlocked ? 'No Funds Issue' : 'Funds Issue'} <Icon name="ri:lock-line" class="h-3.5 w-3.5" />
<span>{comment.fundsBlocked ? 'No Funds Issue' : 'Funds Issue'}</span>
</button> </button>
<div class="bg-night-500 h-5 w-px"></div>
{
comment.rating && (
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.ratingActive
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
)}
data-action="toggle-rating-active"
data-value={!comment.ratingActive}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:star-line" class="h-3.5 w-3.5" />
<span>{comment.ratingActive ? 'Disable Rating' : 'Enable Rating'}</span>
</button>
)
}
</div> </div>
<div class="mt-2 space-y-1.5"> <div class="mt-2 space-y-1.5">
@@ -208,7 +243,7 @@ if (!user || !user.admin || !user.moderator) return null
<div class="flex gap-1"> <div class="flex gap-1">
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'APPROVED' comment.orderIdStatus === 'APPROVED'
? 'border border-green-500/30 bg-green-500/20 text-green-400' ? 'border border-green-500/30 bg-green-500/20 text-green-400'
: 'bg-night-700 hover:bg-green-500/20 hover:text-green-400' : 'bg-night-700 hover:bg-green-500/20 hover:text-green-400'
@@ -218,11 +253,12 @@ 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}
> >
Approve <Icon name="ri:check-line" class="h-3.5 w-3.5" />
<span>Approve</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'REJECTED' comment.orderIdStatus === 'REJECTED'
? 'border border-red-500/30 bg-red-500/20 text-red-400' ? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400' : 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
@@ -232,11 +268,12 @@ 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}
> >
Reject <Icon name="ri:close-line" class="h-3.5 w-3.5" />
<span>Reject</span>
</button> </button>
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'PENDING' comment.orderIdStatus === 'PENDING'
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400' ? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400' : 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
@@ -246,7 +283,23 @@ 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}
> >
Pending <Icon name="ri:time-line" class="h-3.5 w-3.5" />
<span>Pending</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'WITHDRAWN'
? 'border-night-400 bg-night-500/50 text-night-300 border'
: 'bg-night-700 hover:bg-night-500/50 hover:text-night-300'
)}
data-action="order-id-status"
data-value="WITHDRAWN"
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:arrow-go-back-line" class="h-3.5 w-3.5" />
<span>Withdrawn</span>
</button> </button>
</div> </div>
</div> </div>
@@ -280,7 +333,8 @@ if (!user || !user.admin || !user.moderator) return null
action === 'suspicious' || action === 'suspicious' ||
action === 'requires-admin-review' || action === 'requires-admin-review' ||
action === 'kyc-requested' || action === 'kyc-requested' ||
action === 'funds-blocked' action === 'funds-blocked' ||
action === 'toggle-rating-active'
? value === 'true' ? value === 'true'
: value, : value,
}) })
@@ -290,7 +344,8 @@ if (!user || !user.admin || !user.moderator) return null
if (action === 'status') { if (action === 'status') {
window.location.reload() window.location.reload()
} else if (action === 'suspicious') { } else if (action === 'suspicious') {
btn.textContent = value === 'true' ? 'Not Sus' : 'Sus' const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'Not Spam' : 'Spam'
btn.classList.toggle('bg-yellow-500/20') btn.classList.toggle('bg-yellow-500/20')
btn.classList.toggle('text-yellow-400') btn.classList.toggle('text-yellow-400')
btn.classList.toggle('border-yellow-500/30') btn.classList.toggle('border-yellow-500/30')
@@ -298,7 +353,8 @@ if (!user || !user.admin || !user.moderator) return null
btn.classList.toggle('bg-night-700') btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true') btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'requires-admin-review') { } else if (action === 'requires-admin-review') {
btn.textContent = value === 'true' ? 'No Review' : 'Review' const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No Admin Review' : 'Needs Admin Review'
btn.classList.toggle('bg-purple-500/20') btn.classList.toggle('bg-purple-500/20')
btn.classList.toggle('text-purple-400') btn.classList.toggle('text-purple-400')
btn.classList.toggle('border-purple-500/30') btn.classList.toggle('border-purple-500/30')
@@ -309,7 +365,8 @@ if (!user || !user.admin || !user.moderator) return null
// Refresh to show updated order ID status // Refresh to show updated order ID status
window.location.reload() window.location.reload()
} else if (action === 'kyc-requested') { } else if (action === 'kyc-requested') {
btn.textContent = value === 'true' ? 'No KYC Issue' : 'KYC Issue' const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No KYC Issue' : 'KYC Issue'
btn.classList.toggle('bg-red-500/20') btn.classList.toggle('bg-red-500/20')
btn.classList.toggle('text-red-400') btn.classList.toggle('text-red-400')
btn.classList.toggle('border-red-500/30') btn.classList.toggle('border-red-500/30')
@@ -317,13 +374,23 @@ if (!user || !user.admin || !user.moderator) return null
btn.classList.toggle('bg-night-700') btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true') btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'funds-blocked') { } else if (action === 'funds-blocked') {
btn.textContent = value === 'true' ? 'No Funds Issue' : 'Funds Issue' const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No Funds Issue' : 'Funds Issue'
btn.classList.toggle('bg-red-500/20') btn.classList.toggle('bg-red-500/20')
btn.classList.toggle('text-red-400') btn.classList.toggle('text-red-400')
btn.classList.toggle('border-red-500/30') btn.classList.toggle('border-red-500/30')
btn.classList.toggle('border') btn.classList.toggle('border')
btn.classList.toggle('bg-night-700') btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true') btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'toggle-rating-active') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'Disable Rating' : 'Enable Rating'
btn.classList.toggle('bg-blue-500/20')
btn.classList.toggle('text-blue-400')
btn.classList.toggle('border-blue-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} }
} else { } else {
console.error('Error moderating comment:', error) console.error('Error moderating comment:', error)

View File

@@ -1,11 +1,14 @@
import { prisma } from './prisma' import { prisma } from './prisma'
import type { Prisma } from '@prisma/client' import type { Prisma, PrismaClient } from '@prisma/client'
export async function getOrCreateNotificationPreferences<T extends Prisma.NotificationPreferencesSelect>( export async function getOrCreateNotificationPreferences<T extends Prisma.NotificationPreferencesSelect>(
userId: number, userId: number,
select: { [K in keyof T]: K extends keyof Prisma.NotificationPreferencesSelect ? T[K] : never }, select: { [K in keyof T]: K extends keyof Prisma.NotificationPreferencesSelect ? T[K] : never },
tx: Prisma.TransactionClient = prisma tx:
| Parameters<Parameters<(typeof prisma)['$transaction']>[0]>[0]
| Prisma.TransactionClient
| PrismaClient = prisma
) { ) {
return ( return (
(await tx.notificationPreferences.findUnique({ where: { userId }, select })) ?? (await tx.notificationPreferences.findUnique({ where: { userId }, select })) ??

View File

@@ -76,6 +76,7 @@ To list a new service, it must fulfill these requirements:
- Terms of service or FAQ document - Terms of service or FAQ document
For examples: For examples:
- Just a Telegram link or a criptocurrency itself is not a valid service. - Just a Telegram link or a criptocurrency itself is not a valid service.
### Suggestion Review Process ### Suggestion Review Process

View File

@@ -27,11 +27,11 @@ Fetches details for a single service by various lookup criteria.
### Request Parameters ### Request Parameters
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
| ------------ | ------ | -------- | ----------- | | ------------ | ------ | -------- | --------------------------------------------------------------------------- |
| `id` | number | No* | Service ID | | `id` | number | No\* | Service ID |
| `slug` | string | No* | Service URL slug (lowercase letters, numbers, and hyphens only) | | `slug` | string | No\* | Service URL slug (lowercase letters, numbers, and hyphens only) |
| `serviceUrl` | string | No* | Service URL. May be web, onion, or i2p. May just be a domain or a full URL. | | `serviceUrl` | string | No\* | Service URL. May be web, onion, or i2p. May just be a domain or a full URL. |
\* At least one of the marked parameters is required. \* At least one of the marked parameters is required.
@@ -79,41 +79,46 @@ type ServiceResponse = {
#### KYC Levels #### KYC Levels
<ul> <ul>
{kycLevels.map((level) => ( {kycLevels.map((level) => (
<li key={level.id}> <li key={level.id}>
<strong>{level.id}</strong>: {level.name} - {level.description} <strong>{level.id}</strong>: {level.name} - {level.description}
</li> </li>
))} ))}
</ul> </ul>
#### Verification Status #### Verification Status
<ul> <ul>
{verificationStatuses.map((status) => ( {verificationStatuses.map((status) => (
<li key={status.value}> <li key={status.value}>
<strong>{status.value}</strong>: {status.description} <strong>{status.value}</strong>: {status.description}
</li> </li>
))} ))}
</ul> </ul>
#### Service Visibility #### Service Visibility
<ul> <ul>
{serviceVisibilities.filter((visibility) => visibility.value === 'PUBLIC' || visibility.value === 'ARCHIVED' || visibility.value === 'UNLISTED').map((visibility) => ( {serviceVisibilities
<li key={visibility.value}> .filter(
<strong>{visibility.value}</strong>: {visibility.longDescription} (visibility) =>
</li> visibility.value === 'PUBLIC' || visibility.value === 'ARCHIVED' || visibility.value === 'UNLISTED'
))} )
.map((visibility) => (
<li key={visibility.value}>
<strong>{visibility.value}</strong>: {visibility.longDescription}
</li>
))}
</ul> </ul>
#### KYC Level Clarifications #### KYC Level Clarifications
<ul> <ul>
{kycLevelClarifications.map((clarification) => ( {kycLevelClarifications.map((clarification) => (
<li key={clarification.value}> <li key={clarification.value}>
<strong>{clarification.value}</strong>: {clarification.description} <strong>{clarification.value}</strong>: {clarification.description}
</li> </li>
))} ))}
</ul> </ul>
### Examples ### Examples