update
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-04-22 13:54:18 +03:00
parent 03b2a8306d
commit 5213e7b2e5
15 changed files with 550 additions and 245 deletions

View File

@@ -1,19 +1,17 @@
<template>
<div class="flex row items-center no-wrap logo-component">
<LogoIcon
v-bind="$attrs"
class="logo-svg"
:class="{
'is-animated': animated,
'hide-bg': !withBackground
}"
:style="{ color: iconColor }"
:style="{ color: iconColor, transform: 'scale(1.08)', height: '1cap' }"
/>
<div
:class="'text-' + textColor"
class="logo-text"
style="margin-left: 0.05em;"
style="margin-left: 0.15em;"
>
<span>tg</span>
<span class="text-bold">Crew</span>
@@ -23,7 +21,6 @@
<script setup>
import LogoIcon from 'components/BaseLogoSvg.vue'
defineProps({
textColor: {
type: String,
@@ -45,38 +42,4 @@
</script>
<style lang="scss" scoped>
.logo-svg {
height: 1em;
width: 1em;
:deep(.logo-bg) {
transition: opacity 0.3s;
}
&.hide-bg :deep(.logo-bg) {
display: none;
}
:deep(.x), :deep(.c) {
transform-box: fill-box;
}
&.is-animated {
:deep(.x) { animation: expand-r 6s ease-in-out infinite; }
:deep(.c) { animation: o 6s ease-in-out infinite; }
:deep(.l) {
stroke-dasharray: 1;
stroke-dashoffset: 1;
stroke-width: 1.5;
animation: draw 6s ease-in-out infinite;
}
}
}
@keyframes expand-r { 50% { r: 8px; } }
@keyframes o { 50% { transform: var(--t); } }
@keyframes draw {
0%, 100% { stroke-dashoffset: 0; }
50% { stroke-dashoffset: 1; }
}
</style>

View File

@@ -2,13 +2,22 @@
<svg
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
v-bind="$attrs"
:class="{ 'is-animated': animated }"
class="base-logo-svg"
>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="logo-gradient">
<stop stop-color="#2AABEE" offset="0%"></stop>
<stop stop-color="#229ED9" offset="100%"></stop>
</linearGradient>
<radialGradient id="logo-gradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" stop-color="white" />
<stop offset="25%" stop-color="var(--q-primary, #1976D2)" stop-opacity="0.5" />
<stop offset="50%" stop-color="var(--q-primary, #1976D2)">
<animate
attributeName="offset"
values="1;0.6;1"
dur="6s"
repeatCount="indefinite"
/>
</stop>
</radialGradient>
</defs>
<rect class="logo-bg" width="32" height="32" rx="4" ry="4" fill="url(#logo-gradient)" />
@@ -26,8 +35,35 @@
</svg>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
<script setup>
defineProps({
animated: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.base-logo-svg {
.x, .c { transform-box: fill-box; }
.l { stroke-width: 1.5; }
&.is-animated {
.x { animation: expand-r 6s infinite; }
.c { animation: o 6s infinite; }
.l {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: draw 6s infinite;
}
}
}
@keyframes expand-r { 50% { r: 8px; } }
@keyframes o { 50% { transform: var(--t); } }
@keyframes draw {
0%, 100% { stroke-dashoffset: 0; }
50% { stroke-dashoffset: 1; }
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<div class="w100">
<div class="row justify-center w100">
<q-resize-observer @resize="checkSpace" />
<div
class="tabs-wrapper no-pointer-events"
style="position: absolute !important; visibility: hidden; height: 0"
>
<q-tabs
ref="hiddenTabsRef"
dense
shrink
align="center"
class="text-grey custom-tabs"
active-color="primary"
indicator-color="primary"
narrow-indicator
:arrows="false"
:outside-arrows="false"
:mobile-arrows="false"
>
<q-tab
v-for="tabItem in tabItems"
:key="tabItem.name"
:name="tabItem.name"
:label="$t(tabItem.label)"
class="custom-tab-item"
/>
</q-tabs>
</div>
<div :class="['tabs-wrapper', fogClass]">
<q-tabs
v-model="tab"
ref="tabsRef"
dense
shrink
align="center"
class="text-grey custom-tabs"
active-color="primary"
indicator-color="primary"
narrow-indicator
:arrows="false"
:outside-arrows="false"
:mobile-arrows="false"
:no-caps="isOverflowing || useTabsNoCaps"
@update:model-value="onTabChange"
>
<q-tab
v-for="tabItem in tabItems"
:key="tabItem.name"
:name="tabItem.name"
:label="$t(tabItem.label)"
class="custom-tab-item"
/>
<q-resize-observer @resize="updateScrollInfo" />
</q-tabs>
</div>
</div>
<div class="column items-center w100">
<slot :item="activeItem"/>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted } from 'vue'
const props = defineProps({
tabItems: { type: Array, required: true },
useTabsNoCaps: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:tabs-no-caps'])
const tab = ref(props.tabItems[0]?.name)
const tabsRef = ref(null)
const hiddenTabsRef = ref(null)
const canScroll = ref(false)
const isOverflowing = ref(false)
const updateScrollInfo = () => {
const el = tabsRef.value?.$el.querySelector('.q-tabs__content')
if (el) canScroll.value = el.scrollWidth > el.clientWidth + 1
}
const checkSpace = (size) => {
const hiddenTabs = hiddenTabsRef.value?.$el.querySelector('.q-tabs__content')
if (hiddenTabs) {
isOverflowing.value = hiddenTabs.scrollWidth > size.width
if (isOverflowing.value !== props.useTabsNoCaps) {
emit('update:tabs-no-caps', isOverflowing.value)
}
}
}
const activeItem = computed(() =>
props.tabItems.find(t => t.name === tab.value) || props.tabItems[0]
)
const fogClass = computed(() => {
const activeNoCaps = isOverflowing.value || props.useTabsNoCaps
if (!canScroll.value || !activeNoCaps) return ''
const index = props.tabItems.findIndex(t => t.name === tab.value)
if (index === 0) return 'is-right'
if (index === props.tabItems.length - 1) return 'is-left'
return 'is-both'
})
const onTabChange = async() => {
await nextTick()
if (tabsRef.value) updateScrollInfo()
}
onMounted(async () => {
await nextTick()
updateScrollInfo()
})
</script>
<style scoped lang="scss">
.tabs-wrapper {
position: relative;
display: inline-block;
max-width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 60px;
z-index: 10;
pointer-events: none;
transition: opacity 0.3s;
opacity: 0;
}
&::before {
left: 0;
background: linear-gradient(to right, $grey-11, transparent);
}
&::after {
right: 0;
background: linear-gradient(to left, $grey-11, transparent);
}
&.is-left::before,
&.is-both::before { opacity: 1; }
&.is-right::after,
&.is-both::after { opacity: 1; }
}
.custom-tabs {
background: transparent !important;
:deep(.q-tabs__content) {
overflow: hidden !important;
}
:deep(.q-tabs__arrow),
:deep(.q-icon) {
display: none !important;
}
:deep(.q-tab--no-caps) {
padding: 0 8px;
}
}
.custom-tab-item {
flex-shrink: 0;
white-space: nowrap;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="column bg-grey-4 rounded-card q-px-xl q-pt-md">
<div class="row no-wrap items-center q-pb-md">
<div class="text-h6" style="flex-grow: 2;">
{{ $t(question) }}
{{ $t(question ?? '') }}
</div>
<div class="q-pl-md">
<q-btn
@@ -20,7 +20,7 @@
<q-slide-transition>
<div v-if="showAnswer">
<div class="text-body text-grey-9 q-pb-md">
{{ $t(answer) }}
{{ $t(answer ?? '') }}
</div>
</div>
</q-slide-transition>
@@ -28,7 +28,7 @@
</div>
</template>
<script setup lang="ts">
<script setup>
import { ref } from 'vue'
defineProps({

View File

@@ -1,48 +1,74 @@
<template>
<div class="w100 flex justify-between text-caption q-pa-md q-gutter-y-md text-grey-9">
<div class="flex column col-12 col-sm">
<base-logo class="text-body1 q-pb-sm"/>
<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 (916) 439-04-25</span>
</div>
<div class="flex items-center">
<q-icon name="mdi-email-outline" color="brand" class="q-pr-sm"/>
<a
href="mailto:info@tgcrew.ru"
style="text-decoration: none; color: inherit"
>
info@tgcrew.ru
</a>
<div class="q-pl-sm">
<base-logo
withBackground
animated
class="text-h4"
/>
<div class="flex items-center no-wrap">
<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 no-wrap">
<q-icon name="mdi-phone-outline" color="brand" class="q-pr-sm" />
<span>+7 (916) 439-04-25</span>
</div>
<div class="flex items-center no-wrap">
<q-icon name="mdi-email-outline" color="brand" class="q-pr-sm"/>
<a
href="mailto:info@tgcrew.ru"
style="text-decoration: none; color: inherit"
>
info@tgcrew.ru
</a>
</div>
<br>
<div class="">
{{ $t('footer__contacts_ogrnip') + ' ' }}
<span class="text-no-wrap">
318774600262084
</span>
</div>
<div class="">
{{ $t('footer__contacts_inn') + ' ' }}
<span class="text-no-wrap">
366316608346
</span>
</div>
<div class="">
{{ $t('footer__contacts_pdn') + ' ' }}
<span class="text-no-wrap">
77-25-471585
</span>
</div>
</div>
</div>
<div class="flex column col-12 col-sm">
<span class="text-body1 text-bold">
{{ $t('footer__docs') }}
</span>
<div
v-for="item in docs"
:key="item.id"
class="text-caption"
>
<span
@click="router.push({ name: item.route })"
class="cursor-pointer"
style="text-decoration: underline; color: inherit"
>
{{ $t(item.translationKey) }}
<div class="q-pl-sm">
<span class="text-body1 text-bold">
{{ $t('footer__docs') }}
</span>
<div
v-for="item in docs"
:key="item.id"
class="text-caption"
>
<span
@click="router.push({ name: item.route })"
class="cursor-pointer"
style="text-decoration: underline; color: inherit"
>
{{ $t(item.translationKey) }}
</span>
</div>
</div>
</div>
<div class="flex column col-12 col-sm">
<div class="text-grey">
<div class="text-grey q-pl-sm">
{{ $t('footer__description_user_data') }}
</div>
</div>

View File

@@ -1,71 +1,56 @@
<template>
<slide-template
title="how_it_works__title"
>
<q-tabs
v-model="tab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
outside-arrows
mobile-arrows
<slide-template
title="how_it_works__title"
class="overflow-hidden w100"
>
<q-tab
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
:label="$t(tab.label)"
/>
</q-tabs>
<q-tab-panels
v-model="tab"
animated
class="w60 bg-transparent"
>
<q-tab-panel
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
<custom-tabs
v-bind="$attrs"
:tabItems
:useTabsNoCaps
>
<div
class="relative-position w100">
<div
class="abolute-position w100"
style="height: 60vh;"
>
<iframe
src="https://kinescope.io/embed/5yesvjfi6XAYRuxzbsYGHZ"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; gyroscope; accelerometer; clipboard-write; screen-wake-lock; fullscreen;" frameborder="0" style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"
>
</iframe>
</div>
</div>
<!-- <q-skeleton
style="height: calc(100vw * 0.5 / 16 * 9)"
square
/> -->
</q-tab-panel>
</q-tab-panels>
</slide-template>
<template #default="{ item }">
<div class="relative-position w60 q-pt-md">
<div style="height: 60vh; position: relative;">
<iframe
:key="item.name"
:src="item.url"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; gyroscope; accelerometer; clipboard-write; screen-wake-lock; fullscreen;"
frameborder="0"
style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"
/>
</div>
</div>
</template>
</custom-tabs>
</slide-template>
</template>
<script setup lang="ts">
import { ref } from 'vue'
<script setup>
import SlideTemplate from 'components/SlideTemplate.vue'
const tabs = [
{ name: 'intro', label: 'how_it_works__intro'},
{ name: 'admin', label: 'how_it_works__admin'},
{ name: 'user', label: 'how_it_works__user'}
import CustomTabs from 'components/CustomTabs.vue'
defineProps({
useTabsNoCaps: Boolean
})
const tabItems = [
{
name: 'intro',
label: 'how_it_works__intro',
url: 'https://kinescope.io/embed/5yesvjfi6XAYRuxzbsYGHZ'
},
{
name: 'admin',
label: 'how_it_works__admin',
url: 'https://kinescope.io/embed/5yesvjfi6XAYRuxzbsYGHZ'
},
{
name: 'user',
label: 'how_it_works__user',
url: 'https://kinescope.io/embed/5yesvjfi6XAYRuxzbsYGHZ'
}
]
const tab = ref(tabs[0].name)
</script>
<style scoped>
<style scoped lang="scss">
</style>

View File

@@ -3,99 +3,94 @@
title="price__title"
subtitle="price__subtitle"
>
<q-tabs
v-model="tab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
<custom-tabs
v-bind="$attrs"
:tabItems
:useTabsNoCaps
>
<q-tab
v-for="tab in tabs"
:key="tab.name"
:name="tab.name"
:label="$t(tab.label)"
/>
</q-tabs>
<div
class="fit row q-pb-lg"
>
<div class="col-md-3 col-sm-6 col-xs-12 q-pa-lg"
v-for="(item, idx) in tariff"
:key="idx"
>
<price-section-item
:name="item.name"
:chats-qty="item.chatsQty"
:price="tab === 'legal' ? item.price_rub : item.price"
:price_unit="tab === 'legal' ? 'rub' : 'stars'"
/>
</div>
</div>
<q-card
flat
class="bg-white rounded-card"
>
<q-item v-if="tab === 'legal'">
<q-item-section avatar>
<span class="text-h4 text-grey q-px-sm">
</span>
</q-item-section>
<q-item-section>
<q-item-label class="text-grey">
{{ $t('price__rub_pay') }}
</q-item-label>
<q-item-label class="text-h6">
{{ $t('price__rub_resident') }}
</q-item-label>
<q-item-label class="text-grey">
{{ $t('price__rub_closing_documents') }}
</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="tab === 'individual'">
<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>
</slide-template>
<template #default="{ item }">
<div
class="fit row q-pb-lg"
>
<div class="col-md-3 col-sm-6 col-xs-12 q-pa-lg"
v-for="(tariff, idx) in tariffs"
:key="idx"
>
<price-section-item
:name="tariff.name"
:chats-qty="tariff.chatsQty ?? 0"
:price="item.name === 'legal' ? tariff.price_rub : tariff.price"
:price_unit="item.name === 'legal' ? 'rub' : 'stars'"
/>
</div>
</div>
<q-card
flat
class="bg-white rounded-card"
>
<q-item v-if="item.name === 'legal'">
<q-item-section avatar>
<span class="text-h4 text-grey q-px-sm">
</span>
</q-item-section>
<q-item-section>
<q-item-label class="text-grey">
{{ $t('price__rub_pay') }}
</q-item-label>
<q-item-label class="text-h6">
{{ $t('price__rub_resident') }}
</q-item-label>
<q-item-label class="text-grey">
{{ $t('price__rub_closing_documents') }}
</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="item.name === 'individual'">
<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>
</template>
</custom-tabs>
</slide-template>
</template>
<script setup lang="ts">
import { ref } from 'vue'
<script setup>
import SlideTemplate from 'components/SlideTemplate.vue'
import CustomTabs from 'components/CustomTabs.vue'
import PriceSectionItem from 'components/PriceSectionItem.vue'
import telegramStar from 'components/TelegramStar.vue'
import TelegramStar from 'components/TelegramStar.vue'
const tabs = [
defineProps({
useTabsNoCaps: Boolean
})
const tabItems = [
{ name: 'legal', label: 'price__legal'},
{ name: 'individual', label: 'price__individual'}
]
const tab = ref(tabs[0].name)
const tariff = [
const tariffs = [
{ id: 1, name: 'TEST', price: null, price_rub: null, chatsQty: 5 },
{ id: 2, name: 'START', price: 1000, price_rub: 2000, chatsQty: 15 },
{ id: 3, name: 'PRO', price: 5000, price_rub: 10000, chatsQty: 40 },
{ id: 4, name: 'VIP', price: 12000, price_rub: 24000, chatsQty: null }
]
</script>
<style scoped>
<style scoped lang="scss">
</style>

View File

@@ -6,10 +6,10 @@
color="primary"
/>
<div class="text-bold text-h5 text-center q-pt-md">
{{ $t(title) }}
{{ $t(title ?? '') }}
</div>
<div class="text-grey text-body1 text-center q-pt-sm text-wrap">
{{ $t(description) }}
{{ $t(description ?? '') }}
</div>
</div>
</template>