flatten easySite directory: remove extra easySite/easySite nesting

- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/
- Updated docker-compose.yml build context path
- Deleted empty nested easySite/ directory
This commit is contained in:
valitovgaziz
2026-06-12 11:16:15 +05:00
parent 888bb2d87b
commit 2941b14b38
116 changed files with 1 additions and 30 deletions
@@ -0,0 +1,39 @@
<template>
<div class="modal-overlay" @click="$emit('close')">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2 class="modal-title">Бронирование</h2>
<button class="modal-close" @click="$emit('close')"></button>
</div>
<div class="modal-body">
<!-- Контент модального окна бронирования -->
<div class="booking-summary">
<h3>{{ object?.title }}</h3>
<p>{{ object?.city }}, {{ object?.address }}</p>
</div>
<!-- Форма бронирования -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
// Определяем тип объекта недвижимости
interface RentalObject {
title: string;
city?: string; // опционально, если может отсутствовать
address?: string; // опционально
// другие поля при необходимости
}
defineProps<{
object: RentalObject | null | undefined;
dates: unknown;
guests: string;
}>()
defineEmits<{
close: [];
confirm: [bookingData: unknown];
}>()
</script>
@@ -0,0 +1,33 @@
<template>
<div class="gallery-overlay" @click="$emit('close')">
<div class="gallery-content" @click.stop>
<button class="gallery-close" @click="$emit('close')"></button>
<img :src="currentImage" :alt="`Image ${currentIndex + 1}`" class="gallery-image" >
<button class="gallery-nav prev" @click="prevImage"></button>
<button class="gallery-nav next" @click="nextImage"></button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
images: string[]
initialIndex: number
}>()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits<{
close: []
}>()
const currentIndex = ref(props.initialIndex)
const currentImage = computed(() => props.images[currentIndex.value])
const nextImage = () => {
currentIndex.value = (currentIndex.value + 1) % props.images.length
}
const prevImage = () => {
currentIndex.value = (currentIndex.value - 1 + props.images.length) % props.images.length
}
</script>
@@ -0,0 +1,91 @@
<template>
<div class="card cursor-pointer" @click="$emit('click')">
<div class="relative">
<img
:src="imageSrc"
:alt="object.title"
class="w-full h-48 object-cover"
>
<div class="absolute top-2 right-2">
<span class="badge" :class="statusBadgeClass">
{{ statusLabel }}
</span>
</div>
</div>
<div class="card-body">
<h3 class="text-lg font-semibold mb-2">{{ object.title || object.short_name }}</h3>
<p class="text-gray-600 text-sm mb-3 line-clamp-2">
{{ object.address || 'Адрес не указан' }}
</p>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1">
<span class="text-yellow-500"></span>
<span class="text-sm font-medium">{{ averageScore }}</span>
</div>
<div class="text-right">
<div class="font-bold text-primary-600">
{{ formatPrice(object.price) }}
</div>
<div class="text-xs text-gray-500">{{ object.price_period || 'за единицу' }}</div>
</div>
</div>
<div class="mt-3 flex items-center text-sm text-gray-500">
<span class="mr-2">📍</span>
<span>{{ object.address || 'Адрес не указан' }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ObjectShortResponse } from '~/types/objects'
interface Props {
object: ObjectShortResponse
}
const props = defineProps<Props>()
defineEmits<{ click: [] }>()
const imageSrc = computed(() => {
return '/images/placeholder.jpg'
})
const averageScore = computed(() => {
return props.object.tourist_average_score || props.object.entrepreneur_average_score || '—'
})
const statusLabel = computed(() => {
const labels: Record<string, string> = {
active: 'Активен',
draft: 'Черновик',
moderation: 'На модерации',
inactive: 'Неактивен',
rejected: 'Отклонён'
}
return labels[props.object.status] || props.object.status
})
const statusBadgeClass = computed(() => {
const classes: Record<string, string> = {
active: 'badge-success',
draft: 'badge-secondary',
moderation: 'badge-warning',
inactive: 'badge-secondary',
rejected: 'badge-error'
}
return classes[props.object.status] || 'badge-secondary'
})
const formatPrice = (price: number | undefined) => {
if (!price && price !== 0) return '—'
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0
}).format(price)
}
</script>
@@ -0,0 +1,194 @@
<template>
<form class="space-y-6" @submit.prevent="handleSubmit">
<div class="card">
<div class="card-header">
<h3 class="text-lg font-semibold">Основная информация</h3>
</div>
<div class="card-body space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label class="form-label">Название объекта *</label>
<input
v-model="formData.short_name"
type="text"
class="form-input"
required
placeholder="Короткое название">
</div>
<div class="form-group">
<label class="form-label">Полное название</label>
<input
v-model="formData.long_name"
type="text"
class="form-input"
placeholder="Полное название (если отличается)">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label class="form-label">Тип объекта *</label>
<select v-model="formData.type" class="form-select" required>
<option value="">Выберите тип</option>
<option value="hotel">Гостиница</option>
<option value="sanatorium">Санаторий</option>
<option value="guest_house">Гостевой дом</option>
<option value="tour">Тур</option>
<option value="excursion">Экскурсия</option>
<option value="restaurant">Ресторан</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Статус</label>
<select v-model="formData.status" class="form-select">
<option value="draft">Черновик</option>
<option value="moderation">Отправить на модерацию</option>
<option value="active">Активен</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Описание</label>
<textarea
v-model="formData.description" class="form-input" rows="4"
placeholder="Подробное описание объекта"/>
</div>
<div class="form-group">
<label class="form-label">Краткое описание</label>
<textarea
v-model="formData.short_description" class="form-input" rows="2"
placeholder="Краткое описание для списка"/>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="text-lg font-semibold">Местоположение</h3>
</div>
<div class="card-body space-y-4">
<div class="form-group">
<label class="form-label">Адрес</label>
<input
v-model="formData.address" type="text" class="form-input"
placeholder="Полный адрес">
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="text-lg font-semibold">Цены и контакты</h3>
</div>
<div class="card-body space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-group">
<label class="form-label">Цена</label>
<input v-model="formData.price" type="number" class="form-input" placeholder="0">
</div>
<div class="form-group">
<label class="form-label">Период цены</label>
<select v-model="formData.price_period" class="form-select">
<option value="">Не указано</option>
<option value="per_night">За ночь</option>
<option value="per_person">За человека</option>
<option value="per_tour">За тур</option>
<option value="per_hour">За час</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Сайт</label>
<input
v-model="formData.site" type="url" class="form-input"
placeholder="https://">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
<label class="form-label">Телефон</label>
<input
v-model="formData.phone" type="tel" class="form-input"
placeholder="+7 (XXX) XXX-XX-XX">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input
v-model="formData.email" type="email" class="form-input"
placeholder="email@example.com">
</div>
</div>
</div>
</div>
<div class="flex gap-4 justify-end">
<button type="button" class="btn btn-outline" :disabled="loading" @click="$emit('cancel')">
Отмена
</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span v-if="loading">Сохранение...</span>
<span v-else>{{ object ? 'Обновить' : 'Создать' }}</span>
</button>
</div>
</form>
</template>
<script setup lang="ts">
interface ObjectFormData {
short_name: string
long_name: string
type: string
description: string
short_description: string
address: string
price: number | null
price_period: string
phone: string
email: string
site: string
status: string
}
interface Props {
object?: ObjectFormData | null
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
object: null,
loading: false
})
const emit = defineEmits<{
submit: [formData: ObjectFormData]
cancel: []
}>()
const formData = reactive<ObjectFormData>({
short_name: '',
long_name: '',
type: '',
description: '',
short_description: '',
address: '',
price: null,
price_period: '',
phone: '',
email: '',
site: '',
status: 'draft'
})
watch(() => props.object, (newObject) => {
if (newObject) {
Object.assign(formData, newObject)
}
}, { immediate: true })
const handleSubmit = () => {
const data = { ...formData }
if (data.price === null) {
delete (data as Record<string, unknown>).price
}
if (!data.price_period) delete (data as Record<string, unknown>).price_period
emit('submit', data)
}
</script>
@@ -0,0 +1,72 @@
<template>
<form @submit.prevent="handleSubmit" class="space-y-6">
<div class="form-group">
<label class="form-label">Email</label>
<input
v-model="form.email"
type="email"
class="form-input"
placeholder="your@email.com"
required
>
</div>
<div class="form-group">
<label class="form-label">Пароль</label>
<input
v-model="form.password"
type="password"
class="form-input"
placeholder="Введите пароль"
required
>
</div>
<div class="flex items-center justify-between">
<label class="flex items-center">
<input type="checkbox" class="rounded border-gray-300">
<span class="ml-2 text-sm text-gray-600">Запомнить меня</span>
</label>
<a href="#" class="text-sm text-primary-600 hover:text-primary-700">
Забыли пароль?
</a>
</div>
<button
type="submit"
class="btn btn-primary w-full"
:disabled="loading"
>
<span v-if="loading">Вход...</span>
<span v-else>Войти</span>
</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const { login } = useAuth()
const form = ref({
email: '',
password: ''
})
const loading = ref(false)
const handleSubmit = async () => {
loading.value = true
try {
await login(form.value)
await navigateTo('/profile')
} catch (error) {
console.error('Login error:', error)
// Здесь можно добавить уведомление об ошибке
} finally {
loading.value = false
}
}
</script>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,394 @@
<template>
<footer class="footer">
<div class="container">
<!-- Основное содержимое футера -->
<div class="footer-content">
<!-- Информация о компании -->
<div class="footer-section">
<div class="footer-logo">
<div class="logo-icon">
<span class="logo-text">ES</span>
</div>
<span class="logo-name">EasySite</span>
</div>
<p class="footer-description">
Платформа для агрегации туристических объектов.
</p>
<div class="social-links">
<a href="https://vk.com/club222248484" target="_blank" class="social-link" aria-label="ВКонтакте">
<span class="social-icon">📘</span>
</a>
<a href="https://t.me/+oYymS0r6qG9lYWJi" target="_blank" class="social-link" aria-label="Telegram">
<span class="social-icon">📧</span>
</a>
<a href="https://rutube.ru/channel/26509398/" target="_blank" class="social-link" aria-label="RuTube">
<span class="social-icon">📺</span>
</a>
</div>
</div>
<!-- Реквизиты -->
<div class="footer-section">
<h3 class="footer-title"><NuxtLink href="/requisites">Реквизиты</NuxtLink></h3>
<div class="footer-info">
<div class="info-item">
<span class="info-label">ОГРН:</span>
<span class="info-value">1220200038112</span>
</div>
<div class="info-item">
<span class="info-label">ИНН:</span>
<span class="info-value">0234009584</span>
</div>
<div class="info-item">
<span class="info-label">КПП:</span>
<span class="info-value">023401001</span>
</div>
<div class="info-item">
<span class="info-label">Юр. адрес:</span>
<span class="info-value">Нет</span>
</div>
</div>
</div>
<!-- Контакты -->
<div class="footer-section">
<h3 class="footer-title"><NuxtLink href="/contact">Контакты</NuxtLink></h3>
<div class="footer-info">
<div class="info-item">
<span class="info-icon">📞</span>
<a href="tel:+79625439343" class="info-link">8 (962) 543-93-43</a>
</div>
<div class="info-item">
<span class="info-icon"></span>
<a href="mailto:valitovgaziz@yandex.ru" class="info-link">valitovgaziz@yandex.ru</a>
</div>
<div class="info-item">
<span class="info-icon">🕒</span>
<span class="info-value">Пн-Пт: 9:00-18:00</span>
</div>
<div class="info-item">
<span class="info-icon">📍</span>
<span class="info-value">г. УФа</span>
</div>
</div>
</div>
<!-- Вакансии -->
<div class="footer-section">
<h3 class="footer-title"><NuxtLink to="/vacations">Карьера</NuxtLink></h3>
<div class="footer-links">
<NuxtLink href="/vacations" class="footer-link">💼 Открытые вакансии</NuxtLink>
<NuxtLink href="/partner" class="footer-link">👥 Стать партнером</NuxtLink>
<NuxtLink href="/about" class="footer-link">🏢 О компании</NuxtLink>
<NuxtLink href="/news" class="footer-link">📰 Новости</NuxtLink>
</div>
</div>
<!-- Навигация -->
<div class="footer-section">
<h3 id="poleznoe" class="footer-title">Полезное</h3>
<div class="footer-links">
<NuxtLink to="/objects" class="footer-link">🔍 Все объекты</NuxtLink>
<NuxtLink to="/objects/create" class="footer-link"> Добавить объект</NuxtLink>
<NuxtLink href="/support" class="footer-link"> Помощь</NuxtLink>
<NuxtLink href="/rules" class="footer-link">📋 Тарифные планы</NuxtLink>
</div>
</div>
</div>
<!-- Нижняя часть футера -->
<div class="footer-bottom">
<div class="footer-bottom-content">
<div class="copyright">
© 2025 TravelEasy. Все права защищены.
</div>
<div class="footer-bottom-links">
<nuxt-link href="/agreements/privacy" class="footer-bottom-link">Политика конфиденциальности</nuxt-link>
<nuxt-link href="/agreements/userAgreement" class="footer-bottom-link">Пользовательское
соглашение</nuxt-link>
<a href="/sitemap.xml" class="footer-bottom-link">Карта сайта</a>
</div>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
// Компонент футера
</script>
<style scoped>
.footer {
background: var(--bg-primary);
color: var(--text-primary);
margin-top: auto;
border-top: 1px solid var(--border-light);
transition: all 0.3s ease;
}
.footer-content {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
gap: 3rem;
padding: 3rem 0 2rem;
}
.footer-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.footer-logo {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.logo-icon {
width: 2.5rem;
height: 2.5rem;
background: var(--primary-500);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.logo-text {
color: var(--text-inverse);
font-weight: bold;
font-size: 1.125rem;
}
.logo-name {
font-weight: bold;
font-size: 1.5rem;
color: var(--text-primary);
transition: color 0.3s ease;
}
.footer-description {
color: var(--text-secondary);
line-height: 1.6;
font-size: 0.9rem;
transition: color 0.3s ease;
}
.social-links {
display: flex;
gap: 1rem;
}
.social-link {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 50%;
transition: all 0.3s ease;
text-decoration: none;
}
.social-link:hover {
background: var(--primary-500);
border-color: var(--primary-500);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.social-icon {
font-size: 1.2rem;
}
.footer-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
transition: text-shadow 0.3s ease, color 0.3s ease;
}
.footer-title:hover {
text-shadow: 0 0 5px #fff, /* Белая тень (основное свечение) */
0 0 10px #fff, /* Более широкая белая тень */
0 0 15px #00ffff, /* Голубой оттенок свечения */
0 0 20px #00ffff; /* Ещё шире голубой оттенок */
}
#poleznoe:hover {
text-shadow: none;
}
.footer-title-margin {
margin-top: 1.5rem;
}
.footer-info {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.9rem;
}
.info-label {
color: var(--text-secondary);
min-width: 80px;
transition: color 0.3s ease;
}
.info-value {
color: var(--text-secondary);
line-height: 1.4;
transition: color 0.3s ease;
}
.info-icon {
font-size: 1rem;
min-width: 1.5rem;
color: var(--text-tertiary);
transition: color 0.3s ease;
}
.info-link {
color: var(--text-secondary);
text-decoration: none;
transition: all 0.3s ease;
}
.info-link:hover {
color: var(--primary-500);
text-decoration: underline;
}
.footer-links {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-link {
color: var(--text-secondary);
text-decoration: none;
transition: all 0.3s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.footer-link:hover {
color: var(--primary-500);
transform: translateX(4px);
}
.footer-bottom {
border-top: 1px solid var(--border-light);
padding: 1.5rem 0;
transition: border-color 0.3s ease;
}
.footer-bottom-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.copyright {
color: var(--text-tertiary);
font-size: 0.875rem;
transition: color 0.3s ease;
}
.footer-bottom-links {
display: flex;
gap: 2rem;
}
.footer-bottom-link {
color: var(--text-tertiary);
text-decoration: none;
font-size: 0.875rem;
transition: all 0.3s ease;
}
.footer-bottom-link:hover {
color: var(--primary-500);
text-decoration: underline;
}
/* Адаптивность */
@media (max-width: 1024px) {
.footer-content {
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr;
gap: 2rem;
padding: 2rem 0 1.5rem;
}
.footer-bottom-content {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.footer-bottom-links {
flex-direction: column;
gap: 0.5rem;
}
.footer-logo {
justify-content: center;
}
.footer-section {
text-align: center;
}
.info-item {
justify-content: center;
}
.social-links {
justify-content: center;
}
}
@media (max-width: 480px) {
.footer-content {
gap: 1.5rem;
}
.footer-title {
font-size: 1rem;
}
.logo-name {
font-size: 1.25rem;
}
.footer-description {
font-size: 0.85rem;
}
}
</style>
@@ -0,0 +1,501 @@
<template>
<div class="hamburger-menu">
<!-- Кнопка гамбургера -->
<button
class="hamburger-button"
:class="{ 'active': isOpen }"
aria-label="Меню"
@click="toggleMenu">
<span class="hamburger-line"/>
<span class="hamburger-line"/>
<span class="hamburger-line"/>
</button>
<!-- Затемнение фона -->
<div v-if="isOpen" class="menu-overlay" @click="closeMenu"/>
<!-- Само меню -->
<transition name="slide-left">
<div v-if="isOpen" class="menu-content">
<!-- Заголовок меню -->
<div class="menu-header">
<div v-if="false" class="user-info"> <!-- Можно добавить позже -->
<div class="user-avatar">👤</div>
<div class="user-details">
<div class="user-name">Гость</div>
<div class="user-status">Не авторизован</div>
</div>
</div>
<h3 class="menu-title">Меню</h3>
<button class="close-button" aria-label="Закрыть" @click="closeMenu">
</button>
</div>
<!-- Настройки тем -->
<div class="menu-section">
<h4 class="menu-section-title">Настройки</h4>
<!-- Переключатель темы -->
<div class="menu-item toggle-item">
<div class="toggle-label">
<span class="toggle-icon">🌙</span>
<span class="toggle-text">Темная тема</span>
</div>
<button
aria-label="Переключить тему"
class="theme-toggle"
:class="{ 'active': theme === 'dark' }"
@click="toggleTheme">
<span class="toggle-slider"/>
</button>
</div>
<!-- Переключатель шрифтов -->
<div class="menu-item toggle-item">
<div class="toggle-label">
<span class="toggle-icon">🔤</span>
<span class="toggle-text">Элегантный шрифт</span>
</div>
<button
class="font-toggle"
:class="{ 'active': fontSet === 'elegant' }"
aria-label="Переключить шрифт"
@click="toggleFontSet">
<span class="toggle-slider"/>
</button>
</div>
</div>
<!-- Навигация -->
<div class="menu-section">
<h4 class="menu-section-title">Аккаунт</h4>
<NuxtLink to="/profile" class="menu-item" @click="closeMenu">
<span class="menu-icon">👤</span>
<span class="menu-text">Профиль</span>
</NuxtLink>
<NuxtLink to="/auth/register" class="menu-item" @click="closeMenu">
<span class="menu-icon">📝</span>
<span class="menu-text">Регистрация</span>
</NuxtLink>
<NuxtLink to="/auth/login" class="menu-item" @click="closeMenu">
<span class="menu-icon">🔑</span>
<span class="menu-text">Вход</span>
</NuxtLink>
</div>
<!-- Дополнительные ссылки -->
<div class="menu-section">
<h4 class="menu-section-title">Навигация</h4>
<NuxtLink to="/" class="menu-item" @click="closeMenu">
<span class="menu-icon">🏠</span>
<span class="menu-text">Главная</span>
</NuxtLink>
<NuxtLink to="/objects" class="menu-item" @click="closeMenu">
<span class="menu-icon">🔍</span>
<span class="menu-text">Все объекты</span>
</NuxtLink>
<NuxtLink to="/objects/create" class="menu-item" @click="closeMenu">
<span class="menu-icon"></span>
<span class="menu-text">Добавить объект</span>
</NuxtLink>
<!-- В секции "Навигация" добавьте: -->
<NuxtLink to="/about" class="menu-item" @click="closeMenu">
<span class="menu-icon"></span>
<span class="menu-text">О компании</span>
</NuxtLink>
<a href="/support" class="menu-item" @click="closeMenu">
<span class="menu-icon"></span>
<span class="menu-text">Помощь</span>
</a>
<a href="/contact" class="menu-item" @click="closeMenu">
<span class="menu-icon">📞</span>
<span class="menu-text">Контакты</span>
</a>
</div>
<!-- Футер меню -->
<div class="menu-footer">
<div class="menu-version">v1.0.0</div>
<div class="menu-copyright">© 2024 EasySite</div>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
const isOpen = ref(false)
const { theme, toggleTheme, fontSet, toggleFontSet } = useTheme()
const toggleMenu = () => {
isOpen.value = !isOpen.value
}
const closeMenu = () => {
isOpen.value = false
}
// Закрытие меню при нажатии ESC
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen.value) {
closeMenu()
}
}
// Закрытие меню при изменении размера окна на десктоп
const handleResize = () => {
if (window.innerWidth >= 1024 && isOpen.value) {
closeMenu()
}
}
onMounted(() => {
document.addEventListener('keydown', handleEscape)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleEscape)
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.hamburger-menu {
position: relative;
}
/* Кнопка гамбургера */
.hamburger-button {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 2rem;
height: 1.5rem;
background: none;
border: none;
cursor: pointer;
padding: 0;
transition: all 0.3s ease;
}
.hamburger-button:hover {
opacity: 0.8;
transform: scale(1.05);
}
.hamburger-line {
width: 100%;
height: 2px;
background: var(--text-primary);
transition: all 0.3s ease;
border-radius: 1px;
}
.hamburger-button.active .hamburger-line:nth-child(1) {
transform: rotate(45deg) translate(6px, 6px);
}
.hamburger-button.active .hamburger-line:nth-child(2) {
opacity: 0;
}
.hamburger-button.active .hamburger-line:nth-child(3) {
transform: rotate(-45deg) translate(6px, -6px);
}
/* Затемнение фона */
.menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 998;
}
/* Контент меню */
.menu-content {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 380px;
max-width: 90vw;
background: var(--bg-primary);
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.1);
z-index: 999;
display: flex;
flex-direction: column;
overflow-y: auto;
border-left: 1px solid var(--border-light);
}
/* Анимация появления */
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform 0.3s ease;
}
.slide-left-enter-from,
.slide-left-leave-to {
transform: translateX(100%);
}
/* Заголовок меню */
.menu-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border-light);
background: var(--bg-secondary);
}
.user-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-avatar {
width: 2.5rem;
height: 2.5rem;
background: var(--primary-500);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
color: var(--text-primary);
font-size: 0.95rem;
}
.user-status {
font-size: 0.8rem;
color: var(--text-tertiary);
}
.menu-title {
font-family: var(--font-heading);
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
padding: 0.25rem;
border-radius: var(--radius-sm);
transition: all 0.2s ease;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
/* Секции меню */
.menu-section {
padding: 1.5rem;
border-bottom: 1px solid var(--border-light);
}
.menu-section:last-child {
border-bottom: none;
}
.menu-section-title {
font-family: var(--font-primary);
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
/* Элементы меню */
.menu-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
text-decoration: none;
color: var(--text-primary);
transition: all 0.2s ease;
border-radius: var(--radius-md);
cursor: pointer;
}
.menu-item:hover {
background: var(--bg-secondary);
color: var(--primary-600);
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.menu-icon {
font-size: 1.25rem;
width: 1.5rem;
text-align: center;
}
.menu-text {
font-family: var(--font-primary);
font-weight: 500;
font-size: 0.95rem;
flex: 1;
}
/* Переключатели */
.toggle-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
}
.toggle-label {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}
.toggle-icon {
font-size: 1.25rem;
width: 1.5rem;
text-align: center;
}
.toggle-text {
font-family: var(--font-primary);
font-weight: 500;
font-size: 0.95rem;
color: var(--text-primary);
}
/* Стили для тогглов */
.theme-toggle,
.font-toggle {
position: relative;
width: 3rem;
height: 1.5rem;
background: var(--gray-300);
border: none;
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
padding: 0;
flex-shrink: 0;
}
.theme-toggle:hover,
.font-toggle:hover {
transform: scale(1.05);
}
.theme-toggle.active,
.font-toggle.active {
background: var(--primary-500);
}
.toggle-slider {
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.theme-toggle.active .toggle-slider,
.font-toggle.active .toggle-slider {
transform: translateX(1.5rem);
}
/* Футер меню */
.menu-footer {
margin-top: auto;
padding: 1.5rem;
border-top: 1px solid var(--border-light);
text-align: center;
background: var(--bg-secondary);
}
.menu-version {
font-size: 0.75rem;
color: var(--text-tertiary);
margin-bottom: 0.25rem;
}
.menu-copyright {
font-size: 0.75rem;
color: var(--text-tertiary);
}
/* Адаптивность для десктопа */
@media (min-width: 1024px) {
.menu-content {
width: 400px;
}
.hamburger-button {
width: 2.25rem;
height: 1.75rem;
}
}
@media (max-width: 480px) {
.menu-content {
width: 300px;
}
.menu-section {
padding: 1rem;
}
.menu-header {
padding: 1rem;
}
.menu-footer {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,127 @@
<template>
<header class="header">
<div class="container">
<div class="header-content">
<!-- Логотип -->
<NuxtLink to="/about" class="logo">
<div class="logo-icon">
<span class="logo-text">ES</span>
</div>
<span class="logo-name">EasySite102</span>
</NuxtLink>
<div class="header-paln-menu">
<div class="header-plan">
<!-- Трифы -->
<Plan />
</div>
<!-- Гамбургер меню для всех устройств -->
<div class="hamburger-nav">
<HamburgerMenu />
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import Plan from '~/components/layout/PlanBadge.vue'
import HamburgerMenu from '~/components/layout/HamburgerMenu.vue'
</script>
<style scoped>
.header {
background: var(--bg-primary);
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 50;
border-bottom: 1px solid var(--border-light);
width: 100%;
transition: all 0.3s ease;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
width: 100%;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
color: var(--text-primary);
transition: color 0.3s ease;
}
.logo:hover {
color: var(--primary-500);
}
.logo-icon {
width: 2.5rem;
height: 2.5rem;
background: var(--primary-500);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s ease;
}
.logo-text {
color: var(--text-inverse);
font-weight: bold;
font-size: 1.125rem;
}
.logo-name {
font-weight: bold;
font-size: 1.5rem;
white-space: nowrap;
transition: color 0.3s ease;
}
.header-paln-menu {
display: flex;
flex-direction: row;
}
.header-plan {
margin: 0 1rem 0 0;
}
.hamburger-nav {
display: block;
}
@media (max-width: 768px) {
.header-content {
padding: 0.75rem 0;
}
.logo-name {
font-size: 1.25rem;
}
.logo-icon {
width: 2rem;
height: 2rem;
}
.logo-text {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.logo-name {
display: none;
}
}
</style>
@@ -0,0 +1,119 @@
<!-- components/layout/ObjectsNavigation.vue -->
<template>
<nav class="objects-navigation">
<div class="nav-container">
<!-- Основные ссылки -->
<div class="nav-links">
<NuxtLink to="/objects" class="nav-link" :class="{ active: $route.path === '/objects' }">
🏢 Все объекты
</NuxtLink>
<NuxtLink
to="/objects/my-objects"
class="nav-link"
:class="{ active: $route.path === '/objects/my-objects' }">
📝 Мои объекты
</NuxtLink>
<NuxtLink to="/objects/create" class="nav-link" :class="{ active: $route.path === '/objects/create' }">
Добавить объект
</NuxtLink>
</div>
<!-- Дополнительные действия -->
<div class="nav-actions">
<NuxtLink to="/" class="nav-action">
🏠 На главную
</NuxtLink>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
</script>
<style scoped>
.objects-navigation {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-light);
margin-bottom: 2rem;
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.nav-links {
display: flex;
gap: 1.5rem;
align-items: center;
}
.nav-link {
padding: 0.75rem 1.25rem;
text-decoration: none;
border-radius: var(--radius-lg);
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid transparent;
color: var(--text-primary);
}
.nav-link:hover {
background: var(--bg-secondary);
color: var(--primary-600);
text-decoration: none;
}
.nav-link.active {
background: var(--primary-500);
color: var(--text-inverse);
border-color: var(--primary-500);
}
.nav-actions {
display: flex;
gap: 1rem;
}
.nav-action {
padding: 0.5rem 1rem;
text-decoration: none;
border-radius: var(--radius-md);
font-size: 0.9rem;
color: var(--text-secondary);
transition: all 0.3s ease;
}
.nav-action:hover {
color: var(--primary-500);
text-decoration: none;
}
/* Адаптивность */
@media (max-width: 768px) {
.nav-container {
flex-direction: column;
gap: 1rem;
padding: 0.75rem 0;
}
.nav-links {
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
}
.nav-link {
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.nav-actions {
width: 100%;
justify-content: center;
}
}
</style>
@@ -0,0 +1,124 @@
<!-- components/PlanBadge.vue -->
<template>
<div class="plan-badge-wrapper relative inline-flex">
<NuxtLink
:to="'/rules'"
class="plan-badge group"
:class="badgeClass"
@click.prevent="navigateToRules"
>
<span class="text-sm font-semibold">{{ planLabel }}</span>
<!-- CSS Tooltip -->
<div class="tooltip">
{{ tooltipText }}
</div>
</NuxtLink>
</div>
</template>
<script setup>
const props = defineProps({
plan: {
type: String,
default: 'start',
validator: (value) => ['start', 'pro', 'vip'].includes(value)
},
isCurrentPlan: {
type: Boolean,
default: false
}
})
const planLabels = {
start: 'Старт',
pro: 'Профи',
vip: 'VIP'
}
const planDescriptions = {
start: 'Базовый план для начала работы',
pro: 'Расширенные возможности для профессионалов',
vip: 'Премиум решение для максимального роста'
}
const planColors = {
start: 'bg-green-100 text-green-800 border-green-200 hover:bg-green-200',
pro: 'bg-blue-100 text-blue-800 border-blue-200 hover:bg-blue-200',
vip: 'bg-purple-100 text-purple-800 border-purple-200 hover:bg-purple-200'
}
const planLabel = computed(() => planLabels[props.plan])
const badgeClass = computed(() => `inline-flex items-center px-3 py-1 rounded-full border cursor-pointer transition-colors duration-200 ${planColors[props.plan]}`)
// Текст подсказки
const tooltipText = computed(() => {
if (props.isCurrentPlan) {
return `Это ваш тарифный план: ${planDescriptions[props.plan]}`
}
return planDescriptions[props.plan]
})
// Навигация на страницу правил
const navigateToRules = () => {
navigateTo('/rules')
}
</script>
<style scoped>
.plan-badge-wrapper {
display: inline-flex;
}
.plan-badge {
position: relative;
display: inline-flex;
align-items: center;
text-decoration: none;
}
/* Стили для CSS tooltip (всплывает вниз) */
.tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(8px);
background: #1f2937; /* gray-800 */
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease-in-out;
pointer-events: none;
z-index: 50;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
min-width: 180px;
text-align: center;
}
/* Стрелка для tooltip (теперь сверху) */
.tooltip::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-bottom-color: #1f2937; /* gray-800 */
}
/* Показываем tooltip при наведении */
.plan-badge:hover .tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(4px);
}
/* Альтернативный вариант - подсказка появляется сразу без задержки */
.plan-badge .tooltip {
transition-delay: 0s;
}
</style>
@@ -0,0 +1,170 @@
<!-- polacyPrivacy.vue -->
<template>
<div class="privacy-page">
<div class="container">
<div class="page-header">
<h1 class="page-title">Политика конфиденциальности</h1>
<div class="page-actions">
<button class="btn btn-primary" @click="downloadDocument">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Скачать PDF
</button>
</div>
</div>
<div class="document-container">
<div class="document-content">
<h2>ПОЛИТИКА КОНФИДЕНЦИАЛЬНОСТИ</h2>
<h3>Общие положения</h3>
<p>Настоящая Политика конфиденциальности (далее «Политика») определяет порядок обработки и защиты Обществом с ограниченной ответственностью «Информационно консультационный центр ЯЛ АРБА» (далее «Общество», «Мы») информации о физических лицах (далее «Пользователи», «Вы»), которая может быть получена Обществом при использовании Пользователями сайтов, мобильных приложений и иных сервисов, входящих в экосистему «ЯЛ АРБА» (далее «Сервисы»).</p>
<p>Используя наши Сервисы, Вы выражаете свое безоговорочное согласие с условиями данной Политики. Если Вы не согласны с этими условиями, не используйте наши Сервисы.</p>
<h3>1. Оператор персональных данных</h3>
<p>Оператором ваших персональных данных является:</p>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ИНФОРМАЦИОННО КОНСУЛЬТАЦИОННЫЙ ЦЕНТР ЯЛ АРБА"</p>
<p>ИНН: 0234009584</p>
<p>ОГРН: 1220200038112</p>
<p>Контактный адрес электронной почты: valitovgaziz@yandex.ru</p>
<h3>2. Состав и цели обработки персональных данных</h3>
<p>Мы можем обрабатывать следующие категории ваших данных:</p>
<ul>
<li>Данные, предоставляемые при регистрации и использовании Сервисов: имя, фамилия, адрес электронной почты, номер телефона, данные аккаунта в социальных сетях (если используется для входа).</li>
<li>Данные, создаваемые при использовании Сервисов: история поисковых запросов, бронирований, покупок, предпочтения, отзывы и рейтинги, данные о взаимодействии с контентом предпринимателей.</li>
<li>Технические данные: IP-адрес, данные cookie-файлов, информация о браузере и устройстве, журналы доступа.</li>
</ul>
<p>Цели обработки:</p>
<ul>
<li>Предоставление доступа к функционалу Сервисов (для предпринимателей размещение услуг, для туристов поиск и бронирование).</li>
<li>Связь с Пользователем для информирования о заказах, услугах и изменениях в Сервисах.</li>
<li>Аналитика и улучшение работы Сервисов, разработка новых функций.</li>
<li>Обеспечение безопасности и предотвращение мошенничества.</li>
<li>Выполнение требований применимого законодательства.</li>
</ul>
<h3>3. Условия обработки персональных данных</h3>
<p>Обработка ваших персональных данных осуществляется с вашего согласия, выражаемого путем совершения конклюдентных действий (регистрация в Сервисе, отметка о согласии в чекбоксе, использование функционала). Мы принимаем все необходимые меры для защиты ваших данных от несанкционированного доступа, изменения, раскрытия или уничтожения.</p>
<h3>4. Передача персональных данных</h3>
<p>Мы не передаем ваши персональные данные третьим лицам, за исключением случаев:</p>
<ul>
<li>Когда это необходимо для оказания услуги (например, передача данных туриста предпринимателю для выполнения бронирования).</li>
<li>По требованию уполномоченных государственных органов в соответствии с законодательством РФ.</li>
<li>При реорганизации Общества (например, слиянии) с уведомлением пользователей.</li>
</ul>
<h3>5. Права субъекта персональных данных</h3>
<p>Вы имеете право:</p>
<ul>
<li>На доступ к своим персональным данным, их уточнение и блокирование.</li>
<li>На отзыв согласия на обработку персональных данных.</li>
<li>На удаление своих персональных данных.</li>
<li>На обжалование действий или бездействия Оператора в уполномоченный орган.</li>
</ul>
<p>Для реализации этих прав направьте запрос по контактному адресу электронной почты, указанному в разделе 1.</p>
<h3>6. Заключительные положения</h3>
<p>Мы вправе вносить изменения в настоящую Политику. Новая редакция вступает в силу с момента ее размещения в соответствующем Сервисе, если иное не предусмотрено новой редакцией Политики.</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'PolacyPrivacy',
methods: {
downloadDocument() {
// В реальном приложении здесь будет логика скачивания документа
alert('Функция скачивания PDF будет реализована в бэкенде');
}
}
}
</script>
<style scoped>
.privacy-page {
padding: var(--space-xl) 0;
background: var(--bg-secondary);
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xl);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--border-light);
}
.page-title {
font-family: var(--font-heading);
font-size: var(--text-3xl);
color: var(--text-primary);
margin-bottom: 0;
}
.page-actions {
display: flex;
gap: var(--space-md);
}
.document-container {
background: var(--bg-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
overflow: hidden;
}
.document-content {
padding: var(--space-2xl);
max-height: 70vh;
overflow-y: auto;
}
.document-content h2 {
text-align: center;
margin-bottom: var(--space-xl);
color: var(--primary-600);
}
.document-content h3 {
margin-top: var(--space-xl);
margin-bottom: var(--space-md);
color: var(--primary-500);
}
.document-content p {
margin-bottom: var(--space-md);
line-height: var(--leading-relaxed);
}
.document-content ul {
margin-bottom: var(--space-md);
padding-left: var(--space-lg);
}
.document-content li {
margin-bottom: var(--space-xs);
line-height: var(--leading-relaxed);
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: var(--space-md);
align-items: flex-start;
}
.document-content {
padding: var(--space-lg);
}
}
</style>
@@ -0,0 +1,8 @@
<!-- layouts/default.vue -->
<template>
<div>
<UToast />
<Notifications position="top-right" />
<slot />
</div>
</template>
@@ -0,0 +1,282 @@
<!-- components/pricing/PricingSection.vue -->
<template>
<section class="pricing-section">
<div class="container">
<div class="pricing-header">
<h1 class="text-hero text-center">Тарифы для вашего бизнеса</h1>
<p class="lead text-center">Выберите оптимальное решение для продвижения ваших туристических услуг</p>
</div>
<div class="pricing-grid">
<!-- Тариф Старт -->
<div class="pricing-card">
<div class="pricing-card__header">
<h3 class="h3">Старт</h3>
<div class="pricing-card__price">
990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Идеально для начинающих</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>1 объект/услуга</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Базовый шаблон сайта</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Система бронирования</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Базовая SEO-оптимизация</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Статистика просмотров</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Поддержка по email</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-outline btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
<!-- Тариф Профессионал (рекомендуемый) -->
<div class="pricing-card featured">
<div class="pricing-card__badge">Самое популярное</div>
<div class="pricing-card__header">
<h3 class="h3">Профессионал</h3>
<div class="pricing-card__price">
2 990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Для растущего бизнеса</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>До 5 объектов/услуг</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Расширенные шаблоны</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Приоритетная SEO-оптимизация</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Интеграция с календарями</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Email-рассылки для клиентов</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Подробная аналитика</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Приоритетная поддержка</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-primary btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
<!-- Тариф Бизнес -->
<div class="pricing-card">
<div class="pricing-card__header">
<h3 class="h3">Бизнес</h3>
<div class="pricing-card__price">
7 990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Для крупных компаний</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Неограниченное количество объектов</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Кастомизация дизайна</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Продвинутое SEO + контекстная реклама</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Интеграция с CRM</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>API доступ</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Личный менеджер</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Поддержка 24/7</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-outline btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
</div>
<!-- Дополнительная информация -->
<div class="pricing-info text-center">
<p class="small">Все тарифы включают 14-дневный пробный период. Отмена в любой момент.</p>
</div>
</div>
</section>
</template>
<script setup>
// Логика для переключения тарифов может быть добавлена здесь
</script>
<style scoped>
.pricing-section {
padding: var(--space-2xl) 0;
background: var(--bg-secondary);
}
.pricing-header {
text-align: center;
margin-bottom: var(--space-2xl);
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--space-xl);
max-width: 1000px;
margin: 0 auto;
}
.pricing-card {
background: var(--bg-primary);
border-radius: var(--radius-xl);
padding: var(--space-2xl);
border: 2px solid var(--border-light);
transition: all 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
}
.pricing-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.pricing-card.featured {
border-color: var(--primary-500);
transform: scale(1.05);
}
.pricing-card.featured:hover {
transform: scale(1.05) translateY(-4px);
}
.pricing-card__badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-500);
color: var(--text-inverse);
padding: var(--space-xs) var(--space-lg);
border-radius: var(--radius-lg);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
}
.pricing-card__header {
text-align: center;
margin-bottom: var(--space-xl);
}
.pricing-card__price {
font-family: var(--font-heading);
font-size: var(--text-4xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin: var(--space-md) 0;
}
.pricing-card__period {
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.pricing-features {
flex-grow: 1;
margin-bottom: var(--space-xl);
}
.pricing-feature {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
color: var(--text-secondary);
}
.pricing-feature__icon {
color: var(--success-500);
font-weight: bold;
}
.pricing-card__footer {
margin-top: auto;
}
.btn-block {
width: 100%;
justify-content: center;
}
.pricing-info {
margin-top: var(--space-xl);
padding-top: var(--space-lg);
border-top: 1px solid var(--border-light);
}
/* Адаптивность */
@media (max-width: 768px) {
.pricing-grid {
grid-template-columns: 1fr;
gap: var(--space-lg);
}
.pricing-card.featured {
transform: none;
}
.pricing-card.featured:hover {
transform: translateY(-4px);
}
}
</style>
@@ -0,0 +1,174 @@
<!-- userAgreement.vue -->
<template>
<div class="agreement-page">
<div class="container">
<div class="page-header">
<h1 class="page-title">Пользовательское соглашение</h1>
<div class="page-actions">
<button class="btn btn-primary" @click="downloadDocument">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Скачать PDF
</button>
</div>
</div>
<div class="document-container">
<div class="document-content">
<h2>ПОЛЬЗОВАТЕЛЬСКОЕ СОГЛАШЕНИЕ</h2>
<h3>Общие положения</h3>
<p>Настоящее Пользовательское соглашение (далее «Соглашение») регулирует отношения между Обществом с ограниченной ответственностью «Информационно консультационный центр ЯЛ АРБА» (далее «Администрация», «Мы») и любым физическим лицом (далее «Пользователь», «Вы») в связи с использованием сайтов, мобильных приложений и иных сервисов, входящих в экосистему «ЯЛ АРБА» (далее «Сервисы»).</p>
<p>Используя любой из Сервисов, Вы в полном объеме принимаете условия настоящего Соглашения. Если Вы не согласны с каким-либо условием, Вы не вправе использовать наши Сервисы.</p>
<h3>1. Термины</h3>
<ul>
<li><strong>Экосистема «ЯЛ АРБА»</strong> совокупность взаимосвязанных цифровых платформ (сайтов, приложений), предназначенных для взаимодействия предпринимателей и туристов.</li>
<li><strong>Предприниматель</strong> Пользователь, размещающий в Сервисах информацию о товарах, услугах и мероприятиях для туристов.</li>
<li><strong>Турист</strong> Пользователь, использующий Сервисы для поиска, планирования и бронирования туристических услуг.</li>
<li><strong>Контент</strong> любая информация, размещаемая Пользователями в Сервисах: тексты, фото, видео, описания услуг, отзывы и т.д.</li>
</ul>
<h3>2. Общие условия использования</h3>
<p><strong>2.1.</strong> Администрация предоставляет Вам неисключительную, непередаваемую лицензию на использование функционала Сервисов для их целевого назначения.</p>
<p><strong>2.2.</strong> Вы обязуетесь использовать Сервисы добросовестно и исключительно в законных целях.</p>
<p><strong>2.3.</strong> Для доступа к полному функционалу Сервисов необходима регистрация. Вы несете ответственность за сохранность своих учетных данных.</p>
<h3>3. Функционал для Предпринимателей</h3>
<p><strong>3.1.</strong> Размещая Контент в Сервисах, Предприниматель гарантирует, что обладает всеми необходимыми правами на него, и что он является достоверным и не нарушает законодательство РФ и права третьих лиц.</p>
<p><strong>3.2.</strong> Предприниматель самостоятельно несет ответственность перед Туристом за качество и предоставление размещенных услуг. Администрация не является стороной в договоре между Предпринимателем и Туристом и не несет ответственности за их взаимодействие.</p>
<p><strong>3.3.</strong> Администрация оставляет за собой право модерировать и удалять Контент Предпринимателя в случае его несоответствия условиям Соглашения.</p>
<h3>4. Функционал для Туристов</h3>
<p><strong>4.1.</strong> Бронируя услуги через Сервисы, Турист вступает в прямые договорные отношения с Предпринимателем. Все претензии по качеству услуги направляются непосредственно Предпринимателю.</p>
<p><strong>4.2.</strong> Размещая отзывы и рейтинги, Турист обязуется быть объективным и корректым. Запрещены оскорбления, клевета и спам.</p>
<h3>5. Интеллектуальная собственность</h3>
<p><strong>5.1.</strong> Все объекты в составе Сервисов (дизайн, текст, графика, логотипы, программный код) являются интеллектуальной собственностью Администрации или правообладателей.</p>
<p><strong>5.2.</strong> Размещая Контент, Вы предоставляете Администрации право на его использование в целях функционирования Сервисов (воспроизведение, публичный показ, доведение до всеобщего сведения).</p>
<h3>6. Ответственность и ограничения</h3>
<p><strong>6.1.</strong> Сервисы предоставляются на условиях «как есть». Администрация не гарантирует бесперебойную и безошибочную работу Сервисов.</p>
<p><strong>6.2.</strong> Администрация не несет ответственности за убытки, прямые или косвенные, возникшие в связи с использованием или невозможностью использования Сервисов, включая упущенную выгоду.</p>
<p><strong>6.3.</strong> Пользователь несет ответственность за любой Контент, который он размещает, и за любые последствия такого размещения.</p>
<h3>7. Конфиденциальность</h3>
<p>Обработка персональных данных Пользователя осуществляется в соответствии с отдельной Политикой конфиденциальности, размещенной в Сервисах.</p>
<h3>8. Разрешение споров</h3>
<p><strong>8.1.</strong> Все споры подлежат разрешению в порядке, предусмотренном законодательством Российской Федерации.</p>
<p><strong>8.2.</strong> Обязательный досудебный (претензионный) порядок урегулирования споров является обязательным. Срок ответа на претензию 30 (тридцать) календарных дней.</p>
<h3>9. Реквизиты Администрации</h3>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ИНФОРМАЦИОННО КОНСУЛЬТАЦИОННЫЙ ЦЕНТР ЯЛ АРБА"</p>
<p>ИНН 0234009584</p>
<p>ОГРН 1220200038112</p>
<p>Расчётный счёт: 40702810106000055253</p>
<p>Банк: ПАО Сбербанк</p>
<p>БИК: 048073601</p>
<p>Корр. счёт: 30101810300000000601</p>
<p>Контактный e-mail: valitovgaziz@yandex.ru</p>
<h3>10. Заключительные положения</h3>
<p>Администрация вправе в одностороннем порядке изменять настоящее Соглашение. Изменения вступают в силу с момента их публикации в Сервисе, если иной срок не указан в соответствующей публикации.</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UserAgreement',
methods: {
downloadDocument() {
// В реальном приложении здесь будет логика скачивания документа
alert('Функция скачивания PDF будет реализована в бэкенде');
}
}
}
</script>
<style scoped>
.agreement-page {
padding: var(--space-xl) 0;
background: var(--bg-secondary);
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-xl);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--border-light);
}
.page-title {
font-family: var(--font-heading);
font-size: var(--text-3xl);
color: var(--text-primary);
margin-bottom: 0;
}
.page-actions {
display: flex;
gap: var(--space-md);
}
.document-container {
background: var(--bg-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
overflow: hidden;
}
.document-content {
padding: var(--space-2xl);
max-height: 70vh;
overflow-y: auto;
}
.document-content h2 {
text-align: center;
margin-bottom: var(--space-xl);
color: var(--primary-600);
}
.document-content h3 {
margin-top: var(--space-xl);
margin-bottom: var(--space-md);
color: var(--primary-500);
}
.document-content p {
margin-bottom: var(--space-md);
line-height: var(--leading-relaxed);
}
.document-content ul {
margin-bottom: var(--space-md);
padding-left: var(--space-lg);
}
.document-content li {
margin-bottom: var(--space-xs);
line-height: var(--leading-relaxed);
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: var(--space-md);
align-items: flex-start;
}
.document-content {
padding: var(--space-lg);
}
}
</style>
@@ -0,0 +1,116 @@
<template>
<div class="card cursor-pointer hover:shadow-lg transition-shadow" @click="$emit('click')">
<div class="relative">
<img
:src="object.images[0] || '/images/placeholder.jpg'"
:alt="object.title"
class="w-full h-48 object-cover"
>
<div class="absolute top-3 right-3">
<span class="badge badge-primary">
{{ getTypeLabel(object.type) }}
</span>
</div>
</div>
<div class="card-body">
<h3 class="font-semibold text-lg mb-2 line-clamp-2">{{ object.title }}</h3>
<p class="text-gray-600 mb-3 flex items-center gap-1">
📍 {{ object.city }}, {{ object.country }}
</p>
<p class="text-sm text-gray-600 mb-4 line-clamp-2">
{{ object.description }}
</p>
<div class="flex flex-wrap gap-1 mb-4">
<span
v-for="amenity in object.amenities.slice(0, 3)"
:key="amenity"
class="badge badge-secondary text-xs"
>
{{ getAmenityLabel(amenity) }}
</span>
<span
v-if="object.amenities.length > 3"
class="badge badge-outline text-xs"
>
+{{ object.amenities.length - 3 }}
</span>
</div>
<div class="flex items-center justify-between">
<div class="text-lg font-semibold text-primary-600">
{{ formatPrice(object.price) }}
<span class="text-sm text-gray-500">{{ getPriceUnitLabel(object.priceUnit) }}</span>
</div>
<div v-if="object.rating" class="flex items-center gap-1">
<span class="text-yellow-500"></span>
<span class="font-medium">{{ object.rating }}</span>
<span class="text-gray-500 text-sm">({{ object.reviewCount }})</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ObjectItem } from '~/composables/useObjects'
interface Props {
object: ObjectItem
}
defineProps<Props>()
defineEmits<{
click: []
}>()
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
hotel: 'Отель',
apartment: 'Апартаменты',
villa: 'Вилла',
camping: 'Кемпинг',
restaurant: 'Ресторан',
attraction: 'Достопримечательность'
}
return types[type] || type
}
const getAmenityLabel = (amenity: string) => {
const amenities: Record<string, string> = {
wifi: 'Wi-Fi',
parking: 'Парковка',
breakfast: 'Завтрак',
pool: 'Бассейн',
spa: 'СПА',
kitchen: 'Кухня',
washing_machine: 'Стиральная машина'
}
return amenities[amenity] || amenity
}
const getPriceUnitLabel = (unit: string) => {
const units: Record<string, string> = {
per_night: '/ночь',
per_person: '/чел',
fixed: ''
}
return units[unit] || unit
}
const formatPrice = (price: number) => {
return new Intl.NumberFormat('ru-RU').format(price) + ' ₽'
}
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
@@ -0,0 +1,90 @@
<!-- components/pricing/FAQSection.vue -->
<template>
<section class="faq-section">
<div class="container">
<div class="faq-header">
<h2 class="h2 text-center">Часто задаваемые вопросы</h2>
</div>
<div class="faq-grid">
<div class="faq-item">
<h4 class="h4">Можно ли изменить тариф позже?</h4>
<p>Да, вы можете перейти на другой тариф в любой момент. Изменения вступят в силу со следующего платежного периода.</p>
</div>
<div class="faq-item">
<h4 class="h4">Что входит в пробный период?</h4>
<p>14-дневный пробный период включает полный доступ ко всем функциям выбранного тарифа. Никаких платежей не требуется.</p>
</div>
<div class="faq-item">
<h4 class="h4">Есть ли ограничения по трафику?</h4>
<p>Нет, все тарифы включают неограниченный трафик. Мы обеспечиваем стабильную работу ваших сайтов при любой нагрузке.</p>
</div>
<div class="faq-item">
<h4 class="h4">Предоставляете ли вы домены?</h4>
<p>Да, мы можем помочь с регистрацией домена или подключением существующего. Домены оплачиваются отдельно.</p>
</div>
<div class="faq-item">
<h4 class="h4">Как происходит перенос существующих сайтов?</h4>
<p>Наша команда поможет бесплатно перенести ваши существующие сайты и данные на нашу платформу.</p>
</div>
<div class="faq-item">
<h4 class="h4">Есть ли скидки при годовой оплате?</h4>
<p>Да, при годовой оплате мы предоставляем скидку 20% на любой тариф.</p>
</div>
</div>
</div>
</section>
</template>
<script setup>
// Логика для аккордеона может быть добавлена здесь
</script>
<style scoped>
.faq-section {
padding: var(--space-2xl) 0;
background: var(--bg-primary);
}
.faq-header {
margin-bottom: var(--space-2xl);
}
.faq-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: var(--space-xl);
max-width: 800px;
margin: 0 auto;
}
.faq-item {
background: var(--bg-secondary);
padding: var(--space-lg);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
}
.faq-item h4 {
color: var(--primary-600);
margin-bottom: var(--space-sm);
}
.faq-item p {
color: var(--text-secondary);
line-height: var(--leading-relaxed);
margin: 0;
}
@media (max-width: 768px) {
.faq-grid {
grid-template-columns: 1fr;
gap: var(--space-lg);
}
}
</style>
@@ -0,0 +1,282 @@
<!-- components/pricing/PricingSection.vue -->
<template>
<section class="pricing-section">
<div class="container">
<div class="pricing-header">
<h1 class="text-hero text-center">Тарифы для вашего бизнеса</h1>
<p class="lead text-center">Выберите оптимальное решение для продвижения ваших туристических услуг</p>
</div>
<div class="pricing-grid">
<!-- Тариф Старт -->
<div class="pricing-card">
<div class="pricing-card__header">
<h3 class="h3">Старт</h3>
<div class="pricing-card__price">
990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Идеально для начинающих</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>1 объект/услуга</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Базовый шаблон сайта</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Система бронирования</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Базовая SEO-оптимизация</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Статистика просмотров</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Поддержка по email</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-outline btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
<!-- Тариф Профессионал (рекомендуемый) -->
<div class="pricing-card featured">
<div class="pricing-card__badge">Самое популярное</div>
<div class="pricing-card__header">
<h3 class="h3">Профессионал</h3>
<div class="pricing-card__price">
2 990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Для растущего бизнеса</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>До 5 объектов/услуг</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Расширенные шаблоны</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Приоритетная SEO-оптимизация</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Интеграция с календарями</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Email-рассылки для клиентов</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Подробная аналитика</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Приоритетная поддержка</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-primary btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
<!-- Тариф Бизнес -->
<div class="pricing-card">
<div class="pricing-card__header">
<h3 class="h3">Бизнес</h3>
<div class="pricing-card__price">
7 990 <span class="pricing-card__period">/месяц</span>
</div>
<p class="small">Для крупных компаний</p>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Неограниченное количество объектов</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Кастомизация дизайна</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Продвинутое SEO + контекстная реклама</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Интеграция с CRM</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>API доступ</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Личный менеджер</span>
</div>
<div class="pricing-feature">
<span class="pricing-feature__icon"></span>
<span>Поддержка 24/7</span>
</div>
</div>
<div class="pricing-card__footer">
<button class="btn btn-outline btn-block">Начать 14-дневный пробный период</button>
</div>
</div>
</div>
<!-- Дополнительная информация -->
<div class="pricing-info text-center">
<p class="small">Все тарифы включают 14-дневный пробный период. Отмена в любой момент.</p>
</div>
</div>
</section>
</template>
<script setup>
// Логика для переключения тарифов может быть добавлена здесь
</script>
<style scoped>
.pricing-section {
padding: var(--space-2xl) 0;
background: var(--bg-secondary);
}
.pricing-header {
text-align: center;
margin-bottom: var(--space-2xl);
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--space-xl);
max-width: 1000px;
margin: 0 auto;
}
.pricing-card {
background: var(--bg-primary);
border-radius: var(--radius-xl);
padding: var(--space-2xl);
border: 2px solid var(--border-light);
transition: all 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
}
.pricing-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.pricing-card.featured {
border-color: var(--primary-500);
transform: scale(1.05);
}
.pricing-card.featured:hover {
transform: scale(1.05) translateY(-4px);
}
.pricing-card__badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-500);
color: var(--text-inverse);
padding: var(--space-xs) var(--space-lg);
border-radius: var(--radius-lg);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
}
.pricing-card__header {
text-align: center;
margin-bottom: var(--space-xl);
}
.pricing-card__price {
font-family: var(--font-heading);
font-size: var(--text-4xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin: var(--space-md) 0;
}
.pricing-card__period {
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.pricing-features {
flex-grow: 1;
margin-bottom: var(--space-xl);
}
.pricing-feature {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
color: var(--text-secondary);
}
.pricing-feature__icon {
color: var(--success-500);
font-weight: bold;
}
.pricing-card__footer {
margin-top: auto;
}
.btn-block {
width: 100%;
justify-content: center;
}
.pricing-info {
margin-top: var(--space-xl);
padding-top: var(--space-lg);
border-top: 1px solid var(--border-light);
}
/* Адаптивность */
@media (max-width: 768px) {
.pricing-grid {
grid-template-columns: 1fr;
gap: var(--space-lg);
}
.pricing-card.featured {
transform: none;
}
.pricing-card.featured:hover {
transform: translateY(-4px);
}
}
</style>