Release 202506042153

This commit is contained in:
pluja
2025-06-04 21:53:07 +00:00
parent 144af17a70
commit 6b54db8822
4 changed files with 113 additions and 128 deletions

View File

@@ -8,7 +8,7 @@ type ServiceSuggestionStatusInfo<T extends string | null | undefined = string> =
slug: string slug: string
label: string label: string
icon: string icon: string
iconClass: string color: string
default: boolean default: boolean
} }
@@ -28,7 +28,7 @@ export const {
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-line',
iconClass: 'text-current/60', color: 'gray',
default: false, default: false,
}), }),
[ [
@@ -37,7 +37,7 @@ export const {
slug: 'pending', slug: 'pending',
label: 'Pending', label: 'Pending',
icon: 'ri:time-line', icon: 'ri:time-line',
iconClass: 'text-yellow-400', color: 'yellow',
default: true, default: true,
}, },
{ {
@@ -45,7 +45,7 @@ export const {
slug: 'approved', slug: 'approved',
label: 'Approved', label: 'Approved',
icon: 'ri:check-line', icon: 'ri:check-line',
iconClass: 'text-green-400', color: 'green',
default: false, default: false,
}, },
{ {
@@ -53,7 +53,7 @@ export const {
slug: 'rejected', slug: 'rejected',
label: 'Rejected', label: 'Rejected',
icon: 'ri:close-line', icon: 'ri:close-line',
iconClass: 'text-red-400', color: 'red',
default: false, default: false,
}, },
{ {
@@ -61,7 +61,7 @@ export const {
slug: 'withdrawn', slug: 'withdrawn',
label: 'Withdrawn', label: 'Withdrawn',
icon: 'ri:arrow-left-line', icon: 'ri:arrow-left-line',
iconClass: 'text-gray-400', color: 'gray',
default: false, default: false,
}, },
] as const satisfies ServiceSuggestionStatusInfo<ServiceSuggestionStatus>[] ] as const satisfies ServiceSuggestionStatusInfo<ServiceSuggestionStatus>[]

View File

@@ -147,7 +147,7 @@ The privacy score measures how well a service protects user privacy, using a tra
3. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL. 3. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL.
4. **I2P URL:** **+5 points** if the service offers at least one I2P URL. 4. **I2P URL:** **+5 points** if the service offers at least one I2P URL.
5. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method. 5. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method.
6. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. 6. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes).
7. **Final Score Range:** The final score is always kept between 0 and 100. 7. **Final Score Range:** The final score is always kept between 0 and 100.
#### Trust Score #### Trust Score
@@ -160,7 +160,7 @@ The trust score represents how reliable and trustworthy a service is, based on o
- **Approved:** +5 points - **Approved:** +5 points
- **Community Contributed:** 0 points - **Community Contributed:** 0 points
- **Verification Failed (SCAM):** -50 points - **Verification Failed (SCAM):** -50 points
3. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. 3. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes).
4. **Recently Listed Penalty & Flag:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed. 4. **Recently Listed Penalty & Flag:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed.
5. **Final Score Range:** The final score is always kept between 0 and 100. 5. **Final Score Range:** The final score is always kept between 0 and 100.

View File

@@ -1,10 +1,11 @@
--- ---
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { actions } from 'astro:actions' import { actions, isInputError } from 'astro:actions'
import BadgeSmall from '../../../components/BadgeSmall.astro' import BadgeSmall from '../../../components/BadgeSmall.astro'
import Button from '../../../components/Button.astro' import Button from '../../../components/Button.astro'
import Chat from '../../../components/Chat.astro' import Chat from '../../../components/Chat.astro'
import InputSelect from '../../../components/InputSelect.astro'
import ServiceCard from '../../../components/ServiceCard.astro' import ServiceCard from '../../../components/ServiceCard.astro'
import UserBadge from '../../../components/UserBadge.astro' import UserBadge from '../../../components/UserBadge.astro'
import { import {
@@ -17,12 +18,20 @@ import { cn } from '../../../lib/cn'
import { parseIntWithFallback } from '../../../lib/numbers' import { parseIntWithFallback } from '../../../lib/numbers'
import { prisma } from '../../../lib/prisma' import { prisma } from '../../../lib/prisma'
import { makeLoginUrl } from '../../../lib/redirectUrls' import { makeLoginUrl } from '../../../lib/redirectUrls'
import { formatDateShort } from '../../../lib/timeAgo'
import BadgeStandard from '../../../components/BadgeStandard.astro'
const user = Astro.locals.user const user = Astro.locals.user
if (!user?.admin) { if (!user?.admin) {
return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Admin access required' })) return Astro.redirect(makeLoginUrl(Astro.url, { message: 'Admin access required' }))
} }
const serviceSuggestionUpdateResult = Astro.getActionResult(actions.admin.serviceSuggestions.update)
Astro.locals.banners.addIfSuccess(serviceSuggestionUpdateResult, 'Service suggestion updated successfully')
const serviceSuggestionUpdateInputErrors = isInputError(serviceSuggestionUpdateResult?.error)
? serviceSuggestionUpdateResult.error.fields
: {}
const { id: serviceSuggestionIdRaw } = Astro.params const { id: serviceSuggestionIdRaw } = Astro.params
const serviceSuggestionId = parseIntWithFallback(serviceSuggestionIdRaw) const serviceSuggestionId = parseIntWithFallback(serviceSuggestionIdRaw)
if (!serviceSuggestionId) { if (!serviceSuggestionId) {
@@ -100,114 +109,88 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
<BaseLayout <BaseLayout
pageTitle={`${serviceSuggestion.service.name} | Admin Service Suggestion`} pageTitle={`${serviceSuggestion.service.name} | Admin Service Suggestion`}
htmx description="View and manage service suggestion"
widthClassName="max-w-screen-md" widthClassName="max-w-screen-md"
htmx
> >
<div class="mb-4 flex items-center gap-4"> <h1 class="font-title mt-12 mb-6 text-center text-3xl font-bold">Service suggestion</h1>
<Button
as="a"
href="/admin/service-suggestions"
color="success"
variant="faded"
size="md"
icon="ri:arrow-left-s-line"
label="Back"
/>
<h1 class="font-title text-day-200 text-xl">Service suggestion</h1> <ServiceCard service={serviceSuggestion.service} class="mb-6" />
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} /> <section class="border-night-400 bg-night-600 rounded-lg border p-6">
</div> <div class="text-day-200 xs:grid-cols-2 grid gap-2 text-sm sm:grid-cols-3">
<div class="flex flex-wrap items-center gap-2">
<div class="mb-6 grid grid-cols-1 gap-6 md:grid-cols-2"> <span class="font-title font-bold">Status:</span>
<div> <BadgeSmall color={statusInfo.color} text={statusInfo.label} icon={statusInfo.icon} />
<ServiceCard service={serviceSuggestion.service} class="mx-auto max-w-full" />
</div>
<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(
'inline-flex w-fit items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
statusInfo.iconClass
)}
>
<Icon name={statusInfo.icon} class="mr-1 size-3" />
{statusInfo.label}
</span>
<span class="font-title text-gray-400">Submitted by:</span>
<UserBadge class="text-gray-300" user={serviceSuggestion.user} size="md" />
<span class="font-title text-gray-400">Submitted at:</span>
<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="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]"
/>
</a>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Type:</span>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Author:</span>
<UserBadge class="text-gray-300" user={serviceSuggestion.user} size="md" />
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Submitted:</span>
<span>
{
formatDateShort(serviceSuggestion.createdAt, {
prefix: false,
hourPrecision: true,
caseType: 'sentence',
})
}
</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Service:</span>
<a href={`/service/${serviceSuggestion.service.slug}`} class="hover:text-day-200 text-green-400">
Open <Icon name="ri:external-link-line" class="ml-0.5 inline-block size-3 align-[-0.05em]" />
</a>
</div>
</div>
<div class="bg-night-700 -mx-2 mt-4 rounded-lg p-2 text-sm">
<span class="font-title block font-bold">Notes for moderators:</span>
{ {
serviceSuggestion.notes && ( serviceSuggestion.notes ? (
<div class="mb-4"> <div class="mt-1 text-sm wrap-anywhere whitespace-pre-wrap" set:text={serviceSuggestion.notes} />
<h3 class="font-title mb-1 text-sm text-gray-400">Notes from user:</h3> ) : (
<div <div class="text-day-400 my-4 text-center text-sm italic">Empty</div>
class="rounded-md border border-gray-700 bg-black/50 p-3 text-sm wrap-anywhere whitespace-pre-wrap text-gray-300"
set:text={serviceSuggestion.notes}
/>
</div>
) )
} }
</div> </div>
</div>
<div class="rounded-lg bg-black/40 p-6 backdrop-blur-xs"> <form method="POST" action={actions.admin.serviceSuggestions.update} class="mt-6 flex items-end gap-2">
<div class="flex items-center justify-between"> <input type="hidden" name="suggestionId" value={serviceSuggestion.id} />
<h2 class="font-title text-day-200 text-lg">Messages</h2> <InputSelect
name="status"
label="Update status"
options={serviceSuggestionStatuses.map((status) => ({
label: status.label,
value: status.value,
}))}
selectProps={{ value: serviceSuggestion.status }}
class="flex-1"
error={serviceSuggestionUpdateInputErrors.status}
/>
<Button as="button" type="submit" color="success" size="md" icon="ri:save-line" label="Update" />
</form>
</section>
<form method="POST" action={actions.admin.serviceSuggestions.update} class="flex gap-2"> <Chat
<input type="hidden" name="suggestionId" value={serviceSuggestion.id} /> messages={serviceSuggestion.messages}
<select title="Chat with moderators"
name="status" userId={user.id}
class="font-title w-full rounded-md border border-green-500/30 bg-black/50 p-2 text-sm text-gray-300 placeholder-gray-500 focus:border-green-500 focus:ring-green-500 disabled:opacity-50" action={actions.admin.serviceSuggestions.message}
> formData={{
{ suggestionId: serviceSuggestion.id,
serviceSuggestionStatuses.map((status) => ( }}
<option value={status.value} selected={serviceSuggestion.status === status.value}> class="mt-12"
{status.label} />
</option>
))
}
</select>
<Button
as="button"
type="submit"
color="success"
variant="faded"
size="md"
icon="ri:save-line"
label="Update"
/>
</form>
</div>
<Chat
messages={serviceSuggestion.messages}
userId={user.id}
action={actions.admin.serviceSuggestions.message}
formData={{
suggestionId: serviceSuggestion.id,
}}
/>
</div>
</BaseLayout> </BaseLayout>

View File

@@ -111,9 +111,9 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
}, },
]} ]}
> >
<div class="mt-12 mb-6 flex flex-col items-center justify-center gap-3"> <div class="mt-12 mb-6 text-center">
<div class="flex items-center gap-2"> <h1 class="font-title text-center text-3xl font-bold">Service suggestion</h1>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} /> <div class="flex items-center justify-center gap-2">
<AdminOnly> <AdminOnly>
<Button <Button
as="a" as="a"
@@ -124,29 +124,24 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
/> />
</AdminOnly> </AdminOnly>
</div> </div>
<h1 class="font-title text-center text-3xl font-bold">Service suggestion</h1>
</div> </div>
<ServiceCard service={serviceSuggestion.service} class="mb-6" /> <ServiceCard service={serviceSuggestion.service} class="mb-6" />
<section class="border-night-400 bg-night-600 rounded-lg border p-6"> <section class="border-night-400 bg-night-600 rounded-lg border p-6">
<div class="text-day-200 grid grid-cols-2 gap-6 text-sm"> <div class="text-day-200 xs:grid-cols-2 grid gap-2 text-sm">
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<span>Status:</span> <span class="font-title font-bold">Status:</span>
<span <BadgeSmall color={statusInfo.color} text={statusInfo.label} icon={statusInfo.icon} />
class={cn(
'border-night-500 bg-night-800 box-content inline-flex h-8 items-center justify-center gap-1 rounded-full border px-2',
statusInfo.iconClass
)}
>
<Icon name={statusInfo.icon} class="size-4" />
{statusInfo.label}
</span>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<span>Submitted:</span> <span class="font-title font-bold">Type:</span>
<BadgeSmall color={typeInfo.color} text={typeInfo.label} icon={typeInfo.icon} />
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Submitted:</span>
<span> <span>
{ {
formatDateShort(serviceSuggestion.createdAt, { formatDateShort(serviceSuggestion.createdAt, {
@@ -157,15 +152,22 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
} }
</span> </span>
</div> </div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-title font-bold">Service:</span>
<a href={`/service/${serviceSuggestion.service.slug}`} class="hover:text-day-200 text-green-400">
Open <Icon name="ri:external-link-line" class="ml-0.5 inline-block size-3 align-[-0.05em]" />
</a>
</div>
</div> </div>
<div class="mt-6"> <div class="bg-night-700 -mx-2 mt-4 rounded-lg p-2 text-sm">
<div class="text-day-200 mb-2 text-sm">Notes for moderators:</div> <span class="font-title block font-bold">Notes for moderators:</span>
{ {
serviceSuggestion.notes ? ( serviceSuggestion.notes ? (
<div class="text-sm wrap-anywhere whitespace-pre-wrap" set:text={serviceSuggestion.notes} /> <div class="mt-1 text-sm wrap-anywhere whitespace-pre-wrap" set:text={serviceSuggestion.notes} />
) : ( ) : (
<span class="text-sm italic">Empty</span> <div class="text-day-400 my-4 text-center text-sm italic">Empty</div>
) )
} }
</div> </div>