initial
This commit is contained in:
24
apps/client/.gitignore
vendored
Normal file
24
apps/client/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
2
apps/client/.npmrc
Normal file
2
apps/client/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
13
apps/client/app.vue
Normal file
13
apps/client/app.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtLoadingIndicator :height="4" />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useHead({
|
||||
title: 'Indefiti',
|
||||
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
|
||||
})
|
||||
</script>
|
||||
7
apps/client/assets/styles.scss
Normal file
7
apps/client/assets/styles.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#__nuxt {
|
||||
height: 100%;
|
||||
}
|
||||
165
apps/client/components/asset/card.vue
Normal file
165
apps/client/components/asset/card.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="asset-card" :class="{ 'is-active': isActive, 'is-disabled': disabled }">
|
||||
<UiCoin
|
||||
class="asset-card__coin"
|
||||
:code="code"
|
||||
/>
|
||||
|
||||
<div class="asset-card__name-wrapper">
|
||||
<p class="asset-card__code">
|
||||
{{ code }}
|
||||
</p>
|
||||
|
||||
<p class="asset-card__name">
|
||||
{{ name }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="asset-card__money">
|
||||
<p class="asset-card__balance">
|
||||
<span
|
||||
v-if="showConverted"
|
||||
class="asset-card__converted"
|
||||
>
|
||||
{{ convertedBalance }}
|
||||
</span>
|
||||
<span>{{ balance }}</span>
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="withdraw"
|
||||
class="asset-card__withdraw"
|
||||
>
|
||||
<span
|
||||
v-if="showConverted"
|
||||
class="asset-card__converted"
|
||||
>
|
||||
{{ convertedWithdraw }}
|
||||
</span>
|
||||
<span>{{ withdraw }}</span>
|
||||
<UiIconSUpRight class="asset-card__withdraw-icon" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type Decimal from 'decimal.js'
|
||||
|
||||
interface Props {
|
||||
code: string
|
||||
name?: string
|
||||
balance: Decimal.Value
|
||||
withdraw?: Decimal.Value
|
||||
rate: Decimal.Value
|
||||
isActive?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const showConverted = computed(() => !!props.rate && props.rate !== 1)
|
||||
const convertedBalance = computed(() => $money.fullFormat($money.convert(props.balance, props.rate), 'USDT'))
|
||||
const convertedWithdraw = computed(() => $money.fullFormat($money.convert(props.balance, props.rate), 'USDT'))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.asset-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
background-color: $clr-grey-100;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
cursor: pointer;
|
||||
transition: .2s ease-out;
|
||||
transition-property: outline-color, background-color;
|
||||
min-height: 97px;
|
||||
|
||||
&:hover {
|
||||
outline-color: $clr-grey-300;
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&:active {
|
||||
background-color: $clr-grey-200;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
outline-color: $clr-cyan-300;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__name-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__coin {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__code {
|
||||
@include txt-l-sb;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include txt-r-m;
|
||||
|
||||
margin-top: 2px;
|
||||
color: $clr-grey-400;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__money {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__balance {
|
||||
@include txt-l-sb;
|
||||
}
|
||||
|
||||
&__withdraw {
|
||||
@include txt-i-sb;
|
||||
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__balance,
|
||||
&__withdraw {
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&__withdraw-icon {
|
||||
color: $clr-cyan-500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__converted {
|
||||
@include txt-i-m;
|
||||
|
||||
vertical-align: middle;
|
||||
color: $clr-grey-400;
|
||||
|
||||
&::after {
|
||||
content: '/';
|
||||
margin-inline: 4px;
|
||||
color: $clr-grey-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
apps/client/components/asset/sidebar.vue
Normal file
73
apps/client/components/asset/sidebar.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<Sidebar id="asset" class="asset-sidebar" @close="$emit('close')">
|
||||
<UiCoin :code="asset.code" class="asset-sidebar__coin" />
|
||||
|
||||
<div>
|
||||
<MoneyAmount class="asset-sidebar__balance" :value="asset.balance" :currency="asset.code" />
|
||||
</div>
|
||||
|
||||
<div class="asset-sidebar__actions">
|
||||
<UiButton type="ghost" size="small" icon="s-up-right" :href="`/create/withdraw/${projectId}/${asset.code}`">
|
||||
Отправить
|
||||
</UiButton>
|
||||
|
||||
<UiButton type="ghost" size="small" icon="s-down-left" :href="`/create/invoice/${projectId}`">
|
||||
Получить
|
||||
</UiButton>
|
||||
|
||||
<UiButton type="ghost" color="secondary" size="small" icon="s-exchange" disabled>
|
||||
Обменять
|
||||
</UiButton>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Sidebar } from '#components'
|
||||
|
||||
defineProps({
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
asset: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.asset-sidebar {
|
||||
text-align: center;
|
||||
|
||||
&__coin {
|
||||
border-radius: 9px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__balance {
|
||||
@include h3('money-amount-value', true);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
--button-ghost-primary-color: #{$clr-black};
|
||||
--button-icon-color: #{$clr-cyan-500};
|
||||
--button-icon-disabled-color: #{$clr-grey-400};
|
||||
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
background-color: $clr-grey-100;
|
||||
border-radius: 12px;
|
||||
margin-top: 40px;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
apps/client/components/authorization/email-confirmation.vue
Normal file
107
apps/client/components/authorization/email-confirmation.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="email-confirmation">
|
||||
<img src="/email-confirmation.svg" alt="logo" draggable="false">
|
||||
|
||||
<h1 class="email-confirmation__title">
|
||||
{{ $t('check_email') }}
|
||||
</h1>
|
||||
|
||||
<div class="email-confirmation__content">
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="we_have_sent_you_an_email_to"
|
||||
tag="p"
|
||||
class="email-confirmation__text"
|
||||
>
|
||||
<strong class="email-confirmation__email">{{ email }}</strong>
|
||||
</i18n-t>
|
||||
|
||||
<div class="email-confirmation__instructions">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="email-confirmation__upper-text">
|
||||
<i18n-t class="mb-18" keypath="check_spam_folder" tag="p" scope="global">
|
||||
<strong class="text-grey-600">«{{ $t('spam') }}»</strong>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div v-if="resendFn" class="email-confirmation__resend">
|
||||
<span>{{ $t('did_not_get_mail') }}</span>
|
||||
|
||||
<UiButton class="ml-4" type="link" @click="resendFn">
|
||||
{{ $t('send_again') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
email: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
resendFn: { type: Function },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.email-confirmation {
|
||||
width: 516px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
background-color: $clr-grey-200;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-l;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__instructions {
|
||||
@include txt-l-sb;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__email {
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__upper-text {
|
||||
@include txt-m;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__resend {
|
||||
@include txt-i-m;
|
||||
|
||||
color: $clr-grey-600;
|
||||
}
|
||||
|
||||
&__under-text {
|
||||
@include txt-i-m;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
apps/client/components/authorization/faq-password.vue
Normal file
48
apps/client/components/authorization/faq-password.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<UiAccordion>
|
||||
<UiAccordionItem
|
||||
:title="$t('forgot_password_questions.forgot_password.title')"
|
||||
>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="forgot_password_questions.forgot_password.content"
|
||||
tag="p"
|
||||
>
|
||||
<UiButton type="link" href="/reset-password">
|
||||
{{ $t('can_reset_password') }}
|
||||
</UiButton>
|
||||
<UiButton type="link">
|
||||
{{ $t('write_us') }}
|
||||
</UiButton>
|
||||
</i18n-t>
|
||||
</UiAccordionItem>
|
||||
|
||||
<UiAccordionItem
|
||||
:title="$t('forgot_password_questions.access_to_mail.title')"
|
||||
>
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="forgot_password_questions.access_to_mail.content"
|
||||
tag="p"
|
||||
>
|
||||
<UiButton type="link">
|
||||
{{ $t('fill_the_form') }}
|
||||
</UiButton>
|
||||
</i18n-t>
|
||||
</UiAccordionItem>
|
||||
|
||||
<UiAccordionItem :title="$t('forgot_password_questions.no_mail.title')">
|
||||
<i18n-t
|
||||
keypath="forgot_password_questions.no_mail.content"
|
||||
tag="p"
|
||||
scope="global"
|
||||
>
|
||||
<UiButton type="link">
|
||||
{{ $t('write_us') }}
|
||||
</UiButton>
|
||||
</i18n-t>
|
||||
</UiAccordionItem>
|
||||
</UiAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
131
apps/client/components/checkbox-button.vue
Normal file
131
apps/client/components/checkbox-button.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
cn.b(),
|
||||
cn.is('checked', checked),
|
||||
cn.is('invalid', invalid),
|
||||
cn.is('disabled', disabled),
|
||||
cn.is('focused', focused),
|
||||
]"
|
||||
@click="handleChange"
|
||||
>
|
||||
<UiIconSCheck :class="[cn.e('checkmark')]" />
|
||||
|
||||
<p :class="[cn.e('label')]">
|
||||
<slot v-bind="{ checked }">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
id: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
modelValue?: boolean | string | number
|
||||
trueValue?: boolean | string | number
|
||||
falseValue?: boolean | string | number
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'CheckboxButton',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
required: false,
|
||||
modelValue: undefined,
|
||||
})
|
||||
const slots = useSlots()
|
||||
const { checked, invalid, focused, handleChange } = useCheckbox(props, slots)
|
||||
|
||||
const cn = useClassname('checkbox-button')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.checkbox-button {
|
||||
$self: &;
|
||||
|
||||
@include txt-i-m;
|
||||
|
||||
--border-color: #{$clr-grey-300};
|
||||
--border-width: 1px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
outline: var(--border-width) solid var(--border-color);
|
||||
outline-offset: calc(var(--border-width) * -1);
|
||||
cursor: pointer;
|
||||
transition: .2s ease-out;
|
||||
transition-property: outline-color, background-color, color;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
--border-color: transparent;
|
||||
|
||||
background-color: $clr-grey-200;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
--border-color: transparent;
|
||||
|
||||
background-color: $clr-cyan-500;
|
||||
color: $clr-white;
|
||||
|
||||
&:hover {
|
||||
background-color: $clr-cyan-400;
|
||||
}
|
||||
}
|
||||
|
||||
//&.is-invalid {
|
||||
// --border-color: var(--checkbox-invalid-border-color);
|
||||
//}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&__checkmark {
|
||||
--border-color: var(--checkbox-border-color);
|
||||
--border-width: 1px;
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 4px;
|
||||
outline: var(--border-width) solid var(--border-color);
|
||||
outline-offset: calc(var(--border-width) * -1);
|
||||
color: transparent;
|
||||
cursor: pointer;
|
||||
transition: .2s ease-out;
|
||||
transition-property: outline-color, background-color, color;
|
||||
margin-right: 8px;
|
||||
|
||||
#{$self}.has-label & {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
#{$self}.is-checked & {
|
||||
--border-width: 0;
|
||||
|
||||
color: var(--checkbox-checked-color);
|
||||
background-color: var(--checkbox-checked-background);
|
||||
}
|
||||
|
||||
#{$self}.is-disabled & {
|
||||
--border-color: var(--checkbox-disabled-border-color);
|
||||
}
|
||||
|
||||
#{$self}.is-disabled.is-checked & {
|
||||
color: var(--checkbox-disabled-checked-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
apps/client/components/currency-name.vue
Normal file
73
apps/client/components/currency-name.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div
|
||||
class="currency-name"
|
||||
:class="`currency-name--${size}`"
|
||||
>
|
||||
<UiCoin
|
||||
class="currency-name__coin"
|
||||
:code="code"
|
||||
/>
|
||||
|
||||
<span
|
||||
v-if="size !== 'small'"
|
||||
class="currency-name__name"
|
||||
>
|
||||
{{ name || code }}
|
||||
</span>
|
||||
<span class="currency-name__code">{{ code }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
code: string
|
||||
name?: string
|
||||
size?: 'small' | 'medium'
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'CurrencyName',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'medium',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.currency-name {
|
||||
&--medium {
|
||||
display: grid;
|
||||
grid-template-columns: 32px auto;
|
||||
gap: var(--currency-name-gap, 4px 8px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&--small {
|
||||
--coin-size: 20px;
|
||||
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--currency-name-gap, 4px);
|
||||
}
|
||||
|
||||
&__coin {
|
||||
grid-column: 1;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include txt-r-sb('currency-name');
|
||||
|
||||
color: $clr-black;
|
||||
}
|
||||
|
||||
&__code {
|
||||
@include txt-s-m('currency-name-code');
|
||||
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
apps/client/components/form-header.vue
Normal file
35
apps/client/components/form-header.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="form-header">
|
||||
<UiButton
|
||||
icon="chevron-left"
|
||||
color="secondary"
|
||||
type="link"
|
||||
class="form-header__back"
|
||||
:href="backLink"
|
||||
>
|
||||
{{ backText }}
|
||||
</UiButton>
|
||||
|
||||
<slot name="title">
|
||||
<h1 class="form-header__title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
backLink: { type: String, required: true },
|
||||
backText: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-header {
|
||||
&__back {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
apps/client/components/formatted-date.vue
Normal file
44
apps/client/components/formatted-date.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="formatted-date">
|
||||
<p class="formatted-date__date">
|
||||
{{ date }} <span class="formatted-date__time">{{ time }}</span>
|
||||
</p>
|
||||
<p class="formatted-date__zone">
|
||||
(UTC+3)
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: String, required: true },
|
||||
})
|
||||
|
||||
const dayObj = computed(() => dayjs(props.value))
|
||||
const date = computed(() => dayObj.value.format('DD.MM.YY'))
|
||||
const time = computed(() => dayObj.value.format('HH:mm'))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.formatted-date {
|
||||
&__date {
|
||||
@include txt-r-sb;
|
||||
|
||||
color: $clr-black;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__time {
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
|
||||
&__zone {
|
||||
@include txt-s-m;
|
||||
|
||||
color: $clr-grey-500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
apps/client/components/invoices/empty.vue
Normal file
46
apps/client/components/invoices/empty.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="invoices-empty">
|
||||
<img
|
||||
src="/flag.svg"
|
||||
class="mb-40"
|
||||
>
|
||||
|
||||
<h2>
|
||||
{{ $t('your_invoices_will_be_displayed_here.title') }}
|
||||
</h2>
|
||||
|
||||
<h4 class="invoices-empty__text">
|
||||
{{ $t('your_invoices_will_be_displayed_here.content') }}
|
||||
</h4>
|
||||
|
||||
<UiButton
|
||||
size="large"
|
||||
class="mt-40 w-100"
|
||||
:href="`/create/invoice/${id}`"
|
||||
>
|
||||
{{ $t('create_an_invoice') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ id: { type: String, required: true } })
|
||||
|
||||
defineEmits(['create'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoices-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 422px;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
|
||||
&__text {
|
||||
color: $clr-grey-400;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
79
apps/client/components/navigation/item.vue
Normal file
79
apps/client/components/navigation/item.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
class="nav-item"
|
||||
:class="{
|
||||
'nav-item--active': isActive,
|
||||
}"
|
||||
:to="to"
|
||||
>
|
||||
<slot name="icon">
|
||||
<Component
|
||||
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||
class="nav-item__icon"
|
||||
/>
|
||||
</slot>
|
||||
<span class="nav-item__title">
|
||||
{{ title }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { UiIcon } from '#build/types/ui/icons'
|
||||
|
||||
interface Props {
|
||||
to: string
|
||||
icon: UiIcon
|
||||
title: string
|
||||
matcher?: () => boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const isActive = computed(() => (props.matcher ? props.matcher() : false))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.nav-item {
|
||||
--link-color: #{$clr-white};
|
||||
|
||||
@include txt-i-sb;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--link-color);
|
||||
|
||||
transition: color 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
--link-color: #{$clr-grey-500};
|
||||
}
|
||||
|
||||
&--active,
|
||||
&:active {
|
||||
--link-color: #{$clr-cyan-300} !important;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__notification-icon {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: -8px;
|
||||
right: -24px;
|
||||
background: $clr-white;
|
||||
color: $clr-white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
10
apps/client/components/navigation/logo.vue
Normal file
10
apps/client/components/navigation/logo.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<NuxtLink to="/projects">
|
||||
<img
|
||||
class="app-logo__img"
|
||||
src="/logo-with-text.svg"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
46
apps/client/components/navigation/sidebar.vue
Normal file
46
apps/client/components/navigation/sidebar.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<nav class="nav-sidebar">
|
||||
<div
|
||||
v-if="$slots.logo"
|
||||
class="nav-sidebar__logo"
|
||||
>
|
||||
<slot name="logo" />
|
||||
</div>
|
||||
<div class="nav-sidebar__content">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="nav-sidebar__bottom">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.nav-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: #{$clr-black};
|
||||
color: #{$clr-white};
|
||||
padding: 32px 27px 32px 27px;
|
||||
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&__logo {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
gap: 20px;
|
||||
margin-inline: -27px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
183
apps/client/components/network-select.vue
Normal file
183
apps/client/components/network-select.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div
|
||||
class="network-select network-select--secondary network-select--large"
|
||||
:class="{
|
||||
'has-value': !!field.value.value,
|
||||
'is-disabled': disabled,
|
||||
'is-invalid': invalid,
|
||||
}"
|
||||
v-bind="$attrs"
|
||||
@click="show"
|
||||
>
|
||||
<label
|
||||
ref="wrapper"
|
||||
class="network-select__wrapper"
|
||||
tabindex="0"
|
||||
@keydown.enter="show"
|
||||
@keydown.space="show"
|
||||
>
|
||||
|
||||
<span class="network-select__content">
|
||||
<div
|
||||
v-if="!!field.value.value"
|
||||
class="network-select__value"
|
||||
>
|
||||
<UiCoin
|
||||
:code="assetCode"
|
||||
class="network-select__coin"
|
||||
/>
|
||||
<span>{{ modelValue }}</span>
|
||||
</div>
|
||||
|
||||
<p class="network-select__label">{{ $t('network') }}</p>
|
||||
|
||||
<UiButton
|
||||
class="network-select__action"
|
||||
type="ghost"
|
||||
size="small"
|
||||
disabled
|
||||
>
|
||||
{{ actionText }}
|
||||
</UiButton>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="invalid"
|
||||
class="network-select__bottom"
|
||||
>
|
||||
<div class="network-select__validation-message">
|
||||
{{ field.errorMessage.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { useField } from 'vee-validate'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
assetCode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const id = toRef(props, 'id')
|
||||
|
||||
const { t } = useI18n()
|
||||
const field = useField(id, 'required', {
|
||||
validateOnValueUpdate: false,
|
||||
syncVModel: true,
|
||||
initialValue: !isEmptyValue(props.modelValue) ? props.modelValue : undefined,
|
||||
})
|
||||
|
||||
const wrapper = ref()
|
||||
const active = ref(false)
|
||||
|
||||
const invalid = computed(() => !props.disabled && !!field.errorMessage.value)
|
||||
|
||||
const actionText = computed(() =>
|
||||
field.value.value ? t('change') : t('select'),
|
||||
)
|
||||
|
||||
watch(field.value, hide)
|
||||
|
||||
watch(active, (value) => {
|
||||
if (!value)
|
||||
wrapper.value?.focus()
|
||||
})
|
||||
|
||||
function show() {
|
||||
active.value = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
active.value = false
|
||||
}
|
||||
|
||||
function isEmptyValue(value) {
|
||||
return [null, undefined, ''].includes(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.network-select {
|
||||
$self: &;
|
||||
|
||||
&__wrapper {
|
||||
display: block;
|
||||
border-radius: 12px;
|
||||
outline: 1px solid $clr-grey-300;
|
||||
outline-offset: -1px;
|
||||
padding-inline: 16px;
|
||||
background-color: $clr-white;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include txt-i-m;
|
||||
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 15px;
|
||||
left: 0;
|
||||
color: $clr-grey-400;
|
||||
transform-origin: 0 0;
|
||||
|
||||
#{$self}.has-value & {
|
||||
transform: translateY(-7px) scale(0.78);
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
@include txt-i-m;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-block: 22px 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__coin {
|
||||
--coin-size: 16px;
|
||||
--coin-border-radius: 3px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
@include txt-s-m;
|
||||
|
||||
margin-top: 4px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
&__validation-message {
|
||||
color: var(--input-validation-message-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
110
apps/client/components/notification-card/base.vue
Normal file
110
apps/client/components/notification-card/base.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="notification-card" :class="{ 'not-read': !read }">
|
||||
<div class="notification-card__icon">
|
||||
<Component :is="resolveComponent(`ui-icon-${icon}`)" />
|
||||
</div>
|
||||
|
||||
<div class="notification-card__content">
|
||||
<p class="notification-card__title">
|
||||
{{ title }}
|
||||
</p>
|
||||
|
||||
<div class="notification-card__subtitle-wrapper">
|
||||
<span class="notification-card__subtitle">
|
||||
{{ subtitle }}
|
||||
</span>
|
||||
|
||||
<span class="notification-card__date">
|
||||
Сегодня в 14:40
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UiIcon } from '#build/types/ui/icons'
|
||||
|
||||
export interface Props {
|
||||
icon: UiIcon
|
||||
title: string
|
||||
subtitle: string
|
||||
read: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notification-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 32px auto;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
outline: 1px solid transparent;
|
||||
outline-offset: -1px;
|
||||
transition: .2s ease-out;
|
||||
transition-property: background-color, outline-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #F7F9FF;
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline-color: $clr-cyan-300;
|
||||
}
|
||||
|
||||
&.not-read {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: $clr-red-500;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
color: var(--notification-card-icon-color, $clr-grey-500);
|
||||
background-color: var(--notification-card-icon-background, $clr-grey-200);
|
||||
}
|
||||
|
||||
&__content {
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include txt-r-sb;
|
||||
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__subtitle-wrapper {
|
||||
@include txt-t-m;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: $clr-grey-600;
|
||||
}
|
||||
|
||||
&__date {
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<NotificationCardBase icon="s-up-right" v-bind="props" class="card">
|
||||
<p class="amount">
|
||||
<span>Валюта: <strong>USDT</strong></span>
|
||||
<span>Сумма: <strong>500</strong></span>
|
||||
</p>
|
||||
</NotificationCardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Props } from './base.vue'
|
||||
|
||||
const props = defineProps<Omit<Props, 'icon'>>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
--notification-card-icon-color: #{$clr-cyan-600};
|
||||
}
|
||||
|
||||
.amount {
|
||||
@include txt-s;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
color: $clr-grey-500;
|
||||
margin-top: 8px;
|
||||
background-color: $clr-grey-200;
|
||||
height: 32px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
apps/client/components/notification-card/deposit.vue
Normal file
39
apps/client/components/notification-card/deposit.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<NotificationCardBase icon="s-down-left" v-bind="props" class="card">
|
||||
<p class="amount">
|
||||
<span>Валюта: <strong>USDT</strong></span>
|
||||
<span>Сумма: <strong>500</strong></span>
|
||||
</p>
|
||||
</NotificationCardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Props } from './base.vue'
|
||||
|
||||
const props = defineProps<Omit<Props, 'icon'>>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
--notification-card-icon-color: #{$clr-green-500};
|
||||
--notification-card-icon-background: #{$clr-green-100};
|
||||
}
|
||||
|
||||
.amount {
|
||||
@include txt-s;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
color: $clr-grey-500;
|
||||
margin-top: 8px;
|
||||
background-color: $clr-grey-200;
|
||||
height: 32px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
43
apps/client/components/notification-card/sign-in.vue
Normal file
43
apps/client/components/notification-card/sign-in.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<NotificationCardBase icon="s-exit" v-bind="props">
|
||||
<p class="location">
|
||||
Местоположение: Moscow, Russia
|
||||
</p>
|
||||
|
||||
<p class="alert">
|
||||
Если это были не вы срочно
|
||||
<UiButton type="link" size="small">
|
||||
смените пароль
|
||||
</UiButton>
|
||||
или
|
||||
<UiButton type="link" size="small">
|
||||
свяжитесь с поддержкой
|
||||
</UiButton>
|
||||
</p>
|
||||
</NotificationCardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Props } from './base.vue'
|
||||
|
||||
const props = defineProps<Omit<Props, 'icon'>>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.location {
|
||||
@include txt-s-m;
|
||||
|
||||
color: $clr-grey-400;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
@include txt-r-m;
|
||||
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: $clr-grey-200;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<NotificationCardBase icon="s-clock" v-bind="props" class="card">
|
||||
<div class="row">
|
||||
<p class="amount">
|
||||
<span>Валюта: <strong>USDT</strong></span>
|
||||
<span>Сумма: <strong>500</strong></span>
|
||||
</p>
|
||||
|
||||
<UiButton class="support" size="small" type="outlined" color="secondary">
|
||||
Support
|
||||
</UiButton>
|
||||
</div>
|
||||
</NotificationCardBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Props } from './base.vue'
|
||||
|
||||
const props = defineProps<Omit<Props, 'icon'>>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
--notification-card-icon-color: #{$clr-warn-500};
|
||||
--notification-card-icon-background: #{$clr-warn-200};
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
@include txt-s;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
color: $clr-grey-500;
|
||||
background-color: $clr-grey-200;
|
||||
height: 32px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
apps/client/components/notifications-dropdown.vue
Normal file
107
apps/client/components/notifications-dropdown.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<UiDropdown
|
||||
class="notifications-dropdown"
|
||||
dropdown-class="notifications-dropdown__content"
|
||||
placement="bottom-end"
|
||||
trigger="hover"
|
||||
:offset="12"
|
||||
teleport
|
||||
>
|
||||
<template #default="{ isActive }">
|
||||
<NotifyButton
|
||||
icon="bell"
|
||||
:count="0"
|
||||
class="notifications-dropdown__trigger"
|
||||
:state="isActive ? 'hover' : undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #dropdown>
|
||||
<div class="notifications-dropdown__header">
|
||||
<h3>Notifications</h3>
|
||||
|
||||
<UiButton class="ml-a" size="small" icon="s-check-seen" type="ghost">
|
||||
Mark all as read
|
||||
</UiButton>
|
||||
|
||||
<UiButton class="ml-4" icon="s-kebab-android" type="link" size="small" color="secondary" />
|
||||
</div>
|
||||
|
||||
<form class="notifications-dropdown__filter">
|
||||
<UiSwitcher
|
||||
id="notification_type"
|
||||
:options="[
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Поступления', value: 'p' },
|
||||
{ label: 'Списания', value: 's' },
|
||||
]"
|
||||
size="small"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div class="notifications-dropdown__list">
|
||||
<NotificationCardWithdrawCreated title="Новый вывод средств создан" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||
<NotificationCardDeposit title="На ваш кошелек поступил новый платеж" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||
<NotificationCardCompletedWithdraw title="Вывод средств был проведен успешно" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||
<NotificationCardSignIn title="Вход в учетную запись" subtitle="IP: 185.218.108.156" read />
|
||||
</div>
|
||||
</template>
|
||||
</UiDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
useForm({
|
||||
keepValuesOnUnmount: true,
|
||||
initialValues: {
|
||||
notification_type: 'all',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notifications-dropdown {
|
||||
cursor: pointer;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__filter {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
//margin-inline: -16px;
|
||||
//padding-inline: 16px;
|
||||
//max-height: 150px;
|
||||
//overflow: auto;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
width: 406px;
|
||||
padding: 16px !important;
|
||||
box-shadow: 0px 4px 4px 0px #6C86AD40;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 12px;
|
||||
background-color: $clr-white;
|
||||
border-radius: 2px;
|
||||
transform: rotateZ(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
apps/client/components/notify-button.vue
Normal file
57
apps/client/components/notify-button.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<UiButton
|
||||
:class="[cn.b(), cn.has('one', count === 1), cn.has('few', count > 1)]"
|
||||
:icon="icon"
|
||||
type="ghost"
|
||||
color="secondary"
|
||||
:data-count="countContent"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ icon: { type: String }, count: { type: Number } })
|
||||
|
||||
const cn = useClassname('notify-button')
|
||||
|
||||
const countContent = computed(() => {
|
||||
return props.count > 9 ? '9+' : props.count
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notify-button {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
@include txt-s-m;
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: $clr-red-500;
|
||||
color: $clr-white;
|
||||
}
|
||||
|
||||
&.has-one {
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-few {
|
||||
&::after {
|
||||
content: attr(data-count);
|
||||
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
padding-inline: 4px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
apps/client/components/operation-type.vue
Normal file
21
apps/client/components/operation-type.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="operation-type">
|
||||
<span class="mr-4">{{ type }}</span>
|
||||
<UiIconSUpRight class="text-clr-cyan-500" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.operation-type{
|
||||
@include txt-r-sb;
|
||||
}
|
||||
</style>
|
||||
77
apps/client/components/page-form.vue
Normal file
77
apps/client/components/page-form.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<form
|
||||
class="page-form"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UiButton
|
||||
icon="chevron-left"
|
||||
color="secondary"
|
||||
type="link"
|
||||
class="mb-8"
|
||||
:href="backLink"
|
||||
>
|
||||
{{ backText }}
|
||||
</UiButton>
|
||||
|
||||
<slot name="title">
|
||||
<h1 :class="titleOffset ? 'mb-32' : ''">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</slot>
|
||||
|
||||
<div class="page-form__summary">
|
||||
<StaticError class="mb-16" />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<slot name="submit" v-bind="{ isSubmitting }">
|
||||
<UiButton
|
||||
class="page-form__submit"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
{{ submitText }}
|
||||
</UiButton>
|
||||
</slot>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useForm } from 'vee-validate'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true },
|
||||
backLink: { type: String, required: true },
|
||||
backText: { type: String, required: true },
|
||||
submitText: { type: String, required: true },
|
||||
titleOffset: { type: Boolean, default: true },
|
||||
handler: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { handleSubmit, isSubmitting } = useForm()
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
await props.handler(values)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-form {
|
||||
margin: 0 auto;
|
||||
width: 500px;
|
||||
|
||||
&__summary {
|
||||
padding-top: var(--page-form-padding-top, 24px);
|
||||
padding-bottom: var(--page-form-padding-bottom, 16px);
|
||||
}
|
||||
|
||||
&__submit {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
apps/client/components/page/block.vue
Normal file
51
apps/client/components/page/block.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="page-block">
|
||||
<div class="page-block__header">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center" style="gap: 16px">
|
||||
<h3 v-if="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
|
||||
<slot name="badge" />
|
||||
</div>
|
||||
<span v-if="subTitle" class="page-block__subtitle">{{ subTitle }}</span>
|
||||
</div>
|
||||
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String },
|
||||
subTitle: { type: String },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--page-block-gap, 8px);
|
||||
|
||||
border-radius: 12px;
|
||||
padding: var(--page-block-padding, 16px);
|
||||
background-color: $clr-white;
|
||||
|
||||
&__header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__subtitle{
|
||||
@include txt-i-m;
|
||||
margin-top: 4px;
|
||||
color: $clr-grey-500;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
apps/client/components/page/footer-info-block.vue
Normal file
78
apps/client/components/page/footer-info-block.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="footer-info-block">
|
||||
<div class="footer-info-block__left">
|
||||
<UiIconAskForDiscountFilled class="footer-info-block__icon" />
|
||||
<div>
|
||||
<h3 class="footer-info-block__title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="footer-info-block__text">
|
||||
{{ text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-info-block__right">
|
||||
<UiButton
|
||||
color="secondary"
|
||||
right-icon="s-chevron-right"
|
||||
>
|
||||
{{ action }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
text: { type: String, required: true },
|
||||
action: { type: String, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.footer-info-block {
|
||||
display: flex;
|
||||
|
||||
&__title {
|
||||
color: $clr-grey-600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $clr-grey-500;
|
||||
width: 512px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: $clr-market-500 !important;
|
||||
margin-right: 32px;
|
||||
padding: 9px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
border-bottom-left-radius: 12px;
|
||||
border-top-left-radius: 12px;
|
||||
|
||||
background-color: $clr-grey-200;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-bottom-right-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
background-color: $clr-grey-300;
|
||||
|
||||
width: 286px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
apps/client/components/page/header.vue
Normal file
90
apps/client/components/page/header.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<header class="page-header">
|
||||
<div class="page-header__title">
|
||||
<UiButton
|
||||
v-if="withBackButton"
|
||||
icon="arrow-left"
|
||||
color="secondary"
|
||||
class="mr-24"
|
||||
href="/projects"
|
||||
type="outlined"
|
||||
/>
|
||||
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<div
|
||||
v-if="$slots.default"
|
||||
class="page-header__default"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header__right">
|
||||
<slot name="right">
|
||||
<div class="d-flex align-items-center" style="gap: 24px;">
|
||||
<NotificationsDropdown />
|
||||
<ProfileDropdown />
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
withBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const notify = useNotify()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'sass:color';
|
||||
|
||||
.page-header {
|
||||
position: sticky;
|
||||
backdrop-filter: blur(4px);
|
||||
background-size: 4px 4px;
|
||||
background-image: radial-gradient(
|
||||
color.change($clr-grey-100, $alpha: 0.7) 2px,
|
||||
$clr-cyan-200
|
||||
);
|
||||
border-bottom: 1px solid #f4f6ff;
|
||||
margin-inline: -16px;
|
||||
padding-inline: 16px;
|
||||
top: 0;
|
||||
z-index: 6000;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
//margin-bottom: 32px;
|
||||
padding-block: 32px;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
apps/client/components/page/info-block.vue
Normal file
97
apps/client/components/page/info-block.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="info-block">
|
||||
<div class="d-flex justify-content-between mb-16">
|
||||
<p class="info-block__title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<UiBadge v-if="badge" type="marketing">
|
||||
{{ badge }}
|
||||
</UiBadge>
|
||||
</div>
|
||||
|
||||
<slot name="info" />
|
||||
|
||||
<span class="info-block__text">{{ text }}</span>
|
||||
|
||||
<UiButton
|
||||
class="info-block__action"
|
||||
type="outlined"
|
||||
:href="link"
|
||||
right-icon="s-chevron-right"
|
||||
size="small"
|
||||
>
|
||||
{{ action }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
badge: {
|
||||
type: String,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.info-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 270px;
|
||||
min-height: 179px;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
|
||||
background-color: $clr-grey-100;
|
||||
|
||||
&__title {
|
||||
@include txt-m-sb;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-s;
|
||||
margin-bottom: 16px;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__action {
|
||||
margin-top: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.info-block-addInfo {
|
||||
@include txt-s;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 4px;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__sum {
|
||||
@include txt-m-sb;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-r;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
apps/client/components/page/toolbar.vue
Normal file
32
apps/client/components/page/toolbar.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="page-toolbar">
|
||||
<slot name="prefix" />
|
||||
|
||||
<UiSearch
|
||||
class="flex-1"
|
||||
size="large"
|
||||
:label="searchLabel"
|
||||
:model-value="search"
|
||||
@update:model-value="$emit('update:search', $event)"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
searchLabel: { type: String, required: true },
|
||||
search: { type: String },
|
||||
})
|
||||
|
||||
defineEmits(['update:search'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-toolbar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
92
apps/client/components/profile-dropdown.vue
Normal file
92
apps/client/components/profile-dropdown.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<UiDropdown
|
||||
v-if="authenticated"
|
||||
class="profile-dropdown"
|
||||
dropdown-class="profile-dropdown__content"
|
||||
placement="bottom-end"
|
||||
trigger="hover"
|
||||
:offset="12"
|
||||
teleport
|
||||
>
|
||||
<template #default="{ isActive }">
|
||||
<UiButton class="profile-dropdown__trigger" icon="circle-userpic" :state="isActive ? 'hover' : undefined" />
|
||||
</template>
|
||||
|
||||
<template #dropdown>
|
||||
<UiDropdownItem class="mb-12" @click="navigateTo('/2fa')">
|
||||
<template #icon>
|
||||
<UiIconSProtection class="text-clr-cyan-400" />
|
||||
</template>
|
||||
|
||||
<span>Безопасность</span>
|
||||
</UiDropdownItem>
|
||||
|
||||
<UiDropdownItem class="mb-12" @click="navigateTo('/verification/status')">
|
||||
<template #icon>
|
||||
<UiIconSOverview class="text-clr-cyan-400" />
|
||||
</template>
|
||||
|
||||
<span>Лимиты</span>
|
||||
</UiDropdownItem>
|
||||
|
||||
<UiDropdownItem @click="navigateTo('/settings')">
|
||||
<template #icon>
|
||||
<UiIconSSettings class="text-clr-cyan-400" />
|
||||
</template>
|
||||
|
||||
<span>Настройки</span>
|
||||
</UiDropdownItem>
|
||||
|
||||
<UiDropdownSeparator />
|
||||
|
||||
<UiDropdownItem @click="logout">
|
||||
<template #icon>
|
||||
<UiIconSExit class="text-clr-grey-400" />
|
||||
</template>
|
||||
|
||||
<span>Logout</span>
|
||||
</UiDropdownItem>
|
||||
</template>
|
||||
</UiDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { logout, authenticated } = useAuth()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-dropdown {
|
||||
cursor: pointer;
|
||||
|
||||
&__trigger {
|
||||
--button-color: #{$clr-grey-500};
|
||||
--button-background: #{$clr-grey-200};
|
||||
--button-hover-color: #{$clr-white};
|
||||
--button-active-color: #{$clr-white};
|
||||
}
|
||||
|
||||
&__content {
|
||||
@include txt-s-m('dropdown-item', true);
|
||||
|
||||
--dropdown-item-padding: 8px 12px;
|
||||
|
||||
position: relative;
|
||||
color: $clr-grey-600;
|
||||
width: 233px;
|
||||
padding: 16px !important;
|
||||
box-shadow: 0px 4px 4px 0px #6C86AD40;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transform: rotateZ(45deg);
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 12px;
|
||||
background-color: $clr-white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
apps/client/components/project/create.vue
Normal file
35
apps/client/components/project/create.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="project-create">
|
||||
<img src="/flag.svg" class="mb-40">
|
||||
|
||||
<i18n-t scope="global" keypath="create_your_project.title" tag="h2">
|
||||
<span class="text-clr-cyan-500">
|
||||
{{ $t('create_your_project.create') }}
|
||||
</span>
|
||||
</i18n-t>
|
||||
|
||||
<h4 class="project-create__text">
|
||||
{{ $t('create_your_project.content') }}
|
||||
</h4>
|
||||
|
||||
<UiButton size="large" class="mt-40 w-100" href="/projects/create">
|
||||
{{ $t('create') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.project-create {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 422px;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
|
||||
&__text {
|
||||
color: $clr-grey-400;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
apps/client/components/project/info-columns.vue
Normal file
104
apps/client/components/project/info-columns.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="info-columns">
|
||||
<PageInfoBlock
|
||||
title="More limits"
|
||||
text="Get increased limits and advanced features by providing a little more profile information"
|
||||
action="Increase the limit"
|
||||
link="/verification/status"
|
||||
badge="BASIC"
|
||||
style="
|
||||
background-image: url('/block2.jpg');
|
||||
background-size: cover;
|
||||
background-position: bottom right;
|
||||
"
|
||||
>
|
||||
<template #info>
|
||||
<div
|
||||
v-if="!maxState"
|
||||
class="limits"
|
||||
>
|
||||
<p class="limits__date">
|
||||
Updated on <strong>{{ dayjs().format('DD.MM.YYYY') }}</strong>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p class="limits__title">
|
||||
Remaining limit
|
||||
</p>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-between">
|
||||
<span class="limits__current">{{ dayLimit }} <span class="limits__current-text">/ per day</span></span>
|
||||
|
||||
<span class="limits__total">{{ curDayLimit }} USDT</span>
|
||||
</div>
|
||||
<UiProgressBar :progress="(curDayLimit / dayLimit) * 100" />
|
||||
|
||||
<div class="mb-4 mt-24 d-flex justify-content-between">
|
||||
<span class="limits__current">{{ monthLimit }} <span class="limits__current-text">/ per month</span></span>
|
||||
|
||||
<span class="limits__total">{{ curMonthLimit }} USDT</span>
|
||||
</div>
|
||||
<UiProgressBar :progress="(curMonthLimit / monthLimit) * 100" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageInfoBlock>
|
||||
|
||||
<PageInfoBlock
|
||||
title="2FA authentication"
|
||||
text="Complete the verification process to remove withdrawal limits."
|
||||
action="Add 2FA"
|
||||
link="/2fa"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const maxState = ref(false)
|
||||
|
||||
const curDayLimit = ref(100)
|
||||
const curMonthLimit = ref(5200)
|
||||
|
||||
const dayLimit = ref(1000)
|
||||
const monthLimit = ref(10000)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.info-columns {
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.limits {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&__date {
|
||||
@include txt-s;
|
||||
color: $clr-grey-500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include txt-s-m;
|
||||
color: $clr-grey-400;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__total {
|
||||
@include txt-s-m;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&__current {
|
||||
@include txt-m-sb;
|
||||
}
|
||||
|
||||
&__current-text{
|
||||
@include txt-r-sb;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
162
apps/client/components/projects-table.vue
Normal file
162
apps/client/components/projects-table.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<UiPlainTable class="projects-table" :columns="columns" :data="projects">
|
||||
<template #cell(name)="{ row }">
|
||||
<div class="name-cell">
|
||||
<div class="name-cell__initials">
|
||||
{{ getProjectInitials(row.original) }}
|
||||
</div>
|
||||
<div class="name-cell__name">
|
||||
{{ row.original.name }}
|
||||
</div>
|
||||
<div class="name-cell__id">
|
||||
id {{ row.original.id }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(balance)="{ row }">
|
||||
<MoneyAmount :value="row.original.account.balance" currency="USDT" />
|
||||
</template>
|
||||
|
||||
<template #cell(withdraw)="{ row }">
|
||||
<MoneyAmount value="0" currency="USDT" />
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ row }">
|
||||
<UiButton class="mr-16" color="secondary">
|
||||
API
|
||||
</UiButton>
|
||||
<UiButton class="mr-16" color="secondary">
|
||||
Settings
|
||||
</UiButton>
|
||||
<UiButton :href="`/projects/${row.original.id}`">
|
||||
Open
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiPlainTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { createColumnHelper } from '@tanstack/vue-table'
|
||||
|
||||
interface ProjectListItem {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
defineProps({
|
||||
projects: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const columnHelper = createColumnHelper<ProjectListItem>()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Name',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'balance',
|
||||
header: 'Balance',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'withdraw',
|
||||
header: 'In sending',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
}),
|
||||
]
|
||||
|
||||
function getProjectInitials(project: ProjectListItem) {
|
||||
return project.name.slice(0, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.projects-table {
|
||||
@include txt-i-sb('money-amount-value', true);
|
||||
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
text-align: left;
|
||||
margin-block: -8px;
|
||||
|
||||
td,
|
||||
th {
|
||||
margin: 0;
|
||||
padding-inline: 16px;
|
||||
background-color: $clr-grey-100;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
@include txt-i-m;
|
||||
|
||||
color: $clr-grey-500;
|
||||
padding-block: 16px;
|
||||
|
||||
&.name {
|
||||
padding-left: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding-block: 24px;
|
||||
height: 95.8px;
|
||||
|
||||
&.actions {
|
||||
background-color: $clr-grey-200;
|
||||
padding-left: 32px;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 32px auto;
|
||||
gap: 4px 16px;
|
||||
|
||||
&__initials {
|
||||
@include font(20px, 500, 32px);
|
||||
|
||||
background-color: $clr-grey-400;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
color: $clr-white;
|
||||
text-transform: uppercase;
|
||||
grid-column: 1;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include txt-i-sb;
|
||||
|
||||
grid-column: 2;
|
||||
color: $clr-black;
|
||||
}
|
||||
|
||||
&__id {
|
||||
@include txt-s-m;
|
||||
|
||||
grid-column: 2;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
apps/client/components/radio-button.vue
Normal file
106
apps/client/components/radio-button.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
cn.b(),
|
||||
cn.is('checked', checked),
|
||||
cn.is('disabled', disabled),
|
||||
cn.is('focused', focused),
|
||||
cn.is('disabled', disabled),
|
||||
]"
|
||||
@click="handleChange"
|
||||
>
|
||||
<p :class="[cn.e('label')]">
|
||||
<slot>
|
||||
{{ label }}
|
||||
</slot>
|
||||
</p>
|
||||
|
||||
<p v-if="caption || $slots.caption" :class="[cn.e('caption')]">
|
||||
<slot name="caption">
|
||||
{{ caption }}
|
||||
</slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
id: string
|
||||
value: string | number
|
||||
label?: string
|
||||
caption?: string
|
||||
disabled?: boolean
|
||||
modelValue?: string | number
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'RadioButton',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
required: false,
|
||||
modelValue: undefined,
|
||||
})
|
||||
const slots = useSlots()
|
||||
const { checked, focused, handleChange } = useRadio(props, slots)
|
||||
|
||||
const cn = useClassname('radio-button')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.radio-button {
|
||||
$self: &;
|
||||
|
||||
@include txt-i-m;
|
||||
|
||||
--border-color: #{$clr-grey-300};
|
||||
--border-width: 1px;
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
outline: var(--border-width) solid var(--border-color);
|
||||
outline-offset: calc(var(--border-width) * -1);
|
||||
cursor: pointer;
|
||||
transition: .2s ease-out;
|
||||
transition-property: outline-color, background-color, color;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
--border-color: transparent;
|
||||
|
||||
background-color: $clr-grey-200;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
--border-color: transparent;
|
||||
|
||||
background-color: $clr-cyan-500;
|
||||
color: $clr-white;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__caption {
|
||||
@include txt-r-m;
|
||||
|
||||
color: $clr-cyan-400;
|
||||
|
||||
#{$self}.is-checked & {
|
||||
color: $clr-cyan-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
apps/client/components/resource-filter/base.vue
Normal file
145
apps/client/components/resource-filter/base.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<UiDropdown
|
||||
class="resource-filter"
|
||||
position="bottom-start"
|
||||
:offset="0"
|
||||
dropdown-class="resource-filter__dropdown"
|
||||
transition-name="ui-select"
|
||||
>
|
||||
<template #default>
|
||||
<div class="resource-filter__wrapper">
|
||||
<p class="resource-filter__label">
|
||||
{{ label }}
|
||||
</p>
|
||||
|
||||
<div class="resource-filter__content">
|
||||
<p class="resource-filter__value">
|
||||
{{ value }}
|
||||
</p>
|
||||
|
||||
<i
|
||||
v-if="filled"
|
||||
class="resource-filter__clear icon-s-cross-compact"
|
||||
tabindex="0"
|
||||
@click.stop="$emit('clear')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UiIconChevronDown class="resource-filter__chevron" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #dropdown>
|
||||
<slot />
|
||||
</template>
|
||||
</UiDropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
label: string
|
||||
value: string
|
||||
filled?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
filled: false,
|
||||
})
|
||||
|
||||
defineEmits(['clear'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.resource-filter {
|
||||
$self: &;
|
||||
|
||||
&__wrapper {
|
||||
display: grid;
|
||||
grid-template-areas: 'label chevron' 'content chevron';
|
||||
grid-template-columns: 1fr auto;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
background-color: var(--select-background);
|
||||
transition: 0.2s ease-out;
|
||||
transition-property: background-color, border-radius, box-shadow;
|
||||
outline: none;
|
||||
width: 169px;
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
background-color: var(--select-hover-background);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--select-active-background);
|
||||
}
|
||||
|
||||
#{$self}.is-active & {
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 4px 4px 0 #6c86ad40;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include txt-s-m;
|
||||
|
||||
grid-area: label;
|
||||
color: var(--select-label-color);
|
||||
transition: color 0.2s ease-out;
|
||||
user-select: none;
|
||||
|
||||
#{$self}.is-active &,
|
||||
#{$self}__wrapper:hover &,
|
||||
#{$self}__wrapper:focus-visible & {
|
||||
color: var(--select-label-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
grid-area: content;
|
||||
}
|
||||
|
||||
&__value {
|
||||
@include txt-i-m;
|
||||
|
||||
background: none;
|
||||
color: var(--select-color);
|
||||
padding: 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
appearance: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--select-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
color: var(--select-clear-color);
|
||||
transition: color 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--select-clear-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
color: var(--select-chevron-color);
|
||||
grid-area: chevron;
|
||||
}
|
||||
|
||||
&__dropdown {
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 4px 4px 0 #6c86ad40;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
apps/client/components/resource-filter/calendar.vue
Normal file
51
apps/client/components/resource-filter/calendar.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<ResourceFilterBase
|
||||
:label="label"
|
||||
:value="value"
|
||||
:filled="modelValue && modelValue.length > 0"
|
||||
@clear="$emit('update:modelValue', [])"
|
||||
>
|
||||
<UiCalendar
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
</ResourceFilterBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export interface Props {
|
||||
label: string
|
||||
placeholder: string
|
||||
modelValue?: [from?: number, to?: number]
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'ResourceFilterCalendar',
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const value = computed(() => {
|
||||
if (!props.modelValue)
|
||||
return props.placeholder
|
||||
|
||||
const [start, end] = props.modelValue
|
||||
|
||||
if (!start)
|
||||
return props.placeholder
|
||||
|
||||
if (start && end) {
|
||||
const from = dayjs(start).format('DD.MM')
|
||||
const to = dayjs(end).format('DD.MM.YY')
|
||||
|
||||
return `${from}–${to}`
|
||||
}
|
||||
else if (start) {
|
||||
return dayjs(start).format('DD.MM.YY')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
23
apps/client/components/resource-filter/select.vue
Normal file
23
apps/client/components/resource-filter/select.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<UiSelect
|
||||
class="resource-filter resource-filter--select"
|
||||
v-bind="props"
|
||||
:model-value="modelValue"
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Props } from 'ui-layer/components/select/types'
|
||||
|
||||
defineOptions({
|
||||
name: 'ResourceFilterSelect',
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
68
apps/client/components/resource-filters.vue
Normal file
68
apps/client/components/resource-filters.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="resource-filters">
|
||||
<div class="resource-filters__filters">
|
||||
<Component
|
||||
:is="getFilterComponent(filter)"
|
||||
v-for="filter in schema as Filters"
|
||||
:id="`resource_filter.${filter.key}`"
|
||||
v-bind="filter"
|
||||
:key="filter.key"
|
||||
:model-value="appliedFiltersRaw[filter.key]"
|
||||
@update:model-value="apply(filter.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UiButton
|
||||
v-if="!empty"
|
||||
type="outlined"
|
||||
icon="s-cross"
|
||||
class="resource-filters__clear"
|
||||
@click="reset"
|
||||
>
|
||||
Clear filters
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Filter, Filters } from '#imports'
|
||||
|
||||
const { schema, appliedFiltersRaw, empty, apply, reset } = inject(
|
||||
filtersContextKey,
|
||||
{
|
||||
schema: [],
|
||||
appliedFiltersRaw: {},
|
||||
appliedFilters: computed(() => ({})),
|
||||
empty: true,
|
||||
apply: () => {},
|
||||
reset: () => {},
|
||||
},
|
||||
)
|
||||
|
||||
function getFilterComponent(filter: Filter) {
|
||||
switch (filter.type) {
|
||||
case 'calendar':
|
||||
return resolveComponent('ResourceFilterCalendar')
|
||||
case 'select':
|
||||
default:
|
||||
return resolveComponent('ResourceFilterSelect')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.resource-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__clear {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<SettingsRawTable :columns="columns" :data="data">
|
||||
<template #cell(finance_notifications)="{ row }">
|
||||
<div class="d-flex align-items-center text-clr-grey-600">
|
||||
<Component :is="resolveComponent(`ui-icon-${row.original.icon}`)" :style="`color: ${row.original.color}`" />
|
||||
<h4 class="ml-10">
|
||||
{{ row.original.finance_notifications }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(telegram)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="telegram" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(mail)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="mail" :model-value="true" disabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(push)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="push" :model-value="true" />
|
||||
</div>
|
||||
</template>
|
||||
</SettingsRawTable>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createColumnHelper } from '@tanstack/vue-table'
|
||||
|
||||
const columnHelper = createColumnHelper()
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: 'finance_notifications',
|
||||
header: 'Finance Notifications',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'telegram',
|
||||
header: 'Telegram',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'mail',
|
||||
header: 'Mail',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'push',
|
||||
header: 'Push',
|
||||
}),
|
||||
]
|
||||
|
||||
const data = [
|
||||
{ finance_notifications: 'Поступления средств', icon: 'ArrowReceive', color: '#10C44C' },
|
||||
{ finance_notifications: 'Выводы', icon: 'ArrowSend', color: '#1464D3' },
|
||||
{ finance_notifications: 'Счет частично оплачен', icon: 'InstallmentPlan' },
|
||||
]
|
||||
</script>
|
||||
45
apps/client/components/settings/limit-progress.vue
Normal file
45
apps/client/components/settings/limit-progress.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="block">
|
||||
<span class="text-small">Spent</span>
|
||||
<span class="text-small">Remaining limit </span>
|
||||
</div>
|
||||
|
||||
<UiProgressBar class="progress" :progress="(amount / maxAmount) * 100" />
|
||||
|
||||
<div class="block">
|
||||
<span class="text-amount">{{ amount }} <span class="text-currency">{{ currency }}</span></span>
|
||||
<span class="text-amount">{{ maxAmount - amount }} <span class="text-currency">{{ currency }}</span> </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
currency: { type: String, required: true },
|
||||
amount: { type: Number, required: true },
|
||||
maxAmount: { type: Number, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.block{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.progress{
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
@include txt-r;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
.text-currency {
|
||||
@include txt-i-sb;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
.text-amount{
|
||||
@include txt-i-sb;
|
||||
}
|
||||
</style>
|
||||
50
apps/client/components/settings/property-item.vue
Normal file
50
apps/client/components/settings/property-item.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="settings-property-item">
|
||||
<Component
|
||||
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||
class="settings-property-item__icon"
|
||||
/>
|
||||
|
||||
<div class="settings-property-item-content">
|
||||
<p class="settings-property-item-content__title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p v-if="text" class="settings-property-item-content__text">
|
||||
{{ text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
text: { type: String },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings-property-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__icon{
|
||||
color: $clr-cyan-500;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.settings-property-item-content{
|
||||
&__title {
|
||||
@include txt-i-sb;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-s-m;
|
||||
margin-top: 4px;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
apps/client/components/settings/property.vue
Normal file
24
apps/client/components/settings/property.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="settings-property">
|
||||
<SettingsPropertyItem :icon="icon" :text="text" :title="title" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
text: { type: String },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings-property {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 62px;
|
||||
}
|
||||
</style>
|
||||
112
apps/client/components/settings/raw-table.vue
Normal file
112
apps/client/components/settings/raw-table.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<table class="settings-table">
|
||||
<thead>
|
||||
<tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
<th
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
:colSpan="header.colSpan"
|
||||
:class="[header.id]"
|
||||
:style="{
|
||||
width: `${header.getSize()}px`,
|
||||
}"
|
||||
>
|
||||
<template v-if="!header.isPlaceholder">
|
||||
<FlexRender
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<slot>
|
||||
<tbody>
|
||||
<tr v-for="row in table.getRowModel().rows" :key="row.id">
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
:class="[cell.column.id]"
|
||||
>
|
||||
<slot :name="`cell(${cell.column.id})`" v-bind="cell.getContext()">
|
||||
<FlexRender
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</slot>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
export interface Props {
|
||||
columns: ColumnDef<unknown>[]
|
||||
data: unknown[]
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'SettingsTable',
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const table = useVueTable({
|
||||
get data() {
|
||||
return props.data
|
||||
},
|
||||
get columns() {
|
||||
return props.columns
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
|
||||
margin-block: -8px;
|
||||
|
||||
th {
|
||||
@include h5;
|
||||
|
||||
background-color: $clr-grey-100;
|
||||
color: $clr-grey-500;
|
||||
|
||||
&:first-child {
|
||||
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
td{
|
||||
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 16px 24px;
|
||||
|
||||
&:first-child {
|
||||
width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<SettingsRawTable :columns="columns" :data="data">
|
||||
<template #cell(service_notifications)="{ row }">
|
||||
<div class="d-flex align-items-center text-clr-grey-600">
|
||||
<Component :is="resolveComponent(`ui-icon-${row.original.icon}`)" />
|
||||
<h4 class="ml-10">
|
||||
{{ row.original.service_notifications }}
|
||||
</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(telegram)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="telegram" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(mail)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="mail" :model-value="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(push)="{ row }">
|
||||
<div class="d-flex justify-content-center">
|
||||
<UiCheckbox id="push" :model-value="true" />
|
||||
</div>
|
||||
</template>
|
||||
</SettingsRawTable>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createColumnHelper } from '@tanstack/vue-table'
|
||||
|
||||
const columnHelper = createColumnHelper()
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: 'service_notifications',
|
||||
header: 'Service Notifications',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'telegram',
|
||||
header: 'Telegram',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'mail',
|
||||
header: 'Mail',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'push',
|
||||
header: 'Push',
|
||||
}),
|
||||
]
|
||||
|
||||
const data = [
|
||||
{ service_notifications: 'Вход в аккаунт', icon: 'signin' },
|
||||
]
|
||||
</script>
|
||||
48
apps/client/components/settings/tariff-card.vue
Normal file
48
apps/client/components/settings/tariff-card.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="settings-tariff-card">
|
||||
<div class="settings-tariff-card__header">
|
||||
<div>
|
||||
<slot name="icon" />
|
||||
<h4 class="d-inline-block ml-8 text-clr-grey-600">
|
||||
{{ title }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<div class="settings-tariff-card__content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settings-tariff-card {
|
||||
&__header{
|
||||
padding: 24px;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
background-color: $clr-grey-100;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__content{
|
||||
padding: 24px;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
border: 2px solid $clr-grey-100;
|
||||
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
87
apps/client/components/sidebar.vue
Normal file
87
apps/client/components/sidebar.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<VueFinalModal
|
||||
:modal-id="modalId"
|
||||
class="sidebar"
|
||||
content-class="sidebar__content"
|
||||
content-transition="sidebar"
|
||||
hide-overlay
|
||||
background="interactive"
|
||||
:click-to-close="false"
|
||||
:z-index-fn="({ index }) => 6000 + 2 * index"
|
||||
>
|
||||
<div class="sidebar__top">
|
||||
<UiButton icon="arrow-left" type="outlined" color="secondary" @click="vfm.close(modalId)" />
|
||||
|
||||
<slot name="top" />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.default" class="sidebar__middle">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.bottom" class="sidebar__bottom">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</VueFinalModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { VueFinalModal, useVfm } from 'vue-final-modal'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
const vfm = useVfm()
|
||||
|
||||
const modalId = computed(() => {
|
||||
if (props.id)
|
||||
return `sidebar-${props.id}`
|
||||
|
||||
return 'sidebar'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar {
|
||||
&__content {
|
||||
width: 353px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $clr-white;
|
||||
height: 100%;
|
||||
gap: 32px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: transform .2s ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-to,
|
||||
&-leave-from {
|
||||
//transform: rotateZ(360deg);
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
apps/client/components/static-error.vue
Normal file
9
apps/client/components/static-error.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<UiAlert v-if="staticError" type="negative" :text="staticError.message" />
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const staticError = useStaticError()
|
||||
</script>
|
||||
139
apps/client/components/stepper.vue
Normal file
139
apps/client/components/stepper.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="form-stepper">
|
||||
<template
|
||||
v-for="(label, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<div
|
||||
class=" form-stepper__step"
|
||||
|
||||
:class="{
|
||||
'form-stepper__step--current': index === step,
|
||||
'form-stepper__step--passed': index < step,
|
||||
}"
|
||||
>
|
||||
<div class="form-stepper__index">
|
||||
<span v-if="index > step - 1 ">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
|
||||
<UiIconCheck
|
||||
v-else
|
||||
class="form-stepper__icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="form-stepper__label"
|
||||
v-html="label"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="index !== items.length - 1"
|
||||
class="form-stepper__line"
|
||||
:class="{
|
||||
'form-stepper__line--passed': index > step - 1,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['set'])
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-stepper {
|
||||
$self: &;
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__line {
|
||||
$line: &;
|
||||
|
||||
height: 3px;
|
||||
flex: 1;
|
||||
background-image: linear-gradient(90deg, #a5e9bc 0%, #a7b9d5 100%);
|
||||
margin-inline: 4px;
|
||||
border-radius: 2px;
|
||||
|
||||
&--passed {
|
||||
background-image: linear-gradient(90deg, #a7b9d5 0%, #dfe5ff 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
&:first-child {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
&--passed {
|
||||
--form-stepper-index-background: #{$clr-green-500};
|
||||
--form-stepper-label-color: #{$clr-green-500};
|
||||
|
||||
//color: var(--form-stepper-index-color, #{$clr-white});
|
||||
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
&--current {
|
||||
--form-stepper-index-background: #{$clr-grey-300};
|
||||
--form-stepper-label-color: #{$clr-grey-600};
|
||||
--form-stepper-index-color: #{$clr-grey-600};
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
--icon-size: 20px;
|
||||
line-height: 18px;
|
||||
color: $clr-white;
|
||||
}
|
||||
|
||||
&__index {
|
||||
@include txt-i-m;
|
||||
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
background: var(--form-stepper-index-background, #{$clr-grey-200});
|
||||
color: var(--form-stepper-index-color, #{$clr-grey-400});
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__label {
|
||||
@include txt-r;
|
||||
|
||||
color: var(--form-stepper-label-color, #{$clr-grey-600});
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
apps/client/components/stripped-table.vue
Normal file
140
apps/client/components/stripped-table.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<UiPlainTable
|
||||
class="stripped-table"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
>
|
||||
<template v-if="loading" #default>
|
||||
<tbody>
|
||||
<tr v-for="i in 6" :key="i">
|
||||
<td :colspan="columns.length">
|
||||
<div class="stripped-table__skeleton">
|
||||
<div
|
||||
v-for="j in i % 2 === 0 ? 7 : 3"
|
||||
:key="j"
|
||||
class="stripped-table__skeleton-cell"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
<template v-else-if="!data.length" #default>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="stripped-table__no-data" :colspan="columns.length">
|
||||
<img src="/no-data.svg" alt="No Data">
|
||||
<h4>No Data</h4>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
|
||||
<template v-for="(_, slot) of $slots" #[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</UiPlainTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
export interface Props {
|
||||
columns: ColumnDef<unknown>[]
|
||||
data: unknown[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.stripped-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 4px;
|
||||
text-align: left;
|
||||
margin-block: -4px;
|
||||
|
||||
td,
|
||||
th {
|
||||
margin: 0;
|
||||
padding: 16px 8px;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 12px;
|
||||
border-bottom-left-radius: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
@include h5;
|
||||
|
||||
background-color: $clr-grey-100;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
td {
|
||||
background-color: $clr-grey-100;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-loading) {
|
||||
tbody {
|
||||
tr:nth-child(even) {
|
||||
td {
|
||||
background-color: $clr-grey-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__no-data {
|
||||
height: 494px;
|
||||
text-align: center;
|
||||
|
||||
h4 {
|
||||
margin-top: 8px;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
|
||||
&__skeleton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__skeleton-cell {
|
||||
border-radius: 12px;
|
||||
background-color: $clr-white;
|
||||
background-image: linear-gradient(
|
||||
110deg,
|
||||
$clr-white 8%,
|
||||
$clr-grey-100 18%,
|
||||
$clr-white 33%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
height: 36px;
|
||||
animation: 1.5s shine linear infinite;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
to {
|
||||
background-position-x: -200%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
163
apps/client/components/verification/base-card.vue
Normal file
163
apps/client/components/verification/base-card.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div
|
||||
class="verification-card"
|
||||
:class="{
|
||||
'verification-card--passed': passed,
|
||||
}"
|
||||
>
|
||||
<div class="verification-card__header">
|
||||
<UiBadge
|
||||
v-if="badge"
|
||||
:type="badge.type"
|
||||
class="mb-24"
|
||||
>
|
||||
{{ badge.title }}
|
||||
</UiBadge>
|
||||
|
||||
<p class="verification-card-info__title">
|
||||
Withdrawal
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="info"
|
||||
class="verification-card-info"
|
||||
>
|
||||
<div>
|
||||
<span class="verification-card-info__per-month-amount">
|
||||
{{ info.perMonth }}
|
||||
</span>
|
||||
|
||||
<span class="verification-card-info__per-month-text"> / month </span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<span class="verification-card-info__per-day-amount">
|
||||
{{ info.perDay }}
|
||||
</span>
|
||||
|
||||
<span class="verification-card-info__per-day-text"> / day </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="verification-card-info"
|
||||
>
|
||||
<span class="verification-card-info__per-month-amount">
|
||||
No limit
|
||||
</span>
|
||||
|
||||
<span class="verification-card-info__per-month-text"> / month & day </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verification-card-body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="verification-card__action">
|
||||
<UiAlert
|
||||
v-if="passed"
|
||||
type="positive"
|
||||
text="Your current status"
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
v-else
|
||||
class="w-100"
|
||||
size="large"
|
||||
:href="link"
|
||||
>
|
||||
Start verification
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
passed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
badge: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
info: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: '#',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.verification-card {
|
||||
$self: &;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: var(--verification-card-width, 405px);
|
||||
//height: 585px;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
background-color: $clr-grey-200;
|
||||
|
||||
&__header {
|
||||
height: 157px;
|
||||
border-bottom: 1px solid $clr-grey-400;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&--passed {
|
||||
background-color: $clr-white;
|
||||
outline: $clr-green-500 solid 2px;
|
||||
outline-offset: -2px;
|
||||
|
||||
.verification-card__title {
|
||||
color: $clr-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verification-card-info {
|
||||
&__per-month-amount {
|
||||
@include h2;
|
||||
}
|
||||
|
||||
&__per-month-text {
|
||||
@include txt-l-sb;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
|
||||
&__per-day-amount {
|
||||
@include txt-l-sb;
|
||||
}
|
||||
|
||||
&__per-day-text {
|
||||
@include txt-l-sb;
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include txt-l-m;
|
||||
color: $clr-grey-500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.verification-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
55
apps/client/components/verification/basic.vue
Normal file
55
apps/client/components/verification/basic.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="block in propertyBlocks"
|
||||
:key="block.title"
|
||||
class="elements"
|
||||
>
|
||||
<span class="title">
|
||||
{{ block.title }}
|
||||
</span>
|
||||
|
||||
<h5 class="text-clr-grey-500">
|
||||
{{ block.underTitle }}
|
||||
</h5>
|
||||
|
||||
<VerificationProperty
|
||||
v-for="property in block.properties"
|
||||
:key="property.title"
|
||||
:title="property.title"
|
||||
:text="property.text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const propertyBlocks=[
|
||||
{
|
||||
title: 'Restrictions',
|
||||
underTitle: 'Invoices',
|
||||
properties:[
|
||||
{title:'Minimum amount in the invoice is', text:' 5 USDT'},
|
||||
{title:'Commission for creating an invoice is', text:'0.5 USDT'},
|
||||
{title:'Limit for creating invoices is', text:'200 per day, 1000 per month'},
|
||||
]},
|
||||
{
|
||||
underTitle: 'Withdrawal of funds',
|
||||
properties:[
|
||||
{title:'Minimum withdrawal amount is', text:'5 USDT'},
|
||||
{title:'Commission for withdrawal is', text:'2.5 USDT + 1%'}
|
||||
]},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.elements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title{
|
||||
@include txt-m-b;
|
||||
}
|
||||
</style>
|
||||
78
apps/client/components/verification/extended.vue
Normal file
78
apps/client/components/verification/extended.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="elements mb-16">
|
||||
<span class="requirements"> Requirements </span>
|
||||
|
||||
<div>
|
||||
<UiIconSSocialCard class="mr-4 text-clr-grey-400 d-inline-block" />
|
||||
<span class="text"> Organization data </span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UiIconSExcursion class="mr-4 text-clr-grey-400" />
|
||||
<span class="text">Identity verification </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="block in propertyBlocks"
|
||||
:key="block.title"
|
||||
class="elements"
|
||||
>
|
||||
<span class="title">
|
||||
{{ block.title }}
|
||||
</span>
|
||||
|
||||
<h5 class="text-clr-grey-500">
|
||||
{{ block.underTitle }}
|
||||
</h5>
|
||||
|
||||
<VerificationProperty
|
||||
v-for="property in block.properties"
|
||||
:key="property.title"
|
||||
:title="property.title"
|
||||
:text="property.text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const propertyBlocks = [
|
||||
{
|
||||
title: 'Restrictions',
|
||||
underTitle: 'Invoices',
|
||||
properties: [
|
||||
{ title: 'Invoice creation', text: 'unlimited' },
|
||||
{ title: 'Commission for creating an invoice is', text: '0.5 USDT' },
|
||||
],
|
||||
},
|
||||
{
|
||||
underTitle: 'Withdrawal of funds',
|
||||
properties: [
|
||||
{ title: 'Commission for withdrawal is', text: '2.5 USDT + 1%' },
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.elements {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title{
|
||||
@include txt-m-b;
|
||||
}
|
||||
|
||||
.text{
|
||||
@include txt-r-m;
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
|
||||
.requirements{
|
||||
@include txt-m-b;
|
||||
}
|
||||
</style>
|
||||
49
apps/client/components/verification/property.vue
Normal file
49
apps/client/components/verification/property.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="varification-property">
|
||||
<Component
|
||||
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||
class="mr-4"
|
||||
:class="icon === 'SCheck' ? 'text-clr-green-500' : ''"
|
||||
/>
|
||||
<span class="varification-property__title">
|
||||
|
||||
{{ title }}
|
||||
|
||||
<span class="varification-property__text">
|
||||
{{ text }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'SCheck',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.varification-property {
|
||||
|
||||
color: $clr-grey-500;
|
||||
|
||||
&__title {
|
||||
@include txt-r;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-r-b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
40
apps/client/components/verification/we-accept.vue
Normal file
40
apps/client/components/verification/we-accept.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="mb-16">
|
||||
<strong> We accept: </strong>
|
||||
</p>
|
||||
<ul class="accept-list">
|
||||
<li
|
||||
v-for="listItem in list"
|
||||
:key="listItem.title"
|
||||
>
|
||||
<span> {{ listItem.title }} </span>
|
||||
<p class="text-clr-grey-500">
|
||||
{{ listItem.underTitle }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.accept-list{
|
||||
list-style-type: disc;
|
||||
padding: 0 0 0 24px;
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
}
|
||||
</style>
|
||||
39
apps/client/composables/api.ts
Normal file
39
apps/client/composables/api.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { NitroFetchRequest } from 'nitropack'
|
||||
import { callWithNuxt } from '#app'
|
||||
|
||||
export function $api<
|
||||
T = unknown,
|
||||
R extends NitroFetchRequest = NitroFetchRequest,
|
||||
>(
|
||||
request: Parameters<typeof $fetch<T, R>>[0],
|
||||
options?: Partial<Parameters<typeof $fetch<T, R>>[1]>,
|
||||
) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const cookies = useRequestHeaders(['cookie'])
|
||||
|
||||
return $fetch<T, R>(request, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
...cookies,
|
||||
},
|
||||
retry: false,
|
||||
baseURL: runtimeConfig.public.apiHost as string,
|
||||
credentials: 'include',
|
||||
onResponseError: async ({ response }) => {
|
||||
if (response.status === 401) {
|
||||
nuxtApp.runWithContext(() => {
|
||||
useCookie('session').value = null
|
||||
})
|
||||
await callWithNuxt(nuxtApp, clearNuxtState, ['user'])
|
||||
await callWithNuxt(nuxtApp, navigateTo, ['/login', { redirectCode: 401 }])
|
||||
}
|
||||
|
||||
// setStaticError({
|
||||
// status: response.status,
|
||||
// message: nuxtApp.$i18n.t('something_went_wrong'),
|
||||
// });
|
||||
},
|
||||
})
|
||||
}
|
||||
16
apps/client/composables/static-error.ts
Normal file
16
apps/client/composables/static-error.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface StaticError {
|
||||
status: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function useStaticError() {
|
||||
return useState<StaticError | undefined>('static-error')
|
||||
}
|
||||
|
||||
export function setStaticError(value?: StaticError) {
|
||||
useStaticError().value = value
|
||||
}
|
||||
|
||||
export function clearStaticError() {
|
||||
clearNuxtState(['static-error'])
|
||||
}
|
||||
69
apps/client/composables/use-auth.ts
Normal file
69
apps/client/composables/use-auth.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const user = useState<User | null>('user')
|
||||
|
||||
const authenticated = computed(() => !!user.value)
|
||||
|
||||
async function getUser() {
|
||||
user.value = await $api('/users/current', { method: 'GET' })
|
||||
}
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
await $api('/sessions', { method: 'POST', body: { email, password } })
|
||||
await getUser()
|
||||
|
||||
navigateTo('/projects')
|
||||
}
|
||||
|
||||
async function register(email: string, password: string) {
|
||||
await $api('/users', { method: 'POST', body: { email, password } })
|
||||
|
||||
navigateTo('/login')
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await $api('/sessions', { method: 'delete', body: {} })
|
||||
}
|
||||
finally {
|
||||
clearNuxtState('user')
|
||||
navigateTo('/login')
|
||||
}
|
||||
}
|
||||
|
||||
async function requestResetPassword(email: string) {
|
||||
await $api('/users/password_reset', { method: 'post', body: { email } })
|
||||
}
|
||||
|
||||
async function resetPassword(newPassword: string, resetCode: string) {
|
||||
await $api('/users/password_reset', {
|
||||
method: 'put',
|
||||
body: {
|
||||
newPassword,
|
||||
resetCode,
|
||||
},
|
||||
})
|
||||
}
|
||||
async function resendVerificationCode(email: string) {
|
||||
await $api('/users/verification', {
|
||||
method: 'put',
|
||||
body: { email },
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
authenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
resendVerificationCode,
|
||||
requestResetPassword,
|
||||
resetPassword,
|
||||
}
|
||||
}
|
||||
156
apps/client/composables/use-filters.ts
Normal file
156
apps/client/composables/use-filters.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { computed, provide, reactive, unref } from 'vue'
|
||||
import type { ComputedRef, InjectionKey, MaybeRef } from 'vue'
|
||||
import { omit } from 'lodash-es'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const DATE_FORMAT = 'DD-MM-YYYY'
|
||||
|
||||
export interface Filter {
|
||||
type?: 'select' | 'calendar'
|
||||
key: string
|
||||
label: string
|
||||
placeholder: string
|
||||
searchable?: boolean
|
||||
multiple?: boolean
|
||||
options?: { label?: string, value: unknown }[]
|
||||
transform?: (value: AppliedFilter) => AppliedFilters
|
||||
}
|
||||
export type Filters = Filter[]
|
||||
|
||||
export type AppliedFilter = null | string | string[]
|
||||
export type AppliedFilters = Record<string, AppliedFilter>
|
||||
|
||||
export interface FiltersContext {
|
||||
schema: MaybeRef<Filters>
|
||||
appliedFiltersRaw: AppliedFilters
|
||||
appliedFilters: ComputedRef<AppliedFilters>
|
||||
empty: boolean | ComputedRef<boolean>
|
||||
apply: (p1: AppliedFilters | string, p2?: AppliedFilter) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const filtersContextKey: InjectionKey<FiltersContext>
|
||||
= Symbol('FILTERS')
|
||||
|
||||
export default (filters: MaybeRef<Filters>) => {
|
||||
const url = useRequestURL()
|
||||
|
||||
const searchString = computed(() => url.search)
|
||||
|
||||
const parsedUrl: { other: Record<string, string>, filters: AppliedFilters }
|
||||
= reactive({
|
||||
other: {},
|
||||
filters: {},
|
||||
})
|
||||
|
||||
const allowedFilters = computed<string[]>(() => {
|
||||
return unref(filters).map(filter => filter.key)
|
||||
})
|
||||
|
||||
parseUrl(searchString.value)
|
||||
|
||||
const appliedFiltersRaw = reactive<AppliedFilters>({
|
||||
...Object.fromEntries(
|
||||
allowedFilters.value.map(key => [key, isMultiple(key) ? [] : null]),
|
||||
),
|
||||
...parsedUrl.filters,
|
||||
})
|
||||
|
||||
const appliedFilters = computed<AppliedFilters>(() => {
|
||||
return Object.entries(appliedFiltersRaw).reduce((result, [key, value]) => {
|
||||
const filter = getFilterByKey(key)!
|
||||
if (filter.transform) {
|
||||
const transformedValue = filter.transform(value)
|
||||
|
||||
if (transformedValue)
|
||||
result = { ...result, ...transformedValue }
|
||||
else
|
||||
result[key] = value
|
||||
}
|
||||
else {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}, {} as AppliedFilters)
|
||||
})
|
||||
|
||||
const empty = computed(() =>
|
||||
Object.values(appliedFiltersRaw).every((value) => {
|
||||
return Array.isArray(value) ? value.length === 0 : value !== null
|
||||
}),
|
||||
)
|
||||
|
||||
function parseUrl(searchString: string) {
|
||||
const params = new URLSearchParams(searchString)
|
||||
|
||||
parsedUrl.other = {}
|
||||
parsedUrl.filters = {}
|
||||
|
||||
for (const [key, value] of Array.from(params.entries())) {
|
||||
if (allowedFilters.value.includes(key)) {
|
||||
let newValue = isMultiple(key) ? value.split(',') : value
|
||||
|
||||
if (isCalendar(key)) {
|
||||
newValue = [...newValue].map(date =>
|
||||
dayjs(date, DATE_FORMAT).valueOf().toString(),
|
||||
)
|
||||
}
|
||||
|
||||
parsedUrl.filters[key] = newValue
|
||||
}
|
||||
else {
|
||||
parsedUrl.other[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
parsedUrl.other = omit(parsedUrl.other, ['page'])
|
||||
}
|
||||
|
||||
function apply(p1: AppliedFilters | string, p2?: AppliedFilter) {
|
||||
if (p2 && typeof p1 === 'string') {
|
||||
appliedFiltersRaw[p1] = p2
|
||||
}
|
||||
else if (typeof p1 === 'object') {
|
||||
for (const [key, value] of Object.entries(p1))
|
||||
appliedFiltersRaw[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const key of Object.keys(appliedFiltersRaw))
|
||||
appliedFiltersRaw[key] = isMultiple(key) ? [] : null
|
||||
}
|
||||
|
||||
function getFilterByKey(key: string) {
|
||||
return unref(filters).find(f => f.key === key)
|
||||
}
|
||||
|
||||
function isMultiple(key: string) {
|
||||
const filter = getFilterByKey(key)
|
||||
|
||||
return filter?.multiple ?? filter?.type === 'calendar' ?? false
|
||||
}
|
||||
|
||||
function isCalendar(key: string) {
|
||||
const filter = getFilterByKey(key)
|
||||
|
||||
return filter?.type === 'calendar' ?? false
|
||||
}
|
||||
|
||||
provide(filtersContextKey, {
|
||||
schema: filters,
|
||||
appliedFiltersRaw,
|
||||
appliedFilters,
|
||||
empty,
|
||||
apply,
|
||||
reset,
|
||||
})
|
||||
|
||||
return {
|
||||
appliedFiltersRaw,
|
||||
appliedFilters,
|
||||
empty,
|
||||
apply,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
11
apps/client/eslint.config.js
Normal file
11
apps/client/eslint.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default await antfu({
|
||||
overrides: {
|
||||
vue: {
|
||||
'vue/block-order': ['error', {
|
||||
order: ['template', 'script', 'style'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
23
apps/client/helpers/invoices.ts
Normal file
23
apps/client/helpers/invoices.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { AlertType } from 'ui-layer/components/alert/types'
|
||||
import type { UiIcon } from '#build/types/ui/icons'
|
||||
|
||||
export function getStatusType(status: string): AlertType {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'positive'
|
||||
case 'expired':
|
||||
return 'negative'
|
||||
default:
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
export function getStatusIcon(status: string): UiIcon {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 's-check'
|
||||
case 'expired':
|
||||
return 's-cross'
|
||||
default:
|
||||
return 's-clock'
|
||||
}
|
||||
}
|
||||
5
apps/client/i18n.config.ts
Normal file
5
apps/client/i18n.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
globalInjection: true,
|
||||
}))
|
||||
7
apps/client/index.d.ts
vendored
Normal file
7
apps/client/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module '#app' {
|
||||
interface PageMeta {
|
||||
centerContent?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
122
apps/client/lang/en.js
Normal file
122
apps/client/lang/en.js
Normal file
@@ -0,0 +1,122 @@
|
||||
export default {
|
||||
validation: {
|
||||
required: 'This field is required',
|
||||
email: 'Enter an email address in the format example{\'@\'}xxxx.xxx',
|
||||
confirmed: 'Passwords must match',
|
||||
password:
|
||||
'Password must contain at least 8 characters, including uppercase letters, numbers, and special characters',
|
||||
max: 'The value must be less than or equal to {0} characters.',
|
||||
url: 'Please enter a valid URL',
|
||||
},
|
||||
field_max_characters: '{0} (maximum {1} characters)',
|
||||
|
||||
invoice_status: {
|
||||
completed: 'Success',
|
||||
expired: 'Expired',
|
||||
pending: 'Pending',
|
||||
awaiting_payment: 'Awaiting',
|
||||
investigating: 'Investigating',
|
||||
broadcasted: 'Broadcasted',
|
||||
overpaid: 'Overpaid',
|
||||
underpaid: 'Underpaid',
|
||||
},
|
||||
|
||||
something_went_wrong: 'Something went wrong',
|
||||
invalid_otp_code: 'Invalid OTP Code',
|
||||
create: 'Create',
|
||||
change: 'Change',
|
||||
select: 'Select',
|
||||
register: 'Register',
|
||||
login: 'Login',
|
||||
privacy_policy: 'Privacy Policy',
|
||||
copyright: '© {year}, Indefiti - the best crypto processing',
|
||||
support: 'Support',
|
||||
network: 'Network',
|
||||
maximum: 'Maximum',
|
||||
continue: 'Continue',
|
||||
fee: 'Fee',
|
||||
login_greeting: 'Hello!',
|
||||
register_greeting: 'Register with Indefiti',
|
||||
to_main: 'To home page',
|
||||
back: 'Back',
|
||||
back_to: 'Back to {0}',
|
||||
next: 'Next',
|
||||
withdraw: 'Withdraw',
|
||||
email: 'E-mail',
|
||||
password: 'Password',
|
||||
repeat_password: 'Repeat Password',
|
||||
what_happened: 'What happened?',
|
||||
what_todo: 'I don\'t remember what to do',
|
||||
write_us: 'Write to us',
|
||||
reset_password: 'Reset Password',
|
||||
lost_password: 'Forgot Password?',
|
||||
can_reset_password: 'Password can be reset',
|
||||
fill_the_form: 'fill out this form',
|
||||
reset_password_alert:
|
||||
'For security reasons, withdrawals will be prohibited for 24 hours after changing the password',
|
||||
check_email: 'Check your e-mail',
|
||||
email_confirmation: 'We have sent you an e-mail to {0}',
|
||||
register_email_instructions:
|
||||
'To confirm your email,<br>follow the link in the email',
|
||||
login_email_instructions:
|
||||
'To reset your password, follow the link in the email',
|
||||
send_again: 'Send again',
|
||||
spam: 'Spam',
|
||||
|
||||
create_your_project: {
|
||||
title: '{0} your first project',
|
||||
create: 'Create',
|
||||
content: 'This is necessary to accept payments on the site',
|
||||
},
|
||||
|
||||
your_invoices_will_be_displayed_here: {
|
||||
title: 'Your invoices will be displayed here',
|
||||
content: 'Click on the create invoice button to start invoicing',
|
||||
},
|
||||
create_an_invoice: 'Сreate an invoice',
|
||||
|
||||
forgot_password_questions: {
|
||||
did_not_get_mail: {
|
||||
title: 'Check the {0} folder if you don\'t see the email',
|
||||
content: 'Didn\'t receive the email? {0}',
|
||||
},
|
||||
|
||||
forgot_password: {
|
||||
title: 'Forgot my password',
|
||||
content:
|
||||
'{0}, if you have access to your email. If you don\'t have access, {1}.',
|
||||
},
|
||||
|
||||
access_to_mail: {
|
||||
title: 'No access to email',
|
||||
content: 'If you don\'t have access to your email, {0}.',
|
||||
},
|
||||
|
||||
no_mail: {
|
||||
title: 'The website says there is no such email',
|
||||
content:
|
||||
'Check which email you received receipts and other notifications to. If you are sure you are entering everything correctly, {0}.',
|
||||
},
|
||||
},
|
||||
|
||||
sign_up_agreement: {
|
||||
base: 'I confirm my agreement with the {0} and {1}',
|
||||
privacy_policy: 'Privacy Policy',
|
||||
user_agreement: 'User Agreement',
|
||||
},
|
||||
apply: 'Apply',
|
||||
try: 'Try',
|
||||
test_functional: 'Test functional',
|
||||
learn_functional:
|
||||
'Here you can familiarize yourself with the main features. To get started with payments, create your first project.',
|
||||
how_does_it_works: 'How does it works',
|
||||
projects: 'Projects',
|
||||
account_created_successfully: 'Account created successfully',
|
||||
account_verification_error: 'An error occurred during account verification',
|
||||
try_again_or_contact_support: 'Try again or contact support',
|
||||
we_have_sent_you_an_email_to: 'We have sent you an email to {0}',
|
||||
check_spam_folder: 'Check the {0} folder if you do not see the letter',
|
||||
did_not_get_mail: 'Did not get the email?',
|
||||
reset_password_instructions:
|
||||
'To reset your password, follow the link from the letter',
|
||||
}
|
||||
1
apps/client/lang/ru.js
Normal file
1
apps/client/lang/ru.js
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
131
apps/client/layouts/auth.vue
Normal file
131
apps/client/layouts/auth.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="auth-layout">
|
||||
<header class="auth-header">
|
||||
<NuxtLink to="/projects">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
>
|
||||
</NuxtLink>
|
||||
|
||||
<UiButton
|
||||
v-if="headerAction"
|
||||
class="ml-a"
|
||||
type="ghost"
|
||||
:href="headerAction.link"
|
||||
size="large"
|
||||
>
|
||||
{{ headerAction.name }}
|
||||
</UiButton>
|
||||
|
||||
<LangSwitcher />
|
||||
</header>
|
||||
|
||||
<main class="auth-main">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="auth-footer">
|
||||
<div class="auth-footer__left">
|
||||
<UiButton
|
||||
type="link"
|
||||
color="secondary"
|
||||
href="#"
|
||||
>
|
||||
{{ $t('privacy_policy') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer__middle">
|
||||
<span class="auth-footer__copyright">
|
||||
{{ $t('copyright', { year }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer__right">
|
||||
<UiButton
|
||||
icon="circle-question"
|
||||
type="link"
|
||||
color="secondary"
|
||||
href="#"
|
||||
>
|
||||
{{ $t('support') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
const year = computed(() => new Date().getFullYear())
|
||||
|
||||
const headerAction = computed(() => {
|
||||
switch (route.name) {
|
||||
case 'login':
|
||||
return {
|
||||
name: t('register'),
|
||||
link: '/register',
|
||||
}
|
||||
case 'register':
|
||||
return {
|
||||
name: t('login'),
|
||||
link: '/login',
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.auth-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
display: flex;
|
||||
padding: 32px 56px 0 56px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auth-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-block: 30px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
padding: 24px 56px 32px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__left {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
&__middle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__right {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
color: $clr-grey-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
apps/client/layouts/default.vue
Normal file
117
apps/client/layouts/default.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="default-layout">
|
||||
<NavigationSidebar>
|
||||
<template #logo>
|
||||
<NavigationLogo />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<NavigationItem
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
:icon="item.icon"
|
||||
:title="item.title"
|
||||
:matcher="item.matcher ?? defaultMatcher(item.to)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<NavigationItem
|
||||
icon="info"
|
||||
title="Help"
|
||||
to="/_"
|
||||
:matcher="() => ['/help'].includes(route.fullPath)"
|
||||
/>
|
||||
</template>
|
||||
</NavigationSidebar>
|
||||
|
||||
<main
|
||||
class="default-layout__main"
|
||||
:class="{
|
||||
center: shouldCenterContent,
|
||||
narrow: isAnySidebarOpened,
|
||||
}"
|
||||
>
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ModalsContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ModalsContainer, useVfm } from 'vue-final-modal'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const { openedModals } = useVfm()
|
||||
|
||||
const defaultMatcher = link => () => route.fullPath === link
|
||||
|
||||
const navItems = computed(() => [
|
||||
{
|
||||
icon: 'merchant',
|
||||
title: t('projects'),
|
||||
to: '/projects',
|
||||
matcher: () =>
|
||||
route.fullPath === '/projects'
|
||||
|| route.fullPath.startsWith('/projects')
|
||||
|| route.meta.alias?.startsWith('/projects'),
|
||||
},
|
||||
])
|
||||
|
||||
const shouldCenterContent = computed(() => route.meta.centerContent)
|
||||
const isAnySidebarOpened = computed(() => openedModals.some(modal => modal.value.modalId.startsWith('sidebar') && modal.value.overlayVisible.value))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.default-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
|
||||
grid-area: sidebar;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
|
||||
&__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 100%;
|
||||
padding: 0 35px 32px;
|
||||
transition: padding-right .2s ease-in-out;
|
||||
|
||||
&.center {
|
||||
padding-top: 32px;
|
||||
|
||||
> .container {
|
||||
margin-block: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
padding-right: calc(353px + 35px);
|
||||
}
|
||||
|
||||
&:not(.center) {
|
||||
> .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
width: 100%;
|
||||
max-width: 1250px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
apps/client/layouts/empty.vue
Normal file
5
apps/client/layouts/empty.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="empty-layout">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
7
apps/client/middleware/01.slash.global.ts
Normal file
7
apps/client/middleware/01.slash.global.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
if (to.path === '/')
|
||||
return navigateTo('/projects')
|
||||
|
||||
if (to.path !== '/' && to.path.endsWith('/'))
|
||||
return navigateTo(to.path.slice(0, -1))
|
||||
})
|
||||
17
apps/client/middleware/02.load-user.global.ts
Normal file
17
apps/client/middleware/02.load-user.global.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { User } from '~/composables/use-auth'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const session = useCookie('session')
|
||||
const { authenticated, user } = useAuth()
|
||||
|
||||
if (session.value && !authenticated.value) {
|
||||
try {
|
||||
user.value = await $api<User>('/users/current', {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
6
apps/client/middleware/auth.ts
Normal file
6
apps/client/middleware/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const { authenticated } = useAuth()
|
||||
|
||||
if (!authenticated.value)
|
||||
return navigateTo('/login')
|
||||
})
|
||||
6
apps/client/middleware/guest-only.ts
Normal file
6
apps/client/middleware/guest-only.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { authenticated } = useAuth()
|
||||
|
||||
if (authenticated.value)
|
||||
return navigateTo('/projects')
|
||||
})
|
||||
48
apps/client/nuxt.config.ts
Normal file
48
apps/client/nuxt.config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
extends: ['../../layers/shared', '../../layers/ui'],
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
[
|
||||
'@nuxtjs/i18n',
|
||||
{
|
||||
vueI18n: './i18n.config.ts',
|
||||
lazy: true,
|
||||
langDir: 'lang',
|
||||
compilation: {
|
||||
strictMessage: false,
|
||||
},
|
||||
locales: [
|
||||
// {
|
||||
// code: 'ru',
|
||||
// name: 'Русский',
|
||||
// file: 'ru.js',
|
||||
// },
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
file: 'en.js',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
strategy: 'no_prefix',
|
||||
detectBrowserLanguage: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
css: ['~/assets/styles.scss', 'vue-final-modal/style.css'],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
host: process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://app.prgms.io',
|
||||
payHost: process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:3001'
|
||||
: 'https://pay.prgms.io',
|
||||
apiHost:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '/api'
|
||||
: 'https://api.prgms.io/api/v1',
|
||||
},
|
||||
},
|
||||
})
|
||||
35
apps/client/package.json
Normal file
35
apps/client/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "client",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.4.11",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"decimal.js": "^10.4.3",
|
||||
"defu": "^6.1.2",
|
||||
"ufo": "^1.3.2",
|
||||
"ui-layer": "*",
|
||||
"shared-layer": "*",
|
||||
"uuid": "^9.0.1",
|
||||
"vue-final-modal": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.1.2",
|
||||
"@nuxt/devtools": "latest",
|
||||
"@nuxtjs/i18n": "^8.0.0-rc.5",
|
||||
"eslint": "^8.54.0",
|
||||
"nuxt": "latest",
|
||||
"sass": "^1.69.0",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vue": "latest",
|
||||
"vue-router": "^4.2.5"
|
||||
}
|
||||
}
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user