first commit
This commit is contained in:
7
src/App.vue
Normal file
7
src/App.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
15
src/assets/quasar-logo-vertical.svg
Normal file
15
src/assets/quasar-logo-vertical.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
||||
<path
|
||||
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
||||
<path fill="#050A14"
|
||||
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
||||
<path fill="#050A14"
|
||||
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
||||
<path fill="#00B4FF"
|
||||
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
||||
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
0
src/boot/.gitkeep
Normal file
0
src/boot/.gitkeep
Normal file
39
src/boot/i18n.js
Normal file
39
src/boot/i18n.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { defineBoot } from '#q-app/wrappers'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import messages from 'src/i18n'
|
||||
|
||||
let i18n = null
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
const getLocale = () => {
|
||||
const saved = localStorage.getItem('lang')
|
||||
if (saved && messages[saved]) return saved
|
||||
|
||||
const browserLang = navigator.language
|
||||
if (messages[browserLang]) return browserLang
|
||||
|
||||
const shortLang = browserLang.split('-')[0]
|
||||
return Object.keys(messages).find(lang => lang.startsWith(shortLang)) || 'en-US'
|
||||
}
|
||||
|
||||
const locale = getLocale()
|
||||
|
||||
i18n = createI18n({
|
||||
locale,
|
||||
fallbackLocale: 'en-US',
|
||||
legacy: false,
|
||||
messages
|
||||
})
|
||||
|
||||
localStorage.setItem('lang', locale)
|
||||
document.documentElement.lang = locale
|
||||
app.use(i18n)
|
||||
})
|
||||
|
||||
export const setGlobalLocale = (newLocale) => {
|
||||
if (i18n && i18n.global) {
|
||||
i18n.global.locale.value = newLocale
|
||||
localStorage.setItem('lang', newLocale)
|
||||
document.documentElement.lang = newLocale
|
||||
}
|
||||
}
|
||||
118
src/components/BaseLogo.vue
Normal file
118
src/components/BaseLogo.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="flex row items-center no-wrap">
|
||||
<svg
|
||||
class="iconcolor q-mr-sm"
|
||||
viewBox="0 0 8.4666662 8.4666662"
|
||||
width="32"
|
||||
height="32"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs id="defs1" />
|
||||
<g id="layer1">
|
||||
<rect
|
||||
style="fill:var(--icon-color);stroke-width:0.233149"
|
||||
id="rect5"
|
||||
width="6.9885192"
|
||||
height="0.35581663"
|
||||
x="3.114475"
|
||||
y="0.86827624"
|
||||
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||
/>
|
||||
<rect
|
||||
style="fill:var(--icon-color);stroke-width:0.24961"
|
||||
id="rect5-7"
|
||||
width="7.4819207"
|
||||
height="0.3809379"
|
||||
x="-3.9267058"
|
||||
y="5.7988153"
|
||||
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||
/>
|
||||
<circle
|
||||
style="fill:var(--icon-color);stroke-width:0.134869"
|
||||
id="path5-8"
|
||||
cx="1.5875"
|
||||
cy="6.8791666"
|
||||
r="1.0583333"
|
||||
/>
|
||||
<circle
|
||||
style="fill:var(--icon-color);stroke-width:0.168586"
|
||||
id="path5-8-5"
|
||||
cx="7.1437502"
|
||||
cy="7.1437502"
|
||||
r="1.3229166"
|
||||
/>
|
||||
<circle
|
||||
style="fill:var(--icon-color);stroke-width:0.118011"
|
||||
id="path5-8-5-1"
|
||||
cx="1.4552083"
|
||||
cy="2.5135417"
|
||||
r="0.92604166"
|
||||
/>
|
||||
<circle
|
||||
style="fill:var(--icon-color);stroke-width:0.101152"
|
||||
id="path5-8-5-1-7"
|
||||
cx="7.1437502"
|
||||
cy="1.3229166"
|
||||
r="0.79374999"
|
||||
/>
|
||||
<circle
|
||||
style="stroke-width:0.23602"
|
||||
id="path5"
|
||||
cx="3.96875"
|
||||
cy="4.4979167"
|
||||
r="1.8520833"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<span class="text-h4 text-bold q-pr-xs" style="color: var(--logo-color-bg-white);">
|
||||
tg
|
||||
</span>
|
||||
|
||||
<span class="text-h4 text-brand text-bold q-pa-0">
|
||||
Projects
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body {
|
||||
background: white;
|
||||
margin: 0rem;
|
||||
min-height: 100vh;
|
||||
font-family: Futura, sans-serif;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.iconcolor {
|
||||
--icon-color: var(--logo-color-bg-white);
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
100%,
|
||||
0% {
|
||||
fill: $light-green-14;
|
||||
}
|
||||
60% {
|
||||
fill: $green-14;
|
||||
}
|
||||
}
|
||||
|
||||
#path5 {
|
||||
animation: blink 3s infinite;
|
||||
}
|
||||
</style>
|
||||
45
src/components/FAQSection.vue
Normal file
45
src/components/FAQSection.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<slide-template title="FAQ__title">
|
||||
<div
|
||||
class="q-pa-md w100 flex justify-center"
|
||||
style="max-width: 800px"
|
||||
>
|
||||
<q-list separator>
|
||||
<q-expansion-item
|
||||
group="FAQgroup"
|
||||
v-for="item in 5"
|
||||
:key="item"
|
||||
switch-toggle-side
|
||||
header-class="text-h5"
|
||||
expand-icon="mdi-plus"
|
||||
expanded-icon="mdi-close"
|
||||
>
|
||||
<template #header="{ expanded }">
|
||||
<div :class="expanded ? 'text-brand' : ''" class="w100">
|
||||
{{ $t('faq__question_' + item) }}
|
||||
</div>
|
||||
</template>
|
||||
<q-card>
|
||||
<q-card-section class="text-h6">
|
||||
{{ $t('faq__answer_' + item) }}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</slide-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideTemplate from 'components/SlideTemplate.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-expansion.q-expansion-item--collapsed :deep(.q-item__section--side .q-icon) {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.custom-expansion.q-expansion-item--expanded :deep(.q-item__section--side .q-icon) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
59
src/components/FooterSection.vue
Normal file
59
src/components/FooterSection.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="q-pa-md w100 flex justify-between">
|
||||
<div class="flex column q-mx-lg justify-center">
|
||||
<div
|
||||
v-for="item in Docs"
|
||||
:key="item.id"
|
||||
>
|
||||
<a
|
||||
:href="item.href"
|
||||
target="_blank"
|
||||
style="text-decoration: none; color: inherit"
|
||||
class="text-h6"
|
||||
>
|
||||
{{ $t(item.name) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex column q-mx-lg">
|
||||
<div class="text-h6 bold">
|
||||
{{ $t('footer__contacts_ip') }}
|
||||
</div>
|
||||
<div class="text-caption">
|
||||
{{ $t('footer__contacts_ip_detail') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="mdi-map-marker-outline" color="brand" class="q-pr-sm"/>
|
||||
<span>{{ $t('footer__contacts_location') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="mdi-phone-outline" color="brand" class="q-pr-sm" />
|
||||
<span>+7 (926) 339-04-25</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="mdi-email-outline" color="brand" class="q-pr-sm"/>
|
||||
<a
|
||||
href="mailto:a-mart@ya.ru"
|
||||
style="text-decoration: none; color: inherit"
|
||||
>
|
||||
a-mart@ya.ru
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const Docs = [
|
||||
{ id: 1, name: 'footer__doc_terms_of_use', href: '' },
|
||||
{ id: 2, name: 'footer__doc_privacy_policy', href: '' }
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
90
src/components/HeroBanner.vue
Normal file
90
src/components/HeroBanner.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w100 q-pa-lg text-white relative-position"
|
||||
:class="!isAlignTop ? 'justify-center' : 'justify-around vert-height'"
|
||||
>
|
||||
<svg v-if ="isAlignTop" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<polygon fill="white" points="0,0 55,0 45,101 0,101"/>
|
||||
</svg>
|
||||
<mesh-background v-if ="isAlignTop" style="z-index: -2"/>
|
||||
<div
|
||||
ref="slogan"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div
|
||||
class="flex column justify-center q-pa-lg q-ma-lg q-gutter-y-md no-wrap bg-transperant"
|
||||
style="max-width: 400px; "
|
||||
:style="!isAlignTop ? 'text-align : center' : ''"
|
||||
>
|
||||
<div class="text-h5 text-grey">
|
||||
{{ $t('banner__slogan_prepend') }} —
|
||||
</div>
|
||||
<div class="text-h4 text-brand">
|
||||
{{ $t('banner__slogan_body') }}
|
||||
<span class="text-no-wrap">
|
||||
<q-icon dense name="telegram" style="color: #27a7e7"/>
|
||||
<span style="color: #27a7e7">Telegram</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
size="lg"
|
||||
color="brand"
|
||||
class="q-mt-xl"
|
||||
>
|
||||
<div class="flex items-center no-wrap">
|
||||
<div>{{ $t('banner__main_btn')}}</div>
|
||||
<q-icon name="keyboard_arrow_right"/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="image"
|
||||
class="text-red flex"
|
||||
>
|
||||
<img
|
||||
src="/img/1.png"
|
||||
class="q-ma-lg"
|
||||
style="object-fit: scale-down;"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<q-resize-observer @resize="checkAlign"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import meshBackground from 'components/meshBackground.vue'
|
||||
|
||||
const slogan = ref(null)
|
||||
const image = ref(null)
|
||||
const isAlignTop = ref(false)
|
||||
|
||||
const checkAlign = () => {
|
||||
if (slogan.value && image.value)
|
||||
isAlignTop.value = (slogan.value.offsetTop === image.value.offsetTop)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vert-height {
|
||||
min-height: calc(100vh * 0.75);
|
||||
}
|
||||
|
||||
.fix-align:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index:-1;
|
||||
}
|
||||
</style>
|
||||
120
src/components/HowWorksSection.vue
Normal file
120
src/components/HowWorksSection.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<slide-template title="how_it_works__title">
|
||||
<q-resize-observer @resize="updateBlock"/>
|
||||
<div
|
||||
v-if="typeComponent !== 'tablet'"
|
||||
class="flex row q-col-gutter-lg q-pa-md justify-center"
|
||||
>
|
||||
<div
|
||||
:class="typeComponent === 'wide' ? 'col' : 'col-12'"
|
||||
v-for="item in 4"
|
||||
:key="item"
|
||||
style="align-items: stretch;"
|
||||
>
|
||||
<q-card
|
||||
class="q-pa-md fit"
|
||||
>
|
||||
<div class="flex column justify-between fit no-wrap">
|
||||
<div class="flex column w100">
|
||||
<span class="text-uppercase text-grey text-h6">
|
||||
{{ $t('how_works__step' + item) }}
|
||||
</span>
|
||||
|
||||
<span class="text-h6">
|
||||
{{ $t('how_works__step' + item + '_description') }}
|
||||
</span>
|
||||
|
||||
<div class="text-grey">
|
||||
{{ $t(item !== 4 ? 'how_works__step_admin': 'how_works__step_user') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex column items-center justify-end q-mt-md text-grey col-grow"
|
||||
>
|
||||
<img
|
||||
:src="'/img/' + item +'.png'"
|
||||
style="object-fit: contain; max-width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="typeComponent === 'tablet'"
|
||||
class="flex row w100 q-pa-none no-wrap items-center no-scroll"
|
||||
>
|
||||
<div class="col-8">
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="item in 4"
|
||||
:key="item"
|
||||
clickable
|
||||
v-ripple
|
||||
@click="activeItem=item"
|
||||
:active="activeItem===item"
|
||||
active-class="primary"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar color="brand" text-color="white">
|
||||
<span v-if="item !== 4">{{item}}</span>
|
||||
<q-icon v-else name="mdi-check"/>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span class="text-h6">
|
||||
{{ $t('how_works__step' + item + '_description') }}
|
||||
</span>
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<q-tab-panels v-model="activeItem" animated>
|
||||
<q-tab-panel
|
||||
v-for="item in 4"
|
||||
:key="item"
|
||||
:name="item"
|
||||
>
|
||||
<div class="flex column items-center">
|
||||
<div class="text-grey">
|
||||
{{ $t(item !== 4 ? 'how_works__step_admin': 'how_works__step_user') }}
|
||||
</div>
|
||||
<img
|
||||
:src="'/img/' + item +'.png'"
|
||||
style="object-fit: contain; max-width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</slide-template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import SlideTemplate from 'components/SlideTemplate.vue'
|
||||
|
||||
const typeComponent = ref('wide')
|
||||
|
||||
const updateBlock = ({ width }) => {
|
||||
typeComponent.value = width >= 1000
|
||||
? 'wide'
|
||||
: width < 600
|
||||
? 'mobile'
|
||||
: 'tablet'
|
||||
}
|
||||
|
||||
const activeItem = ref(1)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
78
src/components/PriceSection.vue
Normal file
78
src/components/PriceSection.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<slide-template title="price__title">
|
||||
<q-card
|
||||
class="flex column justify-center q-my-md items-center q-gutter-y-md"
|
||||
style="max-width: 400px"
|
||||
>
|
||||
<div class="flex column items-center">
|
||||
<div class="flex text-h6">
|
||||
<telegram-star color="gold" size="24px"/>
|
||||
<span class="q-ml-sm" style="text-decoration: line-through;">2</span>
|
||||
<span class="text-bold text-red">0</span>
|
||||
<span>- {{ $t('price__chat_per_day') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<q-badge color="red" class="q-mr-sm">100% OFF</q-badge>
|
||||
<span>{{ $t('price__sale_date') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
class="bg-grey-3"
|
||||
style="border-radius: 12px;"
|
||||
>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<telegram-star color="gold" size="48px"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-grey">
|
||||
{{ $t('price__stars_pay') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="text-h6">
|
||||
Telegram Stars
|
||||
</q-item-label>
|
||||
<q-item-label class="text-grey">
|
||||
{{ $t('price__stars_description') }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card>
|
||||
|
||||
<q-list class="q-my-none q-pa-md">
|
||||
<q-item
|
||||
v-for="item in priceItems"
|
||||
:key="item.id"
|
||||
dense
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar text-color="brand">
|
||||
<q-icon v-if="item.icon" :name="item.icon" size="md"/>
|
||||
<span v-else class="text-bold">{{ item.text }}</span>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ $t(item.label)}}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</slide-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import telegramStar from 'components/TelegramStar.vue'
|
||||
import SlideTemplate from 'components/SlideTemplate.vue'
|
||||
|
||||
const priceItems = [
|
||||
{ id: 1, icon: 'mdi-all-inclusive', label: 'price_unlimited_users' },
|
||||
{ id: 2, icon: 'mdi-all-inclusive', label: 'price_unlimited_projects' },
|
||||
{ id: 3, text: '5', label: 'price_free_chats' },
|
||||
{ id: 4, icon: 'mdi-lifebuoy', label: 'price_support' }
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
89
src/components/ProblemSection.vue
Normal file
89
src/components/ProblemSection.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<slide-template title="problem__title">
|
||||
<div ref="container" class="w100">
|
||||
<q-resize-observer @resize="updateWidth" />
|
||||
<div
|
||||
v-for="(row, rowIndex) in rows"
|
||||
:key="rowIndex"
|
||||
class="row q-py-none"
|
||||
:class="rowClass(row.length)"
|
||||
>
|
||||
<div
|
||||
v-for="(item, itemIndex) in row"
|
||||
:key="itemIndex"
|
||||
class="flex-item"
|
||||
:style="itemStyle(row.length)"
|
||||
>
|
||||
<problem-section-item
|
||||
:icon="item.icon"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
style="overflow: hidden; min-width: 200px; max-width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slide-template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import ProblemSectionItem from 'components/ProblemSectionItem.vue'
|
||||
import SlideTemplate from 'components/SlideTemplate.vue'
|
||||
|
||||
const problems = [
|
||||
{ id: 1, icon: 'mdi-account-group-outline', title: 'problem__address_book', description: 'problem__address_book_description' },
|
||||
{ id: 2, icon: 'mdi-clipboard-outline', title: 'problem__task_manager', description: 'problem__task_manager_description' },
|
||||
{ id: 3, icon: 'mdi-calendar-month', title: 'problem__meeting', description: 'problem__meeting_description' },
|
||||
{ id: 4, icon: 'mdi-folder-open-outline', title: 'problem__files', description: 'problem__files_description' },
|
||||
{ id: 5, icon: 'mdi-lock-outline', title: 'problem__privacy', description: 'problem__privacy_description' }
|
||||
]
|
||||
|
||||
const baseWidth = 250
|
||||
const containerWidth = ref(0)
|
||||
|
||||
const updateWidth = ({ width }) => {
|
||||
containerWidth.value = width
|
||||
}
|
||||
|
||||
const maxPerRow = computed(() => {
|
||||
return Math.max(1, Math.floor(containerWidth.value / baseWidth))
|
||||
})
|
||||
|
||||
const rows = computed(() => {
|
||||
const total = problems.length
|
||||
const maxRow = maxPerRow.value
|
||||
|
||||
if (maxRow >= total) return [problems]
|
||||
|
||||
const rowCount = Math.ceil(total / maxRow)
|
||||
const baseItems = Math.floor(total / rowCount)
|
||||
const extra = total % rowCount
|
||||
const result = []
|
||||
|
||||
let start = 0
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const take = baseItems + (i < extra ? 1 : 0)
|
||||
result.push(problems.slice(start, start + take))
|
||||
start += take
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const rowClass = (count) => {
|
||||
return count === 1 ? 'justify-center' : 'justify-between'
|
||||
}
|
||||
|
||||
const itemStyle = (count) => {
|
||||
return {
|
||||
flex: `0 0 ${100 / count}%`,
|
||||
maxWidth: `${100 / count}%`
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
31
src/components/ProblemSectionItem.vue
Normal file
31
src/components/ProblemSectionItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex column items-center q-pa-md q-ma-md">
|
||||
<div>
|
||||
<q-avatar
|
||||
color="brand"
|
||||
text-color="white"
|
||||
:icon
|
||||
font-size="65px"
|
||||
size="100px"
|
||||
class="q-my-md"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-bold text-h4">
|
||||
{{ $t(title) }}
|
||||
</div>
|
||||
<div class="text-h6 text-grey-8" style="max-width: 250px; text-align: center;">
|
||||
{{ $t(description) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
icon: String,
|
||||
title: String,
|
||||
description: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
21
src/components/SlideTemplate.vue
Normal file
21
src/components/SlideTemplate.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex column no-wrap">
|
||||
<div class="flex w100 justify-center text-h4 q-mt-md text-bold q-pa-md text-grey">
|
||||
{{ $t(title) }}
|
||||
</div>
|
||||
<div class="flex w100 justify-center q-pb-md column items-center">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
defineProps({
|
||||
title: String
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
25
src/components/TelegramStar.vue
Normal file
25
src/components/TelegramStar.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<svg
|
||||
class="q-ma-none q-pa-none"
|
||||
:style="{
|
||||
fill: color ?? 'red',
|
||||
height: size ?? '42px',
|
||||
width: 'auto'
|
||||
}"
|
||||
width="14" height="15" viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z"></path></svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
color: String,
|
||||
size: String,
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.telegram-star-wrapper :deep(.telegram-star svg) {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
145
src/components/meshBackground.vue
Normal file
145
src/components/meshBackground.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div
|
||||
id="background-canvas-wrapper"
|
||||
class="flex fit column"
|
||||
style="background-color: #00c853; opacity:0.65"
|
||||
>
|
||||
<canvas id="canvas" class="fit"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
const canvasBody = document.getElementById("canvas")
|
||||
const drawArea = canvasBody.getContext("2d")
|
||||
|
||||
const opts = {
|
||||
particleColor: "rgb(255,255,255)",
|
||||
lineColor: "rgb(200,200,200)",
|
||||
particleAmount: 50,
|
||||
defaultSpeed: 0.1,
|
||||
variantSpeed: 1,
|
||||
defaultRadius: 3,
|
||||
variantRadius: 2,
|
||||
linkRadius: 200
|
||||
}
|
||||
|
||||
const delay = 200
|
||||
let tid
|
||||
const rgb = opts.lineColor.match(/\d+/g)
|
||||
let w
|
||||
let h
|
||||
const particles = []
|
||||
|
||||
function resizeReset () {
|
||||
w = canvasBody.width = window.innerWidth
|
||||
h = canvasBody.height = window.innerHeight
|
||||
}
|
||||
|
||||
function deBouncer () {
|
||||
clearTimeout(tid)
|
||||
tid = setTimeout(function() {
|
||||
resizeReset()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
function checkDistance (x1, y1, x2, y2) {
|
||||
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
||||
}
|
||||
|
||||
function setup () {
|
||||
|
||||
resizeReset()
|
||||
for (let i = 0; i < opts.particleAmount; i++){
|
||||
particles.push(new Particle())
|
||||
}
|
||||
window.requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
function loop() {
|
||||
window.requestAnimationFrame(loop)
|
||||
drawArea.clearRect(0, 0, w, h)
|
||||
for (let i = 0; i < particles.length; i++){
|
||||
particles[i].update()
|
||||
particles[i].draw()
|
||||
}
|
||||
for (let i = 0; i < particles.length; i++){
|
||||
linkPoints(particles[i], particles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function linkPoints (point1, hubs){
|
||||
for (let i = 0; i < hubs.length; i++) {
|
||||
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
||||
const opacity = 1 - distance / opts.linkRadius
|
||||
if (opacity > 0) {
|
||||
drawArea.lineWidth = 0.5
|
||||
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
|
||||
drawArea.beginPath()
|
||||
drawArea.moveTo(point1.x, point1.y)
|
||||
drawArea.lineTo(hubs[i].x, hubs[i].y)
|
||||
drawArea.closePath()
|
||||
drawArea.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Particle () {
|
||||
this.x = Math.random() * w
|
||||
this.y = Math.random() * h
|
||||
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
||||
this.directionAngle = Math.floor(Math.random() * 360)
|
||||
this.color = opts.particleColor
|
||||
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
|
||||
this.vector = {
|
||||
x: Math.cos(this.directionAngle) * this.speed,
|
||||
y: Math.sin(this.directionAngle) * this.speed
|
||||
};
|
||||
this.update = function(){
|
||||
this.border();
|
||||
this.x += this.vector.x
|
||||
this.y += this.vector.y
|
||||
};
|
||||
this.border = function(){
|
||||
if (this.x >= w || this.x <= 0) {
|
||||
this.vector.x *= -1;
|
||||
}
|
||||
if (this.y >= h || this.y <= 0) {
|
||||
this.vector.y *= -1;
|
||||
}
|
||||
if (this.x > w) this.x = w
|
||||
if (this.y > h) this.y = h
|
||||
if (this.x < 0) this.x = 0
|
||||
if (this.y < 0) this.y = 0
|
||||
}
|
||||
this.draw = function(){
|
||||
drawArea.beginPath()
|
||||
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
||||
drawArea.closePath()
|
||||
drawArea.fillStyle = this.color
|
||||
drawArea.fill()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", function(){ deBouncer() })
|
||||
resizeReset()
|
||||
setup()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#background-canvas-wrapper {
|
||||
position: absolute !important;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
23
src/css/app.scss
Normal file
23
src/css/app.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.text-brand {
|
||||
color: $green-14 !important;
|
||||
}
|
||||
|
||||
.bg-brand {
|
||||
background: $green-14 !important;
|
||||
}
|
||||
|
||||
$base-width: 100;
|
||||
@while $base-width > 0 {
|
||||
.w#{$base-width} { width: #{$base-width}+'%'; }
|
||||
$base-width: $base-width - 10;
|
||||
}
|
||||
|
||||
:root {
|
||||
--body-width: 1200px;
|
||||
--logo-color-bg-white: grey;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: var(--body-width) !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
25
src/css/quasar.variables.scss
Normal file
25
src/css/quasar.variables.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
// Quasar SCSS (& Sass) Variables
|
||||
// --------------------------------------------------
|
||||
// To customize the look and feel of this app, you can override
|
||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
||||
|
||||
// Check documentation for full list of Quasar variables
|
||||
|
||||
// Your own variables (that are declared here) and Quasar's own
|
||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
||||
|
||||
// It's highly recommended to change the default colors
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #1976D2;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
$dark : #1D1D1D;
|
||||
$dark-page : #121212;
|
||||
|
||||
$positive : #21BA45;
|
||||
$negative : #C10015;
|
||||
$info : #31CCEC;
|
||||
$warning : #F2C037;
|
||||
1
src/i18n/en-US/index.js
Normal file
1
src/i18n/en-US/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'How it works?', main__price: 'Price', main__faq: 'FAQ', main__contacts: 'Contacts', banner__slogan_prepend: 'banner__slogan_prepend', banner__slogan_body: 'banner__slogan_body', banner__main_btn: 'Join!', problem__title: 'problem__title', problem__address_book: 'problem__address_book', problem__address_book_description: 'problem__address_book_description', problem__task_manager: 'problem__task_manager', problem__task_manager_description: 'problem__task_manager_description', problem__meeting: 'problem__meeting', problem__meeting_description: 'problem__meeting_description', problem__files: 'problem__files', problem__files_description: 'problem__files_description', problem__privacy: 'problem__privacy', problem__privacy_description: 'problem__privacy_description', how_it_works__title: 'how_it_works__title', how_works__step1: 'Step 1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'Step 2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'Step 3 (optional)', how_works__step3_description: 'how_works__step3_description', how_works__step4: 'Done!', how_works__step4_description: 'how_works__step3_description', how_works__step_user: 'User', how_works__step_admin: 'Administrator', price__title: 'Price', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price_unlimited_users: 'Unlimited users', price_unlimited_projects: 'Unlimited projects', price_free_chats: 'price_free_chats', price_support: 'Support', FAQ__title: 'FAQ', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5', footer__doc_terms_of_use: 'Term of use', footer__doc_privacy_policy: 'Privacy Policy', footer__contacts_ip: 'Individual Entrepreneur/Sole Proprietor Martyshkin Alexey Alexandrovich', footer__contacts_ip_detail: 'PSRNSP 318774600262084, ITN 366316608346', footer__contacts_location: 'Russia, Moscow/Voronezh' }
|
||||
1
src/i18n/en-US/index.ts
Normal file
1
src/i18n/en-US/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'How it works?', main__price: 'Price', main__faq: 'FAQ', main__contacts: 'Contacts', banner__slogan_prepend: 'banner__slogan_prepend', banner__slogan_body: 'banner__slogan_body', banner__main_btn: 'Join!', problem__title: 'problem__title', problem__address_book: 'problem__address_book', problem__address_book_description: 'problem__address_book_description', problem__task_manager: 'problem__task_manager', problem__task_manager_description: 'problem__task_manager_description', problem__meeting: 'problem__meeting', problem__meeting_description: 'problem__meeting_description', problem__files: 'problem__files', problem__files_description: 'problem__files_description', problem__privacy: 'problem__privacy', problem__privacy_description: 'problem__privacy_description', how_it_works__title: 'how_it_works__title', how_works__step1: 'how_works__step1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'how_works__step2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'how_works__step3', how_works__step3_description: 'how_works__step3_description', price__title: 'price__title', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price__include: 'price__include', price_unlimited_users: 'price_unlimited_users', price_unlimited_projects: 'price_unlimited_projects', price_free_chats: 'price_free_chats', price_support: 'price_support', FAQ__title: 'FAQ__title', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5' }
|
||||
7
src/i18n/index.js
Normal file
7
src/i18n/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import enUS from './en-US'
|
||||
import ruRU from './ru-RU'
|
||||
|
||||
export default {
|
||||
'en-US': enUS,
|
||||
'ru-RU': ruRU
|
||||
}
|
||||
1
src/i18n/ru-RU/index.js
Normal file
1
src/i18n/ru-RU/index.js
Normal file
File diff suppressed because one or more lines are too long
1
src/i18n/ru-RU/index.ts
Normal file
1
src/i18n/ru-RU/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default { EN: 'EN', RU: 'RU', '': '', main__how_it_works: 'Как это работает?', main__price: 'Цены', main__faq: 'Вопросы', main__contacts: 'Контакты', banner__slogan_prepend: 'Больше чем просто чаты', banner__slogan_body: 'Управление проектами внутри Telegram', banner__main_btn: 'Присоединиться', problem__title: 'Преимущества', problem__address_book: 'Адресная книга', problem__address_book_description: 'Не нужно угадывать, кто есть кто.', problem__task_manager: 'Задачи', problem__task_manager_description: 'Задачи из разных чатов в одном месте - все будет сделано!', problem__meeting: 'Совещания', problem__meeting_description: 'problem__meeting_description', problem__files: 'Файлы', problem__files_description: 'Не нужно помнить в каком именно чате был нужный файл. Теперь они все в одном месте.', problem__privacy: 'Доступ к информации', problem__privacy_description: 'Пользователям доступна только та информация, что есть в их чатах.', how_it_works__title: 'Как это работает?', how_works__step1: 'how_works__step1', how_works__step1_description: 'how_works__step1_description', how_works__step2: 'how_works__step2', how_works__step2_description: 'how_works__step2_description', how_works__step3: 'how_works__step3', how_works__step3_description: 'how_works__step3_description', price__title: 'price__title', price__chat_per_day: 'price__chat_per_day', price__sale_date: 'price__sale_date', price__stars_pay: 'price__stars_pay', price__stars_description: 'price__stars_description', price__include: 'price__include', price_unlimited_users: 'price_unlimited_users', price_unlimited_projects: 'price_unlimited_projects', price_free_chats: 'price_free_chats', price_support: 'price_support', FAQ__title: 'FAQ__title', faq__question_1: 'faq__question_1', faq__answer_1: 'faq__answer_1', faq__question_2: 'faq__question_2', faq__answer_2: 'faq__answer_2', faq__question_3: 'faq__question_3', faq__answer_3: 'faq__answer_3', faq__question_4: 'faq__question_4', faq__answer_4: 'faq__answer_4', faq__question_5: 'faq__question_5', faq__answer_5: 'faq__answer_5' }
|
||||
185
src/layouts/MainLayout.vue
Normal file
185
src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<q-layout view="lHr lpr lFr" class="bg-transparent">
|
||||
<q-header class="main-content text-grey glass">
|
||||
<div ref="headerContainer" class="flex q-ma-md justify-between no-wrap items-center">
|
||||
<base-logo ref="logo"/>
|
||||
<div
|
||||
ref="menuContainer"
|
||||
class="row items-center q-ml-md no-wrap"
|
||||
style="min-width: 42px"
|
||||
>
|
||||
<div
|
||||
ref="buttonsContainer"
|
||||
:class="{ 'invisible absolute': !showFullMenu }"
|
||||
class="flex row no-wrap"
|
||||
>
|
||||
<q-btn
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
flat
|
||||
no-caps
|
||||
@click="scrollToElement(item.ref)"
|
||||
ref="menuButtons"
|
||||
>
|
||||
<span class="text-no-wrap">{{ $t(item.title) }}</span>
|
||||
</q-btn>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="!showFullMenu"
|
||||
flat
|
||||
round
|
||||
icon="menu"
|
||||
@click="showDrawer = !showDrawer"
|
||||
/>
|
||||
<q-btn outline color="primary" class="q-ml-sm">
|
||||
<div class="flex items-center no-wrap">
|
||||
<span class="text-bold">{{ locale.split('-')[0] }}</span>
|
||||
</div>
|
||||
|
||||
<q-menu>
|
||||
<q-list style="min-width: 100px">
|
||||
<q-item
|
||||
v-for="lang in langNames"
|
||||
:key="lang"
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="setLocale(lang)"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label :class="isCurrentLang(lang.locale) ? 'text-primary' : ''">{{ lang.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
<q-resize-observer @resize="checkSpace"/>
|
||||
</div>
|
||||
</q-header>
|
||||
|
||||
<q-drawer v-model="showDrawer" side="right" overlay>
|
||||
<div class="flex column items-end q-gutter-y-md q-pa-md">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="mdi-close"
|
||||
@click="showDrawer = !showDrawer"
|
||||
/>
|
||||
<q-btn
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
flat
|
||||
no-caps
|
||||
@click="scrollToElement(item.ref)"
|
||||
>
|
||||
<span class="text-no-wrap text-h6">{{ $t(item.title) }}</span>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container
|
||||
class="main-content q-pa-none q-ma-none bg-transparent"
|
||||
>
|
||||
<q-page class="column">
|
||||
<hero-banner/>
|
||||
<problem-section/>
|
||||
<how-works-section id='how_works'/>
|
||||
<price-section id='price'/>
|
||||
<faq-section id='FAQ'/>
|
||||
<footer-section id='contacts' class="bg-grey-14 text-white"/>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import baseLogo from 'components/BaseLogo.vue'
|
||||
import heroBanner from 'components/HeroBanner.vue'
|
||||
import problemSection from 'components/ProblemSection.vue'
|
||||
import HowWorksSection from 'components/HowWorksSection.vue'
|
||||
import PriceSection from 'components/PriceSection.vue'
|
||||
import FaqSection from 'components/FAQSection.vue'
|
||||
import FooterSection from 'components/FooterSection.vue'
|
||||
|
||||
const showDrawer = ref(false)
|
||||
|
||||
const menuItems = [
|
||||
{ id: 0, title: 'main__how_it_works', ref: 'how_works' },
|
||||
{ id: 1, title: 'main__price', ref: 'price' },
|
||||
{ id: 2, title: 'main__faq', ref: 'FAQ' },
|
||||
{ id: 3, title: 'main__contacts', ref: 'contacts' }
|
||||
]
|
||||
|
||||
const showFullMenu = ref(true)
|
||||
const menuButtons = ref([])
|
||||
const headerContainer = ref(null)
|
||||
const logo = ref(null)
|
||||
const menuContainer = ref(null)
|
||||
|
||||
const calculateButtonsWidth = () => {
|
||||
return menuButtons.value.reduce(
|
||||
(total, btn) => total + (btn?.$el.offsetWidth || 0), 0
|
||||
)
|
||||
}
|
||||
|
||||
const checkSpace = () => {
|
||||
if (!headerContainer.value || !logo.value || !menuContainer.value) return
|
||||
|
||||
const headerWidth = headerContainer.value.offsetWidth
|
||||
const logoWidth = logo.value.$el.offsetWidth
|
||||
const menuMargin = parseFloat(getComputedStyle(menuContainer.value).marginLeft) || 0
|
||||
const availableWidth = headerWidth - logoWidth - menuMargin - 40 // 40px - запас
|
||||
|
||||
const buttonsWidth = calculateButtonsWidth()
|
||||
|
||||
showFullMenu.value = buttonsWidth <= availableWidth
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
checkSpace()
|
||||
})
|
||||
|
||||
|
||||
import { scroll } from 'quasar'
|
||||
const { getScrollTarget, setVerticalScrollPosition } = scroll
|
||||
|
||||
const scrollToElement = (id) => {
|
||||
const el = document.querySelector('#' + id)
|
||||
const target = getScrollTarget(el)
|
||||
const offset = el.offsetTop - 12
|
||||
const duration = 300
|
||||
setVerticalScrollPosition(target, offset, duration)
|
||||
showDrawer.value = false
|
||||
}
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setGlobalLocale } from 'src/boot/i18n'
|
||||
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const langNames = [
|
||||
{ locale: 'en-US', label: 'English'},
|
||||
{ locale: 'ru-RU', label: 'Русский'}
|
||||
]
|
||||
|
||||
const isCurrentLang = (lang) => locale.value === lang
|
||||
|
||||
const setLocale = (newLocale) => {
|
||||
setGlobalLocale(newLocale.locale)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invisible {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background-color: rgba(255, 255, 255, 0.8) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
||||
27
src/pages/ErrorNotFound.vue
Normal file
27
src/pages/ErrorNotFound.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||
<div>
|
||||
<div style="font-size: 30vh">
|
||||
404
|
||||
</div>
|
||||
|
||||
<div class="text-h2" style="opacity:.4">
|
||||
Oops. Nothing here...
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="blue"
|
||||
unelevated
|
||||
to="/"
|
||||
label="Go Home"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
13
src/pages/IndexPage.vue
Normal file
13
src/pages/IndexPage.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<q-page class="flex flex-center">
|
||||
<img
|
||||
alt="Quasar logo"
|
||||
src="~assets/quasar-logo-vertical.svg"
|
||||
style="width: 200px; height: 200px"
|
||||
>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
30
src/router/index.js
Normal file
30
src/router/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineRouter } from '#q-app/wrappers'
|
||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import routes from './routes'
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Router instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Router instance.
|
||||
*/
|
||||
|
||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||
})
|
||||
|
||||
return Router
|
||||
})
|
||||
18
src/router/routes.js
Normal file
18
src/router/routes.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('pages/IndexPage.vue') }
|
||||
]
|
||||
},
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
||||
Reference in New Issue
Block a user