Release 2025-05-22-GmO6

This commit is contained in:
pluja
2025-05-22 11:10:18 +00:00
parent ed86f863e3
commit a69c0aeed4
20 changed files with 316 additions and 147 deletions

View File

@@ -2,7 +2,7 @@
import { cn } from '../lib/cn'
import { formatDateShort } from '../lib/timeAgo'
import MyPicture from './MyPicture.astro'
import UserBadge from './UserBadge.astro'
import type { Prisma } from '@prisma/client'
import type { HTMLAttributes } from 'astro/types'
@@ -15,6 +15,7 @@ export type ChatMessage = {
select: {
id: true
name: true
displayName: true
picture: true
}
}>
@@ -71,18 +72,7 @@ const { messages, userId, class: className, ...htmlProps } = Astro.props
)}
>
{!isCurrentUser && !isNextFromSameUser && (
<p class="text-day-500 mb-0.5 text-xs">
{!!message.user.picture && (
<MyPicture
src={message.user.picture}
height={20}
width={20}
class="inline-block size-5 rounded-full align-[-0.5em]"
alt=""
/>
)}
{message.user.name}
</p>
<UserBadge user={message.user} size="sm" class="text-day-500 mb-0.5 text-xs" />
)}
<p
class={cn(

View File

@@ -4,6 +4,7 @@ import { Markdown } from 'astro-remote'
import { Schema } from 'astro-seo-schema'
import { actions } from 'astro:actions'
import { commentStatusById } from '../constants/commentStatus'
import { karmaUnlocksById } from '../constants/karmaUnlocks'
import { getServiceUserRoleInfo } from '../constants/serviceUserRoles'
import { cn } from '../lib/cn'
@@ -18,9 +19,9 @@ import { formatDateShort } from '../lib/timeAgo'
import BadgeSmall from './BadgeSmall.astro'
import CommentModeration from './CommentModeration.astro'
import CommentReply from './CommentReply.astro'
import MyPicture from './MyPicture.astro'
import TimeFormatted from './TimeFormatted.astro'
import Tooltip from './Tooltip.astro'
import UserBadge from './UserBadge.astro'
import type { HTMLAttributes } from 'astro/types'
@@ -156,27 +157,11 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
</label>
<span class="flex items-center gap-1">
{
comment.author.picture && (
<MyPicture
src={comment.author.picture}
alt={`Profile for ${comment.author.displayName ?? comment.author.name}`}
class="size-6 rounded-full bg-zinc-700 object-cover"
height={24}
width={24}
/>
)
}
<a
href={`/u/${comment.author.name}`}
class={cn([
'font-title text-day-300 font-medium hover:underline focus-visible:underline',
isAuthor && 'font-medium text-green-500',
])}
>
{comment.author.displayName ?? comment.author.name}
</a>
<UserBadge
user={comment.author}
size="md"
class={cn('text-day-300', isAuthor && 'font-medium text-green-500')}
/>
{
(comment.author.verified || comment.author.admin || comment.author.verifier) && (
@@ -307,20 +292,35 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
{
comment.status === 'VERIFIED' && (
<BadgeSmall icon="ri:check-double-fill" color="green" text="Verified" inlineIcon />
<BadgeSmall
icon={commentStatusById.VERIFIED.icon}
color={commentStatusById.VERIFIED.color}
text={commentStatusById.VERIFIED.label}
inlineIcon
/>
)
}
{
(comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING') &&
(showPending || isHighlightParent || isAuthorOrPrivileged) && (
<BadgeSmall icon="ri:time-fill" color="yellow" text="Unmoderated" inlineIcon />
<BadgeSmall
icon={commentStatusById.PENDING.icon}
color={commentStatusById.PENDING.color}
text={commentStatusById.PENDING.label}
inlineIcon
/>
)
}
{
comment.status === 'REJECTED' && isAuthorOrPrivileged && (
<BadgeSmall icon="ri:close-circle-fill" color="red" text="Rejected" inlineIcon />
<BadgeSmall
icon={commentStatusById.REJECTED.icon}
color={commentStatusById.REJECTED.color}
text={commentStatusById.REJECTED.label}
inlineIcon
/>
)
}

View File

@@ -11,6 +11,7 @@ import InputHoneypotTrap from './InputHoneypotTrap.astro'
import InputRating from './InputRating.astro'
import InputText from './InputText.astro'
import InputWrapper from './InputWrapper.astro'
import UserBadge from './UserBadge.astro'
import type { Prisma } from '@prisma/client'
import type { HTMLAttributes } from 'astro/types'
@@ -67,7 +68,7 @@ const userCommentsDisabled = user ? user.karmaUnlocks.commentsDisabled : false
<div class="text-day-400 flex items-center gap-2 text-xs peer-checked/use-form-secret-token:hidden">
<Icon name="ri:user-line" class="size-3.5" />
<span>
Commenting as: <span class="font-title font-medium text-green-400">{user.name}</span>
Commenting as: <UserBadge user={user} size="sm" class="text-green-400" />
</span>
</div>

View File

@@ -12,6 +12,7 @@ import HeaderNotificationIndicator from './HeaderNotificationIndicator.astro'
import HeaderSplashTextScript from './HeaderSplashTextScript.astro'
import Logo from './Logo.astro'
import Tooltip from './Tooltip.astro'
import UserBadge from './UserBadge.astro'
const user = Astro.locals.user
const actualUser = Astro.locals.actualUser
@@ -131,9 +132,12 @@ const splashText = showSplashText ? sample(splashTexts) : null
user ? (
<>
{actualUser && (
<span class="text-sm text-white/40 hover:text-white" transition:name="header-actual-user-name">
({actualUser.name})
</span>
<UserBadge
user={actualUser}
size="sm"
class="text-white/40 hover:text-white"
transition:name="header-actual-user-name"
/>
)}
<HeaderNotificationIndicator
@@ -141,13 +145,17 @@ const splashText = showSplashText ? sample(splashTexts) : null
transition:name="header-notification-indicator"
/>
<a
<UserBadge
href="/account"
class="xs:px-3 2xs:px-2 last:xs:-mr-3 last:2xs:-mr-2 flex h-full items-center px-1 text-sm text-zinc-400 transition-colors last:-mr-1 hover:text-zinc-300"
user={user}
size="md"
class="xs:px-3 2xs:px-2 last:xs:-mr-3 last:2xs:-mr-2 h-full px-1 text-zinc-400 transition-colors last:-mr-1 hover:text-zinc-300"
classNames={{
image: 'max-2xs:hidden',
}}
transition:name="header-user-link"
>
{user.name}
</a>
/>
{actualUser ? (
<a
href={makeUnimpersonateUrl(Astro.url)}

View File

@@ -0,0 +1,87 @@
---
import { tv, type VariantProps } from 'tailwind-variants'
import { getSizePxFromTailwindClasses } from '../lib/tailwind'
import MyPicture from './MyPicture.astro'
import type { Prisma } from '@prisma/client'
import type { HTMLAttributes } from 'astro/types'
import type { O } from 'ts-toolbelt'
const userBadge = tv({
slots: {
base: 'group/user-badge font-title inline-flex max-w-full items-center gap-1 overflow-hidden font-medium',
image: 'inline-block rounded-full object-cover',
text: 'truncate',
},
variants: {
size: {
sm: {
base: 'gap-1 text-xs',
image: 'size-4',
},
md: {
base: 'gap-2 text-sm',
image: 'size-5',
},
lg: {
base: 'gap-2 text-base',
image: 'size-6',
},
},
noLink: {
true: {
text: 'cursor-default',
},
false: {
base: 'cursor-pointer',
text: 'group-hover/user-badge:underline',
},
},
},
defaultVariants: {
size: 'sm',
noLink: false,
},
})
type Props = O.Optional<HTMLAttributes<'a'>, 'href'> &
VariantProps<typeof userBadge> & {
user: Prisma.UserGetPayload<{
select: {
name: true
displayName: true
picture: true
}
}>
classNames?: {
image?: string
text?: string
}
children?: never
}
const { user, href, class: className, size = 'sm', classNames, noLink = false, ...htmlProps } = Astro.props
const { base, image, text } = userBadge({ size, noLink })
const imageClassName = image({ class: classNames?.image })
const imageSizePx = getSizePxFromTailwindClasses(imageClassName, 16)
const Tag = noLink ? 'span' : 'a'
---
<Tag
href={Tag === 'a' ? (href ?? `/u/${user.name}`) : undefined}
class={base({ class: className })}
{...htmlProps}
>
{
!!user.picture && (
<MyPicture src={user.picture} height={imageSizePx} width={imageSizePx} class={imageClassName} alt="" />
)
}
<span class={text({ class: classNames?.text })}>
{user.displayName ?? user.name}
</span>
</Tag>

View File

@@ -19,6 +19,11 @@ type Props = {
verificationSummary: true
listedAt: true
createdAt: true
verificationSteps: {
select: {
status: true
}
}
}
}>
}
@@ -67,3 +72,18 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
</div>
) : null
}
{
service.verificationStatus !== 'VERIFICATION_FAILED' &&
service.verificationSteps.some((step) => step.status === 'FAILED') && (
<div class="mb-3 flex items-center gap-2 rounded-md bg-red-900/50 p-2 text-sm text-red-400">
<Icon
name={verificationStatusesByValue.VERIFICATION_FAILED.icon}
class={cn('size-5', verificationStatusesByValue.VERIFICATION_FAILED.classNames.icon)}
/>
<span>
This service has failed one or more verification steps. Review the verification details carefully.
</span>
</div>
)
}

View File

@@ -1,12 +1,15 @@
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
import { transformCase } from '../lib/strings'
import type BadgeSmall from '../components/BadgeSmall.astro'
import type { CommentStatus } from '@prisma/client'
import type { ComponentProps } from 'astro/types'
type CommentStatusInfo<T extends string | null | undefined = string> = {
id: T
icon: string
label: string
color: ComponentProps<typeof BadgeSmall>['color']
creativeWorkStatus: string | undefined
}
@@ -20,37 +23,43 @@ export const {
id,
icon: 'ri:question-line',
label: id ? transformCase(id, 'title') : String(id),
color: 'gray',
creativeWorkStatus: undefined,
}),
[
{
id: 'PENDING',
icon: 'ri:question-line',
label: 'Pending',
label: 'Unmoderated',
color: 'yellow',
creativeWorkStatus: 'Deleted',
},
{
id: 'HUMAN_PENDING',
icon: 'ri:question-line',
label: 'Pending',
label: 'Unmoderated',
color: 'yellow',
creativeWorkStatus: 'Deleted',
},
{
id: 'VERIFIED',
icon: 'ri:check-line',
icon: 'ri:verified-badge-fill',
label: 'Verified',
color: 'blue',
creativeWorkStatus: 'Verified',
},
{
id: 'REJECTED',
icon: 'ri:close-line',
label: 'Rejected',
color: 'red',
creativeWorkStatus: 'Deleted',
},
{
id: 'APPROVED',
icon: 'ri:check-line',
label: 'Approved',
color: 'green',
creativeWorkStatus: 'Active',
},
] as const satisfies CommentStatusInfo<CommentStatus>[]

View File

@@ -1,15 +1,20 @@
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
import { transformCase } from '../lib/strings'
import { commentStatusById } from './commentStatus'
import type BadgeSmall from '../components/BadgeSmall.astro'
import type { Prisma } from '@prisma/client'
import type { ComponentProps } from 'astro/types'
type CommentStatusFilterInfo<T extends string | null | undefined = string> = {
value: T
label: string
color: ComponentProps<typeof BadgeSmall>['color']
icon: string
whereClause: Prisma.CommentWhereInput
styles: {
classNames: {
filter: string
badge: string
}
}
@@ -24,9 +29,10 @@ export const {
value,
label: value ? transformCase(value, 'title') : String(value),
whereClause: {},
styles: {
color: 'gray',
icon: 'ri:question-line',
classNames: {
filter: 'border-zinc-700 transition-colors hover:border-green-500/50',
badge: '',
},
}),
[
@@ -34,84 +40,92 @@ export const {
label: 'All',
value: 'all',
whereClause: {},
styles: {
color: 'gray',
icon: 'ri:question-line',
classNames: {
filter: 'border-green-500 bg-green-500/20 text-green-400',
badge: '',
},
},
{
label: 'Pending',
value: 'pending',
label: commentStatusById.PENDING.label,
color: commentStatusById.PENDING.color,
icon: commentStatusById.PENDING.icon,
whereClause: {
OR: [{ status: 'PENDING' }, { status: 'HUMAN_PENDING' }],
},
styles: {
classNames: {
filter: 'border-blue-500 bg-blue-500/20 text-blue-400',
badge: 'rounded-sm bg-blue-500/20 px-2 py-0.5 text-[12px] font-medium text-blue-500',
},
},
{
label: 'Human Pending',
value: 'human-pending',
label: commentStatusById.HUMAN_PENDING.label,
color: commentStatusById.HUMAN_PENDING.color,
icon: commentStatusById.HUMAN_PENDING.icon,
whereClause: { status: 'HUMAN_PENDING' },
styles: {
classNames: {
filter: 'border-blue-500 bg-blue-500/20 text-blue-400',
badge: 'rounded-sm bg-blue-500/20 px-2 py-0.5 text-[12px] font-medium text-blue-500',
},
},
{
label: 'Rejected',
value: 'rejected',
label: commentStatusById.REJECTED.label,
color: commentStatusById.REJECTED.color,
icon: commentStatusById.REJECTED.icon,
whereClause: {
status: 'REJECTED',
},
styles: {
classNames: {
filter: 'border-red-500 bg-red-500/20 text-red-400',
badge: 'rounded-sm bg-red-500/20 px-2 py-0.5 text-[12px] font-medium text-red-500',
},
},
{
label: 'Suspicious',
value: 'suspicious',
color: 'red',
icon: 'ri:close-circle-fill',
whereClause: {
suspicious: true,
},
styles: {
classNames: {
filter: 'border-red-500 bg-red-500/20 text-red-400',
badge: 'rounded-sm bg-red-500/20 px-2 py-0.5 text-[12px] font-medium text-red-500',
},
},
{
label: 'Verified',
value: 'verified',
label: commentStatusById.VERIFIED.label,
color: commentStatusById.VERIFIED.color,
icon: commentStatusById.VERIFIED.icon,
whereClause: {
status: 'VERIFIED',
},
styles: {
classNames: {
filter: 'border-blue-500 bg-blue-500/20 text-blue-400',
badge: 'rounded-sm bg-blue-500/20 px-2 py-0.5 text-[12px] font-medium text-blue-500',
},
},
{
label: 'Approved',
value: 'approved',
label: commentStatusById.APPROVED.label,
color: commentStatusById.APPROVED.color,
icon: commentStatusById.APPROVED.icon,
whereClause: {
status: 'APPROVED',
},
styles: {
classNames: {
filter: 'border-green-500 bg-green-500/20 text-green-400',
badge: 'rounded-sm bg-green-500/20 px-2 py-0.5 text-[12px] font-medium text-green-500',
},
},
{
label: 'Needs Review',
value: 'needs-review',
color: 'yellow',
icon: 'ri:question-line',
whereClause: {
requiresAdminReview: true,
},
styles: {
classNames: {
filter: 'border-yellow-500 bg-yellow-500/20 text-yellow-400',
badge: 'rounded-sm bg-yellow-500/20 px-2 py-0.5 text-[12px] font-medium text-yellow-500',
},
},
] as const satisfies CommentStatusFilterInfo[]

View File

@@ -78,8 +78,8 @@ export const {
},
{
value: 'MANUAL_ADJUSTMENT',
slug: 'manual-adjustment',
label: 'Manual adjustment',
slug: 'gift',
label: 'Gift',
icon: 'ri:gift-line',
},
] as const satisfies KarmaTransactionActionInfo<KarmaTransactionAction>[]

8
web/src/lib/tailwind.ts Normal file
View File

@@ -0,0 +1,8 @@
import { parseIntWithFallback } from './numbers'
const TW_SIZING_TO_PX_RATIO = 4
export function getSizePxFromTailwindClasses(className: string, fallbackPxSize: number) {
const twSizing = /(?: |^|\n)(?:(?:size-(\d+))|(?:w-(\d+))|(?:h-(\d+)))(?: |$|\n)/.exec(className)?.[1]
return parseIntWithFallback(twSizing, fallbackPxSize / TW_SIZING_TO_PX_RATIO) * TW_SIZING_TO_PX_RATIO
}

View File

@@ -22,7 +22,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
---
<MiniLayout
pageTitle={`Edit Profile - ${user.name}`}
pageTitle={`Edit Profile - ${user.displayName ?? user.name}`}
description="Edit your user profile"
ogImage={{ template: 'generic', title: 'Edit Profile', icon: 'ri:user-settings-line' }}
layoutHeader={{

View File

@@ -8,6 +8,7 @@ import Button from '../../components/Button.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import Tooltip from '../../components/Tooltip.astro'
import UserBadge from '../../components/UserBadge.astro'
import { getKarmaTransactionActionInfo } from '../../constants/karmaTransactionActions'
import { karmaUnlocks, karmaUnlocksById } from '../../constants/karmaUnlocks'
import { SUPPORT_EMAIL } from '../../constants/project'
@@ -65,6 +66,7 @@ const user = await Astro.locals.banners.try('user', async () => {
select: {
name: true,
displayName: true,
picture: true,
},
},
comment: {
@@ -158,11 +160,11 @@ if (!user) return Astro.rewrite('/404')
---
<BaseLayout
pageTitle={`${user.name} - Account`}
pageTitle={`${user.displayName ?? user.name} - Account`}
description="Manage your user profile"
ogImage={{
template: 'generic',
title: `${user.name} | Account`,
title: `${user.displayName ?? user.name} | Account`,
description: 'Manage your user profile',
icon: 'ri:user-3-line',
}}
@@ -199,8 +201,10 @@ if (!user) return Astro.rewrite('/404')
)
}
<div class="grow">
<h1 class="font-title text-lg font-bold tracking-wider text-white">{user.name}</h1>
{user.displayName && <p class="text-day-200">{user.displayName}</p>}
<h1 class="font-title text-lg font-bold tracking-wider text-white">
{user.displayName ?? user.name}
</h1>
{user.displayName && <p class="text-day-200 font-title">{user.name}</p>}
{
(user.admin || user.verified || user.verifier) && (
<div class="mt-1 flex gap-2">
@@ -429,7 +433,7 @@ if (!user) return Astro.rewrite('/404')
<li>
<Button
as="a"
href={`mailto:${SUPPORT_EMAIL}?subject=User verification request - ${user.name}&body=I would like to be verified as related to https://www.example.com`}
href={`mailto:${SUPPORT_EMAIL}?subject=User verification request - ${user.displayName ?? user.name}&body=I would like to be verified as related to https://www.example.com`}
label="Request verification"
size="sm"
/>
@@ -448,7 +452,7 @@ if (!user) return Astro.rewrite('/404')
</h2>
<Button
as="a"
href={`mailto:${SUPPORT_EMAIL}?subject=Service Affiliation Verification Request - ${user.name}&body=I would like to be verified as related to the services ACME as Admin and XYZ as Team Member. Here is the proof...`}
href={`mailto:${SUPPORT_EMAIL}?subject=Service Affiliation Verification Request - ${user.displayName ?? user.name}&body=I would like to be verified as related to the services ACME as Admin and XYZ as Team Member. Here is the proof...`}
label="Request"
size="md"
/>
@@ -916,13 +920,10 @@ if (!user) return Astro.rewrite('/404')
<Icon name={actionInfo.icon} class="size-4" />
{actionInfo.label}
{transaction.action === 'MANUAL_ADJUSTMENT' && transaction.grantedBy && (
<a
href={`/u/${transaction.grantedBy.name}`}
class="text-day-500 ml-1 hover:underline"
>
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
by {transaction.grantedBy.displayName || transaction.grantedBy.name}
</a>
<>
<span class="text-day-500">from</span>
<UserBadge user={transaction.grantedBy} size="sm" class="text-day-500" />
</>
)}
</span>
</td>
@@ -937,7 +938,12 @@ if (!user) return Astro.rewrite('/404')
{transaction.points}
</td>
<td class="text-day-400 px-4 py-3 text-right text-xs whitespace-nowrap">
{new Date(transaction.createdAt).toLocaleDateString()}
<TimeFormatted
date={transaction.createdAt}
prefix={false}
hourPrecision
caseType="sentence"
/>
</td>
</tr>
)

View File

@@ -1,7 +1,12 @@
---
import { z } from 'astro/zod'
import { Icon } from 'astro-icon/components'
import BadgeSmall from '../../components/BadgeSmall.astro'
import CommentModeration from '../../components/CommentModeration.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import UserBadge from '../../components/UserBadge.astro'
import {
commentStatusFilters,
commentStatusFiltersZodEnum,
@@ -36,11 +41,18 @@ const [comments = [], totalComments = 0] = await Astro.locals.banners.try(
prisma.comment.findManyAndCount({
where: statusFilter.whereClause,
include: {
author: true,
author: {
select: {
name: true,
displayName: true,
picture: true,
},
},
service: {
select: {
name: true,
slug: true,
imageUrl: true,
},
},
parent: {
@@ -72,7 +84,7 @@ const totalPages = Math.ceil(totalComments / PAGE_SIZE)
class={cn([
'font-title rounded-md border px-3 py-1 text-sm',
params.status === filter.value
? filter.styles.filter
? filter.classNames.filter
: 'border-zinc-700 transition-colors hover:border-green-500/50',
])}
>
@@ -98,22 +110,7 @@ const totalPages = Math.ceil(totalComments / PAGE_SIZE)
>
<div class="mb-4 flex flex-wrap items-center gap-2">
{/* Author Info */}
<span class="font-title text-sm">{comment.author.name}</span>
{comment.author.admin && (
<span class="rounded-sm bg-yellow-500/20 px-2 py-0.5 text-[12px] font-medium text-yellow-500">
admin
</span>
)}
{comment.author.verified && !comment.author.admin && (
<span class="rounded-sm bg-blue-500/20 px-2 py-0.5 text-[12px] font-medium text-blue-500">
verified
</span>
)}
{comment.author.verifier && !comment.author.admin && (
<span class="rounded-sm bg-green-500/20 px-2 py-0.5 text-[12px] font-medium text-green-500">
verifier
</span>
)}
<UserBadge user={comment.author} size="md" />
{/* Service Link */}
<span class="text-xs text-zinc-500">•</span>
@@ -121,21 +118,55 @@ const totalPages = Math.ceil(totalComments / PAGE_SIZE)
href={`/service/${comment.service.slug}#comment-${comment.id.toString()}`}
class="text-sm text-blue-400 transition-colors hover:text-blue-300"
>
{!!comment.service.imageUrl && (
<MyPicture
src={comment.service.imageUrl}
height={16}
width={16}
class="inline-block size-4 rounded-full align-[-0.2em]"
alt=""
/>
)}
{comment.service.name}
</a>
{/* Date */}
<span class="text-xs text-zinc-500">•</span>
<span class="text-sm text-zinc-400">{new Date(comment.createdAt).toLocaleString()}</span>
<TimeFormatted
date={comment.createdAt}
hourPrecision
caseType="sentence"
class="text-sm text-zinc-400"
/>
<span class="text-xs text-zinc-500">•</span>
{/* Status Badges */}
<span class={comment.statusFilterInfo.styles.badge}>{comment.statusFilterInfo.label}</span>
<BadgeSmall
color={comment.statusFilterInfo.color}
text={comment.statusFilterInfo.label}
icon={comment.statusFilterInfo.icon}
inlineIcon
/>
<span class="text-xs text-zinc-500">•</span>
{/* Link to Comment */}
<a
href={`/service/${comment.service.slug}?showPending=true#comment-${comment.id.toString()}`}
class="text-whit/50 flex items-center gap-1 text-sm transition-colors hover:underline"
>
<Icon name="ri:link" class="size-4" />
Open
</a>
</div>
{/* Parent Comment Context */}
{comment.parent && (
<div class="mb-4 border-l-2 border-zinc-700 pl-4">
<div class="mb-1 text-sm text-zinc-500">Replying to {comment.parent.author.name}:</div>
<div class="mb-1 text-sm opacity-50">
Replying to <UserBadge user={comment.parent.author} size="md" />
</div>
<div class="text-sm text-zinc-400">{comment.parent.content}</div>
</div>
)}

View File

@@ -66,6 +66,7 @@ const serviceSuggestion = await Astro.locals.banners.try('Error fetching service
user: {
select: {
id: true,
displayName: true,
name: true,
picture: true,
},

View File

@@ -6,6 +6,7 @@ import { orderBy as lodashOrderBy } from 'lodash-es'
import SortArrowIcon from '../../../components/SortArrowIcon.astro'
import TimeFormatted from '../../../components/TimeFormatted.astro'
import UserBadge from '../../../components/UserBadge.astro'
import {
getServiceSuggestionStatusInfo,
serviceSuggestionStatuses,
@@ -67,8 +68,9 @@ let suggestions = await prisma.serviceSuggestion.findMany({
createdAt: true,
user: {
select: {
id: true,
displayName: true,
name: true,
picture: true,
},
},
service: {
@@ -293,9 +295,7 @@ const makeSortUrl = (slug: string) => {
</div>
</td>
<td class="px-4 py-3">
<a href={`/admin/users/${suggestion.user.name}`} class="hover:text-green-500">
{suggestion.user.name}
</a>
<UserBadge user={suggestion.user} size="md" />
</td>
<td class="px-4 py-3">
<form method="POST" action={actions.admin.serviceSuggestions.update}>

View File

@@ -10,6 +10,7 @@ import { Icon } from 'astro-icon/components'
import { actions, isInputError } from 'astro:actions'
import MyPicture from '../../../../components/MyPicture.astro'
import UserBadge from '../../../../components/UserBadge.astro'
import { serviceVisibilities } from '../../../../constants/serviceVisibility'
import BaseLayout from '../../../../layouts/BaseLayout.astro'
import { cn } from '../../../../lib/cn'
@@ -80,9 +81,9 @@ const service = await Astro.locals.banners.try('Error fetching service', () =>
id: true,
user: {
select: {
id: true,
name: true,
displayName: true,
picture: true,
},
},
createdAt: true,
@@ -1183,7 +1184,9 @@ const buttonSmallWarningClasses = cn(
<tbody class="divide-y divide-zinc-700/80">
{service.verificationRequests.map((request) => (
<tr class="transition-colors hover:bg-zinc-700/40">
<td class="p-3 text-zinc-300">{request.user.displayName ?? request.user.name}</td>
<td class="p-3 text-zinc-300">
<UserBadge user={request.user} size="md" />
</td>
<td class="p-3 text-zinc-400">{new Date(request.createdAt).toLocaleString()}</td>
</tr>
))}

View File

@@ -116,7 +116,7 @@ if (!user) return Astro.rewrite('/404')
---
<BaseLayout
pageTitle={`User: ${user.name}`}
pageTitle={`${user.displayName ?? user.name} - User`}
widthClassName="max-w-screen-lg"
className={{ main: 'space-y-24' }}
>

View File

@@ -6,6 +6,7 @@ import { orderBy as lodashOrderBy } from 'lodash-es'
import SortArrowIcon from '../../../components/SortArrowIcon.astro'
import TimeFormatted from '../../../components/TimeFormatted.astro'
import Tooltip from '../../../components/Tooltip.astro'
import UserBadge from '../../../components/UserBadge.astro'
import BaseLayout from '../../../layouts/BaseLayout.astro'
import { zodParseQueryParamsStoringErrors } from '../../../lib/parseUrlFilters'
import { pluralize } from '../../../lib/pluralize'
@@ -74,6 +75,8 @@ const dbUsers = await prisma.user.findMany({
select: {
id: true,
name: true,
displayName: true,
picture: true,
verified: true,
admin: true,
verifier: true,
@@ -241,10 +244,10 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
class={`group hover:bg-zinc-700/30 ${user.spammer ? 'bg-red-900/10' : ''}`}
>
<td class="px-4 py-3 text-sm font-medium text-zinc-200">
<div>{user.name}</div>
<UserBadge user={user} size="md" class="flex text-white" />
{user.internalNotes.length > 0 && (
<Tooltip
class="text-2xs mt-1 text-yellow-400"
class="text-2xs font-light text-yellow-200/40"
position="right"
text={user.internalNotes
.map(
@@ -257,7 +260,7 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
)
.join('\n\n')}
>
<Icon name="ri:sticky-note-line" class="mr-1 inline-block size-3" />
<Icon name="ri:sticky-note-line" class="mr-0.5 inline-block size-3" />
{user.internalNotes.length} internal {pluralize('note', user.internalNotes.length)}
</Tooltip>
)}

View File

@@ -467,17 +467,6 @@ const ogImageTemplateData = {
}
<VerificationWarningBanner service={service} />
{
service.verificationSteps.some((step) => step.status === VerificationStepStatus.FAILED) && (
<div class="mb-4 flex items-center gap-2 rounded-md bg-red-900/50 p-2 text-sm text-red-300">
<Icon name="ri:error-warning-line" class="inline-block size-4 shrink-0" />
<span>
This service has failed one or more verification steps. Review the verification details carefully.
</span>
</div>
)
}
<div class="flex items-center gap-4">
{
!!service.imageUrl && (

View File

@@ -10,6 +10,7 @@ import InputTextArea from '../../components/InputTextArea.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import Tooltip from '../../components/Tooltip.astro'
import UserBadge from '../../components/UserBadge.astro'
import { getKarmaTransactionActionInfo } from '../../constants/karmaTransactionActions'
import { karmaUnlocks } from '../../constants/karmaUnlocks'
import { SUPPORT_EMAIL } from '../../constants/project'
@@ -64,6 +65,7 @@ const user = await Astro.locals.banners.try('user', async () => {
select: {
name: true,
displayName: true,
picture: true,
},
},
comment: {
@@ -175,8 +177,8 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
---
<BaseLayout
pageTitle={`${user.name} - Account`}
description="Manage your user profile"
pageTitle={`${user.displayName ?? user.name} - User Profile`}
description={`User profile page of ${user.displayName ?? user.name} in KYCnot.me`}
ogImage={{
template: 'generic',
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -204,7 +206,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
image: user.picture ?? undefined,
url: new URL(`/u/${user.name}`, Astro.url).href,
sameAs: user.link ? [user.link] : undefined,
description: `User account for ${user.displayName ?? user.name} on KYCnot.me`,
description: `User profile page for ${user.displayName ?? user.name} on KYCnot.me`,
identifier: [user.name, user.id.toString()],
jobTitle: user.admin ? 'Administrator' : user.verifier ? 'Moderator' : undefined,
memberOf: KYCNOTME_SCHEMA_MINI,
@@ -259,10 +261,10 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
}
<div class="grow">
<h1 class="font-title text-lg font-bold tracking-wider text-white">
{user.name}
{user.displayName ?? user.name}
{isCurrentUser && <span class="text-day-500 font-normal">(You)</span>}
</h1>
{user.displayName && <p class="text-day-200">{user.displayName}</p>}
{user.displayName && <p class="text-day-200 font-title">{user.name}</p>}
<div class="mt-1 flex gap-2">
{
user.admin && (
@@ -323,7 +325,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
)
}
<a
href={`mailto:${SUPPORT_EMAIL}`}
href={`mailto:${SUPPORT_EMAIL}?subject=User report - ${user.displayName ?? user.name}&body=${Astro.locals.user ? `I'm ${Astro.locals.user.displayName ? `${Astro.locals.user.displayName} (${Astro.locals.user.name})` : user.name}. ` : ''}I'm reporting the user ${user.displayName ? `"${user.displayName}" (${user.name})` : `"${user.name}"`} because...`}
class="inline-flex items-center gap-1 rounded-md border border-red-500/30 bg-red-500/10 px-3 py-1.5 text-sm text-red-400 shadow-xs transition-colors duration-200 hover:bg-red-500/20 focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
<Icon name="ri:alert-line" class="size-4" /> Report
@@ -1002,13 +1004,10 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
<Icon name={actionInfo.icon} class="size-4" />
{actionInfo.label}
{transaction.action === 'MANUAL_ADJUSTMENT' && transaction.grantedBy && (
<a
href={`/u/${transaction.grantedBy.name}`}
class="text-day-500 ml-1 hover:underline"
>
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
by {transaction.grantedBy.displayName || transaction.grantedBy.name}
</a>
<>
<span class="text-day-500">from</span>
<UserBadge user={transaction.grantedBy} size="sm" class="text-day-500" />
</>
)}
</span>
</td>