diff --git a/web/.env.example b/web/.env.example index a64bebe..015618d 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,6 +1,9 @@ DATABASE_URL="postgresql://kycnot:kycnot@localhost:3399/kycnot?schema=public" REDIS_URL="redis://localhost:6379" SOURCE_CODE_URL="https://github.com" +DATABASE_UI_URL="http://localhost:5555" SITE_URL="https://localhost:4321" ONION_ADDRESS="http://kycnotmezdiftahfmc34pqbpicxlnx3jbf5p7jypge7gdvduu7i6qjqd.onion" I2P_ADDRESS="http://nti3rj4j4disjcm2kvp4eno7otcejbbxv3ggxwr5tpfk4jucah7q.b32.i2p" +RELEASE_NUMBER=123 +RELEASE_DATE="2025-05-23T19:00:00.000Z" diff --git a/web/astro.config.mjs b/web/astro.config.mjs index d7f293a..afa4e31 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -170,6 +170,25 @@ export default defineConfig({ url: true, 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, + }), }, }, }) diff --git a/web/src/components/Button.astro b/web/src/components/Button.astro index 74af12e..f3ad4d1 100644 --- a/web/src/components/Button.astro +++ b/web/src/components/Button.astro @@ -58,27 +58,17 @@ const button = tv({ }, }, color: { - 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: { - base: 'border-day-300 bg-day-100 hover:bg-day-200 text-black focus-visible:ring-green-500', - }, - gray: { - base: 'border-day-500 bg-day-400 hover:bg-day-500 text-black focus-visible:ring-white', - }, - success: { - base: 'border-green-600 bg-green-500 text-black hover:bg-green-600', - }, - 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', - }, + black: '', + white: '', + gray: '', + success: '', + danger: '', + warning: '', + info: '', + }, + variant: { + solid: '', + faded: '', }, shadow: { true: { @@ -92,6 +82,107 @@ const button = tv({ }, }, 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', shadow: true, @@ -113,7 +204,7 @@ const button = tv({ class: 'shadow-green-500/30', }, { - color: 'error', + color: 'danger', shadow: true, class: 'shadow-red-500/30', }, @@ -127,6 +218,7 @@ const button = tv({ shadow: true, class: 'shadow-blue-500/30', }, + // Icon only variants { iconOnly: true, size: 'sm', @@ -146,6 +238,7 @@ const button = tv({ defaultVariants: { size: 'md', color: 'black', + variant: 'solid', shadow: false, disabled: false, iconOnly: false, @@ -159,6 +252,7 @@ const { endIcon, size, color, + variant, shadow, class: className, classNames, @@ -174,7 +268,7 @@ const { icon: iconSlot, label: labelSlot, 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 --- diff --git a/web/src/components/FormSection.astro b/web/src/components/FormSection.astro new file mode 100644 index 0000000..b369ad5 --- /dev/null +++ b/web/src/components/FormSection.astro @@ -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 +--- + +
+ {title} + {subtitle &&

{subtitle}

} + + +
diff --git a/web/src/components/FormSubSection.astro b/web/src/components/FormSubSection.astro new file mode 100644 index 0000000..2f7a9c0 --- /dev/null +++ b/web/src/components/FormSubSection.astro @@ -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 +--- + +
+ {title} + {subtitle &&

{subtitle}

} + + +
diff --git a/web/src/components/InputCheckboxGroup.astro b/web/src/components/InputCheckboxGroup.astro index 9a860de..a39d262 100644 --- a/web/src/components/InputCheckboxGroup.astro +++ b/web/src/components/InputCheckboxGroup.astro @@ -12,13 +12,15 @@ type Props = Omit, 'children' | 'inputId'> & options: { label: string value: string - icon?: string + icon?: string[] | string + iconClassName?: string[] | string }[] disabled?: boolean 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 hasError = !!wrapperProps.error && wrapperProps.error.length > 0 @@ -26,23 +28,38 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
-
+
{ - options.map((option) => ( - - )) + options.map((option) => { + const icons = option.icon ? (Array.isArray(option.icon) ? option.icon : [option.icon]) : [] + const iconClassName = option.iconClassName + ? Array.isArray(option.iconClassName) + ? option.iconClassName + : Array.from({ length: icons.length }, () => option.iconClassName) + : [] + return ( + + ) + }) }
diff --git a/web/src/components/InputTextArea.astro b/web/src/components/InputTextArea.astro index ade06ed..451feb3 100644 --- a/web/src/components/InputTextArea.astro +++ b/web/src/components/InputTextArea.astro @@ -10,7 +10,7 @@ import type { ComponentProps, HTMLAttributes } from 'astro/types' type Props = Omit, 'children' | 'inputId' | 'required'> & { inputProps?: Omit, 'name'> - value?: string + value?: string | null | undefined } const { inputProps, value, ...wrapperProps } = Astro.props diff --git a/web/src/constants/eventTypes.ts b/web/src/constants/eventTypes.ts index 7a5350d..f7af342 100644 --- a/web/src/constants/eventTypes.ts +++ b/web/src/constants/eventTypes.ts @@ -1,7 +1,9 @@ import { makeHelpersForOptions } from '../lib/makeHelpersForOptions' import { transformCase } from '../lib/strings' +import type BadgeSmall from '../components/BadgeSmall.astro' import type { EventType } from '@prisma/client' +import type { ComponentProps } from 'astro/types' type EventTypeInfo = { id: T @@ -12,6 +14,7 @@ type EventTypeInfo = { dot: string } icon: string + color: ComponentProps['color'] } export const { @@ -32,6 +35,7 @@ export const { dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50', }, icon: 'ri:question-fill', + color: 'gray', }), [ { @@ -43,6 +47,7 @@ export const { dot: 'bg-amber-900 text-amber-300 ring-amber-900/50', }, icon: 'ri:error-warning-fill', + color: 'yellow', }, { id: 'WARNING_SOLVED', @@ -53,6 +58,7 @@ export const { dot: 'bg-green-900 text-green-300 ring-green-900/50', }, icon: 'ri:check-fill', + color: 'green', }, { id: 'ALERT', @@ -63,6 +69,7 @@ export const { dot: 'bg-red-900 text-red-300 ring-red-900/50', }, icon: 'ri:alert-fill', + color: 'red', }, { id: 'ALERT_SOLVED', @@ -73,6 +80,7 @@ export const { dot: 'bg-green-900 text-green-300 ring-green-900/50', }, icon: 'ri:check-fill', + color: 'green', }, { id: 'INFO', @@ -83,6 +91,7 @@ export const { dot: 'bg-blue-900 text-blue-300 ring-blue-900/50', }, icon: 'ri:information-fill', + color: 'sky', }, { id: 'NORMAL', @@ -93,6 +102,7 @@ export const { dot: 'bg-zinc-700 text-zinc-300 ring-zinc-700/50', }, icon: 'ri:notification-fill', + color: 'green', }, { id: 'UPDATE', @@ -103,6 +113,7 @@ export const { dot: 'bg-sky-900 text-sky-300 ring-sky-900/50', }, icon: 'ri:pencil-fill', + color: 'sky', }, ] as const satisfies EventTypeInfo[] ) diff --git a/web/src/constants/verificationStepStatus.ts b/web/src/constants/verificationStepStatus.ts new file mode 100644 index 0000000..f7434a2 --- /dev/null +++ b/web/src/constants/verificationStepStatus.ts @@ -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 = { + value: T + label: string + icon: string + color: ComponentProps['color'] +} + +export const { + dataArray: verificationStepStatuses, + dataObject: verificationStepStatusesByValue, + getFn: getVerificationStepStatusInfo, +} = makeHelpersForOptions( + 'value', + (value): VerificationStepStatusInfo => ({ + 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[] +) diff --git a/web/src/layouts/MiniLayout.astro b/web/src/layouts/MiniLayout.astro index 5cf11b0..646ac18 100644 --- a/web/src/layouts/MiniLayout.astro +++ b/web/src/layouts/MiniLayout.astro @@ -8,7 +8,7 @@ import BaseLayout from './BaseLayout.astro' import type { ComponentProps } from 'astro/types' type Props = Omit, 'widthClassName'> & { - layoutHeader: { icon: string; title: string; subtitle: string } + layoutHeader: { icon: string; title: string; subtitle?: string } } const { layoutHeader, ...baseLayoutProps } = Astro.props @@ -28,10 +28,19 @@ const { layoutHeader, ...baseLayoutProps } = Astro.props
-

+

{layoutHeader.title}

-

{layoutHeader.subtitle}

+ { + !!layoutHeader.subtitle && ( +

{layoutHeader.subtitle}

+ ) + } diff --git a/web/src/pages/admin/index.astro b/web/src/pages/admin/index.astro index 151a152..e7cebeb 100644 --- a/web/src/pages/admin/index.astro +++ b/web/src/pages/admin/index.astro @@ -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['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', + }, }, ] --- -

+

Admin Dashboard

-
- { - adminLinks.map((link) => ( - - - - {link.title} - - - )) - } -
+
diff --git a/web/src/pages/admin/releases.astro b/web/src/pages/admin/releases.astro new file mode 100644 index 0000000..9c25637 --- /dev/null +++ b/web/src/pages/admin/releases.astro @@ -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 +--- + + +

+ {RELEASE_NUMBER ? `#${RELEASE_NUMBER}` : '???'} +

+ + { + !!releaseDate && ( +

+ () +

+ ) + } +
diff --git a/web/src/pages/admin/services/[slug]/edit.astro b/web/src/pages/admin/services/[slug]/edit.astro index cd25269..521254b 100644 --- a/web/src/pages/admin/services/[slug]/edit.astro +++ b/web/src/pages/admin/services/[slug]/edit.astro @@ -1,22 +1,35 @@ --- -import { EventType, VerificationStepStatus } from '@prisma/client' import { Icon } from 'astro-icon/components' import { actions, isInputError } from 'astro:actions' +import { orderBy } from 'lodash-es' +import BadgeSmall from '../../../../components/BadgeSmall.astro' +import Button from '../../../../components/Button.astro' +import FormSection from '../../../../components/FormSection.astro' +import FormSubSection from '../../../../components/FormSubSection.astro' import InputCardGroup from '../../../../components/InputCardGroup.astro' import InputCheckboxGroup from '../../../../components/InputCheckboxGroup.astro' import InputImageFile from '../../../../components/InputImageFile.astro' +import InputSelect from '../../../../components/InputSelect.astro' import InputSubmitButton from '../../../../components/InputSubmitButton.astro' import InputText from '../../../../components/InputText.astro' import InputTextArea from '../../../../components/InputTextArea.astro' +import MyPicture from '../../../../components/MyPicture.astro' +import ServiceCard from '../../../../components/ServiceCard.astro' +import TimeFormatted from '../../../../components/TimeFormatted.astro' +import Tooltip from '../../../../components/Tooltip.astro' import UserBadge from '../../../../components/UserBadge.astro' +import { getAttributeCategoryInfo } from '../../../../constants/attributeCategories' +import { getAttributeTypeInfo } from '../../../../constants/attributeTypes' import { formatContactMethod } from '../../../../constants/contactMethods' import { currencies } from '../../../../constants/currencies' +import { eventTypes, getEventTypeInfo } from '../../../../constants/eventTypes' import { kycLevels } from '../../../../constants/kycLevels' import { serviceVisibilities } from '../../../../constants/serviceVisibility' import { verificationStatuses } from '../../../../constants/verificationStatus' +import { getVerificationStepStatusInfo } from '../../../../constants/verificationStepStatus' import BaseLayout from '../../../../layouts/BaseLayout.astro' -import { cn } from '../../../../lib/cn' +import { pluralize } from '../../../../lib/pluralize' import { prisma } from '../../../../lib/prisma' const { slug } = Astro.params @@ -55,136 +68,136 @@ const verificationStepUpdateInputErrors = isInputError(verificationStepUpdateRes if (!slug) return Astro.rewrite('/404') -const service = await Astro.locals.banners.try('Error fetching service', () => - prisma.service.findUnique({ - where: { slug }, - include: { - attributes: { - select: { - attribute: { +const [service, categories, attributes] = await Astro.locals.banners.tryMany([ + [ + 'Error fetching service', + () => + prisma.service.findUnique({ + where: { slug }, + include: { + attributes: { + select: { + attribute: { + select: { + id: true, + }, + }, + }, + }, + categories: { select: { id: true, - }, - }, - }, - }, - categories: { - select: { - id: true, - }, - }, - events: { - orderBy: { - startedAt: 'desc', - }, - }, - verificationRequests: { - select: { - id: true, - user: { - select: { name: true, - displayName: true, - picture: true, + icon: true, + }, + }, + events: { + orderBy: { + startedAt: 'desc', + }, + }, + verificationRequests: { + select: { + id: true, + user: { + select: { + name: true, + displayName: true, + picture: true, + }, + }, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, + }, + verificationSteps: { + orderBy: { + createdAt: 'desc', + }, + }, + contactMethods: { + orderBy: { + label: 'asc', + }, + }, + _count: { + select: { + verificationRequests: true, }, }, - createdAt: true, }, - orderBy: { - createdAt: 'desc', - }, - }, - verificationSteps: { - orderBy: { - createdAt: 'desc', - }, - }, - contactMethods: { - orderBy: { - label: 'asc', - }, - }, - _count: { - select: { - verificationRequests: true, - }, - }, - }, - }) -) + }), + ], + [ + 'Error fetching categories', + () => + prisma.category.findMany({ + orderBy: { name: 'asc' }, + }), + [] as [], + ], + [ + 'Error fetching attributes', + () => + prisma.attribute.findMany({ + orderBy: { category: 'asc' }, + }), + [] as [], + ], +]) if (!service) return Astro.rewrite('/404') - -const categories = await Astro.locals.banners.try( - 'Error fetching categories', - () => - prisma.category.findMany({ - orderBy: { name: 'asc' }, - }), - [] -) - -const attributes = await Astro.locals.banners.try( - 'Error fetching attributes', - () => - prisma.attribute.findMany({ - orderBy: { category: 'asc' }, - }), - [] -) - -// Button style constants for admin sections (Events, etc.) -const buttonPrimaryClasses = - 'inline-flex items-center justify-center rounded-md border border-transparent bg-sky-600 px-3 py-1.5 text-sm font-medium text-white shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-zinc-900' - -const buttonSmallBaseClasses = 'rounded-md px-2 py-1 text-xs font-medium' -const buttonSmallPrimaryClasses = cn( - buttonSmallBaseClasses, - 'text-sky-400 hover:bg-sky-700/30 hover:text-sky-300' -) -const buttonSmallSecondaryClasses = cn( - buttonSmallBaseClasses, - 'text-zinc-400 hover:bg-zinc-700/50 hover:text-zinc-300' -) -const buttonSmallDestructiveClasses = cn( - buttonSmallBaseClasses, - 'text-red-400 hover:bg-red-700/30 hover:text-red-300' -) -const buttonSmallWarningClasses = cn( - buttonSmallBaseClasses, - 'text-yellow-400 hover:bg-yellow-700/30 hover:text-yellow-300' -) - -// Legacy classes for existing admin forms (Events, Verification Steps, Contact Methods) -const inputBaseClasses = - 'w-full rounded-md border-zinc-600 bg-zinc-700/80 p-2 text-zinc-200 placeholder-zinc-400 focus:border-sky-500 focus:ring-1 focus:ring-sky-500 text-sm' -const labelBaseClasses = 'block text-sm font-medium text-zinc-300 mb-1' -const errorTextClasses = 'mt-1 text-xs text-red-400' --- -
-
-

- Editing {service.name} [{service.id}] -

- - - View Service Page - +
+
+
+ { + !!service.imageUrl && ( + + ) + } +

+ {service.name} +

+ +
+
+
+
-
-

- Service Details -

+
@@ -210,7 +223,8 @@ const errorTextClasses = 'mt-1 text-xs text-red-400' /> -
+
-
- -
+ -
+ + +
({ label: category.name, @@ -286,77 +313,82 @@ const errorTextClasses = 'mt-1 text-xs text-red-400' selectedValues={service.categories.map((c) => c.id.toString())} error={serviceInputErrors.categories} /> -
-
- ({ - label: `${kycLevel.name} (${kycLevel.value}/4)`, - value: kycLevel.id.toString(), - icon: kycLevel.icon, - description: kycLevel.description, - }))} - selectedValue={service.kycLevel.toString()} - iconSize="md" - cardSize="md" - error={serviceInputErrors.kycLevel} - class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" - /> -
- -
({ - label: `${attribute.title} (${attribute.category})`, - value: attribute.id.toString(), - }))} + size="lg" + options={orderBy( + attributes.map((attribute) => ({ + ...attribute, + categoryInfo: getAttributeCategoryInfo(attribute.category), + typeInfo: getAttributeTypeInfo(attribute.type), + })), + ['categoryInfo.order', 'typeInfo.order'] + ).map((attribute) => { + return { + label: attribute.title, + value: attribute.id.toString(), + icon: [attribute.categoryInfo.icon, attribute.typeInfo.icon], + iconClassName: [attribute.categoryInfo.classNames.icon, attribute.typeInfo.classNames.icon], + } + })} selectedValues={service.attributes.map((a) => a.attribute.id.toString())} error={serviceInputErrors.attributes} />
-
- ({ - label: status.label, - value: status.value, - icon: status.icon, - iconClass: status.classNames.icon, - description: status.description, - }))} - selectedValue={service.verificationStatus} - error={serviceInputErrors.verificationStatus} - cardSize="md" - iconSize="md" - class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" - /> -
+ ({ + label: `${kycLevel.name} (${kycLevel.value}/4)`, + value: kycLevel.id.toString(), + icon: kycLevel.icon, + description: kycLevel.description, + }))} + selectedValue={service.kycLevel.toString()} + iconSize="md" + cardSize="md" + error={serviceInputErrors.kycLevel} + class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" + /> -
- ({ - label: currency.name, - value: currency.id, - icon: currency.icon, - }))} - selectedValue={service.acceptedCurrencies} - error={serviceInputErrors.acceptedCurrencies} - required - multiple - /> -
+ ({ + label: status.label, + value: status.value, + icon: status.icon, + iconClass: status.classNames.icon, + description: status.description, + }))} + selectedValue={service.verificationStatus} + error={serviceInputErrors.verificationStatus} + cardSize="sm" + iconSize="sm" + class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" + /> -
+ ({ + label: currency.name, + value: currency.id, + icon: currency.icon, + }))} + selectedValue={service.acceptedCurrencies} + error={serviceInputErrors.acceptedCurrencies} + required + multiple + /> + +
-
-
-
- -
+ ({ + label: visibility.label, + value: visibility.value, + icon: visibility.icon, + iconClass: visibility.iconClass, + description: visibility.description, + }))} + selectedValue={service.serviceVisibility} + error={serviceInputErrors.serviceVisibility} + cardSize="sm" + /> -
- ({ - label: visibility.label, - value: visibility.value, - icon: visibility.icon, - iconClass: visibility.iconClass, - description: visibility.description, - }))} - selectedValue={service.serviceVisibility} - error={serviceInputErrors.serviceVisibility} - cardSize="sm" - /> -
- - + -
+ - -
-

- Events -

-
- + + { - service.events.length > 0 && ( -
-

Existing Events

- {service.events.map((event) => ( -
-
-
-
- {event.title} - - {event.type} - - {!event.visible && ( - - Hidden - - )} -
-

{event.content}

-
- Started: {new Date(event.startedAt).toLocaleDateString()} - - Ended: - {event.endedAt - ? event.endedAt === event.startedAt - ? '1-time event' - : new Date(event.endedAt).toLocaleDateString() - : 'Ongoing'} - - {event.source && Source: {event.source}} -
-
-
-
- - -
- -
- - -
-
-
- {/* Edit Event Form - Hidden by default */} -