first commit

This commit is contained in:
2025-07-16 22:08:48 +03:00
commit d692b55e9f
52 changed files with 9386 additions and 0 deletions

118
src/components/BaseLogo.vue Normal file
View 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>

View 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>

View 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>

View 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') }} &mdash;
</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>

View 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>

View 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>&nbsp;
<span class="text-bold text-red">0</span>&nbsp;
<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>

View 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>

View 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>

View 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>

View 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>

View 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>