Release 2025-05-23-xzNR
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
DATABASE_URL="postgresql://kycnot:kycnot@localhost:3399/kycnot?schema=public"
|
DATABASE_URL="postgresql://kycnot:kycnot@localhost:3399/kycnot?schema=public"
|
||||||
REDIS_URL="redis://localhost:6379"
|
REDIS_URL="redis://localhost:6379"
|
||||||
SOURCE_CODE_URL="https://github.com"
|
SOURCE_CODE_URL="https://github.com"
|
||||||
|
DATABASE_UI_URL="http://localhost:5555"
|
||||||
SITE_URL="https://localhost:4321"
|
SITE_URL="https://localhost:4321"
|
||||||
ONION_ADDRESS="http://kycnotmezdiftahfmc34pqbpicxlnx3jbf5p7jypge7gdvduu7i6qjqd.onion"
|
ONION_ADDRESS="http://kycnotmezdiftahfmc34pqbpicxlnx3jbf5p7jypge7gdvduu7i6qjqd.onion"
|
||||||
I2P_ADDRESS="http://nti3rj4j4disjcm2kvp4eno7otcejbbxv3ggxwr5tpfk4jucah7q.b32.i2p"
|
I2P_ADDRESS="http://nti3rj4j4disjcm2kvp4eno7otcejbbxv3ggxwr5tpfk4jucah7q.b32.i2p"
|
||||||
|
RELEASE_NUMBER=123
|
||||||
|
RELEASE_DATE="2025-05-23T19:00:00.000Z"
|
||||||
|
|||||||
@@ -170,6 +170,25 @@ export default defineConfig({
|
|||||||
url: true,
|
url: true,
|
||||||
optional: false,
|
optional: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
DATABASE_UI_URL: envField.string({
|
||||||
|
context: 'server',
|
||||||
|
access: 'secret',
|
||||||
|
url: true,
|
||||||
|
optional: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
RELEASE_NUMBER: envField.number({
|
||||||
|
context: 'server',
|
||||||
|
access: 'public',
|
||||||
|
int: true,
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
|
RELEASE_DATE: envField.string({
|
||||||
|
context: 'server',
|
||||||
|
access: 'public',
|
||||||
|
optional: true,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -58,27 +58,17 @@ const button = tv({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
black: {
|
black: '',
|
||||||
base: 'border-night-500 bg-night-800 hover:bg-night-900 hover:text-day-200 focus-visible:bg-night-500 text-white/50 focus-visible:text-white focus-visible:ring-white',
|
white: '',
|
||||||
},
|
gray: '',
|
||||||
white: {
|
success: '',
|
||||||
base: 'border-day-300 bg-day-100 hover:bg-day-200 text-black focus-visible:ring-green-500',
|
danger: '',
|
||||||
},
|
warning: '',
|
||||||
gray: {
|
info: '',
|
||||||
base: 'border-day-500 bg-day-400 hover:bg-day-500 text-black focus-visible:ring-white',
|
},
|
||||||
},
|
variant: {
|
||||||
success: {
|
solid: '',
|
||||||
base: 'border-green-600 bg-green-500 text-black hover:bg-green-600',
|
faded: '',
|
||||||
},
|
|
||||||
error: {
|
|
||||||
base: 'border-red-600 bg-red-500 text-white hover:bg-red-600',
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
base: 'border-yellow-600 bg-yellow-500 text-white hover:bg-yellow-600',
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
base: 'border-blue-600 bg-blue-500 text-white hover:bg-blue-600',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
shadow: {
|
shadow: {
|
||||||
true: {
|
true: {
|
||||||
@@ -92,6 +82,107 @@ const button = tv({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
|
// Color variants - solid
|
||||||
|
{
|
||||||
|
color: 'black',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-night-500 bg-night-800 hover:bg-night-900 hover:text-day-200 focus-visible:bg-night-500 text-white/50 focus-visible:text-white focus-visible:ring-white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'white',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-day-300 bg-day-100 hover:bg-day-200 text-black focus-visible:ring-green-500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'gray',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-day-500 bg-day-400 hover:bg-day-500 text-black focus-visible:ring-white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'success',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-green-600 bg-green-500 text-black hover:bg-green-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'danger',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-red-600 bg-red-500 text-white hover:bg-red-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'warning',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-yellow-600 bg-yellow-500 text-white hover:bg-yellow-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'info',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
base: 'border-blue-600 bg-blue-500 text-white hover:bg-blue-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Color variants - faded
|
||||||
|
{
|
||||||
|
color: 'black',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-night-300/30 bg-night-800/30 hover:bg-night-700/50 text-white/70 hover:text-white/90 focus-visible:ring-white/50',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'white',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-day-300/30 bg-day-100/30 hover:bg-day-200/50 text-white/70 hover:text-white/90 focus-visible:ring-white/50',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'gray',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-day-500/30 bg-day-400/30 hover:bg-day-500/50 text-day-300 hover:text-day-100 focus-visible:ring-white/50',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'success',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-green-600/30 bg-green-500/30 text-green-300 hover:bg-green-500/50 hover:text-green-100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'danger',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-red-600/30 bg-red-500/30 text-red-300 hover:bg-red-500/50 hover:text-red-100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'warning',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-yellow-600/30 bg-yellow-500/30 text-yellow-300 hover:bg-yellow-500/50 hover:text-yellow-100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'info',
|
||||||
|
variant: 'faded',
|
||||||
|
class: {
|
||||||
|
base: 'border-blue-600/30 bg-blue-500/30 text-blue-300 hover:bg-blue-500/50 hover:text-blue-100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Shadow variants
|
||||||
{
|
{
|
||||||
color: 'black',
|
color: 'black',
|
||||||
shadow: true,
|
shadow: true,
|
||||||
@@ -113,7 +204,7 @@ const button = tv({
|
|||||||
class: 'shadow-green-500/30',
|
class: 'shadow-green-500/30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
color: 'error',
|
color: 'danger',
|
||||||
shadow: true,
|
shadow: true,
|
||||||
class: 'shadow-red-500/30',
|
class: 'shadow-red-500/30',
|
||||||
},
|
},
|
||||||
@@ -127,6 +218,7 @@ const button = tv({
|
|||||||
shadow: true,
|
shadow: true,
|
||||||
class: 'shadow-blue-500/30',
|
class: 'shadow-blue-500/30',
|
||||||
},
|
},
|
||||||
|
// Icon only variants
|
||||||
{
|
{
|
||||||
iconOnly: true,
|
iconOnly: true,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
@@ -146,6 +238,7 @@ const button = tv({
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: 'md',
|
size: 'md',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
variant: 'solid',
|
||||||
shadow: false,
|
shadow: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
iconOnly: false,
|
iconOnly: false,
|
||||||
@@ -159,6 +252,7 @@ const {
|
|||||||
endIcon,
|
endIcon,
|
||||||
size,
|
size,
|
||||||
color,
|
color,
|
||||||
|
variant,
|
||||||
shadow,
|
shadow,
|
||||||
class: className,
|
class: className,
|
||||||
classNames,
|
classNames,
|
||||||
@@ -174,7 +268,7 @@ const {
|
|||||||
icon: iconSlot,
|
icon: iconSlot,
|
||||||
label: labelSlot,
|
label: labelSlot,
|
||||||
endIcon: endIconSlot,
|
endIcon: endIconSlot,
|
||||||
} = button({ size, color, shadow, disabled, iconOnly: !label && !(!!icon && !!endIcon) })
|
} = button({ size, color, variant, shadow, disabled, iconOnly: !label && !(!!icon && !!endIcon) })
|
||||||
|
|
||||||
const ActualTag = disabled && Tag === 'a' ? 'span' : Tag
|
const ActualTag = disabled && Tag === 'a' ? 'span' : Tag
|
||||||
---
|
---
|
||||||
|
|||||||
24
web/src/components/FormSection.astro
Normal file
24
web/src/components/FormSection.astro
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { cn } from '../lib/cn'
|
||||||
|
|
||||||
|
import type { AstroChildren } from '../lib/astro'
|
||||||
|
import type { HTMLAttributes } from 'astro/types'
|
||||||
|
|
||||||
|
type Props = HTMLAttributes<'section'> & {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
heading?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||||
|
children: AstroChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subtitle, class: className, heading = 'h2', ...props } = Astro.props
|
||||||
|
|
||||||
|
const HeadingTag = heading
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class={cn('mt-24 space-y-2 first:mt-0', className)} {...props}>
|
||||||
|
<HeadingTag class="font-title text-center text-3xl leading-none font-bold">{title}</HeadingTag>
|
||||||
|
{subtitle && <p class="text-day-400 text-center">{subtitle}</p>}
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
24
web/src/components/FormSubSection.astro
Normal file
24
web/src/components/FormSubSection.astro
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { cn } from '../lib/cn'
|
||||||
|
|
||||||
|
import type { AstroChildren } from '../lib/astro'
|
||||||
|
import type { HTMLAttributes } from 'astro/types'
|
||||||
|
|
||||||
|
type Props = HTMLAttributes<'section'> & {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
heading?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||||
|
children: AstroChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subtitle, class: className, heading = 'h3', ...props } = Astro.props
|
||||||
|
|
||||||
|
const HeadingTag = heading
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class={cn('mt-6 space-y-2 first:mt-0', className)} {...props}>
|
||||||
|
<HeadingTag class="font-title text-day-400 text-center text-lg font-medium">{title}</HeadingTag>
|
||||||
|
{subtitle && <p class="text-day-400 text-center">{subtitle}</p>}
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
@@ -12,13 +12,15 @@ type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId'> &
|
|||||||
options: {
|
options: {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
icon?: string
|
icon?: string[] | string
|
||||||
|
iconClassName?: string[] | string
|
||||||
}[]
|
}[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
selectedValues?: string[]
|
selectedValues?: string[]
|
||||||
|
size?: 'lg' | 'md'
|
||||||
}
|
}
|
||||||
|
|
||||||
const { options, disabled, selectedValues = [], ...wrapperProps } = Astro.props
|
const { options, disabled, selectedValues = [], size = 'md', ...wrapperProps } = Astro.props
|
||||||
|
|
||||||
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
|
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
|
||||||
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
||||||
@@ -26,23 +28,38 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
|||||||
|
|
||||||
<InputWrapper inputId={inputId} {...wrapperProps}>
|
<InputWrapper inputId={inputId} {...wrapperProps}>
|
||||||
<div class={cn(baseInputClassNames.div, hasError && baseInputClassNames.error)}>
|
<div class={cn(baseInputClassNames.div, hasError && baseInputClassNames.error)}>
|
||||||
<div class="h-48 overflow-y-auto mask-y-from-[calc(100%-var(--spacing)*8)] py-5">
|
<div
|
||||||
|
class={cn('h-48 overflow-y-auto mask-y-from-[calc(100%-var(--spacing)*8)] py-5', {
|
||||||
|
'h-96': size === 'lg',
|
||||||
|
'h-48': size === 'md',
|
||||||
|
})}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
options.map((option) => (
|
options.map((option) => {
|
||||||
<label class="hover:bg-night-500 flex cursor-pointer items-center gap-2 px-5 py-1">
|
const icons = option.icon ? (Array.isArray(option.icon) ? option.icon : [option.icon]) : []
|
||||||
<input
|
const iconClassName = option.iconClassName
|
||||||
transition:persist
|
? Array.isArray(option.iconClassName)
|
||||||
type="checkbox"
|
? option.iconClassName
|
||||||
name={wrapperProps.name}
|
: Array.from({ length: icons.length }, () => option.iconClassName)
|
||||||
value={option.value}
|
: []
|
||||||
checked={selectedValues.includes(option.value)}
|
return (
|
||||||
class={cn(hasError && baseInputClassNames.error, disabled && baseInputClassNames.disabled)}
|
<label class="hover:bg-night-500 flex cursor-pointer items-center gap-2 px-5 py-1">
|
||||||
disabled={disabled}
|
<input
|
||||||
/>
|
transition:persist
|
||||||
{option.icon && <Icon name={option.icon} class="size-4" />}
|
type="checkbox"
|
||||||
<span class="text-sm leading-none">{option.label}</span>
|
name={wrapperProps.name}
|
||||||
</label>
|
value={option.value}
|
||||||
))
|
checked={selectedValues.includes(option.value)}
|
||||||
|
class={cn(hasError && baseInputClassNames.error, disabled && baseInputClassNames.disabled)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
{icons.map((icon, index) => (
|
||||||
|
<Icon name={icon} class={cn('size-4', iconClassName[index])} />
|
||||||
|
))}
|
||||||
|
<span class="text-sm leading-none">{option.label}</span>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { ComponentProps, HTMLAttributes } from 'astro/types'
|
|||||||
|
|
||||||
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId' | 'required'> & {
|
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId' | 'required'> & {
|
||||||
inputProps?: Omit<HTMLAttributes<'textarea'>, 'name'>
|
inputProps?: Omit<HTMLAttributes<'textarea'>, 'name'>
|
||||||
value?: string
|
value?: string | null | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const { inputProps, value, ...wrapperProps } = Astro.props
|
const { inputProps, value, ...wrapperProps } = Astro.props
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
||||||
import { transformCase } from '../lib/strings'
|
import { transformCase } from '../lib/strings'
|
||||||
|
|
||||||
|
import type BadgeSmall from '../components/BadgeSmall.astro'
|
||||||
import type { EventType } from '@prisma/client'
|
import type { EventType } from '@prisma/client'
|
||||||
|
import type { ComponentProps } from 'astro/types'
|
||||||
|
|
||||||
type EventTypeInfo<T extends string | null | undefined = string> = {
|
type EventTypeInfo<T extends string | null | undefined = string> = {
|
||||||
id: T
|
id: T
|
||||||
@@ -12,6 +14,7 @@ type EventTypeInfo<T extends string | null | undefined = string> = {
|
|||||||
dot: string
|
dot: string
|
||||||
}
|
}
|
||||||
icon: string
|
icon: string
|
||||||
|
color: ComponentProps<typeof BadgeSmall>['color']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
@@ -32,6 +35,7 @@ export const {
|
|||||||
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:question-fill',
|
icon: 'ri:question-fill',
|
||||||
|
color: 'gray',
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -43,6 +47,7 @@ export const {
|
|||||||
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:error-warning-fill',
|
||||||
|
color: 'yellow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'WARNING_SOLVED',
|
id: 'WARNING_SOLVED',
|
||||||
@@ -53,6 +58,7 @@ export const {
|
|||||||
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:check-fill',
|
icon: 'ri:check-fill',
|
||||||
|
color: 'green',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ALERT',
|
id: 'ALERT',
|
||||||
@@ -63,6 +69,7 @@ export const {
|
|||||||
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:alert-fill',
|
||||||
|
color: 'red',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ALERT_SOLVED',
|
id: 'ALERT_SOLVED',
|
||||||
@@ -73,6 +80,7 @@ export const {
|
|||||||
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
dot: 'bg-green-900 text-green-300 ring-green-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:check-fill',
|
icon: 'ri:check-fill',
|
||||||
|
color: 'green',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'INFO',
|
id: 'INFO',
|
||||||
@@ -83,6 +91,7 @@ export const {
|
|||||||
dot: 'bg-blue-900 text-blue-300 ring-blue-900/50',
|
dot: 'bg-blue-900 text-blue-300 ring-blue-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:information-fill',
|
icon: 'ri:information-fill',
|
||||||
|
color: 'sky',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'NORMAL',
|
id: 'NORMAL',
|
||||||
@@ -93,6 +102,7 @@ export const {
|
|||||||
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:notification-fill',
|
icon: 'ri:notification-fill',
|
||||||
|
color: 'green',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'UPDATE',
|
id: 'UPDATE',
|
||||||
@@ -103,6 +113,7 @@ export const {
|
|||||||
dot: 'bg-sky-900 text-sky-300 ring-sky-900/50',
|
dot: 'bg-sky-900 text-sky-300 ring-sky-900/50',
|
||||||
},
|
},
|
||||||
icon: 'ri:pencil-fill',
|
icon: 'ri:pencil-fill',
|
||||||
|
color: 'sky',
|
||||||
},
|
},
|
||||||
] as const satisfies EventTypeInfo<EventType>[]
|
] as const satisfies EventTypeInfo<EventType>[]
|
||||||
)
|
)
|
||||||
|
|||||||
53
web/src/constants/verificationStepStatus.ts
Normal file
53
web/src/constants/verificationStepStatus.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
|
||||||
|
import { transformCase } from '../lib/strings'
|
||||||
|
|
||||||
|
import type BadgeSmall from '../components/BadgeSmall.astro'
|
||||||
|
import type { VerificationStepStatus } from '@prisma/client'
|
||||||
|
import type { ComponentProps } from 'astro/types'
|
||||||
|
|
||||||
|
type VerificationStepStatusInfo<T extends string | null | undefined = string> = {
|
||||||
|
value: T
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
color: ComponentProps<typeof BadgeSmall>['color']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
dataArray: verificationStepStatuses,
|
||||||
|
dataObject: verificationStepStatusesByValue,
|
||||||
|
getFn: getVerificationStepStatusInfo,
|
||||||
|
} = makeHelpersForOptions(
|
||||||
|
'value',
|
||||||
|
(value): VerificationStepStatusInfo<typeof value> => ({
|
||||||
|
value,
|
||||||
|
label: value ? transformCase(value, 'title') : String(value),
|
||||||
|
icon: 'ri:question-line',
|
||||||
|
color: 'gray',
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: 'PASSED',
|
||||||
|
label: 'Passed',
|
||||||
|
icon: 'ri:verified-badge-fill',
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'IN_PROGRESS',
|
||||||
|
label: 'In Progress',
|
||||||
|
icon: 'ri:loader-line',
|
||||||
|
color: 'yellow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'FAILED',
|
||||||
|
label: 'Failed',
|
||||||
|
icon: 'ri:alert-line',
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'PENDING',
|
||||||
|
label: 'Pending',
|
||||||
|
icon: 'ri:time-line',
|
||||||
|
color: 'sky',
|
||||||
|
},
|
||||||
|
] as const satisfies VerificationStepStatusInfo<VerificationStepStatus>[]
|
||||||
|
)
|
||||||
@@ -8,7 +8,7 @@ import BaseLayout from './BaseLayout.astro'
|
|||||||
import type { ComponentProps } from 'astro/types'
|
import type { ComponentProps } from 'astro/types'
|
||||||
|
|
||||||
type Props = Omit<ComponentProps<typeof BaseLayout>, 'widthClassName'> & {
|
type Props = Omit<ComponentProps<typeof BaseLayout>, 'widthClassName'> & {
|
||||||
layoutHeader: { icon: string; title: string; subtitle: string }
|
layoutHeader: { icon: string; title: string; subtitle?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { layoutHeader, ...baseLayoutProps } = Astro.props
|
const { layoutHeader, ...baseLayoutProps } = Astro.props
|
||||||
@@ -28,10 +28,19 @@ const { layoutHeader, ...baseLayoutProps } = Astro.props
|
|||||||
<Icon name={layoutHeader.icon} class="text-night-800 size-8" />
|
<Icon name={layoutHeader.icon} class="text-night-800 size-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="font-title text-day-200 mt-1 text-center text-3xl font-semibold">
|
<h1
|
||||||
|
class={cn(
|
||||||
|
'font-title text-day-200 mt-1 text-center text-3xl font-semibold',
|
||||||
|
!layoutHeader.subtitle && 'xs:mb-8 mb-6'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{layoutHeader.title}
|
{layoutHeader.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-day-500 xs:mb-8 mt-1 mb-6 text-center">{layoutHeader.subtitle}</p>
|
{
|
||||||
|
!!layoutHeader.subtitle && (
|
||||||
|
<p class="text-day-500 xs:mb-8 mt-1 mb-6 text-center">{layoutHeader.subtitle}</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
|
import { DATABASE_UI_URL } from 'astro:env/server'
|
||||||
|
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||||
|
import { cn } from '../../lib/cn'
|
||||||
|
|
||||||
import type { ComponentProps } from 'astro/types'
|
import type { ComponentProps } from 'astro/types'
|
||||||
|
|
||||||
@@ -9,6 +11,9 @@ type AdminLink = {
|
|||||||
icon: ComponentProps<typeof Icon>['name']
|
icon: ComponentProps<typeof Icon>['name']
|
||||||
title: string
|
title: string
|
||||||
href: string
|
href: string
|
||||||
|
classNames: {
|
||||||
|
base?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminLinks: AdminLink[] = [
|
const adminLinks: AdminLink[] = [
|
||||||
@@ -16,59 +21,98 @@ const adminLinks: AdminLink[] = [
|
|||||||
icon: 'ri:box-3-line',
|
icon: 'ri:box-3-line',
|
||||||
title: 'Services',
|
title: 'Services',
|
||||||
href: '/admin/services',
|
href: '/admin/services',
|
||||||
},
|
classNames: {
|
||||||
{
|
base: 'text-green-300',
|
||||||
icon: 'ri:file-list-3-line',
|
},
|
||||||
title: 'Attributes',
|
|
||||||
href: '/admin/attributes',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'ri:user-3-line',
|
icon: 'ri:user-3-line',
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
href: '/admin/users',
|
href: '/admin/users',
|
||||||
|
classNames: {
|
||||||
|
base: 'text-red-300',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'ri:chat-settings-line',
|
icon: 'ri:chat-4-line',
|
||||||
title: 'Comments',
|
title: 'Comments',
|
||||||
href: '/admin/comments',
|
href: '/admin/comments',
|
||||||
|
classNames: {
|
||||||
|
base: 'text-yellow-300',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'ri:lightbulb-line',
|
icon: 'ri:lightbulb-line',
|
||||||
title: 'Service suggestions',
|
title: 'Suggestions',
|
||||||
href: '/admin/service-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',
|
icon: 'ri:megaphone-line',
|
||||||
title: 'Announcements',
|
title: 'Announcements',
|
||||||
href: '/admin/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',
|
icon: 'ri:database-2-line',
|
||||||
title: 'Database',
|
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">
|
<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]" />
|
<Icon name="ri:home-gear-line" class="me-1 inline-block size-10 align-[-0.35em]" />
|
||||||
Admin Dashboard
|
Admin Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
|
<nav>
|
||||||
{
|
<ol class="grid grid-cols-[repeat(auto-fill,minmax(calc(var(--spacing)*40),1fr))] gap-4">
|
||||||
adminLinks.map((link) => (
|
{
|
||||||
<a
|
adminLinks.map((link) => (
|
||||||
href={link.href}
|
<li
|
||||||
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"
|
class={cn(
|
||||||
>
|
'group ease-out-back transition-transform duration-250 hover:-translate-y-0.5 hover:scale-105',
|
||||||
<Icon name={link.icon} class="size-8 text-zinc-400 transition-colors group-hover:text-green-400" />
|
link.classNames.base
|
||||||
<span class="font-title text-xl leading-none font-semibold text-zinc-100 transition-colors group-hover:text-green-400">
|
)}
|
||||||
{link.title}
|
>
|
||||||
</span>
|
<a
|
||||||
</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"
|
||||||
}
|
>
|
||||||
</div>
|
<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>
|
</BaseLayout>
|
||||||
|
|||||||
44
web/src/pages/admin/releases.astro
Normal file
44
web/src/pages/admin/releases.astro
Normal 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
@@ -4,6 +4,7 @@ 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 FormSection from '../../../components/FormSection.astro'
|
||||||
import InputCardGroup from '../../../components/InputCardGroup.astro'
|
import InputCardGroup from '../../../components/InputCardGroup.astro'
|
||||||
import InputImageFile from '../../../components/InputImageFile.astro'
|
import InputImageFile from '../../../components/InputImageFile.astro'
|
||||||
import InputSelect from '../../../components/InputSelect.astro'
|
import InputSelect from '../../../components/InputSelect.astro'
|
||||||
@@ -171,90 +172,88 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<FormSection title="Edit profile">
|
||||||
method="POST"
|
<form
|
||||||
action={actions.admin.user.update}
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
action={actions.admin.user.update}
|
||||||
class="space-y-2"
|
enctype="multipart/form-data"
|
||||||
data-astro-reload
|
class="space-y-2"
|
||||||
>
|
data-astro-reload
|
||||||
<h2 class="font-title text-center text-3xl leading-none font-bold">Edit profile</h2>
|
>
|
||||||
|
<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
|
||||||
<InputText
|
label="Display Name"
|
||||||
label="Name"
|
name="displayName"
|
||||||
name="name"
|
error={updateInputErrors.displayName}
|
||||||
error={updateInputErrors.name}
|
inputProps={{ value: user.displayName ?? '', maxlength: 50 }}
|
||||||
inputProps={{ value: user.name, required: true }}
|
/>
|
||||||
|
|
||||||
|
<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
|
<InputCardGroup
|
||||||
label="Display Name"
|
name="type"
|
||||||
name="displayName"
|
label="Type"
|
||||||
error={updateInputErrors.displayName}
|
options={[
|
||||||
inputProps={{ value: user.displayName ?? '', maxlength: 50 }}
|
{ 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
|
<InputSubmitButton label="Save" icon="ri:save-line" hideCancel />
|
||||||
label="Link"
|
</form>
|
||||||
name="link"
|
</FormSection>
|
||||||
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>
|
|
||||||
|
|
||||||
|
<FormSection title="Internal Notes">
|
||||||
{
|
{
|
||||||
user.internalNotes.length === 0 ? (
|
user.internalNotes.length === 0 ? (
|
||||||
<p class="text-day-300 text-center">No internal notes yet.</p>
|
<p class="text-day-300 text-center">No internal notes yet.</p>
|
||||||
@@ -294,7 +293,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action={actions.admin.user.internalNotes.delete}
|
action={actions.admin.user.internalNotes.delete}
|
||||||
class="contents"
|
class="space-y-2"
|
||||||
data-astro-reload
|
data-astro-reload
|
||||||
>
|
>
|
||||||
<input type="hidden" name="noteId" value={note.id} />
|
<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 />
|
<InputSubmitButton label="Add" icon="ri:add-line" hideCancel />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</FormSection>
|
||||||
|
|
||||||
<section class="space-y-2">
|
|
||||||
<h2 class="font-title text-center text-3xl leading-none font-bold">Service Affiliations</h2>
|
|
||||||
|
|
||||||
|
<FormSection title="Service Affiliations">
|
||||||
{
|
{
|
||||||
user.serviceAffiliations.length === 0 ? (
|
user.serviceAffiliations.length === 0 ? (
|
||||||
<p class="text-day-200 text-center">No service affiliations yet.</p>
|
<p class="text-day-200 text-center">No service affiliations yet.</p>
|
||||||
@@ -380,7 +377,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
method="POST"
|
method="POST"
|
||||||
action={actions.admin.user.serviceAffiliations.remove}
|
action={actions.admin.user.serviceAffiliations.remove}
|
||||||
data-astro-reload
|
data-astro-reload
|
||||||
class="contents"
|
class="space-y-2"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={affiliation.id} />
|
<input type="hidden" name="id" value={affiliation.id} />
|
||||||
<button type="submit" class="text-day-300 transition-colors hover:text-red-400">
|
<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 />
|
<InputSubmitButton label="Add Affiliation" icon="ri:link" hideCancel />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</FormSection>
|
||||||
|
|
||||||
<form method="POST" action={actions.admin.user.karmaTransactions.add} data-astro-reload class="space-y-2">
|
<FormSection title="Grant/Remove Karma">
|
||||||
<h2 class="font-title text-center text-3xl leading-none font-bold">Grant/Remove Karma</h2>
|
<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
|
<InputTextArea
|
||||||
label="Points"
|
label="Description"
|
||||||
name="points"
|
name="description"
|
||||||
error={addKarmaTransactionResult?.error?.message}
|
error={addKarmaTransactionResult?.error?.message}
|
||||||
inputProps={{ type: 'number', required: true }}
|
inputProps={{ required: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputTextArea
|
<InputSubmitButton label="Submit" icon="ri:add-line" hideCancel />
|
||||||
label="Description"
|
</form>
|
||||||
name="description"
|
</FormSection>
|
||||||
error={addKarmaTransactionResult?.error?.message}
|
|
||||||
inputProps={{ required: true }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputSubmitButton label="Submit" icon="ri:add-line" hideCancel />
|
|
||||||
</form>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user