Compare commits

..

5 Commits

Author SHA1 Message Date
pluja
845aa1185c Release 2025-05-21-AQ5C 2025-05-21 07:03:39 +00:00
pluja
17b3642f7e Update favicon 2025-05-20 11:27:55 +00:00
pluja
d64268d396 fix logout issue 2025-05-20 11:12:55 +00:00
pluja
9c289753dd fix generate 2025-05-20 11:00:28 +00:00
pluja
8bdbe8ea36 small updates 2025-05-20 10:29:03 +00:00
39 changed files with 363 additions and 241 deletions

View File

@@ -2,3 +2,5 @@ DATABASE_URL="postgresql://kycnot:kycnot@localhost:3399/kycnot?schema=public"
REDIS_URL="redis://localhost:6379" REDIS_URL="redis://localhost:6379"
SOURCE_CODE_URL="https://github.com" SOURCE_CODE_URL="https://github.com"
SITE_URL="https://localhost:4321" SITE_URL="https://localhost:4321"
ONION_ADDRESS="http://kycnotmezdiftahfmc34pqbpicxlnx3jbf5p7jypge7gdvduu7i6qjqd.onion"
I2P_ADDRESS="http://nti3rj4j4disjcm2kvp4eno7otcejbbxv3ggxwr5tpfk4jucah7q.b32.i2p"

View File

@@ -74,6 +74,18 @@ export default defineConfig({
url: true, url: true,
optional: false, optional: false,
}), }),
I2P_ADDRESS: envField.string({
context: 'server',
access: 'public',
url: true,
optional: false,
}),
ONION_ADDRESS: envField.string({
context: 'server',
access: 'public',
url: true,
optional: false,
}),
REDIS_URL: envField.string({ REDIS_URL: envField.string({
context: 'server', context: 'server',

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 566 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 566 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 692 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 566 B

View File

@@ -151,15 +151,10 @@ export const accountActions = {
permissions: 'user', permissions: 'user',
input: z.object({ input: z.object({
id: z.coerce.number().int().positive(), id: z.coerce.number().int().positive(),
displayName: z.string().max(100, 'Display name must be 100 characters or less').optional().nullable(), displayName: z.string().max(100, 'Display name must be 100 characters or less').nullable(),
link: z link: z.string().url('Must be a valid URL').max(255, 'URL must be 255 characters or less').nullable(),
.string()
.url('Must be a valid URL')
.max(255, 'URL must be 255 characters or less')
.optional()
.nullable(),
pictureFile: imageFileSchema, pictureFile: imageFileSchema,
removePicture: z.coerce.boolean().default(false), removePicture: z.coerce.boolean(),
}), }),
handler: async (input, context) => { handler: async (input, context) => {
if (input.id !== context.locals.user.id) { if (input.id !== context.locals.user.id) {
@@ -170,7 +165,7 @@ export const accountActions = {
} }
if ( if (
input.displayName !== undefined && input.displayName !== null &&
input.displayName !== context.locals.user.displayName && input.displayName !== context.locals.user.displayName &&
!context.locals.user.karmaUnlocks.displayName !context.locals.user.karmaUnlocks.displayName
) { ) {
@@ -181,7 +176,7 @@ export const accountActions = {
} }
if ( if (
input.link !== undefined && input.link !== null &&
input.link !== context.locals.user.link && input.link !== context.locals.user.link &&
!context.locals.user.karmaUnlocks.websiteLink !context.locals.user.karmaUnlocks.websiteLink
) { ) {
@@ -198,6 +193,13 @@ export const accountActions = {
}) })
} }
if (input.removePicture && !context.locals.user.karmaUnlocks.profilePicture) {
throw new ActionError({
code: 'FORBIDDEN',
message: makeKarmaUnlockMessage(karmaUnlocksById.profilePicture),
})
}
const pictureUrl = const pictureUrl =
input.pictureFile && input.pictureFile.size > 0 input.pictureFile && input.pictureFile.size > 0
? await saveFileLocally( ? await saveFileLocally(
@@ -210,9 +212,13 @@ export const accountActions = {
const user = await prisma.user.update({ const user = await prisma.user.update({
where: { id: context.locals.user.id }, where: { id: context.locals.user.id },
data: { data: {
displayName: input.displayName ?? null, displayName: context.locals.user.karmaUnlocks.displayName ? (input.displayName ?? null) : undefined,
link: input.link ?? null, link: context.locals.user.karmaUnlocks.websiteLink ? (input.link ?? null) : undefined,
picture: input.removePicture ? null : (pictureUrl ?? undefined), picture: context.locals.user.karmaUnlocks.profilePicture
? input.removePicture
? null
: (pictureUrl ?? undefined)
: undefined,
}, },
}) })

View File

@@ -34,7 +34,7 @@ const Tag = announcement.link ? 'a' : 'div'
target={announcement.link ? '_blank' : undefined} target={announcement.link ? '_blank' : undefined}
rel="noopener noreferrer" rel="noopener noreferrer"
class={cn( class={cn(
'group relative isolate z-50 flex items-center justify-center gap-x-6 overflow-hidden border-b border-zinc-800 bg-black px-6 py-2 focus-visible:outline-none sm:px-3.5', 'group xs:px-6 2xs:px-4 relative isolate z-50 flex items-center justify-center gap-x-2 overflow-hidden border-b border-zinc-800 bg-black px-2 py-2 focus-visible:outline-none sm:gap-x-6 sm:px-3.5',
className className
)} )}
{...props} {...props}
@@ -66,24 +66,27 @@ const Tag = announcement.link ? 'a' : 'div'
</div> </div>
</div> </div>
<div class="flex items-center justify-between gap-x-3 md:justify-center"> <div class={cn('flex items-center justify-between gap-x-3 md:justify-center', typeInfo.classNames.icon)}>
<div class={cn('flex items-center gap-x-3', typeInfo.classNames.icon)}>
<Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} /> <Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} />
<span <span
class={cn( class={cn(
'font-title bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-clip-text text-sm leading-tight text-transparent [&_a]:underline', 'font-title line-clamp-3 bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-clip-text text-sm leading-tight text-pretty text-transparent [&_a]:underline',
typeInfo.classNames.content typeInfo.classNames.content
)} )}
> >
{announcement.content} {announcement.content}
</span> </span>
</div> </div>
</div>
<div <div
class="text-day-300 group-focus-visible:outline-primary transition-background relative inline-flex h-full min-w-[120px] cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-full border border-white/20 bg-black/10 p-[1px] px-3 py-1 text-sm font-medium shadow-sm backdrop-blur-3xl transition-colors group-hover:bg-white/5 group-focus-visible:ring-2 group-focus-visible:ring-blue-500 group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-black/80" class="text-day-300 group-focus-visible:outline-primary transition-background 2xs:px-4 relative inline-flex h-full shrink-0 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-full border border-white/20 bg-black/10 p-[1px] px-1 py-1 text-sm font-medium shadow-sm backdrop-blur-3xl transition-colors group-hover:bg-white/5 group-focus-visible:ring-2 group-focus-visible:ring-blue-500 group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-black/80 sm:min-w-[120px]"
> >
<span class="2xs:inline-block hidden">
{announcement.linkText} {announcement.linkText}
<Icon name="ri:arrow-right-line" class="size-4 transition-transform group-hover:translate-x-0.5" /> </span>
<Icon
name="ri:arrow-right-line"
class="size-4 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</div> </div>
</Tag> </Tag>

View File

@@ -1,6 +1,7 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { isInputError, type ActionAccept, type ActionClient } from 'astro:actions' import { isInputError, type ActionAccept, type ActionClient } from 'astro:actions'
import { Image } from 'astro:assets'
import { CAPTCHA_LENGTH, generateCaptcha } from '../lib/captcha' import { CAPTCHA_LENGTH, generateCaptcha } from '../lib/captcha'
import { cn } from '../lib/cn' import { cn } from '../lib/cn'

View File

@@ -75,9 +75,9 @@ const { messages, userId, class: className, ...htmlProps } = Astro.props
{!!message.user.picture && ( {!!message.user.picture && (
<MyPicture <MyPicture
src={message.user.picture} src={message.user.picture}
height={16} height={20}
width={16} width={20}
class="inline-block rounded-full align-[-0.33em]" class="inline-block size-5 rounded-full align-[-0.5em]"
alt="" alt=""
/> />
)} )}
@@ -86,16 +86,15 @@ const { messages, userId, class: className, ...htmlProps } = Astro.props
)} )}
<p <p
class={cn( class={cn(
'rounded-xl p-3 text-sm whitespace-pre-wrap', 'rounded-xl p-3 text-sm wrap-anywhere whitespace-pre-wrap',
isCurrentUser ? 'bg-blue-900 text-white' : 'bg-night-500 text-day-300', isCurrentUser ? 'bg-blue-900 text-white' : 'bg-night-500 text-day-300',
isCurrentUser ? 'rounded-br-xs' : 'rounded-bl-xs', isCurrentUser ? 'rounded-br-xs' : 'rounded-bl-xs',
isCurrentUser && isNextFromSameUser && isNextSameDate && 'rounded-tr-xs', isCurrentUser && isNextFromSameUser && isNextSameDate && 'rounded-tr-xs',
!isCurrentUser && isNextFromSameUser && isNextSameDate && 'rounded-tl-xs' !isCurrentUser && isNextFromSameUser && isNextSameDate && 'rounded-tl-xs'
)} )}
id={`message-${message.id.toString()}`} id={`message-${message.id.toString()}`}
> set:text={message.content}
{message.content} />
</p>
{(!isPrevFromSameUser || !isPrevSameDate) && ( {(!isPrevFromSameUser || !isPrevSameDate) && (
<p class="text-day-500 mt-0.5 mb-2 text-xs">{message.formattedCreatedAt}</p> <p class="text-day-500 mt-0.5 mb-2 text-xs">{message.formattedCreatedAt}</p>
)} )}

View File

@@ -371,8 +371,9 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
comment.communityNote && ( comment.communityNote && (
<div class="mt-2 peer-checked/collapse:hidden"> <div class="mt-2 peer-checked/collapse:hidden">
<div class="border-l-2 border-zinc-600 bg-zinc-900/30 py-0.5 pl-2 text-xs"> <div class="border-l-2 border-zinc-600 bg-zinc-900/30 py-0.5 pl-2 text-xs">
<span class="font-medium text-zinc-400">Added context:</span> <span class="prose prose-sm prose-invert prose-strong:text-zinc-300/90 text-xs text-zinc-300">
<span class="text-zinc-300">{comment.communityNote}</span> <Markdown content={`**Added context:** ${comment.communityNote}`} />
</span>
</div> </div>
</div> </div>
) )

View File

@@ -110,16 +110,18 @@ if (!user || !user.admin || !user.verifier) return null
<button <button
class={cn( class={cn(
'rounded-sm px-1.5 py-0.5 text-xs transition-colors', 'rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === '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'
)} )}
data-action="status" data-action="status"
data-value={comment.status === 'PENDING' ? 'APPROVED' : 'PENDING'} data-value={comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING'
? 'APPROVED'
: 'PENDING'}
data-comment-id={comment.id} data-comment-id={comment.id}
data-user-id={user.id} data-user-id={user.id}
> >
{comment.status === 'PENDING' ? 'Approve' : 'Pending'} {comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING' ? 'Approve' : 'Pending'}
</button> </button>
<button <button

View File

@@ -1,6 +1,6 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import QRCode from 'qrcode' import * as QRCode from 'qrcode'
import { cn } from '../lib/cn' import { cn } from '../lib/cn'
@@ -26,7 +26,7 @@ function getAddressURI(address: string, cryptoName: string) {
} }
const qrCodeDataURL = await QRCode.toDataURL(getAddressURI(address, cryptoName), { const qrCodeDataURL = await QRCode.toDataURL(getAddressURI(address, cryptoName), {
width: 128, width: 256,
margin: 1, margin: 1,
color: { color: {
dark: '#ffffff', dark: '#ffffff',
@@ -41,13 +41,18 @@ const qrCodeDataURL = await QRCode.toDataURL(getAddressURI(address, cryptoName),
<Icon name={cryptoIcon} class="size-6 text-white" /> <Icon name={cryptoIcon} class="size-6 text-white" />
<span class="font-title text-base font-semibold text-white">{cryptoName}</span> <span class="font-title text-base font-semibold text-white">{cryptoName}</span>
</div> </div>
<p class="px-7 font-mono text-base leading-snug tracking-wide break-all text-white"> <p
<span class="cursor-pointer px-7 font-mono text-base leading-snug tracking-wide break-all text-white select-all"
class="cursor-pointer select-all" >
set:html={address.length > 12 {
? `<span class="font-bold mr-0.5 text-green-500">${address.substring(0, 6)}</span>${address.substring(6, address.length - 6)}<span class="font-bold ml-0.5 text-green-500">${address.substring(address.length - 6)}</span>` address.length > 12
: `<span class="font-bold">${address}</span>`} ? [
/> <span class="mr-0.5 font-bold text-green-500">{address.substring(0, 6)}</span>,
address.substring(6, address.length - 6),
<span class="ml-0.5 font-bold text-green-500">{address.substring(address.length - 6)}</span>,
]
: address
}
</p> </p>
</div> </div>
<img <img
@@ -55,6 +60,6 @@ const qrCodeDataURL = await QRCode.toDataURL(getAddressURI(address, cryptoName),
alt={`${cryptoName} QR code`} alt={`${cryptoName} QR code`}
width="128" width="128"
height="128" height="128"
class="mr-4 hidden size-36 rounded sm:block" class="mr-4 hidden size-32 rounded sm:block"
/> />
</div> </div>

View File

@@ -1,6 +1,6 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { SOURCE_CODE_URL } from 'astro:env/server' import { SOURCE_CODE_URL, I2P_ADDRESS, ONION_ADDRESS } from 'astro:env/server'
import { cn } from '../lib/cn' import { cn } from '../lib/cn'
@@ -11,10 +11,22 @@ type Props = HTMLAttributes<'footer'>
const links = [ const links = [
{ {
href: SOURCE_CODE_URL, href: SOURCE_CODE_URL,
label: 'Source Code', label: 'Code',
icon: 'ri:git-repository-line', icon: 'ri:git-repository-line',
external: true, external: true,
}, },
{
href: ONION_ADDRESS,
label: 'Tor',
icon: 'onion',
external: true,
},
{
href: I2P_ADDRESS,
label: 'I2P',
icon: 'i2p',
external: true,
},
{ {
href: '/about', href: '/about',
label: 'About', label: 'About',

View File

@@ -159,7 +159,6 @@ const splashText = showSplashText ? sample(splashTexts) : null
<Icon name="ri:user-shared-2-line" class="size-4" /> <Icon name="ri:user-shared-2-line" class="size-4" />
</a> </a>
) : ( ) : (
DEPLOYMENT_MODE !== 'production' && (
<a <a
href="/account/logout" href="/account/logout"
data-astro-prefetch="tap" data-astro-prefetch="tap"
@@ -169,7 +168,6 @@ const splashText = showSplashText ? sample(splashTexts) : null
> >
<Icon name="ri:logout-box-r-line" class="size-4" /> <Icon name="ri:logout-box-r-line" class="size-4" />
</a> </a>
)
)} )}
</> </>
) : ( ) : (

View File

@@ -25,8 +25,19 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
<InputWrapper inputId={inputId} {...wrapperProps}> <InputWrapper inputId={inputId} {...wrapperProps}>
{ {
!!removeCheckbox && ( !!removeCheckbox && (
<label class="flex cursor-pointer items-center gap-2 py-1 pl-1 text-sm leading-none"> <label
<input transition:persist type="checkbox" name={removeCheckbox.name} data-remove-checkbox /> class={cn(
'flex cursor-pointer items-center gap-2 py-1 pl-1 text-sm leading-none',
disabled && 'cursor-not-allowed opacity-50'
)}
>
<input
transition:persist
type="checkbox"
name={removeCheckbox.name}
data-remove-checkbox
disabled={disabled}
/>
{removeCheckbox.label || 'Remove'} {removeCheckbox.label || 'Remove'}
</label> </label>
) )

View File

@@ -31,6 +31,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
hasError && baseInputClassNames.error, hasError && baseInputClassNames.error,
!!inputProps?.disabled && baseInputClassNames.disabled !!inputProps?.disabled && baseInputClassNames.disabled
)} )}
name={wrapperProps.name}>{value}</textarea name={wrapperProps.name}
> set:text={value}
/>
</InputWrapper> </InputWrapper>

View File

@@ -4,6 +4,7 @@ import type { ComponentProps } from 'react'
import { Picture } from 'astro:assets' import { Picture } from 'astro:assets'
import defaultServiceImage from '../assets/fallback-service-image.jpg' import defaultServiceImage from '../assets/fallback-service-image.jpg'
import { cn } from '../lib/cn'
const fallbackImages = { const fallbackImages = {
service: defaultServiceImage, service: defaultServiceImage,
@@ -20,6 +21,7 @@ const {
fallback = undefined as keyof typeof fallbackImages | undefined, fallback = undefined as keyof typeof fallbackImages | undefined,
height, height,
width, width,
pictureAttributes,
...props ...props
} = Astro.props } = Astro.props
@@ -36,6 +38,10 @@ const fallbackImage = fallback ? fallbackImages[fallback] : undefined
formats={formats} formats={formats}
height={height ? Number(height) * 2 : undefined} height={height ? Number(height) * 2 : undefined}
width={width ? Number(width) * 2 : undefined} width={width ? Number(width) * 2 : undefined}
pictureAttributes={{
...pictureAttributes,
class: cn('shrink-0', pictureAttributes?.class),
}}
{...(props as any)} {...(props as any)}
/> />
) )

View File

@@ -36,7 +36,7 @@ const {
class={cn( class={cn(
'pointer-events-none hidden select-none group-hover/tooltip:flex', 'pointer-events-none hidden select-none group-hover/tooltip:flex',
'ease-out-cubic scale-75 opacity-0 transition-all transition-discrete duration-100 group-hover/tooltip:scale-100 group-hover/tooltip:opacity-100 starting:group-hover/tooltip:scale-75 starting:group-hover/tooltip:opacity-0', 'ease-out-cubic scale-75 opacity-0 transition-all transition-discrete duration-100 group-hover/tooltip:scale-100 group-hover/tooltip:opacity-100 starting:group-hover/tooltip:scale-75 starting:group-hover/tooltip:opacity-0',
'z-1000 w-max max-w-sm rounded-lg px-3 py-2 font-sans text-sm font-normal tracking-normal text-pretty whitespace-pre-wrap', 'z-1000 w-max max-w-sm rounded-lg px-3 py-2 font-sans text-sm font-normal tracking-normal text-pretty wrap-anywhere whitespace-pre-wrap',
// Position classes // Position classes
{ {
'absolute -top-2 left-1/2 origin-bottom -translate-x-1/2 translate-y-[calc(-100%+0.5rem)] text-center group-hover/tooltip:-translate-y-full starting:group-hover/tooltip:translate-y-[calc(-100%+0.25rem)]': 'absolute -top-2 left-1/2 origin-bottom -translate-x-1/2 translate-y-[calc(-100%+0.5rem)] text-center group-hover/tooltip:-translate-y-full starting:group-hover/tooltip:translate-y-[calc(-100%+0.25rem)]':
@@ -85,9 +85,8 @@ const {
}, },
classNames?.tooltip classNames?.tooltip
)} )}
> set:text={text}
{text} />
</span>
) )
} }
</Component> </Component>

View File

@@ -45,7 +45,7 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
)} )}
</p> </p>
{!!service.verificationSummary && ( {!!service.verificationSummary && (
<div class="mt-2 whitespace-pre-wrap">{service.verificationSummary}</div> <div class="mt-2 wrap-anywhere whitespace-pre-wrap" set:text={service.verificationSummary} />
)} )}
</div> </div>
) : service.verificationStatus === 'COMMUNITY_CONTRIBUTED' ? ( ) : service.verificationStatus === 'COMMUNITY_CONTRIBUTED' ? (

View File

@@ -32,7 +32,7 @@ export const {
{ {
id: 'HUMAN_PENDING', id: 'HUMAN_PENDING',
icon: 'ri:question-line', icon: 'ri:question-line',
label: 'Pending 2', label: 'Pending',
creativeWorkStatus: 'Deleted', creativeWorkStatus: 'Deleted',
}, },
{ {

View File

@@ -50,6 +50,15 @@ export const {
badge: 'rounded-sm bg-blue-500/20 px-2 py-0.5 text-[12px] font-medium text-blue-500', 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',
whereClause: { status: 'HUMAN_PENDING' },
styles: {
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', label: 'Rejected',
value: 'rejected', value: 'rejected',
@@ -123,10 +132,12 @@ export function getCommentStatusFilterValue(
if (comment.suspicious) return 'suspicious' if (comment.suspicious) return 'suspicious'
switch (comment.status) { switch (comment.status) {
case 'PENDING': case 'PENDING': {
case 'HUMAN_PENDING': {
return 'pending' return 'pending'
} }
case 'HUMAN_PENDING': {
return 'human-pending'
}
case 'VERIFIED': { case 'VERIFIED': {
return 'verified' return 'verified'
} }

View File

@@ -14,4 +14,7 @@ export const splashTexts: string[] = [
'Ditch the gatekeepers.', 'Ditch the gatekeepers.',
'Own your identity.', 'Own your identity.',
'Financial privacy matters.', 'Financial privacy matters.',
'Surveillance is the enemy of the soul.',
'Privacy is freedom.',
'Privacy is the freedom to try things out.',
] ]

View File

@@ -70,7 +70,7 @@ export const {
description: description:
'Thoroughly tested and verified by the team. But things might change, this is not a guarantee.', 'Thoroughly tested and verified by the team. But things might change, this is not a guarantee.',
privacyPoints: 0, privacyPoints: 0,
trustPoints: 5, trustPoints: 10,
classNames: { classNames: {
icon: 'text-[#40e6c2]', icon: 'text-[#40e6c2]',
badgeBig: 'bg-green-800/50 text-green-100', badgeBig: 'bg-green-800/50 text-green-100',

View File

@@ -39,16 +39,19 @@ const {
</p> </p>
{ {
(DEPLOYMENT_MODE !== 'production' || Astro.locals.user?.admin) && ( (DEPLOYMENT_MODE !== 'production' || Astro.locals.user?.admin) && (
<div class="bg-night-800 mt-4 block max-h-96 min-h-32 w-full max-w-4xl overflow-auto rounded-lg p-4 text-left text-sm break-words whitespace-pre-wrap"> <div
{error instanceof Error class="bg-night-800 mt-4 block max-h-96 min-h-32 w-full max-w-4xl overflow-auto rounded-lg p-4 text-left text-sm wrap-anywhere whitespace-pre-wrap"
set:text={
error instanceof Error
? error.message ? error.message
: error === undefined : error === undefined
? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
message || 'undefined' message || 'undefined'
: typeof error === 'object' : typeof error === 'object'
? JSON.stringify(error, null, 2) ? JSON.stringify(error, null, 2)
: String(error as unknown)} : String(error as unknown)
</div> }
/>
) )
} }

View File

@@ -202,12 +202,10 @@ If you like this project, you can **support** it through these methods:
## Contact ## Contact
You can contact via direct chat or via email. You can contact via direct chat:
- [SimpleX Chat](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FcgKHYUYnpAIVoGb9lxb0qEMEpvYIvc1O%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAIW_JSq8wOsLKG4Xv4O54uT2D_l8MJBYKQIFj1FjZpnU%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion) - [SimpleX Chat](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FcgKHYUYnpAIVoGb9lxb0qEMEpvYIvc1O%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAIW_JSq8wOsLKG4Xv4O54uT2D_l8MJBYKQIFj1FjZpnU%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion)
- If you use ProtonMail or Tutanota, you can have E2E encrypted communications with us directly. We also offer a [PGP Key](/pgp). Otherwise, we recommend reaching out via SimpleX chat for encrypted communications.
## Disclaimer ## Disclaimer

View File

@@ -34,9 +34,10 @@ if (reasonType === 'admin-required' && Astro.locals.user?.admin) {
<h1 class="font-title mt-8 text-3xl font-semibold tracking-wide text-white sm:mt-12 sm:text-5xl"> <h1 class="font-title mt-8 text-3xl font-semibold tracking-wide text-white sm:mt-12 sm:text-5xl">
Access denied Access denied
</h1> </h1>
<p class="mt-8 text-lg leading-7 text-balance whitespace-pre-wrap text-red-400"> <p
{reason} class="mt-8 text-lg leading-7 text-balance wrap-anywhere whitespace-pre-wrap text-red-400"
</p> set:text={reason}
/>
<div class="mt-12 flex flex-wrap items-center justify-center"> <div class="mt-12 flex flex-wrap items-center justify-center">
<a href="/" class="focus-visible:outline-primary group flex items-center gap-2 px-3.5 py-2.5 text-white"> <a href="/" class="focus-visible:outline-primary group flex items-center gap-2 px-3.5 py-2.5 text-white">

View File

@@ -48,7 +48,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
name="displayName" name="displayName"
error={inputErrors.displayName} error={inputErrors.displayName}
inputProps={{ inputProps={{
value: user.displayName ?? '', value: user.displayName,
maxlength: 100, maxlength: 100,
disabled: !user.karmaUnlocks.displayName, disabled: !user.karmaUnlocks.displayName,
}} }}
@@ -62,7 +62,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
name="link" name="link"
error={inputErrors.link} error={inputErrors.link}
inputProps={{ inputProps={{
value: user.link ?? '', value: user.link,
type: 'url', type: 'url',
placeholder: 'https://example.com', placeholder: 'https://example.com',
disabled: !user.karmaUnlocks.websiteLink, disabled: !user.karmaUnlocks.websiteLink,

View File

@@ -181,13 +181,14 @@ if (!user) return Astro.rewrite('/404')
]} ]}
> >
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"> <section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center gap-4"> <header class="flex flex-wrap items-center justify-center gap-4">
<div class="flex grow flex-wrap items-center justify-center gap-4">
{ {
user.picture ? ( user.picture ? (
<MyPicture <MyPicture
src={user.picture} src={user.picture}
alt="" alt=""
class="ring-day-500/30 size-16 rounded-full ring-2" class="ring-day-500/30 xs:size-14 size-12 rounded-full ring-2 sm:size-16"
width={64} width={64}
height={64} height={64}
/> />
@@ -197,34 +198,33 @@ if (!user) return Astro.rewrite('/404')
</div> </div>
) )
} }
<div> <div class="grow">
<h1 class="font-title text-lg font-bold tracking-wider text-white">{user.name}</h1> <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>} {user.displayName && <p class="text-day-200">{user.displayName}</p>}
<div class="mt-1 flex gap-2">
{ {
user.admin && ( (user.admin || user.verified || user.verifier) && (
<div class="mt-1 flex gap-2">
{user.admin && (
<span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400"> <span class="rounded-full border border-red-500/50 bg-red-500/20 px-2 py-0.5 text-xs text-red-400">
admin admin
</span> </span>
) )}
} {user.verified && (
{
user.verified && (
<span class="rounded-full border border-blue-500/50 bg-blue-500/20 px-2 py-0.5 text-xs text-blue-400"> <span class="rounded-full border border-blue-500/50 bg-blue-500/20 px-2 py-0.5 text-xs text-blue-400">
verified verified
</span> </span>
) )}
} {user.verifier && (
{
user.verifier && (
<span class="rounded-full border border-green-500/50 bg-green-500/20 px-2 py-0.5 text-xs text-green-400"> <span class="rounded-full border border-green-500/50 bg-green-500/20 px-2 py-0.5 text-xs text-green-400">
verifier verifier
</span> </span>
)}
</div>
) )
} }
</div> </div>
</div> </div>
<nav class="ml-auto flex items-center gap-2"> <nav class="flex flex-wrap items-center justify-center gap-2">
<Tooltip <Tooltip
as="a" as="a"
href={`/u/${user.name}`} href={`/u/${user.name}`}

View File

@@ -180,7 +180,8 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
required required
rows="3" rows="3"
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 placeholder-zinc-500 focus:border-green-500 focus:ring-green-500 focus:outline-none" class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 placeholder-zinc-500 focus:border-green-500 focus:ring-green-500 focus:outline-none"
></textarea> set:text=""
/>
{ {
createInputErrors.description && ( createInputErrors.description && (
<p class="mt-1 text-sm text-red-400">{createInputErrors.description.join(', ')}</p> <p class="mt-1 text-sm text-red-400">{createInputErrors.description.join(', ')}</p>
@@ -593,9 +594,8 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
required required
rows="3" rows="3"
class="mt-1 w-full rounded-md border border-zinc-600 bg-zinc-800 px-3 py-2 text-sm text-zinc-200 placeholder-zinc-500 focus:border-blue-500 focus:ring-blue-500 focus:outline-none" class="mt-1 w-full rounded-md border border-zinc-600 bg-zinc-800 px-3 py-2 text-sm text-zinc-200 placeholder-zinc-500 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
> set:text={attribute.description}
{attribute.description} />
</textarea>
</div> </div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4"> <div class="grid grid-cols-2 gap-4 sm:grid-cols-4">

View File

@@ -127,7 +127,7 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
<span class="font-title text-gray-400">Submitted by:</span> <span class="font-title text-gray-400">Submitted by:</span>
<span class="text-gray-300"> <span class="text-gray-300">
<a href={`/admin/users?name=${serviceSuggestion.user.name}`} class="hover:text-green-500"> <a href={`/admin/users/${serviceSuggestion.user.name}`} class="hover:text-green-500">
{serviceSuggestion.user.name} {serviceSuggestion.user.name}
</a> </a>
</span> </span>
@@ -148,9 +148,10 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
serviceSuggestion.notes && ( serviceSuggestion.notes && (
<div class="mb-4"> <div class="mb-4">
<h3 class="font-title mb-1 text-sm text-gray-400">Notes from user:</h3> <h3 class="font-title mb-1 text-sm text-gray-400">Notes from user:</h3>
<div class="rounded-md border border-gray-700 bg-black/50 p-3 text-sm whitespace-pre-wrap text-gray-300"> <div
{serviceSuggestion.notes} class="rounded-md border border-gray-700 bg-black/50 p-3 text-sm wrap-anywhere whitespace-pre-wrap text-gray-300"
</div> set:text={serviceSuggestion.notes}
/>
</div> </div>
) )
} }

View File

@@ -293,7 +293,7 @@ const makeSortUrl = (slug: string) => {
</div> </div>
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<a href={`/admin/users?name=${suggestion.user.name}`} class="hover:text-green-500"> <a href={`/admin/users/${suggestion.user.name}`} class="hover:text-green-500">
{suggestion.user.name} {suggestion.user.name}
</a> </a>
</td> </td>

View File

@@ -211,8 +211,9 @@ const buttonSmallWarningClasses = cn(
id="description" id="description"
required required
rows={4} rows={4}
class={inputBaseClasses}>{service.description}</textarea class={inputBaseClasses}
> set:text={service.description}
/>
{ {
serviceInputErrors.description && ( serviceInputErrors.description && (
<p class={errorTextClasses}>{serviceInputErrors.description.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.description.join(', ')}</p>
@@ -243,8 +244,8 @@ const buttonSmallWarningClasses = cn(
id="serviceUrls" id="serviceUrls"
rows={3} rows={3}
placeholder="https://example1.com\nhttps://example2.com" placeholder="https://example1.com\nhttps://example2.com"
>{service.serviceUrls.join('\n')}</textarea set:text={service.serviceUrls.join('\n')}
> />
{ {
serviceInputErrors.serviceUrls && ( serviceInputErrors.serviceUrls && (
<p class={errorTextClasses}>{serviceInputErrors.serviceUrls.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.serviceUrls.join(', ')}</p>
@@ -260,8 +261,8 @@ const buttonSmallWarningClasses = cn(
id="tosUrls" id="tosUrls"
rows={3} rows={3}
placeholder="https://example1.com/tos\nhttps://example2.com/tos" placeholder="https://example1.com/tos\nhttps://example2.com/tos"
>{service.tosUrls.join('\n')}</textarea set:text={service.tosUrls.join('\n')}
> />
{ {
serviceInputErrors.tosUrls && ( serviceInputErrors.tosUrls && (
<p class={errorTextClasses}>{serviceInputErrors.tosUrls.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.tosUrls.join(', ')}</p>
@@ -277,8 +278,8 @@ const buttonSmallWarningClasses = cn(
id="onionUrls" id="onionUrls"
rows={3} rows={3}
placeholder="http://example1.onion\nhttp://example2.onion" placeholder="http://example1.onion\nhttp://example2.onion"
>{service.onionUrls.join('\n')}</textarea set:text={service.onionUrls.join('\n')}
> />
{ {
serviceInputErrors.onionUrls && ( serviceInputErrors.onionUrls && (
<p class={errorTextClasses}>{serviceInputErrors.onionUrls.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.onionUrls.join(', ')}</p>
@@ -294,8 +295,8 @@ const buttonSmallWarningClasses = cn(
id="i2pUrls" id="i2pUrls"
rows={3} rows={3}
placeholder="http://example1.b32.i2p\nhttp://example2.b32.i2p" placeholder="http://example1.b32.i2p\nhttp://example2.b32.i2p"
>{service.i2pUrls.join('\n')}</textarea set:text={service.i2pUrls.join('\n')}
> />
{/* Assuming i2pUrls might have errors, add error display if schema supports it */} {/* Assuming i2pUrls might have errors, add error display if schema supports it */}
{ {
/* serviceInputErrors.i2pUrls && ( /* serviceInputErrors.i2pUrls && (
@@ -515,8 +516,9 @@ const buttonSmallWarningClasses = cn(
class={inputBaseClasses} class={inputBaseClasses}
name="verificationSummary" name="verificationSummary"
id="verificationSummary" id="verificationSummary"
rows={4}>{service.verificationSummary}</textarea rows={4}
> set:text={service.verificationSummary}
/>
{ {
serviceInputErrors.verificationSummary && ( serviceInputErrors.verificationSummary && (
<p class={errorTextClasses}>{serviceInputErrors.verificationSummary.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.verificationSummary.join(', ')}</p>
@@ -531,8 +533,9 @@ const buttonSmallWarningClasses = cn(
class={inputBaseClasses} class={inputBaseClasses}
name="verificationProofMd" name="verificationProofMd"
id="verificationProofMd" id="verificationProofMd"
rows={8}>{service.verificationProofMd}</textarea rows={8}
> set:text={service.verificationProofMd}
/>
{ {
serviceInputErrors.verificationProofMd && ( serviceInputErrors.verificationProofMd && (
<p class={errorTextClasses}>{serviceInputErrors.verificationProofMd.join(', ')}</p> <p class={errorTextClasses}>{serviceInputErrors.verificationProofMd.join(', ')}</p>
@@ -731,9 +734,8 @@ const buttonSmallWarningClasses = cn(
required required
rows={3} rows={3}
class={inputBaseClasses} class={inputBaseClasses}
> set:text={event.content}
{event.content} />
</textarea>
{eventUpdateInputErrors.content && ( {eventUpdateInputErrors.content && (
<p class={errorTextClasses}>{eventUpdateInputErrors.content.join(', ')}</p> <p class={errorTextClasses}>{eventUpdateInputErrors.content.join(', ')}</p>
)} )}
@@ -847,8 +849,14 @@ const buttonSmallWarningClasses = cn(
</div> </div>
<div> <div>
<label for="newEventContent" class={labelBaseClasses}>Content</label> <label for="newEventContent" class={labelBaseClasses}>Content</label>
<textarea name="content" id="newEventContent" required rows={3} class={inputBaseClasses} <textarea
></textarea> name="content"
id="newEventContent"
required
rows={3}
class={inputBaseClasses}
set:text=""
/>
{ {
eventInputErrors.content && ( eventInputErrors.content && (
<p class={errorTextClasses}>{eventInputErrors.content.join(', ')}</p> <p class={errorTextClasses}>{eventInputErrors.content.join(', ')}</p>
@@ -1014,9 +1022,8 @@ const buttonSmallWarningClasses = cn(
id={`stepDescriptionEdit-${step.id}`} id={`stepDescriptionEdit-${step.id}`}
rows={2} rows={2}
class={inputBaseClasses} class={inputBaseClasses}
> set:text={step.description}
{step.description} />
</textarea>
{verificationStepUpdateInputErrors.description && ( {verificationStepUpdateInputErrors.description && (
<p class={errorTextClasses}> <p class={errorTextClasses}>
{verificationStepUpdateInputErrors.description.join(', ')} {verificationStepUpdateInputErrors.description.join(', ')}
@@ -1032,9 +1039,8 @@ const buttonSmallWarningClasses = cn(
id={`stepEvidenceMdEdit-${step.id}`} id={`stepEvidenceMdEdit-${step.id}`}
rows={4} rows={4}
class={inputBaseClasses} class={inputBaseClasses}
> set:text={step.evidenceMd}
{step.evidenceMd} />
</textarea>
{verificationStepUpdateInputErrors.evidenceMd && ( {verificationStepUpdateInputErrors.evidenceMd && (
<p class={errorTextClasses}> <p class={errorTextClasses}>
{verificationStepUpdateInputErrors.evidenceMd.join(', ')} {verificationStepUpdateInputErrors.evidenceMd.join(', ')}
@@ -1104,8 +1110,14 @@ const buttonSmallWarningClasses = cn(
</div> </div>
<div> <div>
<label for="newStepDescription" class={labelBaseClasses}>Description (Max 200 chars)</label> <label for="newStepDescription" class={labelBaseClasses}>Description (Max 200 chars)</label>
<textarea name="description" id="newStepDescription" required rows={3} class={inputBaseClasses} <textarea
></textarea> name="description"
id="newStepDescription"
required
rows={3}
class={inputBaseClasses}
set:text=""
/>
{ {
verificationStepInputErrors.description && ( verificationStepInputErrors.description && (
<p class={errorTextClasses}>{verificationStepInputErrors.description.join(', ')}</p> <p class={errorTextClasses}>{verificationStepInputErrors.description.join(', ')}</p>
@@ -1114,7 +1126,13 @@ const buttonSmallWarningClasses = cn(
</div> </div>
<div> <div>
<label for="newStepEvidenceMd" class={labelBaseClasses}>Evidence (Markdown)</label> <label for="newStepEvidenceMd" class={labelBaseClasses}>Evidence (Markdown)</label>
<textarea name="evidenceMd" id="newStepEvidenceMd" rows={5} class={inputBaseClasses}></textarea> <textarea
name="evidenceMd"
id="newStepEvidenceMd"
rows={5}
class={inputBaseClasses}
set:text=""
/>
{ {
verificationStepInputErrors.evidenceMd && ( verificationStepInputErrors.evidenceMd && (
<p class={errorTextClasses}>{verificationStepInputErrors.evidenceMd.join(', ')}</p> <p class={errorTextClasses}>{verificationStepInputErrors.evidenceMd.join(', ')}</p>

View File

@@ -64,7 +64,8 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
id="description" id="description"
required required
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" 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"
></textarea> set:text=""
/>
{ {
inputErrors.description && ( inputErrors.description && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.description.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.description.join(', ')}</p>
@@ -80,7 +81,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
name="serviceUrls" name="serviceUrls"
id="serviceUrls" id="serviceUrls"
rows={3} rows={3}
placeholder="https://example1.com https://example2.com"></textarea> placeholder="https://example1.com https://example2.com"
set:text=""
/>
{ {
inputErrors.serviceUrls && ( inputErrors.serviceUrls && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.serviceUrls.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.serviceUrls.join(', ')}</p>
@@ -96,7 +99,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
name="tosUrls" name="tosUrls"
id="tosUrls" id="tosUrls"
rows={3} rows={3}
placeholder="https://example1.com/tos https://example2.com/tos"></textarea> placeholder="https://example1.com/tos https://example2.com/tos"
set:text=""
/>
{ {
inputErrors.tosUrls && ( inputErrors.tosUrls && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.tosUrls.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.tosUrls.join(', ')}</p>
@@ -112,7 +117,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
name="onionUrls" name="onionUrls"
id="onionUrls" id="onionUrls"
rows={3} rows={3}
placeholder="http://example.onion"></textarea> placeholder="http://example.onion"
set:text=""
/>
{ {
inputErrors.onionUrls && ( inputErrors.onionUrls && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.onionUrls.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.onionUrls.join(', ')}</p>
@@ -266,7 +273,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
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" 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"
name="verificationSummary" name="verificationSummary"
id="verificationSummary" id="verificationSummary"
rows={3}></textarea> rows={3}
set:text=""
/>
{ {
inputErrors.verificationSummary && ( inputErrors.verificationSummary && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.verificationSummary.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.verificationSummary.join(', ')}</p>
@@ -283,7 +292,9 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
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" 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"
name="verificationProofMd" name="verificationProofMd"
id="verificationProofMd" id="verificationProofMd"
rows={10}></textarea> rows={10}
set:text=""
/>
{ {
inputErrors.verificationProofMd && ( inputErrors.verificationProofMd && (
<p class="font-title mt-1 text-sm text-red-500">{inputErrors.verificationProofMd.join(', ')}</p> <p class="font-title mt-1 text-sm text-red-500">{inputErrors.verificationProofMd.join(', ')}</p>

View File

@@ -306,7 +306,7 @@ if (!user) return Astro.rewrite('/404')
</div> </div>
<div data-note-content> <div data-note-content>
<p class="text-day-200 whitespace-pre-wrap">{note.content}</p> <p class="text-day-200 wrap-anywhere whitespace-pre-wrap" set:text={note.content} />
</div> </div>
<form <form

View File

@@ -336,6 +336,14 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
> >
<Icon name="ri:edit-line" class="size-4" /> <Icon name="ri:edit-line" class="size-4" />
</Tooltip> </Tooltip>
<Tooltip
as="a"
href={`/u/${user.name}`}
class="inline-flex items-center rounded-md border border-green-500/50 bg-green-500/20 px-1 py-1 text-xs text-green-400 transition-colors hover:bg-green-500/30"
text="Public profile"
>
<Icon name="ri:global-line" class="size-4" />
</Tooltip>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -232,7 +232,7 @@ const notifications = dbNotifications.map((notification) => ({
<div> <div>
<div class="font-medium text-zinc-200">{notification.title}</div> <div class="font-medium text-zinc-200">{notification.title}</div>
<p class="text-sm text-zinc-400">{notification.content}</p> <p class="text-sm wrap-anywhere text-zinc-400">{notification.content}</p>
<div class="mt-1 text-xs text-zinc-500"> <div class="mt-1 text-xs text-zinc-500">
<TimeFormatted date={notification.createdAt} prefix={false} caseType="sentence" /> <TimeFormatted date={notification.createdAt} prefix={false} caseType="sentence" />
</div> </div>

View File

@@ -149,9 +149,13 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
<div class="mt-6"> <div class="mt-6">
<div class="text-day-200 mb-2 text-sm">Notes for moderators:</div> <div class="text-day-200 mb-2 text-sm">Notes for moderators:</div>
<div class="text-sm whitespace-pre-wrap"> {
{serviceSuggestion.notes ?? <span class="italic">Empty</span>} serviceSuggestion.notes ? (
</div> <div class="text-sm wrap-anywhere whitespace-pre-wrap" set:text={serviceSuggestion.notes} />
) : (
<span class="text-sm italic">Empty</span>
)
}
</div> </div>
</section> </section>

View File

@@ -240,13 +240,14 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
]} ]}
> >
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"> <section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs">
<header class="flex items-center gap-4"> <header class="flex flex-wrap items-center justify-center gap-4">
<div class="flex grow flex-wrap items-center justify-center gap-4">
{ {
user.picture ? ( user.picture ? (
<MyPicture <MyPicture
src={user.picture} src={user.picture}
alt="" alt=""
class="ring-day-500/30 size-16 rounded-full ring-2" class="ring-day-500/30 xs:size-14 size-12 rounded-full ring-2 sm:size-16"
width={64} width={64}
height={64} height={64}
/> />
@@ -256,7 +257,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
</div> </div>
) )
} }
<div> <div class="grow">
<h1 class="font-title text-lg font-bold tracking-wider text-white"> <h1 class="font-title text-lg font-bold tracking-wider text-white">
{user.name} {user.name}
{isCurrentUser && <span class="text-day-500 font-normal">(You)</span>} {isCurrentUser && <span class="text-day-500 font-normal">(You)</span>}
@@ -286,7 +287,8 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
} }
</div> </div>
</div> </div>
<nav class="ml-auto flex items-center gap-2"> </div>
<nav class="flex items-center gap-2">
<AdminOnly> <AdminOnly>
<Tooltip <Tooltip
as="a" as="a"
@@ -521,7 +523,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
<TimeFormatted date={note.createdAt} hourPrecision /> <TimeFormatted date={note.createdAt} hourPrecision />
</span> </span>
</div> </div>
<div class="text-day-200 whitespace-pre-wrap">{note.content}</div> <div class="text-day-200 wrap-anywhere whitespace-pre-wrap" set:text={note.content} />
</div> </div>
))} ))}
</div> </div>