Compare commits
2 Commits
release-36
...
release-38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e0d41cc7a | ||
|
|
70a097054b |
@@ -177,6 +177,12 @@ export default defineConfig({
|
|||||||
url: true,
|
url: true,
|
||||||
optional: false,
|
optional: false,
|
||||||
}),
|
}),
|
||||||
|
LOGS_UI_URL: envField.string({
|
||||||
|
context: 'server',
|
||||||
|
access: 'secret',
|
||||||
|
url: true,
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
|
|
||||||
RELEASE_NUMBER: envField.number({
|
RELEASE_NUMBER: envField.number({
|
||||||
context: 'server',
|
context: 'server',
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ type EventTypeInfo<T extends string | null | undefined = string> = {
|
|||||||
}
|
}
|
||||||
icon: string
|
icon: string
|
||||||
color: ComponentProps<typeof BadgeSmall>['color']
|
color: ComponentProps<typeof BadgeSmall>['color']
|
||||||
|
isSolved: boolean
|
||||||
|
showBanner: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
@@ -36,6 +38,8 @@ export const {
|
|||||||
},
|
},
|
||||||
icon: 'ri:question-fill',
|
icon: 'ri:question-fill',
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: false,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -46,8 +50,10 @@ export const {
|
|||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:error-warning-fill',
|
icon: 'ri:alert-fill',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'WARNING_SOLVED',
|
id: 'WARNING_SOLVED',
|
||||||
@@ -55,10 +61,12 @@ export const {
|
|||||||
label: 'Warning Solved',
|
label: 'Warning Solved',
|
||||||
description: 'A previously reported warning has been solved',
|
description: 'A previously reported warning has been solved',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
dot: 'bg-amber-900 text-amber-300 ring-amber-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:check-fill',
|
icon: 'ri:alert-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
isSolved: true,
|
||||||
|
showBanner: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ALERT',
|
id: 'ALERT',
|
||||||
@@ -68,8 +76,10 @@ export const {
|
|||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:alert-fill',
|
icon: 'ri:spam-fill',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ALERT_SOLVED',
|
id: 'ALERT_SOLVED',
|
||||||
@@ -77,10 +87,12 @@ export const {
|
|||||||
label: 'Alert Solved',
|
label: 'Alert Solved',
|
||||||
description: 'A previously reported alert has been solved',
|
description: 'A previously reported alert has been solved',
|
||||||
classNames: {
|
classNames: {
|
||||||
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
dot: 'bg-red-900 text-red-300 ring-red-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:check-fill',
|
icon: 'ri:spam-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
isSolved: true,
|
||||||
|
showBanner: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'INFO',
|
id: 'INFO',
|
||||||
@@ -92,6 +104,8 @@ export const {
|
|||||||
},
|
},
|
||||||
icon: 'ri:information-fill',
|
icon: 'ri:information-fill',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'NORMAL',
|
id: 'NORMAL',
|
||||||
@@ -103,6 +117,8 @@ export const {
|
|||||||
},
|
},
|
||||||
icon: 'ri:notification-fill',
|
icon: 'ri:notification-fill',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'UPDATE',
|
id: 'UPDATE',
|
||||||
@@ -114,6 +130,8 @@ export const {
|
|||||||
},
|
},
|
||||||
icon: 'ri:pencil-fill',
|
icon: 'ri:pencil-fill',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
|
isSolved: false,
|
||||||
|
showBanner: false,
|
||||||
},
|
},
|
||||||
] as const satisfies EventTypeInfo<EventType>[]
|
] as const satisfies EventTypeInfo<EventType>[]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const SUPPORT_EMAIL = 'support@kycnot.me'
|
export const SUPPORT_EMAIL = 'contact@kycnot.me'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
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 BaseLayout from '../../layouts/BaseLayout.astro'
|
||||||
import { cn } from '../../lib/cn'
|
import { cn } from '../../lib/cn'
|
||||||
@@ -81,6 +81,18 @@ const adminLinks: AdminLink[] = [
|
|||||||
base: 'text-gray-300',
|
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>
|
<nav>
|
||||||
<ol class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
|
<ol class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
|
||||||
{
|
{
|
||||||
adminLinks.map((link) => (
|
adminLinks
|
||||||
<li
|
.filter((link) => link.href)
|
||||||
class={cn(
|
.map((link) => (
|
||||||
'group ease-out-back transition-transform duration-250 hover:-translate-y-0.5 hover:scale-105',
|
<li
|
||||||
link.classNames.base
|
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
|
<a
|
||||||
name={link.icon}
|
href={link.href}
|
||||||
class="size-8 text-current opacity-50 transition-opacity duration-250 group-hover:opacity-100"
|
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"
|
||||||
/>
|
>
|
||||||
<span class="font-title text-xl leading-none font-semibold text-current">{link.title}</span>
|
<Icon
|
||||||
</a>
|
name={link.icon}
|
||||||
</li>
|
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>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -685,9 +685,14 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
label="Started At"
|
label="Started At"
|
||||||
name="startedAt"
|
name="startedAt"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
type: 'date',
|
type: 'datetime-local',
|
||||||
required: true,
|
required: true,
|
||||||
value: new Date(event.startedAt).toISOString().split('T')[0],
|
value: new Date(
|
||||||
|
new Date(event.startedAt).getTime() -
|
||||||
|
new Date(event.startedAt).getTimezoneOffset() * 60000
|
||||||
|
)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 16),
|
||||||
}}
|
}}
|
||||||
error={eventUpdateInputErrors.startedAt}
|
error={eventUpdateInputErrors.startedAt}
|
||||||
/>
|
/>
|
||||||
@@ -696,7 +701,15 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
label="Ended At"
|
label="Ended At"
|
||||||
name="endedAt"
|
name="endedAt"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
value: event.endedAt ? new Date(event.endedAt).toISOString().split('T')[0] : '',
|
type: 'datetime-local',
|
||||||
|
value: event.endedAt
|
||||||
|
? new Date(
|
||||||
|
new Date(event.endedAt).getTime() -
|
||||||
|
new Date(event.endedAt).getTimezoneOffset() * 60000
|
||||||
|
)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 16)
|
||||||
|
: '',
|
||||||
}}
|
}}
|
||||||
error={eventUpdateInputErrors.endedAt}
|
error={eventUpdateInputErrors.endedAt}
|
||||||
description="- Empty: Event is ongoing.\n- Filled: Event with specific end date.\n- Same as start date: One-time event."
|
description="- Empty: Event is ongoing.\n- Filled: Event with specific end date.\n- Same as start date: One-time event."
|
||||||
@@ -756,9 +769,11 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
label="Started At"
|
label="Started At"
|
||||||
name="startedAt"
|
name="startedAt"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
type: 'date',
|
type: 'datetime-local',
|
||||||
required: true,
|
required: true,
|
||||||
value: new Date().toISOString().split('T')[0],
|
value: new Date(Date.now() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 16),
|
||||||
}}
|
}}
|
||||||
error={eventInputErrors.startedAt}
|
error={eventInputErrors.startedAt}
|
||||||
/>
|
/>
|
||||||
@@ -767,7 +782,10 @@ if (!service) return Astro.rewrite('/404')
|
|||||||
label="Ended At"
|
label="Ended At"
|
||||||
name="endedAt"
|
name="endedAt"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
value: new Date().toISOString().split('T')[0],
|
type: 'datetime-local',
|
||||||
|
value: new Date(Date.now() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 16),
|
||||||
}}
|
}}
|
||||||
error={eventInputErrors.endedAt}
|
error={eventInputErrors.endedAt}
|
||||||
description="- Empty: Event is ongoing.\n- Filled: Event with specific end date.\n- Same as start date: One-time event."
|
description="- Empty: Event is ongoing.\n- Filled: Event with specific end date.\n- Same as start date: One-time event."
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type { Prisma } from '@prisma/client'
|
|||||||
|
|
||||||
const { data: filters } = zodParseQueryParamsStoringErrors(
|
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'),
|
'sort-order': z.enum(['asc', 'desc']).default('desc'),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
role: z.enum(['user', 'admin', 'moderator', 'verified', 'spammer']).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
|
// Set up Prisma orderBy with correct typing
|
||||||
const prismaOrderBy =
|
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-by'] === 'karma' ? 'totalKarma' : filters['sort-by']]:
|
||||||
filters['sort-order'] === 'asc' ? 'asc' : 'desc',
|
filters['sort-order'] === 'asc' ? 'asc' : 'desc',
|
||||||
@@ -86,6 +89,7 @@ const dbUsers = await prisma.user.findMany({
|
|||||||
totalKarma: true,
|
totalKarma: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
|
lastLoginAt: true,
|
||||||
internalNotes: {
|
internalNotes: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -218,16 +222,29 @@ const makeSortUrl = (sortBy: NonNullable<(typeof filters)['sort-by']>) => {
|
|||||||
<th
|
<th
|
||||||
class="w-[15%] px-4 py-3 text-center text-xs font-medium tracking-wider text-zinc-400 uppercase"
|
class="w-[15%] px-4 py-3 text-center text-xs font-medium tracking-wider text-zinc-400 uppercase"
|
||||||
>
|
>
|
||||||
<a
|
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||||
href={makeSortUrl('createdAt')}
|
<a
|
||||||
class="flex items-center justify-center hover:text-zinc-200"
|
href={makeSortUrl('lastLoginAt')}
|
||||||
>
|
class="flex items-center justify-center hover:text-zinc-200"
|
||||||
Joined <SortArrowIcon
|
>
|
||||||
active={filters['sort-by'] === 'createdAt'}
|
Login <SortArrowIcon
|
||||||
sortOrder={filters['sort-order']}
|
active={filters['sort-by'] === 'lastLoginAt'}
|
||||||
/>
|
sortOrder={filters['sort-order']}
|
||||||
</a>
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
<th
|
<th
|
||||||
class="w-[15%] px-4 py-3 text-center text-xs font-medium tracking-wider text-zinc-400 uppercase"
|
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}
|
{user.totalKarma}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center text-sm text-zinc-400">
|
<td class="px-4 py-3 text-center text-sm">
|
||||||
<TimeFormatted date={user.createdAt} hourPrecision hoursShort prefix={false} />
|
<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>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex justify-center gap-3">
|
<div class="flex justify-center gap-3">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { VerificationStepStatus } from '@prisma/client'
|
import { VerificationStepStatus, EventType } from '@prisma/client'
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { Markdown } from 'astro-remote'
|
import { Markdown } from 'astro-remote'
|
||||||
import { Schema } from 'astro-seo-schema'
|
import { Schema } from 'astro-seo-schema'
|
||||||
@@ -380,6 +380,10 @@ const ogImageTemplateData = {
|
|||||||
} satisfies OgImageAllTemplatesWithProps
|
} satisfies OgImageAllTemplatesWithProps
|
||||||
|
|
||||||
const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility)
|
const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility)
|
||||||
|
|
||||||
|
const activeAlertOrWarningEvents = service.events.filter(
|
||||||
|
(event) => getEventTypeInfo(event.type).showBanner && (event.endedAt === null || event.endedAt >= now)
|
||||||
|
)
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -480,6 +484,32 @@ const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility
|
|||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
activeAlertOrWarningEvents.length > 0 && (
|
||||||
|
<a
|
||||||
|
href="#events"
|
||||||
|
class={cn(
|
||||||
|
'mb-4 block rounded-md px-3 py-2 text-sm font-medium',
|
||||||
|
activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||||
|
? 'bg-red-900/50 text-red-300 hover:bg-red-800/60'
|
||||||
|
: 'bg-yellow-900/50 text-yellow-300 hover:bg-yellow-800/60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||||
|
? 'ri:alert-fill'
|
||||||
|
: 'ri:alarm-warning-fill'
|
||||||
|
}
|
||||||
|
class="me-1.5 inline-block size-4 align-[-0.15em]"
|
||||||
|
/>
|
||||||
|
{activeAlertOrWarningEvents.some((e) => e.type === EventType.ALERT)
|
||||||
|
? 'There is an active alert for this service. Click to see details.'
|
||||||
|
: 'There is an active warning for this service. Click to see details.'}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
(serviceVisibilityInfo.value === 'UNLISTED' || serviceVisibilityInfo.value === 'ARCHIVED') && (
|
(serviceVisibilityInfo.value === 'UNLISTED' || serviceVisibilityInfo.value === 'ARCHIVED') && (
|
||||||
<div class={cn('mb-4 rounded-md bg-yellow-900/50 px-3 py-2 text-sm text-yellow-400')}>
|
<div class={cn('mb-4 rounded-md bg-yellow-900/50 px-3 py-2 text-sm text-yellow-400')}>
|
||||||
@@ -1182,6 +1212,7 @@ const serviceVisibilityInfo = getServiceVisibilityInfo(service.serviceVisibility
|
|||||||
|
|
||||||
<div class="mt-3 max-w-md pe-8">
|
<div class="mt-3 max-w-md pe-8">
|
||||||
<h3 class="font-title text-lg leading-tight font-semibold text-pretty text-white">
|
<h3 class="font-title text-lg leading-tight font-semibold text-pretty text-white">
|
||||||
|
{typeInfo.isSolved && <BadgeSmall text="Solved" icon="ri:check-line" color="green" />}
|
||||||
{event.title}
|
{event.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const user = await Astro.locals.banners.try('user', async () => {
|
|||||||
verifiedLink: true,
|
verifiedLink: true,
|
||||||
totalKarma: true,
|
totalKarma: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
lastLoginAt: true,
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
comments: true,
|
comments: true,
|
||||||
@@ -469,6 +470,24 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</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 && (
|
user.verifiedLink && (
|
||||||
<li class="flex items-start">
|
<li class="flex items-start">
|
||||||
|
|||||||
Reference in New Issue
Block a user