initial
This commit is contained in:
94
apps/client/pages/2fa/index.vue
Normal file
94
apps/client/pages/2fa/index.vue
Normal 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>
|
||||
186
apps/client/pages/2fa/setup.vue
Normal file
186
apps/client/pages/2fa/setup.vue
Normal 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
833
apps/client/pages/_.vue
Normal 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>
|
||||
146
apps/client/pages/create/invoice/[projectId].vue
Normal file
146
apps/client/pages/create/invoice/[projectId].vue
Normal 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>
|
||||
451
apps/client/pages/create/withdraw/[projectId]/[assetCode].vue
Normal file
451
apps/client/pages/create/withdraw/[projectId]/[assetCode].vue
Normal 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>
|
||||
95
apps/client/pages/login/index.vue
Normal file
95
apps/client/pages/login/index.vue
Normal 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>
|
||||
97
apps/client/pages/projects/[projectId].vue
Normal file
97
apps/client/pages/projects/[projectId].vue
Normal 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>
|
||||
110
apps/client/pages/projects/[projectId]/index.vue
Normal file
110
apps/client/pages/projects/[projectId]/index.vue
Normal 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>
|
||||
288
apps/client/pages/projects/[projectId]/invoices.vue
Normal file
288
apps/client/pages/projects/[projectId]/invoices.vue
Normal 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>
|
||||
220
apps/client/pages/projects/[projectId]/transactions.vue
Normal file
220
apps/client/pages/projects/[projectId]/transactions.vue
Normal 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>
|
||||
78
apps/client/pages/projects/create.vue
Normal file
78
apps/client/pages/projects/create.vue
Normal 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>
|
||||
137
apps/client/pages/projects/index.vue
Normal file
137
apps/client/pages/projects/index.vue
Normal 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>
|
||||
23
apps/client/pages/register/email-confirmation.vue
Normal file
23
apps/client/pages/register/email-confirmation.vue
Normal 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>
|
||||
92
apps/client/pages/register/index.vue
Normal file
92
apps/client/pages/register/index.vue
Normal 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>
|
||||
178
apps/client/pages/report/[projectId].vue
Normal file
178
apps/client/pages/report/[projectId].vue
Normal 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>
|
||||
23
apps/client/pages/reset-password/email-confirmation.vue
Normal file
23
apps/client/pages/reset-password/email-confirmation.vue
Normal 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>
|
||||
48
apps/client/pages/reset-password/index.vue
Normal file
48
apps/client/pages/reset-password/index.vue
Normal 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>
|
||||
65
apps/client/pages/reset-password/new-authorized.vue
Normal file
65
apps/client/pages/reset-password/new-authorized.vue
Normal 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>
|
||||
57
apps/client/pages/reset-password/new.vue
Normal file
57
apps/client/pages/reset-password/new.vue
Normal 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>
|
||||
45
apps/client/pages/settings.vue
Normal file
45
apps/client/pages/settings.vue
Normal 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>
|
||||
62
apps/client/pages/settings/index.vue
Normal file
62
apps/client/pages/settings/index.vue
Normal 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>
|
||||
130
apps/client/pages/settings/limits.vue
Normal file
130
apps/client/pages/settings/limits.vue
Normal 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>
|
||||
22
apps/client/pages/settings/notifications.vue
Normal file
22
apps/client/pages/settings/notifications.vue
Normal 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>
|
||||
27
apps/client/pages/settings/safety.vue
Normal file
27
apps/client/pages/settings/safety.vue
Normal 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>
|
||||
158
apps/client/pages/verification/index.vue
Normal file
158
apps/client/pages/verification/index.vue
Normal 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>
|
||||
39
apps/client/pages/verification/processing.vue
Normal file
39
apps/client/pages/verification/processing.vue
Normal 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>
|
||||
53
apps/client/pages/verification/status.vue
Normal file
53
apps/client/pages/verification/status.vue
Normal 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>
|
||||
Reference in New Issue
Block a user