This commit is contained in:
Nadar
2026-03-17 13:24:22 +03:00
commit 82e5ac9d81
554 changed files with 29637 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<template>
<form class="twofa" @submit="onSubmit">
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
href="/projects"
>
{{ $t('to_main') }}
</UiButton>
<h1 class="mb-8">
Проверка безопасности
</h1>
<UiAlert class="mb-16">
Введите 6-значный код подтверждения, отправленный на jo**@gmail.com
</UiAlert>
<UiCodeInput id="code">
<template #title>
<span class="twofa__title"> Код из письма </span>
</template>
</UiCodeInput>
<div class="text-align-right mt-8">
<UiButton v-if="!showTimer" type="link" @click="resetTimer">
Отправить код еще раз
</UiButton>
<div v-else class="twofa__timer">
<span>Отправить код повторно через 0:{{ seconds }}</span>
</div>
</div>
<UiButton class="twofa__submit" size="large" native-type="submit">
{{ $t('next') }}
</UiButton>
</form>
</template>
<script setup>
import { useForm } from 'vee-validate'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const seconds = ref(30)
const showTimer = ref(false)
const { handleSubmit, isSubmitting } = useForm()
const onSubmit = handleSubmit(async (values) => {
console.log(values)
navigateTo('/2fa/setup')
})
function resetTimer() {
showTimer.value = true
const timer = setInterval(() => {
if (seconds.value === 0) {
clearInterval(timer)
showTimer.value = false
seconds.value = 30
}
else {
seconds.value--
}
}, 1000)
}
</script>
<style lang="scss">
.twofa {
margin: 0 auto;
width: 500px;
&__title {
@include txt-m-b;
}
&__submit {
margin-top: 32px;
width: 100%;
}
&__timer {
@include txt-i;
color: $clr-grey-500;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<form
class="twofa-setup"
@submit="onSubmit"
>
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
:href="step === 0 ? '/projects' : ''"
@click="step--"
>
{{ step === 0 ? $t('to_main') : $t('back') }}
</UiButton>
<h1 class="twofa-setup__title">
Настройка двухфакторной аутентификации (2FA)
</h1>
<Stepper
class="mb-40"
:step="step"
:items="['Установка', 'Настройка', 'Ввод кода']"
/>
<Transition name="fade" mode="out-in">
<div v-if="step === 0">
<UiAlert class="mb-47">
Установите приложение Google Authenticator или другое из списка, чтобы
получить коды двухфакторной аутентификации.
</UiAlert>
<div class="twofa-setup__app-block">
<p class="mb-8">
Google Authenticator app
</p>
<div class="mb-24">
<UiButton
color="secondary"
type="outlined"
class="mr-24"
>
<img
class="d-flex"
src="/GetOnAppStore.svg"
alt="GetOnAppStore"
>
</UiButton>
<UiButton
color="secondary"
type="outlined"
>
<img
class="d-flex"
src="/GetOnGooglePlay.svg"
alt="GetOnGooglePlay"
>
</UiButton>
</div>
<div>
<span>
Посмотреть весь список 2FA приложений -
<UiButton
type="link"
href="https://www.pcmag.com/picks/the-best-authenticator-apps"
target="_blank"
>
подробнее
</UiButton>
</span>
</div>
</div>
<UiButton
class="twofa-setup__submit mt-47"
size="large"
@click="step++"
>
{{ $t('next') }}
</UiButton>
</div>
<div v-else-if="step === 1">
<UiAlert class="mb-24">
В приложении
<strong>двухфакторной аутентификации</strong> отсканируйте QR-код или
скопируйте и вставьте <strong>ключ настройки</strong> с этого экрана
вручную.
</UiAlert>
<div class="twofa-setup__qrzone">
<img src="/QRCode.svg" alt="QRCode" class="mr-40">
<UiInput
id="ssa"
model-value="GB5AY4JWLUUF2NRT"
readonly
copyable
label="Ключ настройки"
class="flex-1"
/>
</div>
<UiButton
class="twofa-setup__submit mt-32"
size="large"
@click="step++"
>
{{ $t('next') }}
</UiButton>
</div>
<div v-else-if="step === 2">
<UiAlert class="mb-50">
Далее введите на этом экране <strong>шестизначный код</strong> из
приложения двухфакторной аутентификации
</UiAlert>
<UiCodeInput
id="pincode"
class="mb-21"
>
<template #title>
<span class="twofa-setup__pin-title"> 2FA-код </span>
</template>
</UiCodeInput>
<UiButton
class="twofa-setup__submit mt-32"
size="large"
native-type="submit"
>
{{ $t('next') }}
</UiButton>
</div>
</Transition>
</form>
</template>
<script setup>
import { useForm } from 'vee-validate'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const { handleSubmit, isSubmitting } = useForm()
const step = ref(0)
const onSubmit = handleSubmit(async (values) => {
console.log(values)
navigateTo('/projects')
})
</script>
<style lang="scss">
.twofa-setup {
margin: 0 auto;
width: 500px;
&__qrzone {
display: flex;
align-items: center;
justify-content: space-between;
}
&__app-block {
}
&__title {
margin-bottom: 56px;
}
&__pin-title {
@include txt-m-b;
}
&__submit {
//margin-top: 47px;
width: 100%;
}
}
</style>

833
apps/client/pages/_.vue Normal file
View File

@@ -0,0 +1,833 @@
<template>
<div
ref="root"
class="root"
>
<!-- UiButton -->
<div class="doc-block">
<strong>UiButton</strong>
<h1>Кнопки и ссылки</h1>
<div style="display: flex; flex-direction: column; gap: 16px">
<template
v-for="type in ['filled', 'outlined', 'ghost', 'link']"
:key="type"
>
<div
v-for="color in ['primary', 'secondary']"
:key="color"
class="buttons-rows"
>
<div
v-for="size in ['large', 'medium', 'small']"
:key="size"
class="buttons-grid"
>
<template
v-for="(props, index) in [
{
bind: { type, size, color },
icon: size === 'large' ? 'plus' : 's-plus',
chevron:
size === 'large' ? 'chevron-down' : 's-chevron-down',
},
]"
:key="index"
>
<UiButton v-bind="props.bind">
Button
</UiButton>
<UiButton
v-bind="props.bind"
:icon="props.icon"
>
Button
</UiButton>
<UiButton
v-bind="props.bind"
:right-icon="props.icon"
>
Button
</UiButton>
<UiButton
v-bind="props.bind"
:left-icon="props.icon"
:right-icon="props.chevron"
>
Button
</UiButton>
<UiButton
v-bind="props.bind"
loading
>
Button
</UiButton>
<UiButton
v-bind="props.bind"
:icon="props.icon"
/>
<UiButton
v-bind="props.bind"
:left-icon="props.icon"
:right-icon="props.chevron"
/>
<UiButton
v-bind="props.bind"
:icon="props.icon"
loading
/>
</template>
</div>
</div>
</template>
</div>
</div>
<!-- UiSelect -->
<div class="doc-block">
<strong>UiSelect</strong>
<h1>Select</h1>
<div class="two-columns">
<h4 class="group-heading">
Default
</h4>
<h4 class="group-heading">
Multiple
</h4>
<div
v-for="(multiple, index) in [false, true]"
:key="index"
class="bg-clr-grey-100 p-8"
style="display: inline-flex; gap: 8px; justify-self: center"
>
<UiSelect
:id="!multiple ? 'select' : 'select2'"
label="Select"
:options="selectOptions"
:multiple="multiple"
/>
<UiSelect
:id="!multiple ? 'select' : 'select2'"
label="Searchable"
:options="selectOptions"
searchable
:multiple="multiple"
/>
<UiSelect
:id="!multiple ? 'select' : 'select2'"
label="Clearable"
:options="selectOptions"
clearable
:multiple="multiple"
/>
</div>
</div>
</div>
<!-- UiDropdown -->
<div class="doc-block half">
<strong>UiDropdown</strong>
<h1>Dropdown \ Выпадающий список</h1>
<UiDropdown>
<UiButton
right-icon="s-chevron-down"
size="small"
>
Trigger
</UiButton>
<template #dropdown>
<UiDropdownItem
v-for="i in 5"
:key="i"
>
Item {{ i }}
</UiDropdownItem>
</template>
</UiDropdown>
</div>
<!-- UiCalendar -->
<div class="doc-block half">
<strong>UiCalendar</strong>
<h1>Calendar</h1>
<div class="d-inline-block bg-clr-grey-100 p-8">
<UiCalendar style="border-radius: 12px" />
</div>
</div>
<!-- UiInput -->
<div class="doc-block half">
<strong>UiInput</strong>
<h1>Поля ввода</h1>
<div class="two-columns">
<!-- <template v-for="state in []"></template> -->
<h4 class="group-heading">
Normal
</h4>
<h4 class="group-heading">
Disabled
</h4>
<UiInput
id="input1"
label="Basic"
/>
<UiInput
id="input1"
label="Basic"
disabled
/>
<UiInput
id="input2"
label="With caption"
caption="Caption"
/>
<UiInput
id="input2"
label="With caption"
caption="Caption"
disabled
/>
<UiInput
id="input3"
label="Clearable"
clearable
model-value="Value"
/>
<UiInput
id="input3"
label="Clearable"
clearable
disabled
/>
<UiInput
id="input4"
label="Copyable"
copyable
model-value="Value"
/>
<UiInput
id="input4"
label="Copyable"
copyable
disabled
/>
<UiInput
id="input5"
label="Password"
native-type="password"
model-value="superPassword"
/>
<UiInput
id="input5"
label="Password"
native-type="password"
disabled
/>
</div>
</div>
<!-- UiSearch -->
<div class="doc-block half">
<strong>UiSearch</strong>
<h1>Поиск</h1>
<div class="two-columns">
<h4 class="group-heading">
Normal
</h4>
<h4 class="group-heading">
Disabled
</h4>
<UiSearch
v-model="searchTerm"
size="large"
label="Large"
/>
<UiSearch
v-model="searchTerm"
size="large"
label="Large"
disabled
/>
<UiSearch
v-model="searchTerm"
size="medium"
label="Medium"
/>
<UiSearch
v-model="searchTerm"
size="medium"
label="Medium"
disabled
/>
<UiSearch
v-model="searchTerm"
size="small"
label="Small"
/>
<UiSearch
v-model="searchTerm"
size="small"
label="Small"
disabled
/>
</div>
</div>
<!-- UiSwitch -->
<div class="doc-block third">
<strong>UiSwitch</strong>
<h1>Switch</h1>
<div class="two-columns justify-items-center">
<h4 class="group-heading">
Normal
</h4>
<h4 class="group-heading">
Disabled
</h4>
<UiSwitch id="switch" />
<UiSwitch
id="switch"
disabled
/>
<UiSwitch
id="switch2"
:loading="switchLoading"
:before-change="beforeChange"
/>
<UiSwitch
id="switch2"
:loading="switchLoading"
disabled
/>
</div>
</div>
<!-- UiCheckbox -->
<div class="doc-block third">
<strong>UiCheckbox</strong>
<h1>Checkbox</h1>
<div class="two-columns">
<h4 class="group-heading">
Normal
</h4>
<h4 class="group-heading">
Disabled
</h4>
<UiCheckbox
id="checkbox"
label="Label"
true-value="checkbox1"
/>
<UiCheckbox
id="checkbox"
label="Label"
true-value="checkbox1"
disabled
/>
<UiCheckbox
id="checkbox"
true-value="checkbox2"
/>
<UiCheckbox
id="checkbox"
true-value="checkbox2"
disabled
/>
</div>
</div>
<!-- UiRadio -->
<div class="doc-block third">
<strong>UiRadio</strong>
<h1>Radio</h1>
<div class="two-columns">
<h4 class="group-heading">
Normal
</h4>
<h4 class="group-heading">
Disabled
</h4>
<UiRadio
id="radio"
label="Label"
value="radio1"
/>
<UiRadio
id="radio"
label="Label"
value="radio1"
disabled
/>
<UiRadio
id="radio"
value="radio2"
/>
<UiRadio
id="radio"
value="radio2"
disabled
/>
</div>
</div>
<!-- UiAccordion -->
<div class="doc-block">
<strong>UiAccordion</strong>
<h1>Спойлер \ Аккордеон</h1>
<div style="width: 600px; margin: 0 auto">
<UiAccordion>
<UiAccordionItem title="Title">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad
commodi, culpa delectus deleniti doloribus ducimus eaque eos eveniet
expedita harum, in incidunt itaque magnam, nulla quam ratione
recusandae reprehenderit veniam?
</UiAccordionItem>
<UiAccordionItem title="Title 2">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
minima molestias obcaecati odio omnis ratione recusandae,
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
Aut consectetur cum dolores est exercitationem facilis fugiat
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
<br>
<br>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
minima molestias obcaecati odio omnis ratione recusandae,
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
Aut consectetur cum dolores est exercitationem facilis fugiat
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
</UiAccordionItem>
<UiAccordionItem title="Title 3">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
minima molestias obcaecati odio omnis ratione recusandae,
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
Aut consectetur cum dolores est exercitationem facilis fugiat
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
<br>
<br>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
minima molestias obcaecati odio omnis ratione recusandae,
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
Aut consectetur cum dolores est exercitationem facilis fugiat
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
</UiAccordionItem>
</UiAccordion>
</div>
</div>
<!-- UiAlert -->
<div class="doc-block">
<strong>UiAlert</strong>
<h1>Алерты</h1>
<div
class="three-columns"
style="justify-items: center"
>
<div
class="w-100"
style="max-width: 400px"
>
<UiAlert
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
class="my-8"
:type="type"
text="Текст сообщения"
/>
</div>
<div
class="w-100"
style="max-width: 400px"
>
<UiAlert
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
class="my-8"
:type="type"
title="Заголовок"
text="Текст сообщения"
/>
</div>
<div
class="w-100"
style="max-width: 400px"
>
<UiAlert
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
:type="type"
class="my-8"
title="Заголовок"
text="Текст сообщения"
>
<template #action>
<UiButton type="link">
Действие
</UiButton>
</template>
</UiAlert>
</div>
</div>
</div>
<!-- UiBadge -->
<div class="doc-block">
<strong>UiBadge</strong>
<h1>Бейджы</h1>
<div
class="three-columns"
style="justify-items: center"
>
<div>
<UiBadge
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
class="my-8"
:type="type"
text="Текст бейджа"
/>
</div>
<div>
<UiBadge
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
class="my-8"
:type="type"
text="Текст бейджа"
:icon="randomIcon()"
/>
</div>
<div>
<UiBadge
v-for="type in [
'neutral',
'positive',
'warning',
'negative',
'marketing',
]"
:key="type"
:type="type"
class="my-8"
text="Текст бейджа"
>
<template #prefix>
<i :class="`icon-${randomIcon()}`" />
</template>
<template #suffix>
<i :class="`icon-${randomIcon()}`" />
</template>
</UiBadge>
</div>
</div>
</div>
<!-- Notification -->
<div class="doc-block">
<strong>useNotify \ $notify</strong>
<h1>Уведомления</h1>
<div
style="
display: grid;
grid-template-columns: repeat(2, 150px);
justify-content: center;
gap: 8px;
"
>
<UiButton @click="randomNotification('top-left')">
Top left
</UiButton>
<UiButton @click="randomNotification('top-right')">
Top Right
</UiButton>
<UiButton @click="randomNotification('bottom-left')">
Bottom left
</UiButton>
<UiButton @click="randomNotification('bottom-right')">
Bottom Right
</UiButton>
</div>
</div>
<!-- CopyButton -->
<div class="doc-block">
<strong>UiCopyButton</strong>
<h1>Кнопка копирования</h1>
<div
style="
display: grid;
grid-template-columns: repeat(1, 150px);
justify-content: center;
gap: 8px;
"
>
<UiCopyButton
title="Payment link"
pressed-title="Link copied"
value="Test value"
/>
</div>
</div>
<!-- Icons -->
<!-- <div -->
<!-- v-for="(font, index) in [icons, smallIcons]" -->
<!-- :key="font.metadata.name" -->
<!-- class="doc-block half" -->
<!-- > -->
<!-- <strong> -->
<!-- {{ _ === 0 ? '<i class="icon-*" />' : '<i class="icon-s-*" />' }} -->
<!-- </strong> -->
<!-- <h1>Иконки - {{ font.metadata.name }}</h1> -->
<!-- <div> -->
<!-- <div -->
<!-- v-for="(iconSet, index) in font.iconSets" -->
<!-- :key="_" -->
<!-- class="icons-grid" -->
<!-- > -->
<!-- <div -->
<!-- v-for="icon in iconSet.selection" -->
<!-- :key="icon.name" -->
<!-- class="preview-icon" -->
<!-- @click="copy(icon.name)" -->
<!-- > -->
<!-- <i :class="`${font.preferences.fontPref.prefix}${icon.name}`" /> -->
<!-- <span>{{ icon.name }}</span> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
</div>
</template>
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { ALERT_TYPES } from 'ui-layer/components/alert/types'
import type { NotificationPlacement } from 'ui-layer/components/notification/types'
import { sample } from 'lodash-es'
import icons from '#build/ui/available-icons'
definePageMeta({
layout: 'empty',
dev: true,
})
const searchTerm = ref<string>()
const switchLoading = ref(false)
useForm()
function copy(value: string) {
navigator.clipboard.writeText(value)
}
async function beforeChange() {
switchLoading.value = true
const result = await new Promise(resolve =>
setTimeout(() => resolve(true), 1000),
)
switchLoading.value = false
return result
}
const selectOptions = computed(() => {
return Array.from(Array(300).keys()).map(i => `Option ${i + 1}`)
})
let notificationTypeCounter = 0
function randomNotification(placement: NotificationPlacement) {
const notify = useNotify()
notify({
placement,
text: 'Текст',
title: 'Заголовок',
type: ALERT_TYPES[notificationTypeCounter],
})
notificationTypeCounter++
notificationTypeCounter %= ALERT_TYPES.length
}
function randomIcon() {
return sample(icons.filter(icon => icon.startsWith('s-')))
}
</script>
<style lang="scss" scoped>
.root {
padding: 30px;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 30px;
}
.doc-block {
background-color: $clr-white;
border-radius: 30px;
padding: 40px 60px;
grid-column: span 6;
&.third {
grid-column: span 2;
}
&.half {
grid-column: span 3;
}
> strong {
color: $clr-grey-600;
}
> h1 {
margin-bottom: 40px;
}
}
.separator {
width: 100%;
}
.two-columns {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.three-columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.buttons-rows {
display: flex;
flex-direction: column;
gap: 6px;
}
.buttons-grid {
display: flex;
gap: 6px;
}
.icons-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.preview-icon {
display: flex;
border-radius: 8px;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
width: 85px;
height: 65px;
text-align: center;
background-color: $clr-grey-100;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease-out;
&:hover,
&:focus {
background-color: $clr-grey-200;
}
&:active {
background-color: $clr-grey-300;
}
span {
@include txt-s-m;
}
}
.group-heading {
text-align: center;
color: $clr-grey-500;
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<div class="create-invoice">
<Transition
name="fade"
mode="out-in"
>
<PageForm
v-if="!invoiceLink"
title="Creating invoice"
:back-link="`/projects/${$route.params.projectId}/invoices`"
:back-text="$t('back_to', [project.name])"
:submit-text="$t('create')"
:handler="onSubmit"
>
<UiInput
id="amount"
label="Invoice amount"
rules="required"
:mask="{ mask: Number }"
>
<template #suffix>
<CurrencyName
code="USDT"
size="small"
/>
</template>
</UiInput>
</PageForm>
<div
v-else
class="create-invoice__summary"
>
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
:href="`/projects/${$route.params.projectId}/invoices`"
>
{{ $t('back_to', [project.name]) }}
</UiButton>
<h1 class="mb-32">
Invoice created
</h1>
<UiAlert
type="neutral"
class="mb-24"
>
<span> Creation time</span>
{{ dayjs().format('DD.MM.YY HH:mm') }}.
<span>It will expires in </span>
<strong>30 minutes.</strong>
</UiAlert>
<UiInput
id="invoice_link"
copyable
inputmode="none"
label="Ссылка на счет"
:model-value="invoiceLink"
readonly
/>
<UiButton
size="large"
class="w-100 mt-24"
:href="`/projects/${route.params.projectId}/invoices`"
>
Done
</UiButton>
</div>
</Transition>
</div>
</template>
<script setup>
import { v4 as uuidv4 } from 'uuid'
import dayjs from 'dayjs'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const route = useRoute()
const { data: project } = await useAsyncData(
'project',
() =>
$api(`/online_stores/${route.params.projectId}`, {
method: 'get',
}),
{},
)
if (!project.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
})
}
const notify = useNotify()
const runtimeConfig = useRuntimeConfig()
const invoiceLink = ref('')
async function onSubmit(values) {
try {
const result = await $api('/invoices', {
method: 'post',
body: {
...values,
currencyCode: 'USDT',
onlineStoreId: +route.params.projectId,
orderId: uuidv4(),
},
})
invoiceLink.value = `${runtimeConfig.public.payHost}/${result.invoiceId}`
notify({
type: 'positive',
text: 'Инвойс успешно создан',
})
}
catch (e) {
setStaticError({
status: e.status,
message: 'Something went wrong',
})
}
}
</script>
<style lang="scss">
.create-invoice {
&__summary {
width: 500px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,451 @@
<template>
<form
class="create-withdraw"
@submit="onSubmit"
>
<Transition
name="shift"
mode="out-in"
>
<div
v-if="!showSummary"
class="withdraw-form"
title="Withdrawal request"
>
<FormHeader
class="withdraw-form__header"
title="Withdrawal request"
:back-link="`/projects/${$route.params.projectId}`"
:back-text="$t('back_to', [project.name])"
/>
<div class="d-flex align-items-center justify-content-between mb-16">
<CurrencyName
:code="route.params.assetCode"
name="Tether"
class="create-withdraw__currency-name"
/>
<p class="create-withdraw__balance">
{{ balance }}
</p>
</div>
<div
class="p-16 bg-clr-grey-200"
style="border-radius: 16px"
>
<UiInput
id="paymentAddress"
rules="required"
label="Withdrawal wallet"
/>
<NetworkSelect
id="network"
:asset-code="route.params.assetCode"
model-value="TRC20"
class="mt-6"
/>
</div>
<div
class="mt-16 p-16 bg-clr-grey-200"
style="border-radius: 16px"
>
<UiInput
id="amount"
rules="required"
:label="`Минимальная сумма ${minAmount} ${route.params.assetCode}`"
:mask="{
mask: Number,
min: minAmount,
max: maxAmount,
scale: exponent,
}"
>
<template #suffix>
<UiButton
type="link"
size="small"
@click="setValues({ amount: maxAmount }, false)"
>
{{ $t('maximum') }}
</UiButton>
</template>
<template #caption>
<div class="create-withdraw__limit">
<span>Доступный лимит на вывод - 1000 USDT</span>
<UiButton
class="create-withdraw__upgrade"
type="link"
size="small"
href="/verification"
>
Повысить лимит
</UiButton>
</div>
</template>
</UiInput>
</div>
<div class="withdraw-form__bottom">
<div class="withdraw-amount withdraw-amount--fee">
<div class="withdraw-amount__title">
{{ $t('fee') }}
</div>
<div class="withdraw-amount__value">
{{ fee }}
</div>
</div>
<div class="withdraw-amount">
<div class="withdraw-amount__title">
К отправке
</div>
<div class="withdraw-amount__value">
{{ total }}
</div>
</div>
<UiButton
size="large"
class="create-withdraw__continue"
native-type="submit"
:disabled="balance <= 0"
>
{{ $t('continue') }}
</UiButton>
</div>
</div>
<div
v-else
class="withdraw-summary"
>
<FormHeader
class="withdraw-summary__header"
title="Confirm the withdrawal"
:back-link="`/projects/${$route.params.projectId}`"
:back-text="$t('back_to', [project.name])"
/>
<dl class="withdraw-summary__details withdraw-details">
<dt>Withdrawal wallet</dt>
<dd>
<TextShortener :text="values.paymentAddress" />
</dd>
<dt>Network</dt>
<dd class="text-clr-green-500">
<CurrencyName
:code="route.params.assetCode"
size="small"
/>
</dd>
<dt>{{ $t('fee') }}</dt>
<dd>{{ fee }}</dd>
<dt>К отправке</dt>
<dd class="withdraw-details__amount">
{{ total }}
</dd>
</dl>
<UiAlert class="withdraw-summary__alert">
Пожалуйста, подтвердите, что операцию инициировали именно вы.
</UiAlert>
<UiCodeInput
id="pincode"
class="withdraw-summary__pincode"
title="2FA-код"
/>
<div class="withdraw-summary__actions">
<UiButton
color="secondary"
size="large"
@click="showSummary = false"
>
{{ $t('back') }}
</UiButton>
<UiButton
size="large"
native-type="submit"
:loading="isSubmitting"
>
{{ $t('withdraw') }}
</UiButton>
</div>
</div>
</Transition>
</form>
</template>
<script setup>
import { v4 as uuidv4 } from 'uuid'
import { useForm } from 'vee-validate'
import { computedAsync } from '@vueuse/core'
import Decimal from 'decimal.js'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const route = useRoute()
const { data: project } = await useAsyncData(
'project',
() => {
return $api(`/online_stores/${route.params.projectId}`, {
method: 'GET',
})
},
)
if (!project.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
})
}
const notify = useNotify()
const { t } = useI18n()
const { data: tariff } = await useAsyncData('tariff', () => {
return $api(`/tariffs/current?currency=${route.params.assetCode}`)
})
const { data: account } = await useAsyncData('account', () =>
$api(`/accounts/${project.value.accountIds[0]}`, {
method: 'GET',
}))
const showSummary = ref(false)
const { isSubmitting, values, setValues, handleSubmit } = useForm({ keepValuesOnUnmount: true })
const commission = computedAsync(async () => {
if (values.amount) {
try {
const result = await $api(`/withdrawals/commission`, { method: 'GET', params: { amount: values.amount, currency: route.params.assetCode } })
return result?.commission ?? 0
}
catch {
return 0
}
}
else {
return 0
}
}, 0, { lazy: true })
const fee = computed(() => $money.fullFormat(commission.value, route.params.assetCode))
const balance = computed(() => $money.format(account.value.balance, route.params.assetCode))
const maxAmount = computed(() => Math.min(1000, account.value.balance))
const minAmount = computed(() => tariff.value?.data.minAmount)
const exponent = computed(() => $money.getExponent(route.params.assetCode))
const total = computed(() => $money.fullFormat(Decimal.sub(values.amount || 0, commission.value), route.params.assetCode))
const onSubmit = handleSubmit(async (values) => {
if (!showSummary.value) {
showSummary.value = true
return
}
if (values.pincode !== '123123') {
notify({
id: 'withdraw_error',
text: t('invalid_otp_code'),
type: 'negative',
})
return
}
try {
// const idempotencyKey = `${route.params.projectId + values.paymentAddress + route.params.assetCode}TRON${values.amount}`
await $api('/withdrawals', {
method: 'POST',
body: {
...values,
idempotencyKey: uuidv4().substring(0, 20),
amount: +values.amount,
onlineStoreId: +route.params.projectId,
currencyCode: route.params.assetCode,
blockchainCode: 'TRON',
saveWallet: false,
pincode: undefined,
network: undefined,
},
})
notify({
id: 'withdraw_success',
type: 'positive',
text: 'The withdrawal request has been successfully processed',
})
navigateTo(`/projects/${route.params.projectId}`)
}
catch {
notify({
id: 'withdraw_error',
text: t('something_went_wrong'),
type: 'negative',
})
}
})
</script>
<style lang="scss">
.create-withdraw {
&__currency-name {
@include txt-l-sb('currency-name', true);
@include txt-r-m('currency-name-code', true);
--currency-name-gap: 2px 16px;
}
&__balance {
@include txt-l-sb;
}
&__continue {
flex: 1;
}
&__limit {
@include txt-r-m;
display: flex;
gap: 8px;
align-items: center;
color: $clr-grey-500;
margin-top: 8px;
}
}
.withdraw-form,
.withdraw-summary {
width: 500px;
margin: 0 auto;
&__header {
margin-bottom: 32px;
}
}
.withdraw-form {
&__bottom {
display: flex;
gap: 16px;
margin-top: 32px;
}
}
.withdraw-summary {
&__details {
margin-bottom: 16px;
}
&__alert {
margin-bottom: 16px;
}
&__pincode {
padding-block: 16px;
}
&__actions {
display: flex;
gap: 16px;
margin-top: 32px;
> * {
flex: 1;
}
}
}
.withdraw-amount {
$self: &;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
height: 48px;
flex: 0 0 146px;
&--fee {
flex: 0 0 100px
}
&__title {
@include txt-r-b;
color: $clr-grey-500;
}
&__value {
@include txt-l-sb;
white-space: nowrap;
#{$self}--fee & {
@include txt-i-sb;
color: $clr-grey-600;
}
}
& + & {
border-left: 1px solid $clr-grey-300;
padding-left: 16px;
}
}
.withdraw-details {
@include txt-i-m;
@include txt-i-m('text-shortener', true);
@include txt-i-m('currency-name-code', true);
border-radius: 12px;
background-color: $clr-grey-200;
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: 16px;
color: $clr-grey-500;
padding: 16px;
dt, dd {
margin: 0;
}
dt {
color: $clr-grey-600;
}
dd {
text-align: right;
}
&__amount {
@include txt-l-sb;
color: $clr-black;
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<PageForm
back-link="/login"
:back-text="$t('to_main')"
:title="$t('login_greeting')"
:submit-text="$t('next')"
:handler="onSubmit"
>
<UiInput
id="email"
class="mb-6"
:label="$t('email')"
rules="required|email"
/>
<UiInput
id="password"
:label="$t('password')"
native-type="password"
rules="required"
/>
<div class="login-form__forgot-password">
<UiButton
type="link"
href="/reset-password"
>
{{ $t('lost_password') }}
</UiButton>
</div>
</PageForm>
</template>
<script setup>
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
middleware: ['guest-only'],
})
const auth = useAuth()
if (!import.meta.env.SSR) {
const verifiedCookie = useCookie('verified')
const { t } = useI18n()
const notify = useNotify()
if (verifiedCookie.value) {
notify({
id: 'verified',
text: t('account_created_successfully'),
type: 'positive',
})
}
else if (verifiedCookie.value !== undefined) {
notify({
id: 'verified',
title: t('account_verification_error'),
text: t('try_again_or_contact_support'),
type: 'negative',
})
}
verifiedCookie.value = undefined
}
async function onSubmit(values) {
try {
await auth.login(values.email, values.password)
}
catch (e) {
if (e.status === 422) {
setStaticError({
status: e.status,
message: 'Incorrect email or password',
})
}
else {
setStaticError({
status: e.status,
message: 'Something went wrong',
})
}
}
}
</script>
<style lang="scss">
.login-form {
&__forgot-password {
margin-top: -6px;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<PageHeader
:title="project.name"
with-back-button
>
<div class="project-header">
<MoneyAmount
class="project-header__balance"
:value="account.balance"
currency="USDT"
/>
<UiButton
icon="s-restore"
size="small"
type="outlined"
color="secondary"
:loading="accountPending"
@click="accountRefresh()"
/>
</div>
</PageHeader>
<UiTabs v-model="activeTab" class="mb-16" :options="tabs" />
<NuxtPage />
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth'],
})
const router = useRouter()
const route = useRoute()
const activeTab = ref(route.path)
const { data: project } = await useAsyncData('project', () => {
return $api(`/online_stores/${route.params.projectId}`, {
method: 'get',
})
})
if (!project.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
})
}
const {
data: account,
refresh: accountRefresh,
pending: accountPending,
} = await useAsyncData('account', () =>
$api(`/accounts/${project.value.accountIds[0]}`, {
method: 'get',
}))
const intervalId = setInterval(() => refreshNuxtData(['invoices', 'transactions', 'account']), 30000)
const tabs = computed(() => [
{
label: 'Assets',
value: `/projects/${route.params.projectId}`,
},
{
label: 'Invoices',
value: `/projects/${route.params.projectId}/invoices`,
},
{
label: 'Transactions',
value: `/projects/${route.params.projectId}/transactions`,
},
])
watch(activeTab, (value) => {
router.push(value)
})
onUnmounted(() => clearInterval(intervalId))
</script>
<style lang="scss">
.project-header {
display: flex;
align-items: center;
gap: 8px;
&__balance {
@include h2('money-amount-value', true);
color: $clr-grey-500;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<PageToolbar
v-model:search="searchTerm"
search-label="Search an asset"
>
<UiButton
size="large"
@click="notify({ id: 'wip', type: 'warning', text: 'Work in progress' })"
>
Manage assets
</UiButton>
</PageToolbar>
<div class="asset-list">
<AssetCard
v-for="asset in filteredAssets"
:key="asset.code"
v-bind="asset"
:is-active="sidebarOptions.modelValue && sidebarOptions.attrs.asset?.code === asset.code"
/>
</div>
</template>
<script setup>
import { useModal } from 'vue-final-modal'
import { AssetSidebar } from '#components'
definePageMeta({
middleware: ['auth'],
})
const { data: account } = useNuxtData('account')
const route = useRoute()
const notify = useNotify()
const { open, patchOptions, options: sidebarOptions } = useModal({
component: AssetSidebar,
attrs: {
projectId: route.params.projectId,
},
})
const searchTerm = ref('')
const assets = shallowRef([
'USDT:Tether',
'LTC:Litecoin',
'DOGE:Dogecoin',
'BTC:Bitcoin',
'XRP:Ripple',
'BNB:Binance',
'BAT:Basic Attention Token',
'EOS:EOS Network',
'MKR:Maker',
'DAI:Dai',
'ETH:Ethereum',
'DASH:Dash',
].map((item, idx) => {
const [code, name] = item.split(':')
const balance = $money.format(code === 'USDT' ? account.value.balance : 0, code)
const rate = code === 'USDT' ? 1 : 213.25
const withdraw = undefined
// const balance = $money.format(code === 'USDT' ? account.value.balance : 0.002741, code)
// const withdraw = idx % 2 !== 0 ? $money.format(0.15484, code) : undefined
const disabled = code !== 'USDT'
return {
code,
name,
balance,
rate,
withdraw,
disabled,
onClick: () => {
if (!disabled) {
patchOptions({
attrs: {
asset: {
code,
name,
balance,
rate,
withdraw,
},
},
})
open()
}
// else {
// notify({ type: 'warning', title: 'Work in progress', text: 'At the moment we only work with USDT', id: 'wip' })
// }
},
}
}))
const { result: filteredAssets } = useFuseSearch(assets, { keys: ['code', 'name'] }, searchTerm)
</script>
<style lang="scss">
.asset-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
border-radius: 12px;
background-color: $clr-white;
margin-top: 24px;
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div
v-if="!total && isFiltersEmpty && !searchTerm && !isInvoicesFetching"
class="invoice__empty"
>
<InvoicesEmpty :id="$route.params.projectId" />
</div>
<template v-else>
<PageToolbar
v-model:search="searchTerm"
search-label="Search an invoices"
>
<template #prefix>
<div class="invoices-awaiting-sum">
<div class="invoices-awaiting-sum__value">
<span>{{ awaitingSum }}</span> <UiIconSUpRight class="text-clr-cyan-500" />
</div>
<p class="invoices-awaiting-sum__text">
Sum of invoices awaiting payment
</p>
</div>
</template>
<UiButton
type="outlined"
color="secondary"
size="large"
:href="`/report/${$route.params.projectId}`"
icon="csv"
>
Export to CSV
</UiButton>
<UiButton
size="large"
:href="`/create/invoice/${$route.params.projectId}`"
>
Create an invoice
</UiButton>
</PageToolbar>
<div
style="
margin-top: 24px;
padding: 16px;
border-radius: 12px;
background-color: #fff;
"
>
<ResourceFilters
class="mb-16"
:filters="filters"
/>
<StrippedTable
class="invoices__table"
:columns="columns"
:data="invoices"
:loading="isInvoicesFetching"
>
<template #cell(currency)="{ row: { original } }">
<CurrencyName :code="original.currencyCode" />
</template>
<template #cell(amount)="{ row: { original } }">
<MoneyAmount
:value="original.amount"
:currency="original.currencyCode"
/>
</template>
<template #cell(accrued)="{ row: { original } }">
<MoneyAmount
:value="original.accrued"
:currency="original.currencyCode"
/>
</template>
<template #cell(address)="{ row: { original } }">
<span v-if="!original.walletAddress"></span>
<TextShortener
v-else
:text="original.walletAddress"
/>
</template>
<template #cell(orderId)="{ row: { original } }">
<TextShortener :text="original.orderId" />
</template>
<template #cell(date)="{ row: { original } }">
<FormattedDate :value="original.createdAt" />
</template>
<template #cell(status)="{ row: { original } }">
<UiBadge
:text="$t(`invoice_status.${original.status}`)"
:type="getStatusType(original.status)"
:icon="getStatusIcon(original.status)"
/>
</template>
<template #cell(paymentLink)="{ row: { original } }">
<UiCopyButton
title="Payment link"
pressed-title="Link copied"
:value="`${runtimeConfig.public.payHost}/${original.id}`"
/>
</template>
</StrippedTable>
</div>
</template>
</template>
<script setup>
import { createColumnHelper } from '@tanstack/vue-table'
import { computed } from 'vue'
import dayjs from 'dayjs'
import { getStatusIcon, getStatusType } from '~/helpers/invoices.ts'
definePageMeta({
middleware: ['auth'],
})
const runtimeConfig = useRuntimeConfig()
const filters = [
{
key: 'statuses[]',
label: 'Status',
placeholder: 'All statuses',
multiple: true,
options: [
{
label: 'Pending',
value: 'pending',
},
{
label: 'Completed',
value: 'completed',
},
{
label: 'Expired',
value: 'expired',
},
{
label: 'Await Payment',
value: 'awaiting_payment',
},
{
label: 'Overpaid',
value: 'overpaid',
},
{
label: 'Underpaid',
value: 'underpaid',
},
],
},
{
type: 'calendar',
key: 'date',
label: 'Date',
placeholder: 'All period',
transform: (value) => {
const [dateFrom, dateTo] = value
if (!dateFrom)
return
return {
dateFrom: dayjs(dateFrom).format('YYYY-MM-DD'),
dateTo: dateTo ? dayjs(dateTo).format('YYYY-MM-DD') : undefined,
}
},
},
]
const route = useRoute()
const { appliedFilters, empty: isFiltersEmpty } = useFilters(filters)
const searchTerm = ref()
const { data: invoicesData, pending: isInvoicesFetching, refresh } = await useAsyncData(
'invoices',
() =>
$api(`/invoices`, {
method: 'get',
params: {
...appliedFilters.value,
onlineStoreId: route.params.projectId,
query: searchTerm.value,
},
}),
{
watch: [appliedFilters, searchTerm],
},
)
const total = computed(() => invoicesData.value?.meta.total ?? 0)
const invoices = computed(() => invoicesData.value?.invoices)
const awaitingSum = $money.fullFormat(0, 'USDT')
const columnHelper = createColumnHelper()
const columns = [
columnHelper.accessor('currencyCode', {
id: 'currency',
header: 'Currency',
}),
columnHelper.accessor('amount', {
header: 'Amount',
}),
columnHelper.display({
id: 'accrued',
header: 'Accrued',
}),
columnHelper.display({
id: 'address',
header: 'Address',
}),
columnHelper.display({
id: 'orderId',
header: 'Order ID',
}),
columnHelper.accessor('createdAt', {
id: 'date',
header: 'Date',
}),
columnHelper.display({
id: 'status',
header: 'Status',
}),
columnHelper.display({
id: 'paymentLink',
}),
]
</script>
<style lang="scss">
.invoice {
&__empty {
display: inline-flex;
background-color: $clr-white;
border-radius: 12px;
flex: 1;
width: 100%;
padding: 16px;
> :first-child {
margin: 0 auto;
}
}
}
.invoices {
&__table {
td.payment_link {
width: 1%;
text-align: right;
}
}
}
.invoices-awaiting-sum {
padding: 8px 16px;
height: 48px;
border-radius: 12px;
background-color: $clr-grey-200;
&__value {
@include txt-i-sb;
> span {
vertical-align: middle;
}
}
&__text {
@include txt-s-m;
color: $clr-grey-500;
}
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<PageToolbar
v-model:search="searchTerm"
search-label="Find a transaction"
/>
<div
style="
margin-top: 24px;
padding: 16px;
border-radius: 12px;
background-color: #fff;
"
>
<ResourceFilters
class="mb-16"
:filters="filters"
/>
<StrippedTable
class="transactions__table"
:columns="columns"
:data="transactions"
:loading="isTransactionsFetching"
>
<template #cell(currency)="{ row: { original } }">
<CurrencyName :code="original.currencyCode" />
</template>
<template #cell(amount)="{ row: { original } }">
<MoneyAmount
:value="original.amount"
:currency="original.currencyCode"
/>
</template>
<template #cell(accrued)="{ row: { original } }">
<MoneyAmount
:value="original.accrued"
:currency="original.currencyCode"
/>
</template>
<template #cell(address)="{ row: { original } }">
<span v-if="!original.walletAddress"></span>
<TextShortener
v-else
:text="original.walletAddress"
/>
</template>
<template #cell(date)="{ row: { original } }">
<FormattedDate :value="original.createdAt" />
</template>
<template #cell(type)="{ row: { original } }">
<OperationType type="Withdraw" />
</template>
<template #cell(status)="{ row: { original } }">
<UiBadge
:text="$t(`invoice_status.${original.status}`)"
:type="getStatusType(original.status)"
:icon="getStatusIcon(original.status)"
/>
</template>
</StrippedTable>
</div>
</template>
<script setup>
import { createColumnHelper } from '@tanstack/vue-table'
import dayjs from 'dayjs'
import { getStatusIcon, getStatusType } from '~/helpers/invoices.ts'
definePageMeta({
middleware: ['auth'],
})
const runtimeConfig = useRuntimeConfig()
const filters = [
// {
// key: 'networks[]',
// label: 'Networks',
// placeholder: 'All networks',
// multiple: true,
// options: [],
// },
// {
// key: 'assets[]',
// label: 'Assets',
// placeholder: 'All assets',
// multiple: true,
// options: [],
// },
{
key: 'statuses[]',
label: 'Status',
placeholder: 'All statuses',
multiple: true,
options: [
{
label: 'Pending',
value: 'pending',
},
{
label: 'Completed',
value: 'completed',
},
{
label: 'Investigating',
value: 'investigating',
},
{
label: 'Broadcasted',
value: 'broadcasted',
},
],
},
{
type: 'calendar',
key: 'date',
label: 'Date',
placeholder: 'All period',
transform: (value) => {
const [dateFrom, dateTo] = value
if (!dateFrom)
return
return {
dateFrom: dayjs(dateFrom).format('YYYY-MM-DD'),
dateTo: dateTo ? dayjs(dateTo).format('YYYY-MM-DD') : undefined,
}
},
},
]
const route = useRoute()
const { appliedFilters, empty: isFiltersEmpty } = useFilters(filters)
const searchTerm = ref()
const { data: transactionsData, pending: isTransactionsFetching, refresh } = await useAsyncData(
'transactions',
() =>
$api(`/withdrawals`, {
method: 'get',
params: {
...appliedFilters.value,
onlineStoreId: route.params.projectId,
query: searchTerm.value,
},
}),
{
watch: [appliedFilters, searchTerm],
},
)
const transactions = computed(() => transactionsData.value?.withdrawals)
const columnHelper = createColumnHelper()
const columns = [
columnHelper.accessor('currencyCode', {
id: 'currency',
header: 'Currency',
}),
columnHelper.accessor('amount', {
header: 'Amount',
}),
columnHelper.display({
id: 'accrued',
header: 'Accrued',
}),
columnHelper.display({
id: 'address',
header: 'Address',
}),
columnHelper.accessor('createdAt', {
id: 'date',
header: 'Date',
}),
columnHelper.display({
id: 'type',
header: 'Type operation',
}),
columnHelper.display({
id: 'status',
header: 'Status',
}),
]
</script>
<style lang="scss">
.transaction {
&__empty {
display: inline-flex;
background-color: $clr-white;
border-radius: 12px;
flex: 1;
width: 100%;
padding: 16px;
> :first-child {
margin: 0 auto;
}
}
}
.transactions {
&__table {
td.payment_link {
width: 1%;
text-align: right;
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<PageForm
title="Creating a project"
back-link="/projects"
:back-text="$t('back')"
:submit-text="$t('create')"
:handler="onSubmit"
>
<UiInput
id="name"
class="mb-6"
:label="$t('field_max_characters', ['Project name', 16])"
rules="required|max:16"
/>
<UiInput
id="description"
class="mb-6"
label="Description"
rules="required"
/>
<UiInput
id="website"
class="mb-6"
label="Your website"
:rules="{
required: true,
url: { optionalProtocol: true },
}"
/>
<UiInput
id="callbacksUrl"
class="mb-6"
label="Callbacks URL"
rules="required|url"
/>
<UiInput
id="userRedirectUrl"
class="mb-6"
label="User redirect URL"
rules="required|url"
/>
</PageForm>
</template>
<script setup>
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const notify = useNotify()
async function onSubmit(values) {
try {
await $api('/online_stores', {
method: 'post',
body: values,
})
notify({
type: 'positive',
text: 'Мерчант успешно создан',
})
navigateTo('/projects')
}
catch (e) {
setStaticError({
status: e.status,
message: 'Something went wrong',
})
}
}
</script>

View File

@@ -0,0 +1,137 @@
<template>
<PageHeader :title="$t('projects')">
<UiButton
v-if="!!projects"
icon="plus"
href="/projects/create"
>
Create a new one
</UiButton>
<!-- Временно убрано KITD-527 -->
<!-- <UiButton -->
<!-- icon="circle-question" -->
<!-- type="outlined" -->
<!-- :color="!!projects ? 'secondary' : 'primary'" -->
<!-- > -->
<!-- {{ $t('how_does_it_works') }} -->
<!-- </UiButton> -->
</PageHeader>
<div class="projects-content">
<ProjectCreate v-if="!projects" />
<ProjectsTable
v-else
class="projects-content__table"
:projects="projects"
/>
<ProjectInfoColumns />
</div>
<!-- Временно убрано KITD-527 -->
<!-- <PageFooterInfoBlock -->
<!-- :title="$t('test_functional')" -->
<!-- :text="$t('learn_functional')" -->
<!-- :action="$t('try')" -->
<!-- /> -->
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth'],
})
interface Project {
id: number
name: string
account?: ProjectAccount
}
interface ProjectDetailed {
id: number
name: string
accountIds: number[] | string[]
}
interface ProjectAccount {
id?: number
balance: string
}
const { data: projects } = await useAsyncData('projects', async () => {
const nuxtApp = useNuxtApp()
const projects = await $api<Project[]>('/online_stores', {
method: 'get',
})
if (!projects)
return null
const detailedProjects = await nuxtApp.runWithContext(async () => {
const promises = await Promise.allSettled(
projects.map((project) => {
return $api(`/online_stores/${project.id}`, {
method: 'get',
})
}),
)
return (
promises.filter(
({ status }) => status === 'fulfilled',
) as PromiseFulfilledResult<ProjectDetailed>[]
).map(({ value }) => value)
})
const balances = await nuxtApp.runWithContext(async () => {
const promises = await Promise.allSettled(
detailedProjects.map((project) => {
return $api(`/accounts/${project.accountIds[0]}`, {
method: 'get',
})
}),
)
return (
promises.filter(
({ status }) => status === 'fulfilled',
) as PromiseFulfilledResult<ProjectAccount>[]
).map(({ value }) => value)
})
return projects.map((project) => {
const detail = detailedProjects.find(detail => detail.id === project.id)
let balance
if (detail)
balance = balances.find(balance => balance.id === detail.accountIds[0])
balance ??= { balance: 0 } as unknown as ProjectAccount
return {
...project,
account: balance,
}
})
})
</script>
<style lang="scss">
.projects-content {
display: grid;
align-items: flex-start;
grid-template-columns: auto 270px;
background-color: $clr-white;
border-radius: 12px;
padding: 16px;
margin-bottom: 32px;
gap: 16px;
&__table {
align-self: flex-start;
}
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<AuthorizationEmailConfirmation :email="email" :resend-fn="resend">
<div v-html="$t('register_email_instructions')" />
</AuthorizationEmailConfirmation>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
validate: route => !!route.query.email,
middleware: ['guest-only'],
})
const { query } = useRoute()
const { resendVerificationCode } = useAuth()
const email = computed(() => query.email!.toString())
function resend() {
resendVerificationCode(email.value)
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<PageForm
back-link="/login"
:back-text="$t('to_main')"
:title="$t('register_greeting')"
:submit-text="$t('next')"
:handler="onSubmit"
>
<UiInput
id="email"
class="mb-6"
:label="$t('email')"
rules="required|email"
/>
<UiInput
id="password"
:label="$t('password')"
native-type="password"
rules="required|password"
class="mb-6"
/>
<UiInput
id="repeat_password"
:label="$t('repeat_password')"
native-type="password"
rules="required|confirmed:@password"
class="mb-6"
/>
<UiSelect
id="wut"
label="Как вы узнали о нас?"
placeholder="Выбрать вариант"
:options="[
'Рекомендация',
'Статьи на сторонних площадках',
'Увидел рекламу',
]"
class="w-100 mb-26"
/>
<UiCheckbox id="agree" required>
<I18nT keypath="sign_up_agreement.base" tag="span" scope="global">
<UiButton type="link">
{{ $t('sign_up_agreement.privacy_policy') }}
</UiButton>
<UiButton type="link">
{{ $t('sign_up_agreement.user_agreement') }}
</UiButton>
</I18nT>
</UiCheckbox>
</PageForm>
</template>
<script setup>
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
middleware: ['guest-only'],
})
const { register } = useAuth()
async function onSubmit(values) {
try {
await register(values.email, values.password)
navigateTo({
path: '/register/email-confirmation',
query: {
email: values.email,
},
})
}
catch (e) {
if (e.status === 422) {
setStaticError({
status: e.status,
message: 'You are already registered',
})
}
else {
setStaticError({
status: e.status,
message: 'Something went wrong',
})
}
}
}
</script>

View File

@@ -0,0 +1,178 @@
<template>
<form
class="report-form"
@submit="onSubmit"
>
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
:href="`/projects/${$route.params.projectId}/invoices`"
>
{{ $t('back_to', [project.name]) }}
</UiButton>
<h1 class="report-form__title">
Exporting data
</h1>
<div class="report-block mb-16">
<h4 class="report-block__title">
Выберите период
</h4>
<div class="report-block__actions">
<RadioButton id="period" value="today" label="За сегодня" :caption="todayText" required />
<RadioButton id="period" value="week" required label="За неделю" :caption="weekText" />
<RadioButton id="period" value="month" label="За месяц" disabled />
</div>
</div>
<div class="report-block">
<h4 class="report-block__title">
Выберите статус операции
</h4>
<div class="report-block__actions">
<CheckboxButton id="all_statuses" label="All" />
<CheckboxButton
v-for="status in availableStatuses"
id="statuses"
:key="status"
:label="$t(`invoice_status.${status}`)"
:true-value="status"
required
/>
</div>
</div>
<UiButton
class="report-form__submit"
size="large"
native-type="submit"
:loading="isSubmitting"
:disabled="!values.period || !values.statuses.length"
>
Скачать CSV отчет
</UiButton>
</form>
</template>
<script setup lang="ts">
import { useForm } from 'vee-validate'
import dayjs from 'dayjs'
import { downloadFile } from '#imports'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const route = useRoute()
const { data: project } = await useAsyncData(
'project',
() =>
$api(`/online_stores/${route.params.projectId}`, {
method: 'get',
}),
{},
)
if (!project.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
})
}
const availableStatuses = ['completed', 'overpaid', 'underpaid', 'awaiting_payment', 'expired']
const { handleSubmit, isSubmitting, values, defineField } = useForm<{
period: 'today' | 'week' | 'month'
all_statuses?: boolean
'statuses': string[]
}>({
initialValues: {
statuses: [],
},
})
const [allStatuses] = defineField('all_statuses')
const [selectedStatuses] = defineField('statuses')
const todayText = dayjs().format('DD.MM.YYYY')
const weekText = `${dayjs().subtract(1, 'week').get('date')} - ${todayText}`
const onSubmit = handleSubmit(async (values) => {
delete values.all_statuses
const dateFormat = 'YYYY-MM-DD'
let dateFrom: string, dateTo: string
if (values.period === 'today') {
dateFrom = dateTo = dayjs().format(dateFormat)
}
else if (values.period === 'week') {
dateFrom = dayjs().subtract(1, 'week').format(dateFormat)
dateTo = dayjs().format(dateFormat)
}
const csv = await $api<string>('/invoices.csv', {
method: 'GET',
params: {
'onlineStoreId': route.params.projectId,
'statuses[]': values.statuses,
dateFrom,
dateTo,
},
})
downloadFile(`invoices-${dateTo}.csv`, csv)
})
watch(allStatuses, (value) => {
if (!value && selectedStatuses.value.length !== availableStatuses.length)
return
selectedStatuses.value = value ? availableStatuses : []
})
watch(selectedStatuses, (value) => {
allStatuses.value = value.length === availableStatuses.length
})
</script>
<style lang="scss">
.report-form {
margin: 0 auto;
width: 500px;
&__title {
margin-bottom: 32px;
}
&__submit {
width: 100%;
margin-top: 32px;
}
}
.report-block {
padding: 16px;
border-radius: 16px;
background-color: $clr-white;
&__title {
@include font(13px, 500, 18px);
color: $clr-grey-600;
margin-bottom: 16px;
}
&__actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px 10px;
}
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<AuthorizationEmailConfirmation :email="email" :resend-fn="resend">
<div v-html="$t('reset_password_instructions')" />
</AuthorizationEmailConfirmation>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
validate: route => !!route.query.email,
// middleware: ['guest-only'],
})
const { query } = useRoute()
const { requestResetPassword } = useAuth()
const email = computed(() => query.email!.toString())
function resend() {
requestResetPassword(email.value)
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<PageForm
:title="$t('reset_password')"
back-link="/login"
:back-text="$t('to_main')"
:submit-text="$t('next')"
:handler="onSubmit"
>
<UiAlert class="mb-16">
{{ $t('reset_password_alert') }}
</UiAlert>
<UiInput id="email" :label="$t('email')" rules="required|email" />
</PageForm>
</template>
<script setup>
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
middleware: ['guest-only'],
})
const { requestResetPassword } = useAuth()
async function onSubmit(values) {
await requestResetPassword(values.email)
navigateTo({
path: '/reset-password/email-confirmation',
query: {
email: values.email,
},
})
}
</script>
<style lang="scss">
.reset-form {
margin: 0 auto;
width: 500px;
&__summary {
padding-top: 24px;
padding-bottom: 22px;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<PageForm
:title="$t('reset_password')"
back-link="/settings/safety"
:back-text="$t('back')"
:submit-text="$t('next')"
:handler="onSubmit"
:title-offset="false"
class="reset-password"
>
<UiAlert class="mb-40">
В целях безопасности будет запрещен вывод средств в течение 24 часов после изменения пароля
</UiAlert>
<UiInput
id="current_password"
label="Current password"
native-type="password"
rules="required|password"
class="mb-6"
autocomplete="off"
/>
<UiInput
id="new_password"
:label="$t('password')"
native-type="password"
rules="required|password"
class="mb-6"
/>
<UiInput
id="repeat_password"
:label="$t('repeat_password')"
native-type="password"
rules="required|confirmed:@new_password"
/>
</PageForm>
</template>
<script setup>
definePageMeta({
pageTransition: { name: 'fade', mode: 'out-in' },
centerContent: true,
middleware: ['auth'],
})
const { resetPassword } = useAuth()
const { query } = useRoute()
async function onSubmit(values) {
try {
await resetPassword(values.new_password, query.reset_code.toString())
navigateTo('/settings/safety')
}
catch {}
}
</script>
<style lang="scss">
.reset-password {
--page-form-padding-top: 8px;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<PageForm
:title="$t('reset_password')"
back-link="/login"
:back-text="$t('back')"
:submit-text="$t('next')"
:handler="onSubmit"
>
<UiInput
id="new_password"
:label="$t('password')"
native-type="password"
rules="required|password"
class="mb-6"
/>
<UiInput
id="repeat_password"
:label="$t('repeat_password')"
native-type="password"
rules="required|confirmed:@new_password"
/>
</PageForm>
</template>
<script setup>
definePageMeta({
layout: 'auth',
pageTransition: { name: 'fade', mode: 'out-in' },
validate: route => !!route.query.reset_code,
middleware: ['guest-only'],
})
const { resetPassword } = useAuth()
const { query } = useRoute()
async function onSubmit(values) {
try {
await resetPassword(values.new_password, query.reset_code.toString())
navigateTo('/login')
}
catch {}
}
</script>
<style lang="scss">
.reset-form {
margin: 0 auto;
width: 500px;
&__summary {
padding-top: 24px;
padding-bottom: 24px;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<PageHeader title="Settings" />
<UiTabs v-model="activeTab" class="mb-16" :options="tabs" />
<NuxtPage />
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth'],
})
const route = useRoute()
const router = useRouter()
const activeTab = ref(route.path)
const tabs = computed(() => [
{
label: 'Profile',
value: '/settings',
},
{
label: 'Safety',
value: '/settings/safety',
},
{
label: 'Notifications',
value: '/settings/notifications',
},
{
label: 'Limits',
value: '/settings/limits',
},
])
watch(activeTab, (value) => {
router.push(value)
})
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,62 @@
<template>
<!-- <UiSelect id="locale" label="" :model-value="currentLocale" :options="localesOption" /> -->
<!-- <UiSelect id="timezone" v-model="currentTimeZone" label="" class="w-50" searchable :options="['(GMT+03.00) Москва, Россия']" /> -->
<PageBlock title="Time and language">
<SettingsProperty icon="language" title="Language">
<UiDropdown placement="bottom">
<UiButton
right-icon="chevron-down"
type="outlined"
size="large"
color="secondary"
style="text-transform: capitalize"
>
{{ currentLocale }}
</UiButton>
<template #dropdown>
<UiDropdownItem
v-for="locale in locales"
:key="locale.code"
:active="currentLocale === locale.code"
@click="setLocale(locale.code)"
>
{{ locale.name }}
</UiDropdownItem>
</template>
</UiDropdown>
</SettingsProperty>
<SettingsProperty icon="clock" title="Time zone" text="It is used to process transactions, track balances, and create reports.">
<UiDropdown placement="bottom">
<UiButton
right-icon="chevron-down"
type="outlined"
size="large"
color="secondary"
style="text-transform: capitalize"
>
{{ currentTimeZone }}
</UiButton>
<template #dropdown>
<UiDropdownItem
v-for="timezone in ['(GMT+03.00) Москва, Россия']"
:key="timezone"
:active="currentLocale === timezone"
>
{{ timezone }}
</UiDropdownItem>
</template>
</UiDropdown>
</SettingsProperty>
</PageBlock>
</template>
<script setup>
const { locale: currentLocale, locales, setLocale } = useI18n()
const currentTimeZone = '(GMT+03.00) Москва, Россия'
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,130 @@
<template>
<PageBlock
class="settings-limits"
title="Current tariff"
>
<template #badge>
<UiBadge type="marketing">
BASIC
</UiBadge>
</template>
<template #actions>
<UiButton>Increase the limit</UiButton>
</template>
<SettingsTariffCard title="Withdrawal of funds">
<template #icon>
<UiIconArrowSend class="text-clr-cyan-600" />
</template>
<template #subtitle>
<div class="settings-limits__subtitle">
<span>Minimum withdrawal amount <strong>5 usdt</strong></span>
<div class="settings-limits-separator" />
<span>Withdrawal fee <strong>2.5 usdt + 1%</strong></span>
</div>
</template>
<template #content>
<div class="settings-limits-content">
<div class="settings-limits-content__header">
<strong>The limit</strong> <span class="text-clr-grey-500"> for today</span>
<UiBadge type="marketing" class="ml-8">
It will be updated at 16:00
</UiBadge>
</div>
<SettingsLimitProgress currency="USDT" :max-amount="880" :amount="120" />
</div>
<div class="settings-limits-separator" />
<div class="settings-limits-content">
<div class="settings-limits-content__header">
<span class="text-clr-grey-500"> Monthly</span> <strong>limit</strong>
<UiBadge type="marketing" class="ml-8">
Updated on 03.01.2023
</UiBadge>
</div>
<SettingsLimitProgress currency="USDT" :max-amount="10000" :amount="120" />
</div>
</template>
</SettingsTariffCard>
<SettingsTariffCard title="Creating invoices">
<template #icon>
<UiIconArrowReceive class="text-clr-green-500" />
</template>
<template #subtitle>
<div class="settings-limits__subtitle">
<span>The minimum amount in the invoice is <strong>5 usdt</strong></span>
<div class="settings-limits-separator" />
<span>The commission for creating an invoice is <strong> 0.5 usdt</strong></span>
</div>
</template>
<template #content>
<div class="settings-limits-content">
<div class="settings-limits-content__header">
<strong>The limit</strong> <span class="text-clr-grey-500"> for today</span>
<UiBadge type="marketing" class="ml-8">
It will be updated at 16:00
</UiBadge>
</div>
<SettingsLimitProgress currency="invoices" :max-amount="200" :amount="200" />
</div>
<div class="settings-limits-separator" />
<div class="settings-limits-content">
<div class="settings-limits-content__header">
<span class="text-clr-grey-500"> Monthly</span> <strong>limit</strong>
<UiBadge type="marketing" class="ml-8">
Updated on 03.01.2023
</UiBadge>
</div>
<SettingsLimitProgress currency="invoices" :max-amount="1000" :amount="751" />
</div>
</template>
</SettingsTariffCard>
</PageBlock>
</template>
<script setup>
</script>
<style lang="scss">
.settings-limits{
--page-block-gap: 24px;
--page-block-padding: 24px;
&__subtitle{
display: flex;
@include txt-r;
color: $clr-grey-600;
}
}
.settings-limits-separator{
width: 1px;
margin-inline: 16px;
background-color: $clr-grey-300;
}
.settings-limits-content {
width: 544px;
&__header{
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<PageBlock
class="settings-notifications"
title="Notification settings"
sub-title="Set how you want to receive notifications"
>
<SettingsServiceNotificationTable />
<SettingsFinanceNotificationTable />
</PageBlock>
</template>
<script setup>
</script>
<style lang="scss">
.settings-notifications{
--page-block-gap: 24px;
--page-block-padding: 24px;
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<PageBlock title="Account details" class="mb-24">
<SettingsProperty icon="mail" title="Mail" :text="hideEmail(user.email)" />
<SettingsProperty icon="key" title="Password" text="You will log out automatically after changing your password">
<UiButton size="large" type="ghost" color="primary" href="/reset-password/new-authorized">
Change
</UiButton>
</SettingsProperty>
</PageBlock>
<PageBlock title="Two-factor Authentication (2FA)">
<SettingsProperty icon="protection" title="2FA authentication" text="Two-factor authentication (2FA) provides more reliable account protection.">
<UiButton size="large" type="ghost" color="primary" href="/2fa">
Plug
</UiButton>
</SettingsProperty>
</PageBlock>
</template>
<script setup>
const { user } = useAuth()
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,158 @@
<template>
<form
class="verification"
@submit="onSubmit"
>
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
:href="step === 0 ? '/verification/status' : ''"
@click="step--"
>
{{ $t('back') }}
</UiButton>
<h1 class="mb-40">
Identity verification
</h1>
<!-- TODO: бырбырбыр -->
<Stepper
:step="step"
:items="[
'Personal <br> information',
'Identity <br> verification',
'Organization <br> data',
]"
/>
<Transition
name="fade"
mode="out-in"
>
<div v-if="step === 0" class="mt-40">
<UiInput
id="name"
label="Name"
class="mb-10"
rules="required"
/>
<UiInput
id="surname"
label="Surname"
class="mb-10"
rules="required"
/>
<UiInput
id="patronymic"
label="Patronymic (optional)"
class="mb-28"
/>
<UiInput
id="bday"
label="Date of birth"
class="mb-10"
rules="required"
/>
<UiSelect
id="region"
class="w-100"
label="Country / Region of residence"
placeholder="Choose an option"
:options="['Россия', 'священная', 'наша', 'держава']"
/>
<UiButton
class="w-100 mt-40"
size="large"
@click="step++"
>
{{ $t('next') }}
</UiButton>
</div>
<div v-else-if="step === 1" class="mt-40">
<UiAlert class="mb-40">
Upload a photo of the <strong> front </strong> and <strong> back </strong> of your document
</UiAlert>
<VerificationWeAccept
class="mb-40"
:list="[{ title: 'Identity document', underTitle: '(For example: passport, residence permit, driver\'s license.)' }]"
/>
<!-- TODO: zaglushka -->
<div class="zaglushka" />
<UiButton
class="w-100 mt-40"
size="large"
@click="step++"
>
{{ $t('next') }}
</UiButton>
</div>
<div v-else-if="step === 2" class="mt-22">
<UiAlert class="mb-24">
Upload the documents for the right to conduct commercial activities
</UiAlert>
<VerificationWeAccept
class="mb-24"
:list="[
{ title: 'Company registration documents.' },
{ title: 'License to trade.' },
{ title: 'Documents on the identity of the individual to the organization,', underTitle: '(power of attorney, inheritance, donation, sub-sponsorship)' },
]"
/>
<!-- TODO: zaglushka -->
<div class="zaglushka" />
<UiButton
class="w-100 mt-24"
size="large"
href="/verification/processing"
>
{{ $t('next') }}
</UiButton>
</div>
</Transition>
</form>
</template>
<script setup>
import { useForm } from 'vee-validate'
definePageMeta({
middleware: ['auth'],
centerContent: true,
})
const { handleSubmit, isSubmitting } = useForm()
const step = ref(0)
const onSubmit = handleSubmit(async (values) => {
console.log(values)
navigateTo('/projects')
})
</script>
<style lang="scss">
.verification {
margin: 0 auto;
width: 500px;
}
.zaglushka {
height: 143px;
padding: 24px;
border-radius: 12px;
border: 1px dashed var(--grey-grey-400, #A7B9D5);
background: var(--grey-grey-100, #F4F6FF);
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="verification-processing">
<h1 class="mb-56">
We process your data
</h1>
<UiAlert
size="large"
class="mb-40"
>
The verification status of your documents will be displayed here. It usually takes 1-2 business days.
<br> <br>
Also, at the end of the verification, we will send a notification
to the mail john@gmail.com
</UiAlert>
<UiButton
size="large"
class="w-100"
href="/projects"
>
{{ $t('to_main') }}
</UiButton>
</div>
</template>
<script setup>
definePageMeta({
middleware: ['auth'],
centerContent: true,
});
</script>
<style lang="scss">
.verification-processing{
margin: 0 auto;
width: 513px;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="verification-status">
<UiButton
icon="chevron-left"
color="secondary"
type="link"
class="mb-8"
href="/projects"
>
{{ $t('to_main') }}
</UiButton>
<h1 class="mb-32">
Identity verification
</h1>
<div class="verification-cards">
<VerificationBaseCard
passed
:info="{ perMonth: '10 000 USD', perDay: '1 000 USD' }"
:badge="{ type: 'marketing', title: 'Basic' }"
>
<VerificationBasic />
</VerificationBaseCard>
<VerificationBaseCard
:badge="{ type: 'extended', title: 'Extended' }"
link="/verification"
>
<VerificationExtended />
</VerificationBaseCard>
</div>
</div>
</template>
<script setup>
definePageMeta({
// middleware: ['auth'],
centerContent: true,
})
</script>
<style lang="scss">
.verification-status {
margin: 0 auto;
width: 834px;
}
.verification-cards {
display: flex;
gap: 24px;
}
</style>