Release 202506042153
This commit is contained in:
@@ -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>[]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user