Release 202505271800
This commit is contained in:
@@ -177,6 +177,12 @@ export default defineConfig({
|
||||
url: true,
|
||||
optional: false,
|
||||
}),
|
||||
LOGS_UI_URL: envField.string({
|
||||
context: 'server',
|
||||
access: 'secret',
|
||||
url: true,
|
||||
optional: true,
|
||||
}),
|
||||
|
||||
RELEASE_NUMBER: envField.number({
|
||||
context: 'server',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { DATABASE_UI_URL } from 'astro:env/server'
|
||||
import { DATABASE_UI_URL, LOGS_UI_URL } from 'astro:env/server'
|
||||
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import { cn } from '../../lib/cn'
|
||||
@@ -81,6 +81,18 @@ const adminLinks: AdminLink[] = [
|
||||
base: 'text-gray-300',
|
||||
},
|
||||
},
|
||||
...(LOGS_UI_URL
|
||||
? [
|
||||
{
|
||||
icon: 'ri:menu-search-line',
|
||||
title: 'Logs',
|
||||
href: LOGS_UI_URL,
|
||||
classNames: {
|
||||
base: 'text-cyan-300',
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
---
|
||||
|
||||
@@ -93,25 +105,27 @@ const adminLinks: AdminLink[] = [
|
||||
<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"
|
||||
adminLinks
|
||||
.filter((link) => link.href)
|
||||
.map((link) => (
|
||||
<li
|
||||
class={cn(
|
||||
'group ease-out-back transition-transform duration-250 hover:-translate-y-0.5 hover:scale-105',
|
||||
link.classNames.base
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
))
|
||||
<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>
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { Prisma } from '@prisma/client'
|
||||
|
||||
const { data: filters } = zodParseQueryParamsStoringErrors(
|
||||
{
|
||||
'sort-by': z.enum(['name', 'role', 'createdAt', 'karma']).default('createdAt'),
|
||||
'sort-by': z.enum(['name', 'role', 'lastLoginAt', 'karma', 'createdAt']).default('createdAt'),
|
||||
'sort-order': z.enum(['asc', 'desc']).default('desc'),
|
||||
search: z.string().optional(),
|
||||
role: z.enum(['user', 'admin', 'moderator', 'verified', 'spammer']).optional(),
|
||||
@@ -29,7 +29,10 @@ const { data: filters } = zodParseQueryParamsStoringErrors(
|
||||
|
||||
// Set up Prisma orderBy with correct typing
|
||||
const prismaOrderBy =
|
||||
filters['sort-by'] === 'name' || filters['sort-by'] === 'createdAt' || filters['sort-by'] === 'karma'
|
||||
filters['sort-by'] === 'name' ||
|
||||
filters['sort-by'] === 'createdAt' ||
|
||||
filters['sort-by'] === 'lastLoginAt' ||
|
||||
filters['sort-by'] === 'karma'
|
||||
? {
|
||||
[filters['sort-by'] === 'karma' ? 'totalKarma' : filters['sort-by']]:
|
||||
filters['sort-order'] === 'asc' ? 'asc' : 'desc',
|
||||
@@ -86,6 +89,7 @@ const dbUsers = await prisma.user.findMany({
|
||||
totalKarma: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
lastLoginAt: true,
|
||||
internalNotes: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -218,16 +222,29 @@ const makeSortUrl = (sortBy: NonNullable<(typeof filters)['sort-by']>) => {
|
||||
<th
|
||||
class="w-[15%] px-4 py-3 text-center text-xs font-medium tracking-wider text-zinc-400 uppercase"
|
||||
>
|
||||
<a
|
||||
href={makeSortUrl('createdAt')}
|
||||
class="flex items-center justify-center hover:text-zinc-200"
|
||||
>
|
||||
Joined <SortArrowIcon
|
||||
active={filters['sort-by'] === 'createdAt'}
|
||||
sortOrder={filters['sort-order']}
|
||||
/>
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||
<a
|
||||
href={makeSortUrl('lastLoginAt')}
|
||||
class="flex items-center justify-center hover:text-zinc-200"
|
||||
>
|
||||
Login <SortArrowIcon
|
||||
active={filters['sort-by'] === 'lastLoginAt'}
|
||||
sortOrder={filters['sort-order']}
|
||||
/>
|
||||
</a>
|
||||
<span class="text-zinc-600">/</span>
|
||||
<a
|
||||
href={makeSortUrl('createdAt')}
|
||||
class="flex items-center justify-center hover:text-zinc-200"
|
||||
>
|
||||
Joined <SortArrowIcon
|
||||
active={filters['sort-by'] === 'createdAt'}
|
||||
sortOrder={filters['sort-order']}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th
|
||||
class="w-[15%] px-4 py-3 text-center text-xs font-medium tracking-wider text-zinc-400 uppercase"
|
||||
>
|
||||
@@ -305,8 +322,24 @@ const makeSortUrl = (sortBy: NonNullable<(typeof filters)['sort-by']>) => {
|
||||
{user.totalKarma}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-sm text-zinc-400">
|
||||
<TimeFormatted date={user.createdAt} hourPrecision hoursShort prefix={false} />
|
||||
<td class="px-4 py-3 text-center text-sm">
|
||||
<div class="flex flex-wrap items-center justify-center gap-1 text-center">
|
||||
<TimeFormatted
|
||||
class="text-zinc-300"
|
||||
date={user.lastLoginAt}
|
||||
hourPrecision
|
||||
hoursShort
|
||||
prefix={false}
|
||||
/>
|
||||
<span class="text-zinc-600">/</span>
|
||||
<TimeFormatted
|
||||
class="text-zinc-400"
|
||||
date={user.createdAt}
|
||||
hourPrecision
|
||||
hoursShort
|
||||
prefix={false}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex justify-center gap-3">
|
||||
|
||||
@@ -48,6 +48,7 @@ const user = await Astro.locals.banners.try('user', async () => {
|
||||
verifiedLink: true,
|
||||
totalKarma: true,
|
||||
createdAt: true,
|
||||
lastLoginAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
comments: true,
|
||||
@@ -469,6 +470,24 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<AdminOnly>
|
||||
<li class="flex items-start">
|
||||
<span class="text-day-500 mt-0.5 mr-2"><Icon name="ri:calendar-line" class="size-4" /></span>
|
||||
<div>
|
||||
<p class="text-day-500 text-xs">Last login</p>
|
||||
<p class="text-day-300">
|
||||
{
|
||||
formatDateShort(user.lastLoginAt, {
|
||||
prefix: false,
|
||||
hourPrecision: true,
|
||||
caseType: 'sentence',
|
||||
})
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</AdminOnly>
|
||||
|
||||
{
|
||||
user.verifiedLink && (
|
||||
<li class="flex items-start">
|
||||
|
||||
Reference in New Issue
Block a user