This commit is contained in:
pluja
2025-05-19 21:31:29 +00:00
parent 636057f8e0
commit a21dc81099
13 changed files with 135 additions and 93 deletions

View File

@@ -55,7 +55,7 @@ export const adminUserActions = {
.default(null) // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .default(null) // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
.transform((val) => val || null), .transform((val) => val || null),
pictureFile: z.instanceof(File).optional(), pictureFile: z.instanceof(File).optional(),
role: z.enum(['admin', 'verifier', 'spammer']), type: z.array(z.enum(['admin', 'verifier', 'spammer'])),
verifiedLink: z verifiedLink: z
.string() .string()
.url('Invalid URL') .url('Invalid URL')
@@ -69,7 +69,7 @@ export const adminUserActions = {
.default(null) // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing .default(null) // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
.transform((val) => val || null), .transform((val) => val || null),
}), }),
handler: async ({ id, pictureFile, ...valuesToUpdate }) => { handler: async ({ id, pictureFile, type, ...valuesToUpdate }) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id, id,
@@ -94,9 +94,15 @@ export const adminUserActions = {
const updatedUser = await prisma.user.update({ const updatedUser = await prisma.user.update({
where: { id: user.id }, where: { id: user.id },
data: { data: {
...valuesToUpdate, name: valuesToUpdate.name,
link: valuesToUpdate.link,
verifiedLink: valuesToUpdate.verifiedLink,
displayName: valuesToUpdate.displayName,
verified: !!valuesToUpdate.verifiedLink, verified: !!valuesToUpdate.verifiedLink,
picture: pictureUrl, picture: pictureUrl,
admin: type.includes('admin'),
verifier: type.includes('verifier'),
spammer: type.includes('spammer'),
}, },
select: selectUserReturnFields, select: selectUserReturnFields,
}) })

View File

@@ -203,7 +203,13 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
} }
{ {
comment.author.verifier && !comment.author.admin && ( comment.author.verifier && !comment.author.admin && (
<BadgeSmall icon="ri:shield-check-fill" color="teal" text="Moderator" variant="faded" inlineIcon /> <BadgeSmall
icon="ri:graduation-cap-fill"
color="teal"
text="Moderator"
variant="faded"
inlineIcon
/>
) )
} }

View File

@@ -33,10 +33,10 @@ if (!user || !user.admin || !user.verifier) return null
--- ---
<div {...divProps} class={cn('text-xs', className)}> <div {...divProps} class={cn('text-xs', className)}>
<input type="checkbox" id={`mod-toggle-${String(comment.id)}`} class="peer hidden" /> <input type="checkbox" id={`mod-toggle-${String(comment.id)}`} class="peer sr-only" />
<label <label
for={`mod-toggle-${String(comment.id)}`} for={`mod-toggle-${String(comment.id)}`}
class="text-day-500 hover:text-day-300 flex cursor-pointer items-center gap-1" class="text-day-500 hover:text-day-300 peer-focus-visible:ring-offset-night-700 inline-flex cursor-pointer items-center gap-1 rounded-sm peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
> >
<Icon name="ri:shield-keyhole-line" class="h-3.5 w-3.5" /> <Icon name="ri:shield-keyhole-line" class="h-3.5 w-3.5" />
<span class="text-xs">Moderation</span> <span class="text-xs">Moderation</span>
@@ -44,7 +44,7 @@ if (!user || !user.admin || !user.verifier) return null
</label> </label>
<div <div
class="bg-night-600 border-night-500 mt-2 max-h-0 overflow-hidden rounded-md border opacity-0 transition-all duration-200 ease-in-out peer-checked:max-h-[500px] peer-checked:p-2 peer-checked:opacity-100" class="bg-night-600 border-night-500 mt-2 hidden overflow-hidden rounded-md border peer-checked:block peer-checked:p-2"
> >
<div class="border-night-500 flex flex-wrap gap-1 border-b pb-2"> <div class="border-night-500 flex flex-wrap gap-1 border-b pb-2">
<button <button

View File

@@ -20,6 +20,7 @@ type Props<Multiple extends boolean = false> = Omit<
iconClass?: string iconClass?: string
description?: MarkdownString description?: MarkdownString
disabled?: boolean disabled?: boolean
noTransitionPersist?: boolean
}[] }[]
disabled?: boolean disabled?: boolean
selectedValue?: Multiple extends true ? string[] : string selectedValue?: Multiple extends true ? string[] : string
@@ -39,13 +40,11 @@ const {
...wrapperProps ...wrapperProps
} = Astro.props } = Astro.props
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0 const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
--- ---
{/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */} {/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */}
<InputWrapper inputId={inputId} class={cn('@container', className)} {...wrapperProps}> <InputWrapper class={cn('@container', className)} {...wrapperProps}>
<div <div
class={cn( class={cn(
'grid grid-cols-[repeat(auto-fill,minmax(var(--card-min-size),1fr))] gap-2 rounded-lg', 'grid grid-cols-[repeat(auto-fill,minmax(var(--card-min-size),1fr))] gap-2 rounded-lg',
@@ -71,7 +70,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
)} )}
> >
<input <input
transition:persist transition:persist={option.noTransitionPersist ? undefined : true}
type={multiple ? 'checkbox' : 'radio'} type={multiple ? 'checkbox' : 'radio'}
name={wrapperProps.name} name={wrapperProps.name}
value={option.value} value={option.value}

View File

@@ -42,7 +42,7 @@ const inputId = id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
{ {
ratings.toSorted().map((rating) => ( ratings.toSorted().map((rating) => (
<label class="relative cursor-pointer [&:has(~_*:hover),&:hover]:[&>[data-star]]:opacity-100!"> <label class="relative cursor-pointer [&:has(~_*_*:checked)]:[&>[data-star]]:opacity-100 [&:has(~_*:hover),&:hover]:[&>[data-star]]:opacity-100!">
<input <input
type="radio" type="radio"
name={wrapperProps.name} name={wrapperProps.name}
@@ -54,7 +54,7 @@ const inputId = id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
<Icon name="ri:star-line" class="size-6 p-0.5 text-zinc-500" /> <Icon name="ri:star-line" class="size-6 p-0.5 text-zinc-500" />
<Icon <Icon
name="ri:star-fill" name="ri:star-fill"
class="absolute top-0 left-0 size-6 p-0.5 text-yellow-400 not-peer-checked:opacity-0 group-hover/fieldset:opacity-0" class="absolute top-0 left-0 size-6 p-0.5 text-yellow-400 not-peer-checked:opacity-0 group-hover/fieldset:opacity-0!"
data-star data-star
/> />
</label> </label>

View File

@@ -17,7 +17,7 @@ const { name, options, selectedValue, class: className, ...rest } = Astro.props
<div <div
class={cn( class={cn(
'bg-night-500 divide-night-700 flex divide-x-2 overflow-hidden rounded-md text-[0.6875rem]', 'bg-night-500 divide-night-700 has-focus-visible:ring-offset-night-700 flex divide-x-2 overflow-hidden rounded-md text-[0.6875rem] has-focus-visible:ring-2 has-focus-visible:ring-blue-500 has-focus-visible:ring-offset-2',
className className
)} )}
{...rest} {...rest}
@@ -30,7 +30,7 @@ const { name, options, selectedValue, class: className, ...rest } = Astro.props
name={name} name={name}
value={option.value} value={option.value}
checked={selectedValue === option.value} checked={selectedValue === option.value}
class="peer hidden" class="peer sr-only"
/> />
<span class="peer-checked:bg-night-400 inline-block cursor-pointer px-1.5 py-0.5 text-white peer-checked:text-green-500"> <span class="peer-checked:bg-night-400 inline-block cursor-pointer px-1.5 py-0.5 text-white peer-checked:text-green-500">
{option.label} {option.label}

View File

@@ -112,7 +112,7 @@ if (!z.string().url().safeParse(link.url).success) {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class={cn( class={cn(
'2xs:text-sm 2xs:h-8 2xs:gap-2 inline-flex h-6 items-center gap-1 rounded-full bg-white text-xs whitespace-nowrap text-black', '2xs:text-sm 2xs:h-8 2xs:gap-2 focus-visible:ring-offset-night-700 inline-flex h-6 items-center gap-1 rounded-full bg-white text-xs whitespace-nowrap text-black focus-visible:ring-4 focus-visible:ring-orange-500 focus-visible:ring-offset-2 focus-visible:outline-none',
className className
)} )}
{...htmlProps} {...htmlProps}

View File

@@ -97,8 +97,7 @@ const {
<!-- Type Filter --> <!-- Type Filter -->
<fieldset class="mb-6"> <fieldset class="mb-6">
<legend class="font-title mb-3 leading-none text-green-500">Type</legend> <legend class="font-title mb-3 leading-none text-green-500">Type</legend>
<input type="checkbox" id="show-more-categories" class="peer hidden" hx-preserve data-show-more-input /> <ul class="[&:not(:has(~_.peer:checked))]:[&>li:not([data-show-always])]:hidden">
<ul class="not-peer-checked:[&>li:not([data-show-always])]:hidden">
{ {
options.categories?.map((category) => ( options.categories?.map((category) => (
<li data-show-always={category.showAlways ? '' : undefined}> <li data-show-always={category.showAlways ? '' : undefined}>
@@ -122,15 +121,22 @@ const {
{ {
options.categories.filter((category) => category.showAlways).length < options.categories.length && ( options.categories.filter((category) => category.showAlways).length < options.categories.length && (
<> <>
<input
type="checkbox"
id="show-more-categories"
class="peer sr-only"
hx-preserve
data-show-more-input
/>
<label <label
for="show-more-categories" for="show-more-categories"
class="mt-2 block cursor-pointer text-sm text-green-500 peer-checked:hidden" class="peer-focus-visible:ring-offset-night-700 mt-2 block cursor-pointer rounded-sm text-sm text-green-500 peer-checked:hidden peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
> >
+ Show more + Show more
</label> </label>
<label <label
for="show-more-categories" for="show-more-categories"
class="mt-2 hidden cursor-pointer text-sm text-green-500 peer-checked:block" class="peer-focus-visible:ring-offset-night-700 mt-2 hidden cursor-pointer rounded-sm text-sm text-green-500 peer-checked:block peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
> >
- Show less - Show less
</label> </label>
@@ -289,14 +295,8 @@ const {
options.attributesByCategory.map(({ category, attributes }) => ( options.attributesByCategory.map(({ category, attributes }) => (
<fieldset class="min-w-0"> <fieldset class="min-w-0">
<legend class="font-title mb-0.5 text-xs tracking-wide text-white">{category}</legend> <legend class="font-title mb-0.5 text-xs tracking-wide text-white">{category}</legend>
<input
type="checkbox" <ul class="[:not(:has(~_.peer:checked))]:[&>li:not([data-show-always])]:hidden">
id={`show-more-attributes-${category}`}
class="peer hidden"
hx-preserve
data-show-more-input
/>
<ul class="not-peer-checked:[&>li:not([data-show-always])]:hidden">
{attributes.map((attribute) => { {attributes.map((attribute) => {
const inputName = `attr-${attribute.id}` as const const inputName = `attr-${attribute.id}` as const
const yesId = `attr-${attribute.id}=yes` as const const yesId = `attr-${attribute.id}=yes` as const
@@ -306,13 +306,13 @@ const {
return ( return (
<li data-show-always={attribute.showAlways ? '' : undefined} class="cursor-pointer"> <li data-show-always={attribute.showAlways ? '' : undefined} class="cursor-pointer">
<fieldset class="flex max-w-full min-w-0 cursor-pointer items-center text-sm text-white"> <fieldset class="relative flex max-w-full min-w-0 cursor-pointer items-center text-sm text-white">
<legend class="sr-only"> <legend class="sr-only">
{attribute.title} ({attribute._count?.services}) {attribute.title} ({attribute._count?.services})
</legend> </legend>
<input <input
type="radio" type="radio"
class="peer/empty hidden" class="peer/empty sr-only"
id={emptyId} id={emptyId}
name={inputName} name={inputName}
value="" value=""
@@ -324,7 +324,7 @@ const {
name={inputName} name={inputName}
value="yes" value="yes"
id={yesId} id={yesId}
class="peer/yes hidden" class="peer/yes sr-only"
checked={attribute.value === 'yes'} checked={attribute.value === 'yes'}
aria-label="Include" aria-label="Include"
/> />
@@ -333,38 +333,45 @@ const {
name={inputName} name={inputName}
value="no" value="no"
id={noId} id={noId}
class="peer/no hidden" class="peer/no sr-only"
checked={attribute.value === 'no'} checked={attribute.value === 'no'}
aria-label="Exclude" aria-label="Exclude"
/> />
<div class="pointer-events-none absolute inset-y-0 -left-[2px] hidden w-[calc(var(--spacing)*4.5*2+1px)] rounded-md border-2 border-blue-500 peer-focus-visible/empty:block peer-focus-visible/no:block peer-focus-visible/yes:block" />
<label <label
for={yesId} for={yesId}
class="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-l-sm bg-zinc-950 peer-checked/yes:hidden" class="border-night-500 bg-night-600 relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-l-sm border border-r-0 peer-checked/yes:hidden before:absolute before:-inset-[3px] before:-right-[0.5px]"
aria-hidden="true" aria-hidden="true"
> >
<Icon name="ri:check-line" class="size-3" /> <Icon name="ri:check-line" class="size-3" />
</label> </label>
<label <label
for={emptyId} for={emptyId}
class="hidden size-4 shrink-0 cursor-pointer items-center justify-center rounded-l-sm bg-green-600 peer-checked/yes:flex" class="relative hidden h-4 w-[calc(var(--spacing)*4+0.5px)] shrink-0 cursor-pointer items-center justify-center rounded-l-sm bg-green-600 peer-checked/yes:flex before:absolute before:-inset-[2px] before:-right-[0.5px]"
aria-hidden="true" aria-hidden="true"
> >
<Icon name="ri:check-line" class="size-3" /> <Icon name="ri:check-line" class="size-3" />
</label> </label>
<span class="block h-4 w-px border-y-2 border-zinc-950 bg-zinc-800" aria-hidden="true" /> <span
class="bg-night-400 border-night-500 pointer-events-none block h-4 w-px border-y peer-checked/no:w-[0.5px] peer-checked/yes:w-[0.5px]"
aria-hidden="true"
>
<span class="bg-night-400 border-night-600 block h-full w-px border-y-2" />
</span>
<label <label
for={noId} for={noId}
class="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-r-sm bg-zinc-950 peer-checked/no:hidden" class="border-night-500 bg-night-600 relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-r-sm border border-l-0 peer-checked/no:hidden before:absolute before:-inset-[3px] before:-left-[0.5px]"
aria-hidden="true" aria-hidden="true"
> >
<Icon name="ri:close-line" class="size-3" /> <Icon name="ri:close-line" class="size-3" />
</label> </label>
<label <label
for={emptyId} for={emptyId}
class="hidden size-4 shrink-0 cursor-pointer items-center justify-center rounded-r-sm bg-red-600 peer-checked/no:flex" class="relative hidden size-4 w-[calc(var(--spacing)*4+0.5px)] shrink-0 cursor-pointer items-center justify-center rounded-r-sm bg-red-600 peer-checked/no:flex before:absolute before:-inset-[2px] before:-left-[0.5px]"
aria-hidden="true" aria-hidden="true"
> >
<Icon name="ri:close-line" class="size-3" /> <Icon name="ri:close-line" class="size-3" />
@@ -376,8 +383,8 @@ const {
aria-hidden="true" aria-hidden="true"
> >
<Icon <Icon
name={attribute.icon} name={attribute.info.icon}
class={cn('mr-2 size-3 shrink-0 opacity-80', attribute.iconClass)} class={cn('mr-2 size-3 shrink-0 opacity-80', attribute.info.classNames.icon)}
aria-hidden="true" aria-hidden="true"
/> />
<span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap"> <span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
@@ -391,8 +398,8 @@ const {
aria-hidden="true" aria-hidden="true"
> >
<Icon <Icon
name={attribute.icon} name={attribute.info.icon}
class={cn('mr-2 size-3 shrink-0 opacity-100', attribute.iconClass)} class={cn('mr-2 size-3 shrink-0 opacity-100', attribute.info.classNames.icon)}
aria-hidden="true" aria-hidden="true"
/> />
<span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap"> <span class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
@@ -405,17 +412,25 @@ const {
) )
})} })}
</ul> </ul>
{attributes.filter((attribute) => attribute.showAlways).length < attributes.length && ( {attributes.filter((attribute) => attribute.showAlways).length < attributes.length && (
<> <>
<input
type="checkbox"
id={`show-more-attributes-${category}`}
class="peer sr-only"
hx-preserve
data-show-more-input
/>
<label <label
for={`show-more-attributes-${category}`} for={`show-more-attributes-${category}`}
class="mt-2 block cursor-pointer text-sm text-green-500 peer-checked:hidden" class="peer-focus-visible:ring-offset-night-700 mt-2 block cursor-pointer rounded-sm text-sm text-green-500 peer-checked:hidden peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
> >
+ Show more + Show more
</label> </label>
<label <label
for={`show-more-attributes-${category}`} for={`show-more-attributes-${category}`}
class="mt-2 hidden cursor-pointer text-sm text-green-500 peer-checked:block" class="peer-focus-visible:ring-offset-night-700 mt-2 hidden cursor-pointer rounded-sm text-sm text-green-500 peer-checked:block peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
> >
- Show less - Show less
</label> </label>

View File

@@ -34,7 +34,7 @@ export const {
value, value,
slug: value ? value.toLowerCase() : '', slug: value ? value.toLowerCase() : '',
label: value ? transformCase(value, 'title') : String(value), label: value ? transformCase(value, 'title') : String(value),
icon: 'ri:question-line', icon: 'ri:question-fill',
order: Infinity, order: Infinity,
classNames: { classNames: {
container: 'bg-current/30', container: 'bg-current/30',
@@ -50,7 +50,7 @@ export const {
value: 'BAD', value: 'BAD',
slug: 'bad', slug: 'bad',
label: 'Bad', label: 'Bad',
icon: 'ri:close-line', icon: 'ri:close-circle-fill',
order: 1, order: 1,
classNames: { classNames: {
container: 'bg-red-600/30', container: 'bg-red-600/30',
@@ -65,7 +65,7 @@ export const {
value: 'WARNING', value: 'WARNING',
slug: 'warning', slug: 'warning',
label: 'Warning', label: 'Warning',
icon: 'ri:alert-line', icon: 'ri:alert-fill',
order: 2, order: 2,
classNames: { classNames: {
container: 'bg-yellow-600/30', container: 'bg-yellow-600/30',
@@ -80,7 +80,7 @@ export const {
value: 'GOOD', value: 'GOOD',
slug: 'good', slug: 'good',
label: 'Good', label: 'Good',
icon: 'ri:check-line', icon: 'ri:checkbox-circle-fill',
order: 3, order: 3,
classNames: { classNames: {
container: 'bg-green-600/30', container: 'bg-green-600/30',
@@ -95,7 +95,7 @@ export const {
value: 'INFO', value: 'INFO',
slug: 'info', slug: 'info',
label: 'Info', label: 'Info',
icon: 'ri:information-line', icon: 'ri:information-fill',
order: 4, order: 4,
classNames: { classNames: {
container: 'bg-blue-600/30', container: 'bg-blue-600/30',

View File

@@ -49,7 +49,7 @@ export const {
value: 'MODERATOR', value: 'MODERATOR',
slug: 'moderator', slug: 'moderator',
label: 'Moderator', label: 'Moderator',
icon: 'ri:glasses-2-line', icon: 'ri:graduation-cap-fill',
order: 3, order: 3,
color: 'teal', color: 'teal',
}, },

View File

@@ -117,7 +117,6 @@ if (!user) return Astro.rewrite('/404')
<BaseLayout <BaseLayout
pageTitle={`User: ${user.name}`} pageTitle={`User: ${user.name}`}
htmx
widthClassName="max-w-screen-lg" widthClassName="max-w-screen-lg"
className={{ main: 'space-y-24' }} className={{ main: 'space-y-24' }}
> >
@@ -140,9 +139,9 @@ if (!user) return Astro.rewrite('/404')
</h1> </h1>
<div class="mb-4 flex flex-wrap justify-center gap-2"> <div class="mb-4 flex flex-wrap justify-center gap-2">
{user.admin && <BadgeSmall color="green" text="Admin" icon="ri:shield-check-fill" />} {user.admin && <BadgeSmall color="green" text="Admin" icon="ri:shield-star-fill" />}
{user.verified && <BadgeSmall color="cyan" text="Verified" icon="ri:verified-badge-fill" />} {user.verified && <BadgeSmall color="cyan" text="Verified" icon="ri:verified-badge-fill" />}
{user.verifier && <BadgeSmall color="blue" text="Verifier" icon="ri:check-fill" />} {user.verifier && <BadgeSmall color="blue" text="Moderator" icon="ri:graduation-cap-fill" />}
{user.spammer && <BadgeSmall color="red" text="Spammer" icon="ri:alert-fill" />} {user.spammer && <BadgeSmall color="red" text="Spammer" icon="ri:alert-fill" />}
</div> </div>
@@ -172,7 +171,13 @@ if (!user) return Astro.rewrite('/404')
</div> </div>
</div> </div>
<form method="POST" action={actions.admin.user.update} enctype="multipart/form-data" class="space-y-2"> <form
method="POST"
action={actions.admin.user.update}
enctype="multipart/form-data"
class="space-y-2"
data-astro-reload
>
<h2 class="font-title text-center text-3xl leading-none font-bold">Edit profile</h2> <h2 class="font-title text-center text-3xl leading-none font-bold">Edit profile</h2>
<input type="hidden" name="id" value={user.id} /> <input type="hidden" name="id" value={user.id} />
@@ -217,13 +222,19 @@ if (!user) return Astro.rewrite('/404')
/> />
<InputCardGroup <InputCardGroup
name="role" name="type"
label="Role" label="Type"
options={[ options={[
{ label: 'Admin', value: 'admin', icon: 'ri:shield-check-fill' }, { label: 'Admin', value: 'admin', icon: 'ri:shield-star-fill' },
{ label: 'Verified', value: 'verified', icon: 'ri:verified-badge-fill', disabled: true }, { label: 'Moderator', value: 'verifier', icon: 'ri:graduation-cap-fill' },
{ label: 'Verifier', value: 'verifier', icon: 'ri:check-fill' },
{ label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill' }, { label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill' },
{
label: 'Verified',
value: 'verified',
icon: 'ri:verified-badge-fill',
disabled: true,
noTransitionPersist: true,
},
]} ]}
selectedValue={[ selectedValue={[
user.admin ? 'admin' : null, user.admin ? 'admin' : null,
@@ -235,7 +246,7 @@ if (!user) return Astro.rewrite('/404')
cardSize="sm" cardSize="sm"
iconSize="sm" iconSize="sm"
multiple multiple
error={updateInputErrors.role} error={updateInputErrors.type}
/> />
<InputSubmitButton label="Save" icon="ri:save-line" hideCancel /> <InputSubmitButton label="Save" icon="ri:save-line" hideCancel />
@@ -280,7 +291,12 @@ if (!user) return Astro.rewrite('/404')
<Icon name="ri:edit-line" class="size-5" /> <Icon name="ri:edit-line" class="size-5" />
</label> </label>
<form method="POST" action={actions.admin.user.internalNotes.delete} class="contents"> <form
method="POST"
action={actions.admin.user.internalNotes.delete}
class="contents"
data-astro-reload
>
<input type="hidden" name="noteId" value={note.id} /> <input type="hidden" name="noteId" value={note.id} />
<button type="submit" class="text-day-300 p-1 transition-colors hover:text-red-400"> <button type="submit" class="text-day-300 p-1 transition-colors hover:text-red-400">
<Icon name="ri:delete-bin-line" class="size-5" /> <Icon name="ri:delete-bin-line" class="size-5" />
@@ -297,6 +313,7 @@ if (!user) return Astro.rewrite('/404')
method="POST" method="POST"
action={actions.admin.user.internalNotes.update} action={actions.admin.user.internalNotes.update}
data-note-edit-form data-note-edit-form
data-astro-reload
class="mt-4 hidden space-y-4" class="mt-4 hidden space-y-4"
> >
<input type="hidden" name="noteId" value={note.id} /> <input type="hidden" name="noteId" value={note.id} />
@@ -314,7 +331,12 @@ if (!user) return Astro.rewrite('/404')
) )
} }
<form method="POST" action={actions.admin.user.internalNotes.add} class="mt-10 space-y-2"> <form
method="POST"
action={actions.admin.user.internalNotes.add}
class="mt-10 space-y-2"
data-astro-reload
>
<h3 class="font-title mb-0 text-center text-xl leading-none font-bold">Add Note</h3> <h3 class="font-title mb-0 text-center text-xl leading-none font-bold">Add Note</h3>
<input type="hidden" name="userId" value={user.id} /> <input type="hidden" name="userId" value={user.id} />

View File

@@ -10,6 +10,7 @@ import Pagination from '../components/Pagination.astro'
import ServiceFiltersPill from '../components/ServiceFiltersPill.astro' import ServiceFiltersPill from '../components/ServiceFiltersPill.astro'
import ServicesFilters from '../components/ServicesFilters.astro' import ServicesFilters from '../components/ServicesFilters.astro'
import ServicesSearchResults from '../components/ServicesSearchResults.astro' import ServicesSearchResults from '../components/ServicesSearchResults.astro'
import { getAttributeTypeInfo } from '../constants/attributeTypes'
import { import {
currencies, currencies,
currenciesZodEnumBySlug, currenciesZodEnumBySlug,
@@ -31,7 +32,7 @@ import { prisma } from '../lib/prisma'
import { makeSortSeed } from '../lib/sortSeed' import { makeSortSeed } from '../lib/sortSeed'
import { transformCase } from '../lib/strings' import { transformCase } from '../lib/strings'
import type { AttributeType, Prisma } from '@prisma/client' import type { Prisma } from '@prisma/client'
const MIN_CATEGORIES_TO_SHOW = 8 const MIN_CATEGORIES_TO_SHOW = 8
const MIN_ATTRIBUTES_TO_SHOW = 8 const MIN_ATTRIBUTES_TO_SHOW = 8
@@ -324,7 +325,10 @@ const [categories, [services, totalServices, hadToIncludeCommunityContributed]]
}) })
let hadToIncludeCommunityContributed = false let hadToIncludeCommunityContributed = false
if (totalServices === 0 && !where.verificationStatus.in.includes('COMMUNITY_CONTRIBUTED')) { if (
totalServices === 0 &&
areEqualArraysWithoutOrder(where.verificationStatus.in, ['VERIFICATION_FAILED', 'APPROVED'])
) {
const [unsortedServiceCommunityServices, totalCommunityServices] = const [unsortedServiceCommunityServices, totalCommunityServices] =
await prisma.service.findManyAndCount({ await prisma.service.findManyAndCount({
where: { where: {
@@ -408,25 +412,6 @@ const [categories, [services, totalServices, hadToIncludeCommunityContributed]]
], ],
]) ])
const attributeIcons = {
GOOD: {
icon: 'ri:check-line',
iconClass: 'text-green-400',
},
BAD: {
icon: 'ri:close-line',
iconClass: 'text-red-400',
},
WARNING: {
icon: 'ri:alert-line',
iconClass: 'text-yellow-400',
},
INFO: {
icon: 'ri:information-line',
iconClass: 'text-blue-400',
},
} as const satisfies Record<AttributeType, { icon: string; iconClass: string }>
const attributes = await Astro.locals.banners.try( const attributes = await Astro.locals.banners.try(
'Unable to load attribute filters.', 'Unable to load attribute filters.',
() => () =>
@@ -451,12 +436,14 @@ const attributes = await Astro.locals.banners.try(
const attributesByCategory = orderBy( const attributesByCategory = orderBy(
Object.entries( Object.entries(
groupBy( groupBy(
attributes.map((attr) => ({ attributes.map((attr) => {
...attr, return {
...attributeIcons[attr.type], info: getAttributeTypeInfo(attr.type),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ...attr,
value: filters.attr?.[attr.id] || undefined, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
})), value: filters.attr?.[attr.id] || undefined,
}
}),
'category' 'category'
) )
).map(([category, attributes]) => ({ ).map(([category, attributes]) => ({
@@ -533,7 +520,9 @@ const activeAnnouncements = await prisma.announcement.findMany({
<AnnouncementBanner announcements={activeAnnouncements} /> <AnnouncementBanner announcements={activeAnnouncements} />
<div class="flex flex-col gap-4 sm:flex-row sm:gap-8"> <div class="flex flex-col gap-4 sm:flex-row sm:gap-8">
<div class="flex items-stretch sm:hidden"> <div
class='[&:has(~_#show-filters:focus-visible)_[for="show-filters"]]:ring-offset-night-700 flex items-stretch sm:hidden [&:has(~_#show-filters:focus-visible)_[for="show-filters"]]:ring-2 [&:has(~_#show-filters:focus-visible)_[for="show-filters"]]:ring-green-500 [&:has(~_#show-filters:focus-visible)_[for="show-filters"]]:ring-offset-2'
>
{ {
!hasDefaultFilters ? ( !hasDefaultFilters ? (
<div class="-ml-4 flex flex-1 items-center gap-2 overflow-x-auto mask-r-from-[calc(100%-var(--spacing)*16)] pr-12 pl-4"> <div class="-ml-4 flex flex-1 items-center gap-2 overflow-x-auto mask-r-from-[calc(100%-var(--spacing)*16)] pr-12 pl-4">
@@ -656,11 +645,11 @@ const activeAnnouncements = await prisma.announcement.findMany({
type="checkbox" type="checkbox"
id="show-filters" id="show-filters"
name="show-filters" name="show-filters"
class="peer hidden" class="peer sr-only sm:hidden"
checked={Astro.url.searchParams.has('show-filters')} checked={Astro.url.searchParams.has('show-filters')}
/> />
<div <div
class="bg-night-700 fixed top-0 left-0 z-50 h-dvh w-dvw shrink-0 translate-y-full overflow-y-auto overscroll-contain border-t border-green-500/30 px-8 pt-4 transition-transform peer-checked:translate-y-0 sm:relative sm:z-auto sm:h-auto sm:w-64 sm:translate-y-0 sm:overflow-visible sm:border-none sm:bg-none sm:p-0" class="bg-night-700 fixed top-0 left-0 z-50 hidden h-dvh w-dvw shrink-0 translate-y-full overflow-y-auto overscroll-contain border-t border-green-500/30 px-8 pt-4 transition-transform peer-checked:translate-y-0 max-sm:peer-checked:block sm:relative sm:z-auto sm:block sm:h-auto sm:w-64 sm:translate-y-0 sm:overflow-visible sm:border-none sm:bg-none sm:p-0"
> >
<ServicesFilters <ServicesFilters
searchResultsId="search-results" searchResultsId="search-results"

View File

@@ -693,10 +693,15 @@ const ogImageTemplateData = {
</li> </li>
))} ))}
<input type="checkbox" class="peer hidden" id="show-more-links" checked={hiddenLinks.length === 0} /> <input
type="checkbox"
class="peer sr-only checked:hidden"
id="show-more-links"
checked={hiddenLinks.length === 0}
/>
{hiddenLinks.length > 0 && ( {hiddenLinks.length > 0 && (
<li class="peer-checked:hidden"> <li class="peer-focus-visible:ring-offset-night-700 rounded-full peer-checked:hidden peer-focus-visible:ring-4 peer-focus-visible:ring-orange-500 peer-focus-visible:ring-offset-2">
<label <label
for="show-more-links" for="show-more-links"
class="2xs:text-sm 2xs:h-8 2xs:gap-2 2xs:px-4 text-day-100 bg-day-800 hover:bg-day-900 inline-flex h-6 cursor-pointer items-center gap-1 rounded-full px-2 text-xs whitespace-nowrap transition-colors duration-200" class="2xs:text-sm 2xs:h-8 2xs:gap-2 2xs:px-4 text-day-100 bg-day-800 hover:bg-day-900 inline-flex h-6 cursor-pointer items-center gap-1 rounded-full px-2 text-xs whitespace-nowrap transition-colors duration-200"