Release 202507061859
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "commentSectionMessage" TEXT;
|
||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user