Release 202505261804

This commit is contained in:
pluja
2025-05-26 18:04:45 +00:00
parent b361ed3aa8
commit e536ca6519
13 changed files with 244 additions and 79 deletions

View File

@@ -126,11 +126,13 @@ type Props<Tag extends 'a' | 'div' | 'li' = 'div'> = Polymorphic<
VariantProps<typeof badge> & {
as: Tag
icon?: string
endIcon?: string
text: string
inlineIcon?: boolean
classNames?: {
icon?: string
text?: string
endIcon?: string
}
}
>
@@ -138,6 +140,7 @@ type Props<Tag extends 'a' | 'div' | 'li' = 'div'> = Polymorphic<
const {
as: Tag = 'div',
icon: iconName,
endIcon: endIconName,
text: textContent,
inlineIcon,
classNames,
@@ -159,4 +162,9 @@ const { base, icon: iconSlot, text: textSlot } = badge({ color, variant })
)
}
<span class={textSlot({ class: classNames?.text })}>{textContent}</span>
{
!!endIconName && (
<Icon name={endIconName} class={iconSlot({ class: classNames?.endIcon })} is:inline={inlineIcon} />
)
}
</Tag>

View File

@@ -8,11 +8,12 @@ import type { Polymorphic } from 'astro/types'
type Props<Tag extends 'a' | 'div' | 'li' = 'div'> = Polymorphic<{
as: Tag
icon: string
endIcon?: string
text: string
inlineIcon?: boolean
}>
const { icon, text, class: className, inlineIcon, as: Tag = 'div', ...divProps } = Astro.props
const { icon, text, class: className, inlineIcon, endIcon, as: Tag = 'div', ...divProps } = Astro.props
---
<Tag
@@ -24,4 +25,5 @@ const { icon, text, class: className, inlineIcon, as: Tag = 'div', ...divProps }
>
<Icon name={icon} class="size-4" is:inline={inlineIcon} />
<span>{text}</span>
{!!endIcon && <Icon name={endIcon} class="size-4" is:inline={inlineIcon} />}
</Tag>

View File

@@ -0,0 +1,41 @@
---
import { uniq } from 'lodash-es'
import { cn } from '../lib/cn'
import BadgeStandard from './BadgeStandard.astro'
import type { ComponentProps } from 'astro/types'
type Props = Omit<
ComponentProps<typeof BadgeStandard>,
'as' | 'endIcon' | 'href' | 'icon' | 'text' | 'variant'
> & {
name: string
value: string
label: string
icon: string
}
const { name, value, label, icon, ...props } = Astro.props
const selectedValues = Astro.url.searchParams.getAll(name)
const isSelected = selectedValues.includes(value)
const url = new URL(Astro.url)
url.searchParams.delete(name)
const valuesToSet = uniq(isSelected ? selectedValues.filter((v) => v !== value) : [...selectedValues, value])
for (const value of valuesToSet) {
url.searchParams.set(name, value)
}
---
<BadgeStandard
as="a"
href={url.href}
class={cn(isSelected && 'bg-green-950 text-green-500')}
text={label}
icon={icon}
endIcon={isSelected ? 'ri:close-fill' : undefined}
{...props}
/>

View File

@@ -76,7 +76,15 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
/>
<Tooltip text="Send">
<Button type="submit" icon="ri:send-plane-fill" size="lg" color="success" class="h-16" />
<Button
type="submit"
icon="ri:send-plane-fill"
size="lg"
color="success"
class="h-16"
label="Send"
iconOnly
/>
</Tooltip>
</form>
{!!inputErrors.content && <div class="text-sm text-red-500">{inputErrors.content}</div>}

View File

@@ -47,12 +47,9 @@ const averageUserRatingFromQuery =
totalComments > 0 ? sum(ratings.map((stat) => stat.rating * stat.count)) / totalComments : null
if (averageUserRatingFromProps !== undefined) {
if (
averageUserRatingFromQuery !== averageUserRatingFromProps ||
(averageUserRatingFromQuery !== null &&
averageUserRatingFromProps !== null &&
round(averageUserRatingFromQuery, 2) !== round(averageUserRatingFromProps, 2))
) {
const a = averageUserRatingFromQuery === null ? null : round(averageUserRatingFromQuery, 2)
const b = averageUserRatingFromProps === null ? null : round(averageUserRatingFromProps, 2)
if (a !== b) {
console.error(
`The averageUserRating of the comments shown is different from the averageUserRating from the database. Service ID: ${serviceId} ratingUi: ${averageUserRatingFromQuery} ratingDb: ${averageUserRatingFromProps}`
)

View File

@@ -43,7 +43,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
: Array.from({ length: icons.length }, () => option.iconClassName)
: []
return (
<label class="hover:bg-night-500 flex cursor-pointer items-center gap-2 px-5 py-1">
<label class="hover:bg-night-500 flex cursor-pointer items-center gap-2 px-5 py-1 has-checked:bg-green-800/20 has-checked:hover:bg-green-800/30">
<input
transition:persist
type="checkbox"

View File

@@ -1,14 +1,18 @@
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
import { transformCase } from '../lib/strings'
import type BadgeSmall from '../components/BadgeSmall.astro'
import type { ServiceSuggestionType } from '@prisma/client'
import type { ComponentProps } from 'astro/types'
type ServiceSuggestionTypeInfo<T extends string | null | undefined = string> = {
value: T
slug: string
label: string
icon: string
order: number
default: boolean
color: ComponentProps<typeof BadgeSmall>['color']
}
export const {
@@ -25,9 +29,11 @@ export const {
(value): ServiceSuggestionTypeInfo<typeof value> => ({
value,
slug: value ? value.toLowerCase() : '',
label: value ? transformCase(value, 'title') : String(value),
label: value ? transformCase(value.replace('_', ' '), 'title') : String(value),
icon: 'ri:question-line',
order: Infinity,
default: false,
color: 'zinc',
}),
[
{
@@ -35,14 +41,18 @@ export const {
slug: 'create',
label: 'Create',
icon: 'ri:add-line',
order: 1,
default: true,
color: 'green',
},
{
value: 'EDIT_SERVICE',
slug: 'edit',
label: 'Edit',
icon: 'ri:pencil-line',
order: 2,
default: false,
color: 'blue',
},
] as const satisfies ServiceSuggestionTypeInfo<ServiceSuggestionType>[]
)

View File

@@ -71,10 +71,12 @@ All new listings begin as **unlisted** — they're only accessible via direct li
To list a new service, it must fulfill these requirements:
- Offer a service.
- Publicly available website explaining what the service is about
- Terms of service or FAQ document
For example, just a Telegram link is not a valid service.
For examples:
- Just a Telegram link or a criptocurrency itself is not a valid service.
### Suggestion Review Process

View File

@@ -2,6 +2,7 @@
import { Icon } from 'astro-icon/components'
import { actions } from 'astro:actions'
import BadgeSmall from '../../../components/BadgeSmall.astro'
import Button from '../../../components/Button.astro'
import Chat from '../../../components/Chat.astro'
import ServiceCard from '../../../components/ServiceCard.astro'
@@ -10,6 +11,7 @@ import {
getServiceSuggestionStatusInfo,
serviceSuggestionStatuses,
} from '../../../constants/serviceSuggestionStatus'
import { getServiceSuggestionTypeInfo } from '../../../constants/serviceSuggestionType'
import BaseLayout from '../../../layouts/BaseLayout.astro'
import { cn } from '../../../lib/cn'
import { parseIntWithFallback } from '../../../lib/numbers'
@@ -57,6 +59,7 @@ const serviceSuggestion = await Astro.locals.banners.try('Error fetching service
imageUrl: true,
verificationStatus: true,
acceptedCurrencies: true,
serviceVisibility: true,
categories: {
select: {
name: true,
@@ -92,6 +95,7 @@ if (!serviceSuggestion) {
}
const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
---
<BaseLayout
@@ -110,7 +114,9 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
label="Back"
/>
<h1 class="font-title text-xl text-green-500">Service Suggestion</h1>
<h1 class="font-title text-day-200 text-xl">Service suggestion</h1>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
</div>
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2">
@@ -118,12 +124,13 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
<ServiceCard service={serviceSuggestion.service} class="mx-auto max-w-full" />
</div>
<div
class="rounded-lg border border-green-500/30 bg-black/40 p-4 shadow-[0_0_15px_rgba(34,197,94,0.2)] backdrop-blur-xs"
>
<h2 class="font-title mb-3 text-lg text-green-500">Suggestion Details</h2>
<div class="rounded-lg bg-black/40 p-4 backdrop-blur-xs">
<h2 class="font-title text-day-200 mb-3 text-lg">Suggestion Details</h2>
<div class="mb-3 grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 text-sm">
<span class="font-title text-gray-400">Type:</span>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
<span class="font-title text-gray-400">Status:</span>
<span
class={cn(
@@ -142,7 +149,7 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
<span class="text-gray-300">{serviceSuggestion.createdAt.toLocaleString()}</span>
<span class="font-title text-gray-400">Service page:</span>
<a href={`/service/${serviceSuggestion.service.slug}`} class="text-green-400 hover:text-green-500">
<a href={`/service/${serviceSuggestion.service.slug}`} class="hover:text-day-200 text-green-400">
View Service <Icon
name="ri:external-link-line"
class="ml-0.5 inline-block size-3 align-[-0.05em]"
@@ -164,11 +171,9 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
</div>
</div>
<div
class="rounded-lg border border-green-500/30 bg-black/40 p-6 shadow-[0_0_15px_rgba(34,197,94,0.2)] backdrop-blur-xs"
>
<div class="rounded-lg bg-black/40 p-6 backdrop-blur-xs">
<div class="flex items-center justify-between">
<h2 class="font-title text-lg text-green-500">Messages</h2>
<h2 class="font-title text-day-200 text-lg">Messages</h2>
<form method="POST" action={actions.admin.serviceSuggestions.update} class="flex gap-2">
<input type="hidden" name="suggestionId" value={serviceSuggestion.id} />

View File

@@ -4,6 +4,7 @@ import { actions } from 'astro:actions'
import { z } from 'astro:content'
import { orderBy } from 'lodash-es'
import BadgeSmall from '../../../components/BadgeSmall.astro'
import Button from '../../../components/Button.astro'
import SortArrowIcon from '../../../components/SortArrowIcon.astro'
import TimeFormatted from '../../../components/TimeFormatted.astro'
@@ -12,59 +13,67 @@ import UserBadge from '../../../components/UserBadge.astro'
import {
getServiceSuggestionStatusInfo,
serviceSuggestionStatuses,
serviceSuggestionStatusesZodEnumBySlug,
serviceSuggestionStatusSlugToId,
} from '../../../constants/serviceSuggestionStatus'
import {
getServiceSuggestionTypeInfo,
serviceSuggestionTypes,
serviceSuggestionTypeSlugToId,
serviceSuggestionTypesZodEnumBySlug,
} from '../../../constants/serviceSuggestionType'
import BaseLayout from '../../../layouts/BaseLayout.astro'
import { zodParseQueryParamsStoringErrors } from '../../../lib/parseUrlFilters'
import { prisma } from '../../../lib/prisma'
import { makeLoginUrl } from '../../../lib/redirectUrls'
import type { Prisma, ServiceSuggestionStatus } from '@prisma/client'
import type { Prisma } from '@prisma/client'
const user = Astro.locals.user
if (!user?.admin) {
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Admin access required' }))
}
const search = Astro.url.searchParams.get('search') ?? ''
const statusEnumValues = serviceSuggestionStatuses.map((s) => s.value) as [string, ...string[]]
const statusParam = Astro.url.searchParams.get('status')
const statusFilter = z
.enum(statusEnumValues)
.nullable()
.parse(statusParam === '' ? null : statusParam) as ServiceSuggestionStatus | null
const { data: filters } = zodParseQueryParamsStoringErrors(
{
'sort-by': z.enum(['service', 'status', 'user', 'createdAt', 'messageCount']).default('createdAt'),
search: z.string().optional(),
status: serviceSuggestionStatusesZodEnumBySlug
.transform((slug) => serviceSuggestionStatusSlugToId(slug))
.optional(),
type: serviceSuggestionTypesZodEnumBySlug
.transform((slug) => serviceSuggestionTypeSlugToId(slug))
.optional(),
'sort-by': z
.enum(['service', 'status', 'type', 'user', 'createdAt', 'messageCount'])
.default('createdAt'),
'sort-order': z.enum(['asc', 'desc']).default('desc'),
},
Astro
)
const sortBy = filters['sort-by']
const sortOrder = filters['sort-order']
let prismaOrderBy: Prisma.ServiceSuggestionOrderByWithRelationInput = { createdAt: 'desc' }
if (sortBy === 'createdAt') {
prismaOrderBy = { createdAt: sortOrder }
if (filters['sort-by'] === 'createdAt') {
prismaOrderBy = { createdAt: filters['sort-order'] }
}
let suggestions = await prisma.serviceSuggestion.findMany({
where: {
...(search
...(filters.search
? {
OR: [
{ service: { name: { contains: search, mode: 'insensitive' } } },
{ user: { name: { contains: search, mode: 'insensitive' } } },
{ notes: { contains: search, mode: 'insensitive' } },
{ service: { name: { contains: filters.search, mode: 'insensitive' } } },
{ user: { name: { contains: filters.search, mode: 'insensitive' } } },
{ notes: { contains: filters.search, mode: 'insensitive' } },
],
}
: {}),
status: statusFilter ?? undefined,
status: filters.status,
type: filters.type,
},
orderBy: prismaOrderBy,
select: {
id: true,
type: true,
status: true,
notes: true,
createdAt: true,
@@ -119,18 +128,33 @@ let suggestions = await prisma.serviceSuggestion.findMany({
let suggestionsWithDetails = suggestions.map((s) => ({
...s,
statusInfo: getServiceSuggestionStatusInfo(s.status),
typeInfo: getServiceSuggestionTypeInfo(s.type),
messageCount: s._count.messages,
lastMessage: s.messages[0],
}))
if (sortBy === 'service') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, [(s) => s.service.name.toLowerCase()], [sortOrder])
} else if (sortBy === 'status') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, [(s) => s.statusInfo.label], [sortOrder])
} else if (sortBy === 'user') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, [(s) => s.user.name.toLowerCase()], [sortOrder])
} else if (sortBy === 'messageCount') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, ['messageCount'], [sortOrder])
if (filters['sort-by'] === 'service') {
suggestionsWithDetails = orderBy(
suggestionsWithDetails,
[(s) => s.service.name.toLowerCase()],
[filters['sort-order']]
)
} else if (filters['sort-by'] === 'status') {
suggestionsWithDetails = orderBy(
suggestionsWithDetails,
[(s) => s.statusInfo.label],
[filters['sort-order']]
)
} else if (filters['sort-by'] === 'type') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, [(s) => s.typeInfo.label], [filters['sort-order']])
} else if (filters['sort-by'] === 'user') {
suggestionsWithDetails = orderBy(
suggestionsWithDetails,
[(s) => s.user.name.toLowerCase()],
[filters['sort-order']]
)
} else if (filters['sort-by'] === 'messageCount') {
suggestionsWithDetails = orderBy(suggestionsWithDetails, ['messageCount'], [filters['sort-order']])
}
const suggestionCount = suggestionsWithDetails.length
@@ -162,7 +186,7 @@ const makeSortUrl = (slug: string) => {
type="text"
name="search"
id="search"
value={search}
value={filters.search}
placeholder="Search by service, user, notes..."
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-blue-500 focus:ring-blue-500 focus:outline-none"
/>
@@ -177,13 +201,30 @@ const makeSortUrl = (slug: string) => {
<option value="">All Statuses</option>
{
serviceSuggestionStatuses.map((status) => (
<option value={status.value} selected={statusFilter === status.value}>
<option value={status.slug} selected={filters.status === status.value}>
{status.label}
</option>
))
}
</select>
</div>
<div>
<label for="type-filter" class="block text-xs font-medium text-zinc-400">Type</label>
<select
name="type"
id="type-filter"
class="mt-1 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
>
<option value="">All Types</option>
{
serviceSuggestionTypes.map((type) => (
<option value={type.slug} selected={filters.type === type.value}>
{type.label}
</option>
))
}
</select>
</div>
<div class="flex items-end">
<Button
as="button"
@@ -212,7 +253,7 @@ const makeSortUrl = (slug: string) => {
<thead class="bg-zinc-900/30">
<tr>
<th
class="w-[25%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
class="w-[20%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
>
<a href={makeSortUrl('service')} class="flex items-center hover:text-zinc-200">
Service <SortArrowIcon
@@ -222,7 +263,7 @@ const makeSortUrl = (slug: string) => {
</a>
</th>
<th
class="w-[15%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
class="w-[12%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
>
<a href={makeSortUrl('user')} class="flex items-center hover:text-zinc-200">
User <SortArrowIcon
@@ -232,7 +273,17 @@ const makeSortUrl = (slug: string) => {
</a>
</th>
<th
class="w-[15%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
class="w-[10%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
>
<a href={makeSortUrl('type')} class="flex items-center hover:text-zinc-200">
Type <SortArrowIcon
active={filters['sort-by'] === 'type'}
sortOrder={filters['sort-order']}
/>
</a>
</th>
<th
class="w-[13%] px-4 py-3 text-left text-xs font-medium tracking-wider text-zinc-400 uppercase"
>
<a href={makeSortUrl('status')} class="flex items-center hover:text-zinc-200">
Status <SortArrowIcon
@@ -295,6 +346,13 @@ const makeSortUrl = (slug: string) => {
<td class="px-4 py-3">
<UserBadge user={suggestion.user} size="md" />
</td>
<td class="px-4 py-3">
<BadgeSmall
color={suggestion.typeInfo.color}
text={suggestion.typeInfo.label}
icon={suggestion.typeInfo.icon}
/>
</td>
<td class="px-4 py-3">
<form method="POST" action={actions.admin.serviceSuggestions.update}>
<input type="hidden" name="suggestionId" value={suggestion.id} />

View File

@@ -3,9 +3,12 @@ import { Icon } from 'astro-icon/components'
import { actions } from 'astro:actions'
import AdminOnly from '../../components/AdminOnly.astro'
import BadgeSmall from '../../components/BadgeSmall.astro'
import Button from '../../components/Button.astro'
import Chat from '../../components/Chat.astro'
import ServiceCard from '../../components/ServiceCard.astro'
import { getServiceSuggestionStatusInfo } from '../../constants/serviceSuggestionStatus'
import { getServiceSuggestionTypeInfo } from '../../constants/serviceSuggestionType'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { cn } from '../../lib/cn'
import { parseIntWithFallback } from '../../lib/numbers'
@@ -28,6 +31,7 @@ const serviceSuggestion = await Astro.locals.banners.try('Error fetching service
prisma.serviceSuggestion.findUnique({
select: {
id: true,
type: true,
status: true,
notes: true,
createdAt: true,
@@ -42,6 +46,7 @@ const serviceSuggestion = await Astro.locals.banners.try('Error fetching service
imageUrl: true,
verificationStatus: true,
acceptedCurrencies: true,
serviceVisibility: true,
categories: {
select: {
name: true,
@@ -59,6 +64,7 @@ const serviceSuggestion = await Astro.locals.banners.try('Error fetching service
select: {
id: true,
name: true,
displayName: true,
picture: true,
},
},
@@ -81,6 +87,7 @@ if (!serviceSuggestion) {
}
const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
---
<BaseLayout
@@ -104,17 +111,22 @@ const statusInfo = getServiceSuggestionStatusInfo(serviceSuggestion.status)
},
]}
>
<h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Edit service</h1>
<div class="mt-12 mb-6 flex flex-col items-center justify-center gap-3">
<div class="flex items-center gap-2">
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
<AdminOnly>
<Button
as="a"
href={`/admin/service-suggestions/${serviceSuggestionIdRaw}`}
size="sm"
icon="ri:lock-line"
label="View in admin"
/>
</AdminOnly>
</div>
<AdminOnly>
<a
href={`/admin/service-suggestions/${serviceSuggestionIdRaw}`}
class="border-day-500/30 bg-day-500/10 text-day-400 hover:bg-day-500/20 focus:ring-day-500 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm shadow-xs transition-colors duration-200 focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden"
>
<Icon name="ri:lock-line" class="size-4" />
View in admin
</a>
</AdminOnly>
<h1 class="font-title text-center text-3xl font-bold">Service suggestion</h1>
</div>
<ServiceCard service={serviceSuggestion.service} class="mb-6" />

View File

@@ -3,6 +3,8 @@ import { Icon } from 'astro-icon/components'
import { actions } from 'astro:actions'
import { z } from 'astro:content'
import BadgeSmall from '../../components/BadgeSmall.astro'
import BadgeStandardFilter from '../../components/BadgeStandardFilter.astro'
import Button from '../../components/Button.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
@@ -10,10 +12,16 @@ import Tooltip from '../../components/Tooltip.astro'
import {
getServiceSuggestionStatusInfo,
serviceSuggestionStatuses,
serviceSuggestionStatusesZodEnumBySlug,
serviceSuggestionStatusSlugToId,
} from '../../constants/serviceSuggestionStatus'
import { getServiceSuggestionTypeInfo } from '../../constants/serviceSuggestionType'
import {
getServiceSuggestionTypeInfo,
serviceSuggestionTypes,
serviceSuggestionTypeSlugToId,
serviceSuggestionTypesZodEnumBySlug,
} from '../../constants/serviceSuggestionType'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { zodEnumFromConstant } from '../../lib/arrays'
import { cn } from '../../lib/cn'
import { zodParseQueryParamsStoringErrors } from '../../lib/parseUrlFilters'
import { prisma } from '../../lib/prisma'
@@ -26,8 +34,13 @@ if (!user) {
const { data: filters } = zodParseQueryParamsStoringErrors(
{
serviceId: z.array(z.number().int().positive()).default([]),
status: z.array(zodEnumFromConstant(serviceSuggestionStatuses, 'value')).default([]),
serviceId: z.array(z.number().int().positive()),
status: z.array(
serviceSuggestionStatusesZodEnumBySlug.transform((slug) => serviceSuggestionStatusSlugToId(slug))
),
type: z.array(
serviceSuggestionTypesZodEnumBySlug.transform((slug) => serviceSuggestionTypeSlugToId(slug))
),
},
Astro
)
@@ -52,6 +65,7 @@ const serviceSuggestions = await Astro.locals.banners.try('Error fetching servic
where: {
id: filters.serviceId.length > 0 ? { in: filters.serviceId } : undefined,
status: filters.status.length > 0 ? { in: filters.status } : undefined,
type: filters.type.length > 0 ? { in: filters.type } : undefined,
userId: user.id,
},
orderBy: {
@@ -104,6 +118,23 @@ const success = !!createResult && !createResult.error
)
}
<div class="mb-6">
<div class="text-day-200 mb-2 font-medium">Filter by:</div>
<div class="flex flex-wrap gap-2">
{
serviceSuggestionTypes.map((type) => (
<BadgeStandardFilter name="type" value={type.slug} label={type.label} icon={type.icon} />
))
}
{
serviceSuggestionStatuses.map((status) => (
<BadgeStandardFilter name="status" value={status.slug} label={status.label} icon={status.icon} />
))
}
</div>
</div>
{
serviceSuggestions.length === 0 ? (
<p class="text-day-400">No suggestions yet.</p>
@@ -137,15 +168,7 @@ const success = !!createResult && !createResult.error
<span class="shrink truncate">{suggestion.service.name}</span>
</a>
<Tooltip
as="span"
class="inline-flex items-center gap-1"
text={typeInfo.label}
classNames={{ tooltip: 'md:hidden!' }}
>
<Icon name={typeInfo.icon} class="size-4" />
<span class="hidden md:inline">{typeInfo.label}</span>
</Tooltip>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
<Tooltip
as="span"

View File

@@ -225,7 +225,6 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
label="Onion URLs"
name="onionUrls"
inputProps={{
required: true,
placeholder: 'http://example1.onion\nhttp://example2.onion',
}}
error={inputErrors.onionUrls}
@@ -324,7 +323,7 @@ const [categories, attributes] = await Astro.locals.banners.tryMany([
<script>
document.addEventListener('astro:page-load', () => {
const triggerInputs = document.querySelectorAll<HTMLInputElement>('[data-generate-slug] input')
const triggerInputs = document.querySelectorAll<HTMLInputElement>('input[data-generate-slug]')
const slugInputs = document.querySelectorAll<HTMLInputElement>('input[name="slug"]')
triggerInputs.forEach((triggerInput) => {