Release 202507061859

This commit is contained in:
pluja
2025-07-06 18:59:23 +00:00
parent 7a294cb0a1
commit 28b84a7d9b
11 changed files with 120 additions and 61 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Service" ADD COLUMN "commentSectionMessage" TEXT;

View File

@@ -407,6 +407,7 @@ model Service {
affiliatedUsers ServiceUser[] @relation("ServiceUsers") affiliatedUsers ServiceUser[] @relation("ServiceUsers")
strictCommentingEnabled Boolean @default(false) strictCommentingEnabled Boolean @default(false)
commentSectionMessage String?
@@index([listedAt]) @@index([listedAt])
@@index([approvedAt]) @@index([approvedAt])

View File

@@ -721,6 +721,7 @@ const generateFakeService = (users: User[]) => {
{ probability: 0.33 } { probability: 0.33 }
), ),
strictCommentingEnabled: faker.datatype.boolean(0.33333), strictCommentingEnabled: faker.datatype.boolean(0.33333),
commentSectionMessage: faker.helpers.maybe(() => faker.lorem.paragraph(), { probability: 0.3 }),
} as const satisfies Prisma.ServiceCreateInput } as const satisfies Prisma.ServiceCreateInput
} }

View File

@@ -64,6 +64,7 @@ const serviceSchemaBase = z.object({
serviceVisibility: z.nativeEnum(ServiceVisibility), serviceVisibility: z.nativeEnum(ServiceVisibility),
internalNote: z.string().optional(), internalNote: z.string().optional(),
strictCommentingEnabled: z.boolean().optional().default(false), strictCommentingEnabled: z.boolean().optional().default(false),
commentSectionMessage: z.string().trim().min(3).max(1000).optional().nullable().default(null),
}) })
// Define schema for the create action input // Define schema for the create action input
@@ -129,6 +130,7 @@ export const adminServiceActions = {
verificationProofMd: input.verificationProofMd, verificationProofMd: input.verificationProofMd,
acceptedCurrencies: input.acceptedCurrencies, acceptedCurrencies: input.acceptedCurrencies,
strictCommentingEnabled: input.strictCommentingEnabled, strictCommentingEnabled: input.strictCommentingEnabled,
commentSectionMessage: input.commentSectionMessage,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
referral: input.referral || null, referral: input.referral || null,
serviceVisibility: input.serviceVisibility, serviceVisibility: input.serviceVisibility,
@@ -250,6 +252,7 @@ export const adminServiceActions = {
verificationProofMd: input.verificationProofMd, verificationProofMd: input.verificationProofMd,
acceptedCurrencies: input.acceptedCurrencies, acceptedCurrencies: input.acceptedCurrencies,
strictCommentingEnabled: input.strictCommentingEnabled, strictCommentingEnabled: input.strictCommentingEnabled,
commentSectionMessage: input.commentSectionMessage,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
referral: input.referral || null, referral: input.referral || null,
serviceVisibility: input.serviceVisibility, serviceVisibility: input.serviceVisibility,

View File

@@ -17,6 +17,7 @@ import type { CommentStatus, Prisma } from '@prisma/client'
const COMMENT_RATE_LIMIT_WINDOW_MINUTES = 2 const COMMENT_RATE_LIMIT_WINDOW_MINUTES = 2
const MAX_COMMENTS_PER_WINDOW = 1 const MAX_COMMENTS_PER_WINDOW = 1
const MAX_COMMENTS_PER_WINDOW_VERIFIED_USER = 10 const MAX_COMMENTS_PER_WINDOW_VERIFIED_USER = 10
export const COMMENT_ORDER_ID_MAX_LENGTH = 600
export const commentActions = { export const commentActions = {
vote: defineProtectedAction({ vote: defineProtectedAction({
@@ -103,7 +104,7 @@ export const commentActions = {
issueFundsBlocked: z.coerce.boolean().optional(), issueFundsBlocked: z.coerce.boolean().optional(),
issueScam: z.coerce.boolean().optional(), issueScam: z.coerce.boolean().optional(),
issueDetails: z.string().max(120).optional(), issueDetails: z.string().max(120).optional(),
orderId: z.string().max(100).optional(), orderId: z.string().max(COMMENT_ORDER_ID_MAX_LENGTH).optional(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.rating && data.parentId) { if (data.rating && data.parentId) {

View File

@@ -1,7 +1,9 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { Markdown } from 'astro-remote'
import { actions } from 'astro:actions' import { actions } from 'astro:actions'
import { COMMENT_ORDER_ID_MAX_LENGTH } from '../actions/comment'
import { cn } from '../lib/cn' import { cn } from '../lib/cn'
import { makeLoginUrl } from '../lib/redirectUrls' import { makeLoginUrl } from '../lib/redirectUrls'
@@ -21,6 +23,7 @@ type Props = Omit<HTMLAttributes<'form'>, 'action' | 'enctype' | 'method'> & {
parentId?: number parentId?: number
commentId?: number commentId?: number
strictCommentingEnabled?: boolean strictCommentingEnabled?: boolean
commentSectionMessage?: string | null
activeRatingComment?: Prisma.CommentGetPayload<{ activeRatingComment?: Prisma.CommentGetPayload<{
select: { select: {
id: true id: true
@@ -35,6 +38,7 @@ const {
commentId, commentId,
activeRatingComment, activeRatingComment,
strictCommentingEnabled, strictCommentingEnabled,
commentSectionMessage,
class: className, class: className,
...htmlProps ...htmlProps
} = Astro.props } = Astro.props
@@ -97,70 +101,83 @@ const userCommentsDisabled = user ? user.karmaUnlocks.commentsDisabled : false
</div> </div>
{!parentId ? ( {!parentId ? (
<div class="[&:has(input[name='rating'][value='']:checked)_[data-show-if-rating]]:hidden"> <>
<div class="flex flex-wrap gap-4"> <div class="[&:has(input[name='rating'][value='']:checked)_[data-show-if-rating]]:hidden">
<InputRating name="rating" label="Rating" /> <div class="flex flex-wrap gap-4">
<InputRating name="rating" label="Rating" />
<InputWrapper label="I experienced..." 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">
<Icon name="ri:user-forbid-fill" class="size-3" /> <Icon name="ri:user-forbid-fill" class="size-3" />
KYC Issue KYC Issue
</span> </span>
</label> </label>
<label class="flex cursor-pointer items-center gap-2"> <label class="flex cursor-pointer items-center gap-2">
<input type="checkbox" name="issueFundsBlocked" class="text-orange-400" /> <input type="checkbox" name="issueFundsBlocked" class="text-orange-400" />
<span class="flex items-center gap-1 text-xs text-orange-400"> <span class="flex items-center gap-1 text-xs text-orange-400">
<Icon name="ri:wallet-3-fill" class="size-3" /> <Icon name="ri:wallet-3-fill" class="size-3" />
Funds Blocked Funds Blocked
</span> </span>
</label> </label>
</InputWrapper> </InputWrapper>
<InputText <InputText
label="Order ID" label="Order ID"
name="orderId" name="orderId"
inputProps={{ inputProps={{
maxlength: 100, maxlength: COMMENT_ORDER_ID_MAX_LENGTH,
placeholder: 'Order ID / URL / Proof', placeholder: 'Order ID / URL / Proof',
class: 'bg-night-800', class: 'bg-night-800',
required: strictCommentingEnabled, required: strictCommentingEnabled,
}} }}
descriptionLabel="Only visible to admins, to verify your comment" descriptionLabel="Only visible to admins, to verify your comment"
class="grow" class="grow"
/> />
</div> </div>
<div class="mt-4 flex items-start justify-end gap-2"> <div class="mt-4 flex flex-wrap items-start justify-end gap-x-4 gap-y-2">
{!!activeRatingComment?.rating && ( {!!activeRatingComment?.rating && (
<div <div
class="rounded-sm bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-400" class="mt-1 rounded-sm bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-400"
data-show-if-rating data-show-if-rating
>
<Icon name="ri:information-line" class="mr-1 inline size-3.5" />
<a
href={`${Astro.url.origin}${Astro.url.pathname}#comment-${activeRatingComment.id}`}
class="inline-flex items-center gap-1 underline"
target="_blank"
rel="noopener noreferrer"
> >
Your previous rating <Icon name="ri:information-line" class="mr-1 inline size-3.5" />
<Icon name="ri:external-link-line" class="inline size-2.5 align-[-0.1em]" /> <a
</a> href={`${Astro.url.origin}${Astro.url.pathname}#comment-${activeRatingComment.id}`}
of class="inline-flex items-center gap-1 underline"
{[ target="_blank"
activeRatingComment.rating.toLocaleString(), rel="noopener noreferrer"
<Icon name="ri:star-fill" class="inline size-3 align-[-0.1em]" />, >
]} Your previous rating
won't count for the total. <Icon name="ri:external-link-line" class="inline size-2.5 align-[-0.1em]" />
</div> </a>
)} of
{[
activeRatingComment.rating.toLocaleString(),
<Icon name="ri:star-fill" class="inline size-3 align-[-0.1em]" />,
]}
won't count for the total.
</div>
)}
<Button type="submit" label="Send" icon="ri:send-plane-2-line" /> <div class="flex flex-wrap items-start justify-end gap-x-4 gap-y-2">
{!!commentSectionMessage && (
<div class="flex items-start gap-1 pt-1.5">
<Icon name="ri:information-line" class="mt-1.25 inline size-3.5" />
<div class="prose prose-invert prose-sm text-day-200 max-w-none grow">
<Markdown content={commentSectionMessage} />
</div>
</div>
)}
<Button type="submit" label="Send" icon="ri:send-plane-2-line" />
</div>
</div>
</div> </div>
</div> </>
) : ( ) : (
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<Button type="submit" label="Reply" icon="ri:reply-line" /> <Button type="submit" label="Reply" icon="ri:reply-line" />

View File

@@ -36,6 +36,7 @@ type Props = {
description: true description: true
createdAt: true createdAt: true
strictCommentingEnabled: true strictCommentingEnabled: true
commentSectionMessage: true
} }
}> }>
} }
@@ -178,6 +179,7 @@ function makeReplySchema(comment: CommentWithRepliesPopulated): Comment {
serviceId={service.id} serviceId={service.id}
activeRatingComment={activeRatingComment} activeRatingComment={activeRatingComment}
strictCommentingEnabled={service.strictCommentingEnabled} strictCommentingEnabled={service.strictCommentingEnabled}
commentSectionMessage={service.commentSectionMessage}
class="xs:mb-4 mb-2" class="xs:mb-4 mb-2"
/> />

View File

@@ -3,7 +3,6 @@ import { Icon } from 'astro-icon/components'
import { uniq, orderBy } from 'lodash-es' import { uniq, orderBy } from 'lodash-es'
import { getCurrencyInfo } from '../constants/currencies' import { getCurrencyInfo } from '../constants/currencies'
import { getKycLevelInfo } from '../constants/kycLevels'
import { verificationStatusesByValue, getVerificationStatusInfo } from '../constants/verificationStatus' import { verificationStatusesByValue, getVerificationStatusInfo } from '../constants/verificationStatus'
import { areEqualArraysWithoutOrder } from '../lib/arrays' import { areEqualArraysWithoutOrder } from '../lib/arrays'
import { cn } from '../lib/cn' import { cn } from '../lib/cn'
@@ -160,8 +159,7 @@ const searchTitle = (() => {
} }
if (filters['max-kyc'] === 0) { if (filters['max-kyc'] === 0) {
const kycLevelInfo = getKycLevelInfo(String(filters['max-kyc'])) kycLevel = 'without KYC'
kycLevel = `with ${kycLevelInfo.name}`
prefix = '' prefix = ''
} else if (filters['max-kyc'] <= 3) { } else if (filters['max-kyc'] <= 3) {
kycLevel = `with KYC level ${filters['max-kyc']} or better` kycLevel = `with KYC level ${filters['max-kyc']} or better`

View File

@@ -553,6 +553,17 @@ const apiCalls = await Astro.locals.banners.try(
descriptionInline="Require proof of being a client for comments." descriptionInline="Require proof of being a client for comments."
/> />
<InputTextArea
label="Comment Section Message"
name="commentSectionMessage"
value={service.commentSectionMessage ?? ''}
description="Markdown supported"
inputProps={{
rows: 4,
}}
error={serviceInputErrors.commentSectionMessage}
/>
<InputSubmitButton label="Update" icon="ri:save-line" hideCancel /> <InputSubmitButton label="Update" icon="ri:save-line" hideCancel />
</form> </form>
</FormSection> </FormSection>

View File

@@ -376,6 +376,28 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
descriptionInline="Require proof of being a client for comments." descriptionInline="Require proof of being a client for comments."
/> />
<div>
<label for="commentSectionMessage" class="font-title mb-2 block text-sm text-green-500"
>Comment Section Message</label
>
<div class="space-y-2">
<textarea
transition:persist
name="commentSectionMessage"
id="commentSectionMessage"
rows={4}
placeholder="Markdown supported. This message will be displayed in the comment section for root comments."
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"
set:text=""
/>
</div>
{
inputErrors.commentSectionMessage && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.commentSectionMessage.join(', ')}</p>
)
}
</div>
<button <button
type="submit" type="submit"
class="font-title inline-flex justify-center rounded-md border border-green-500/30 bg-green-500/10 px-4 py-2 text-sm text-green-400 shadow-xs transition-colors duration-200 hover:bg-green-500/20 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden" class="font-title inline-flex justify-center rounded-md border border-green-500/30 bg-green-500/10 px-4 py-2 text-sm text-green-400 shadow-xs transition-colors duration-200 hover:bg-green-500/20 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"

View File

@@ -103,6 +103,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
averageUserRating: true, averageUserRating: true,
isRecentlyApproved: true, isRecentlyApproved: true,
strictCommentingEnabled: true, strictCommentingEnabled: true,
commentSectionMessage: true,
contactMethods: { contactMethods: {
select: { select: {
value: true, value: true,