initial
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user