Release 2025-05-23-xzNR

This commit is contained in:
pluja
2025-05-23 21:50:03 +00:00
parent 4806a7fd4e
commit 970622d061
14 changed files with 1202 additions and 1090 deletions

View File

@@ -1,7 +1,9 @@
---
import { Icon } from 'astro-icon/components'
import { DATABASE_UI_URL } from 'astro:env/server'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { cn } from '../../lib/cn'
import type { ComponentProps } from 'astro/types'
@@ -9,6 +11,9 @@ type AdminLink = {
icon: ComponentProps<typeof Icon>['name']
title: string
href: string
classNames: {
base?: string
}
}
const adminLinks: AdminLink[] = [
@@ -16,59 +21,98 @@ const adminLinks: AdminLink[] = [
icon: 'ri:box-3-line',
title: 'Services',
href: '/admin/services',
},
{
icon: 'ri:file-list-3-line',
title: 'Attributes',
href: '/admin/attributes',
classNames: {
base: 'text-green-300',
},
},
{
icon: 'ri:user-3-line',
title: 'Users',
href: '/admin/users',
classNames: {
base: 'text-red-300',
},
},
{
icon: 'ri:chat-settings-line',
icon: 'ri:chat-4-line',
title: 'Comments',
href: '/admin/comments',
classNames: {
base: 'text-yellow-300',
},
},
{
icon: 'ri:lightbulb-line',
title: 'Service suggestions',
title: 'Suggestions',
href: '/admin/service-suggestions',
classNames: {
base: 'text-purple-300',
},
},
{
icon: 'ri:price-tag-3-line',
title: 'Attributes',
href: '/admin/attributes',
classNames: {
base: 'text-blue-300',
},
},
{
icon: 'ri:megaphone-line',
title: 'Announcements',
href: '/admin/announcements',
classNames: {
base: 'text-pink-300',
},
},
{
icon: 'ri:rocket-2-line',
title: 'Releases',
href: '/admin/releases',
classNames: {
base: 'text-orange-300',
},
},
{
icon: 'ri:database-2-line',
title: 'Database',
href: 'https://db.kycnot.me',
href: DATABASE_UI_URL,
classNames: {
base: 'text-gray-300',
},
},
]
---
<BaseLayout pageTitle="Admin Dashboard" widthClassName="max-w-screen-xl">
<h1 class="font-title mb-8 text-3xl font-bold text-zinc-100">
<h1 class="font-title text-day-100 mb-8 text-3xl font-bold">
<Icon name="ri:home-gear-line" class="me-1 inline-block size-10 align-[-0.35em]" />
Admin Dashboard
</h1>
<div class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
{
adminLinks.map((link) => (
<a
href={link.href}
class="group flex flex-col items-center justify-evenly rounded-lg border border-zinc-800 bg-gradient-to-br from-zinc-900/90 to-zinc-900/50 py-3 text-center shadow-lg backdrop-blur-xs transition-all duration-300 hover:-translate-y-0.5 hover:from-zinc-800/90 hover:to-zinc-800/50 hover:shadow-xl hover:shadow-zinc-900/20"
>
<Icon name={link.icon} class="size-8 text-zinc-400 transition-colors group-hover:text-green-400" />
<span class="font-title text-xl leading-none font-semibold text-zinc-100 transition-colors group-hover:text-green-400">
{link.title}
</span>
</a>
))
}
</div>
<nav>
<ol class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
{
adminLinks.map((link) => (
<li
class={cn(
'group ease-out-back transition-transform duration-250 hover:-translate-y-0.5 hover:scale-105',
link.classNames.base
)}
>
<a
href={link.href}
class="flex min-h-24 flex-col items-center justify-around rounded-lg border border-current/4 bg-current/3 py-3 text-center transition-all duration-250 group-hover:border-current/10 group-hover:bg-current/10 group-hover:shadow-xl"
>
<Icon
name={link.icon}
class="size-8 text-current opacity-50 transition-opacity duration-250 group-hover:opacity-100"
/>
<span class="font-title text-xl leading-none font-semibold text-current">{link.title}</span>
</a>
</li>
))
}
</ol>
</nav>
</BaseLayout>

View File

@@ -0,0 +1,44 @@
---
import { RELEASE_DATE, RELEASE_NUMBER } from 'astro:env/server'
import TimeFormatted from '../../components/TimeFormatted.astro'
import MiniLayout from '../../layouts/MiniLayout.astro'
const releaseDate =
RELEASE_DATE && !isNaN(new Date(RELEASE_DATE).getTime()) ? new Date(RELEASE_DATE) : undefined
---
<MiniLayout
pageTitle="Releases"
description="Manage releases"
layoutHeader={{
icon: 'ri:rocket-2-line',
title: 'Releases',
subtitle: 'Current release',
}}
className={{
main: 'flex flex-col items-center justify-center text-center',
}}
>
<p class="text-day-200 font-title text-center text-6xl font-medium tracking-wider">
{RELEASE_NUMBER ? `#${RELEASE_NUMBER}` : '???'}
</p>
<time class="text-day-400 mt-4 block text-center text-xl" datetime={releaseDate?.toISOString()}>
{
releaseDate?.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}) ?? 'Unknown release date'
}
</time>
{
!!releaseDate && (
<p class="text-day-500 mt-2">
(<TimeFormatted date={releaseDate} hourPrecision daysUntilDate={Infinity} />)
</p>
)
}
</MiniLayout>

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import { actions, isInputError } from 'astro:actions'
import BadgeSmall from '../../../components/BadgeSmall.astro'
import Button from '../../../components/Button.astro'
import FormSection from '../../../components/FormSection.astro'
import InputCardGroup from '../../../components/InputCardGroup.astro'
import InputImageFile from '../../../components/InputImageFile.astro'
import InputSelect from '../../../components/InputSelect.astro'
@@ -171,90 +172,88 @@ if (!user) return Astro.rewrite('/404')
</div>
</div>
<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>
<FormSection title="Edit profile">
<form
method="POST"
action={actions.admin.user.update}
enctype="multipart/form-data"
class="space-y-2"
data-astro-reload
>
<input type="hidden" name="id" value={user.id} />
<input type="hidden" name="id" value={user.id} />
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<InputText
label="Name"
name="name"
error={updateInputErrors.name}
inputProps={{ value: user.name, required: true }}
/>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<InputText
label="Name"
name="name"
error={updateInputErrors.name}
inputProps={{ value: user.name, required: true }}
<InputText
label="Display Name"
name="displayName"
error={updateInputErrors.displayName}
inputProps={{ value: user.displayName ?? '', maxlength: 50 }}
/>
<InputText
label="Link"
name="link"
error={updateInputErrors.link}
inputProps={{ value: user.link ?? '', type: 'url' }}
/>
<InputText
label="Verified Link"
name="verifiedLink"
error={updateInputErrors.verifiedLink}
inputProps={{ value: user.verifiedLink, type: 'url' }}
/>
</div>
<InputImageFile
label="Profile Picture Upload"
name="pictureFile"
value={user.picture}
error={updateInputErrors.pictureFile}
square
description="Upload a square image for best results. Supported formats: JPG, PNG, WebP, AVIF. Max size: 5MB."
/>
<InputText
label="Display Name"
name="displayName"
error={updateInputErrors.displayName}
inputProps={{ value: user.displayName ?? '', maxlength: 50 }}
<InputCardGroup
name="type"
label="Type"
options={[
{ label: 'Admin', value: 'admin', icon: 'ri:shield-star-fill' },
{ label: 'Moderator', value: 'moderator', icon: 'ri:graduation-cap-fill' },
{ label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill' },
{
label: 'Verified',
value: 'verified',
icon: 'ri:verified-badge-fill',
disabled: true,
noTransitionPersist: true,
},
]}
selectedValue={[
user.admin ? 'admin' : null,
user.verified ? 'verified' : null,
user.moderator ? 'moderator' : null,
user.spammer ? 'spammer' : null,
].filter((v) => v !== null)}
required
cardSize="sm"
iconSize="sm"
multiple
error={updateInputErrors.type}
/>
<InputText
label="Link"
name="link"
error={updateInputErrors.link}
inputProps={{ value: user.link ?? '', type: 'url' }}
/>
<InputText
label="Verified Link"
name="verifiedLink"
error={updateInputErrors.verifiedLink}
inputProps={{ value: user.verifiedLink, type: 'url' }}
/>
</div>
<InputImageFile
label="Profile Picture Upload"
name="pictureFile"
value={user.picture}
error={updateInputErrors.pictureFile}
square
description="Upload a square image for best results. Supported formats: JPG, PNG, WebP, AVIF. Max size: 5MB."
/>
<InputCardGroup
name="type"
label="Type"
options={[
{ label: 'Admin', value: 'admin', icon: 'ri:shield-star-fill' },
{ label: 'Moderator', value: 'moderator', icon: 'ri:graduation-cap-fill' },
{ label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill' },
{
label: 'Verified',
value: 'verified',
icon: 'ri:verified-badge-fill',
disabled: true,
noTransitionPersist: true,
},
]}
selectedValue={[
user.admin ? 'admin' : null,
user.verified ? 'verified' : null,
user.moderator ? 'moderator' : null,
user.spammer ? 'spammer' : null,
].filter((v) => v !== null)}
required
cardSize="sm"
iconSize="sm"
multiple
error={updateInputErrors.type}
/>
<InputSubmitButton label="Save" icon="ri:save-line" hideCancel />
</form>
<section class="space-y-2">
<h2 class="font-title text-center text-3xl leading-none font-bold">Internal Notes</h2>
<InputSubmitButton label="Save" icon="ri:save-line" hideCancel />
</form>
</FormSection>
<FormSection title="Internal Notes">
{
user.internalNotes.length === 0 ? (
<p class="text-day-300 text-center">No internal notes yet.</p>
@@ -294,7 +293,7 @@ if (!user) return Astro.rewrite('/404')
<form
method="POST"
action={actions.admin.user.internalNotes.delete}
class="contents"
class="space-y-2"
data-astro-reload
>
<input type="hidden" name="noteId" value={note.id} />
@@ -347,11 +346,9 @@ if (!user) return Astro.rewrite('/404')
/>
<InputSubmitButton label="Add" icon="ri:add-line" hideCancel />
</form>
</section>
<section class="space-y-2">
<h2 class="font-title text-center text-3xl leading-none font-bold">Service Affiliations</h2>
</FormSection>
<FormSection title="Service Affiliations">
{
user.serviceAffiliations.length === 0 ? (
<p class="text-day-200 text-center">No service affiliations yet.</p>
@@ -380,7 +377,7 @@ if (!user) return Astro.rewrite('/404')
method="POST"
action={actions.admin.user.serviceAffiliations.remove}
data-astro-reload
class="contents"
class="space-y-2"
>
<input type="hidden" name="id" value={affiliation.id} />
<button type="submit" class="text-day-300 transition-colors hover:text-red-400">
@@ -429,29 +426,29 @@ if (!user) return Astro.rewrite('/404')
<InputSubmitButton label="Add Affiliation" icon="ri:link" hideCancel />
</form>
</section>
</FormSection>
<form method="POST" action={actions.admin.user.karmaTransactions.add} data-astro-reload class="space-y-2">
<h2 class="font-title text-center text-3xl leading-none font-bold">Grant/Remove Karma</h2>
<FormSection title="Grant/Remove Karma">
<form method="POST" action={actions.admin.user.karmaTransactions.add} data-astro-reload class="space-y-2">
<input type="hidden" name="userId" value={user.id} />
<input type="hidden" name="userId" value={user.id} />
<InputText
label="Points"
name="points"
error={addKarmaTransactionResult?.error?.message}
inputProps={{ type: 'number', required: true }}
/>
<InputText
label="Points"
name="points"
error={addKarmaTransactionResult?.error?.message}
inputProps={{ type: 'number', required: true }}
/>
<InputTextArea
label="Description"
name="description"
error={addKarmaTransactionResult?.error?.message}
inputProps={{ required: true }}
/>
<InputTextArea
label="Description"
name="description"
error={addKarmaTransactionResult?.error?.message}
inputProps={{ required: true }}
/>
<InputSubmitButton label="Submit" icon="ri:add-line" hideCancel />
</form>
<InputSubmitButton label="Submit" icon="ri:add-line" hideCancel />
</form>
</FormSection>
</BaseLayout>
<script>