1934 lines
44 KiB
Vue
1934 lines
44 KiB
Vue
<template>
|
||
<div class="reviews-page">
|
||
<!-- Герой-секция -->
|
||
<section class="hero-section">
|
||
<div class="container">
|
||
<div class="hero-content">
|
||
<h1 class="hero-title">⭐ Отзывы участников</h1>
|
||
<p class="hero-subtitle">Реальные истории и достижения наших бегунов</p>
|
||
<div class="hero-stats">
|
||
<div class="stat">
|
||
<div class="stat-number">{{ stats.average_rating || 0 }}</div>
|
||
<div class="stat-label">Средний рейтинг</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-number">{{ stats.total_reviews || 0 }}</div>
|
||
<div class="stat-label">Отзывов</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-number">{{ stats.success_stories || 0 }}</div>
|
||
<div class="stat-label">Историй успеха</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Основной контент -->
|
||
<section class="reviews-section">
|
||
<div class="container">
|
||
<div class="reviews-header">
|
||
<h2 class="section-title">Что говорят наши участники</h2>
|
||
<div class="reviews-controls">
|
||
<div class="sort-controls">
|
||
<label for="sort">Сортировка:</label>
|
||
<select id="sort" v-model="sortBy" @change="loadReviews">
|
||
<option value="newest">Сначала новые</option>
|
||
<option value="oldest">Сначала старые</option>
|
||
<option value="highest">Высокий рейтинг</option>
|
||
<option value="lowest">Низкий рейтинг</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-controls">
|
||
<button v-for="filter in ratingFilters" :key="filter.value"
|
||
:class="['filter-btn', { 'active': activeRatingFilter === filter.value }]"
|
||
@click="filterByRating(filter.value)">
|
||
{{ filter.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Загрузка -->
|
||
<div v-if="loading" class="loading">
|
||
<div class="spinner"></div>
|
||
<p>Загрузка отзывов...</p>
|
||
</div>
|
||
|
||
<!-- Сообщение об отсутствии отзывов -->
|
||
<div v-else-if="reviews.length === 0" class="no-reviews">
|
||
<h3>😔 Отзывов пока нет</h3>
|
||
<p>Будьте первым, кто поделится своим опытом!</p>
|
||
</div>
|
||
|
||
<!-- Сетка отзывов -->
|
||
<div v-else class="reviews-grid">
|
||
<div v-for="review in reviews" :key="review.id" class="review-card"
|
||
:class="getReviewCardClass(review.rating)">
|
||
<div class="review-header">
|
||
<div class="reviewer-info">
|
||
<div class="reviewer-avatar">
|
||
{{ getInitials(review.author) }}
|
||
</div>
|
||
<div class="reviewer-details">
|
||
<h4 class="review-author">{{ review.author.first_name }} {{ review.author.last_name }}</h4>
|
||
<p class="reviewer-achievement" v-if="review.achievement">
|
||
🏆 {{ review.achievement }}
|
||
</p>
|
||
<p class="review-membership">
|
||
Участник клуба
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="review-rating">
|
||
<div class="stars">
|
||
<span v-for="star in 5" :key="star" :class="['star', { 'filled': star <= review.rating }]">
|
||
★
|
||
</span>
|
||
</div>
|
||
<span class="rating-value">{{ review.rating }}/5</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="review-content">
|
||
<p class="review-text">{{ review.text }}</p>
|
||
|
||
<div class="review-meta">
|
||
<div class="meta-items">
|
||
<span class="meta-item" v-if="review.distance">
|
||
🏃 {{ review.distance }}
|
||
</span>
|
||
<span class="meta-item" v-if="review.improvement">
|
||
📈 {{ review.improvement }}
|
||
</span>
|
||
<span class="meta-item" v-if="review.trainings">
|
||
💪 {{ review.trainings }} тренировок
|
||
</span>
|
||
</div>
|
||
<span class="review-date">{{ formatDate(review.created_at) }}</span>
|
||
</div>
|
||
|
||
<div class="review-actions" v-if="review.verified">
|
||
<span class="verified-badge">✅ Проверенный отзыв</span>
|
||
</div>
|
||
|
||
<!-- Действия для своих отзывов -->
|
||
<div class="review-actions" v-if="isMyReview(review)">
|
||
<button @click="editReview(review)" class="btn-edit">✏️ Редактировать</button>
|
||
<button @click="deleteReview(review.id)" class="btn-delete">🗑️ Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Пагинация -->
|
||
<div class="pagination" v-if="totalPages > 1">
|
||
<button class="pagination-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">
|
||
‹
|
||
</button>
|
||
|
||
<button v-for="page in visiblePages" :key="page"
|
||
:class="['pagination-btn', { 'active': currentPage === page }]" @click="changePage(page)">
|
||
{{ page }}
|
||
</button>
|
||
|
||
<button class="pagination-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">
|
||
›
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Статистика отзывов -->
|
||
<div class="reviews-stats">
|
||
<h3>📊 Распределение оценок</h3>
|
||
<div class="rating-bars">
|
||
<div v-for="rating in 5" :key="rating" class="rating-bar">
|
||
<div class="rating-label">
|
||
<span class="stars-small">
|
||
<span v-for="star in 5" :key="star" :class="['star-small', { 'filled': star <= (6 - rating) }]">
|
||
★
|
||
</span>
|
||
</span>
|
||
</div>
|
||
<div class="bar-container">
|
||
<div class="bar-fill" :style="{ width: getRatingPercentage(6 - rating) + '%' }"></div>
|
||
</div>
|
||
<div class="rating-count">
|
||
{{ getRatingCount(6 - rating) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Форма отзыва -->
|
||
<section class="add-review-section" id="add-review">
|
||
<div class="container">
|
||
<div class="add-review-content">
|
||
<div class="form-header">
|
||
<h2 class="section-title">✍️ Поделитесь своим опытом</h2>
|
||
<p>Расскажите о своих достижениях и впечатлениях от тренировок</p>
|
||
</div>
|
||
|
||
<form class="review-form" @submit.prevent="submitReview" v-if="!isAuthenticated">
|
||
<div class="auth-required">
|
||
<div class="auth-message">
|
||
<h3>🔐 Войдите, чтобы оставить отзыв</h3>
|
||
<p>Только участники клуба могут оставлять отзывы</p>
|
||
</div>
|
||
<div class="auth-actions">
|
||
<router-link to="/login" class="btn btn-primary">
|
||
🔑 Войти в аккаунт
|
||
</router-link>
|
||
<router-link to="/register" class="btn btn-secondary">
|
||
🏃 Стать участником
|
||
</router-link>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<form class="review-form" @submit.prevent="submitReview" v-else>
|
||
<div class="form-group">
|
||
<label>Ваша оценка *</label>
|
||
<div class="rating-input">
|
||
<button v-for="star in 5" :key="star" type="button"
|
||
:class="['star-btn', { 'active': newReview.rating >= star }]" @click="newReview.rating = star">
|
||
★
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="achievement">Ваше достижение</label>
|
||
<input id="achievement" v-model="newReview.achievement" type="text"
|
||
placeholder="Например: пробежал первый марафон">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="distance">Любимая дистанция</label>
|
||
<select id="distance" v-model="newReview.distance">
|
||
<option value="">Выберите дистанцию</option>
|
||
<option value="5 км">5 км</option>
|
||
<option value="10 км">10 км</option>
|
||
<option value="21.1 км">Полумарафон (21.1 км)</option>
|
||
<option value="42.2 км">Марафон (42.2 км)</option>
|
||
<option value="Трейл">Трейл</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="review-text">Ваш отзыв *</label>
|
||
<textarea id="review-text" v-model="newReview.text" rows="5"
|
||
placeholder="Расскажите о вашем опыте в клубе, достижениях, атмосфере..." required></textarea>
|
||
<div class="char-counter">
|
||
{{ newReview.text.length }}/500 символов
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn btn-primary" :disabled="!isFormValid || submitting">
|
||
<span v-if="submitting">📤 Отправка...</span>
|
||
<span v-else>📝 {{ editingReview ? 'Обновить отзыв' : 'Опубликовать отзыв' }}</span>
|
||
</button>
|
||
<button type="button" class="btn btn-outline" @click="resetForm">
|
||
🗑️ Очистить
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Призыв к действию -->
|
||
<section class="cta-section">
|
||
<div class="container">
|
||
<div class="cta-content">
|
||
<h2>Готовы присоединиться к нашему сообществу?</h2>
|
||
<p>Станьте следующей историей успеха в нашем клубе!</p>
|
||
|
||
<div class="success-stories-preview">
|
||
<div class="story-card">
|
||
<div class="story-avatar">А</div>
|
||
<div class="story-content">
|
||
<h4>Анна</h4>
|
||
<p>"С нуля до марафона за 1 год! Спасибо тренеру и команде!"</p>
|
||
<span class="story-achievement">🏅 Марафон 4:20:15</span>
|
||
</div>
|
||
</div>
|
||
<div class="story-card">
|
||
<div class="story-avatar">Д</div>
|
||
<div class="story-content">
|
||
<h4>Данил</h4>
|
||
<p>"Нашел друзей и улучшил результат на 10км на 15 минут"</p>
|
||
<span class="story-achievement">⚡ 10км за 42:30</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cta-features">
|
||
<div class="cta-feature">
|
||
<div class="feature-icon">👨🏫</div>
|
||
<div class="feature-text">
|
||
<strong>Профессиональный тренер</strong>
|
||
<span>Мастер спорта с индивидуальным подходом</span>
|
||
</div>
|
||
</div>
|
||
<div class="cta-feature">
|
||
<div class="feature-icon">👥</div>
|
||
<div class="feature-text">
|
||
<strong>Дружное сообщество</strong>
|
||
<span>Поддержка и мотивация единомышленников</span>
|
||
</div>
|
||
</div>
|
||
<div class="cta-feature">
|
||
<div class="feature-icon">📈</div>
|
||
<div class="feature-text">
|
||
<strong>Личный прогресс</strong>
|
||
<span>Регулярное улучшение результатов</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cta-actions">
|
||
<router-link to="/training" class="btn btn-primary btn-large">
|
||
🏃 Записаться на тренировку
|
||
</router-link>
|
||
<router-link to="/register" class="btn btn-secondary">
|
||
👥 Вступить в клуб
|
||
</router-link>
|
||
</div>
|
||
|
||
<div class="cta-contacts">
|
||
<p>Есть вопросы? Свяжитесь с нами:</p>
|
||
<div class="contact-links">
|
||
<a href="https://t.me/begushiybashkir" class="contact-link">📱 Telegram</a>
|
||
<a href="tel:+79273093095" class="contact-link">📞 +7 (927) 30-93-095</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { apiClient } from '../stores/helpers/api';
|
||
|
||
export default {
|
||
// eslint-disable-next-line vue/multi-word-component-names
|
||
name: 'Reviews',
|
||
data() {
|
||
return {
|
||
isAuthenticated: false,
|
||
currentUser: null,
|
||
loading: false,
|
||
sortBy: 'newest',
|
||
activeRatingFilter: 'all',
|
||
currentPage: 1,
|
||
reviewsPerPage: 6,
|
||
totalPages: 1,
|
||
totalItems: 0,
|
||
submitting: false,
|
||
editingReview: null,
|
||
stats: {
|
||
total_reviews: 0,
|
||
average_rating: 0,
|
||
success_stories: 0,
|
||
rating_distribution: {5: 0, 4: 0, 3: 0, 2: 0, 1: 0}
|
||
},
|
||
ratingFilters: [
|
||
{ value: 'all', label: 'Все оценки' },
|
||
{ value: '5', label: '⭐ 5 звезд' },
|
||
{ value: '4', label: '⭐ 4+ звезды' },
|
||
{ value: '3', label: '⭐ 3+ звезды' }
|
||
],
|
||
newReview: {
|
||
rating: 0,
|
||
text: '',
|
||
achievement: '',
|
||
distance: '',
|
||
improvement: '',
|
||
trainings: 0
|
||
},
|
||
reviews: []
|
||
}
|
||
},
|
||
computed: {
|
||
visiblePages() {
|
||
const pages = []
|
||
const startPage = Math.max(1, this.currentPage - 2)
|
||
const endPage = Math.min(this.totalPages, startPage + 4)
|
||
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(i)
|
||
}
|
||
return pages
|
||
},
|
||
isFormValid() {
|
||
return this.newReview.rating > 0 &&
|
||
this.newReview.text.length >= 10 &&
|
||
this.newReview.text.length <= 500
|
||
}
|
||
},
|
||
methods: {
|
||
handleFirstInteraction() {
|
||
if (!this.hasInteracted) {
|
||
this.hasInteracted = true
|
||
this.showContent()
|
||
clearTimeout(this.autoShowTimeout)
|
||
}
|
||
},
|
||
showContent() {
|
||
this.isContentVisible = true
|
||
// Эмитим событие для показа хедера
|
||
this.$emit('show-header')
|
||
},
|
||
async loadReviews() {
|
||
this.loading = true
|
||
try {
|
||
const params = {
|
||
page: this.currentPage,
|
||
limit: this.reviewsPerPage,
|
||
sort: this.sortBy,
|
||
filter: this.activeRatingFilter === 'all' ? '' : this.activeRatingFilter
|
||
}
|
||
|
||
// Удаляем пустые параметры
|
||
Object.keys(params).forEach(key => {
|
||
if (params[key] === '' || params[key] === null || params[key] === undefined) {
|
||
delete params[key]
|
||
}
|
||
})
|
||
|
||
const response = await apiClient.get('/reviews', { params })
|
||
this.reviews = response.data.reviews || []
|
||
this.totalPages = response.data.total_pages || 1
|
||
this.totalItems = response.data.total_items || 0
|
||
this.currentPage = response.data.current_page || 1
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке отзывов:', error)
|
||
this.showNotification('error', 'Ошибка', 'Не удалось загрузить отзывы')
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
async loadStats() {
|
||
try {
|
||
const response = await apiClient.get('/reviews/stats')
|
||
this.stats = response.data
|
||
} catch (error) {
|
||
console.error('Ошибка при загрузке статистики:', error)
|
||
// Устанавливаем значения по умолчанию при ошибке
|
||
this.stats = {
|
||
total_reviews: 0,
|
||
average_rating: 0,
|
||
success_stories: 0,
|
||
rating_distribution: {5: 0, 4: 0, 3: 0, 2: 0, 1: 0}
|
||
}
|
||
// Не показываем ошибку пользователю для статистики
|
||
}
|
||
},
|
||
|
||
async checkAuth() {
|
||
try {
|
||
const token = localStorage.getItem('auth_token')
|
||
if (token) {
|
||
try {
|
||
// Пытаемся получить профиль пользователя для проверки авторизации
|
||
const response = await apiClient.get('/user/profile')
|
||
this.isAuthenticated = true
|
||
this.currentUser = response.data
|
||
} catch (error) {
|
||
console.error('Токен невалиден:', error)
|
||
this.handleAuthError()
|
||
}
|
||
} else {
|
||
this.isAuthenticated = false
|
||
this.currentUser = null
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка проверки авторизации:', error)
|
||
this.handleAuthError()
|
||
}
|
||
},
|
||
|
||
handleAuthError() {
|
||
this.isAuthenticated = false
|
||
this.currentUser = null
|
||
localStorage.removeItem('auth_token')
|
||
// НЕ перенаправляем на login, чтобы пользователь мог просматривать отзывы
|
||
},
|
||
|
||
getInitials(author) {
|
||
if (!author) return '?'
|
||
const firstName = author.first_name || ''
|
||
const lastName = author.last_name || ''
|
||
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase() || '?'
|
||
},
|
||
|
||
getReviewCardClass(rating) {
|
||
return `rating-${rating}`
|
||
},
|
||
|
||
formatDate(dateString) {
|
||
try {
|
||
const date = new Date(dateString)
|
||
return date.toLocaleDateString('ru-RU', {
|
||
day: 'numeric',
|
||
month: 'long',
|
||
year: 'numeric'
|
||
})
|
||
} catch (error) {
|
||
console.log(error)
|
||
return 'Неизвестная дата'
|
||
}
|
||
},
|
||
|
||
filterByRating(rating) {
|
||
this.activeRatingFilter = rating
|
||
this.currentPage = 1
|
||
this.loadReviews()
|
||
},
|
||
|
||
changePage(page) {
|
||
if (page >= 1 && page <= this.totalPages) {
|
||
this.currentPage = page
|
||
this.loadReviews()
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
}
|
||
},
|
||
|
||
getRatingCount(rating) {
|
||
return this.stats.rating_distribution?.[rating] || 0
|
||
},
|
||
|
||
getRatingPercentage(rating) {
|
||
const count = this.getRatingCount(rating)
|
||
const total = this.stats.total_reviews || 1
|
||
return (count / total) * 100
|
||
},
|
||
|
||
isMyReview(review) {
|
||
return this.isAuthenticated &&
|
||
this.currentUser &&
|
||
review.author &&
|
||
review.author.id === this.currentUser.id
|
||
},
|
||
|
||
async submitReview() {
|
||
if (!this.isFormValid) {
|
||
this.showNotification('error', 'Ошибка', 'Пожалуйста, заполните все обязательные поля правильно')
|
||
return
|
||
}
|
||
|
||
this.submitting = true
|
||
|
||
try {
|
||
const reviewData = {
|
||
rating: this.newReview.rating,
|
||
text: this.newReview.text,
|
||
achievement: this.newReview.achievement,
|
||
distance: this.newReview.distance,
|
||
improvement: this.newReview.improvement,
|
||
trainings: this.newReview.trainings
|
||
}
|
||
|
||
if (this.editingReview) {
|
||
// Редактирование существующего отзыва
|
||
await apiClient.put(`/reviews/${this.editingReview.id}`, reviewData)
|
||
this.showNotification('success', 'Успех', 'Отзыв успешно обновлен!')
|
||
this.editingReview = null
|
||
} else {
|
||
// Создание нового отзыва
|
||
await apiClient.post('/reviews', reviewData)
|
||
this.showNotification('success', 'Успех', 'Спасибо за ваш отзыв! После проверки модератором он будет опубликован.')
|
||
}
|
||
|
||
this.resetForm()
|
||
this.loadReviews()
|
||
this.loadStats()
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при отправке отзыва:', error)
|
||
let errorMessage = 'Произошла ошибка при отправке отзыва. Попробуйте еще раз.'
|
||
|
||
if (error.response?.status === 401) {
|
||
errorMessage = 'Необходимо авторизоваться для отправки отзыва'
|
||
this.handleAuthError()
|
||
} else if (error.response?.status === 403) {
|
||
errorMessage = 'У вас нет прав для выполнения этого действия'
|
||
} else if (error.response?.data?.message) {
|
||
errorMessage = error.response.data.message
|
||
}
|
||
|
||
this.showNotification('error', 'Ошибка', errorMessage)
|
||
} finally {
|
||
this.submitting = false
|
||
}
|
||
},
|
||
|
||
async editReview(review) {
|
||
this.editingReview = review
|
||
this.newReview = {
|
||
rating: review.rating,
|
||
text: review.text,
|
||
achievement: review.achievement || '',
|
||
distance: review.distance || '',
|
||
improvement: review.improvement || '',
|
||
trainings: review.trainings || 0
|
||
}
|
||
|
||
// Прокручиваем к форме
|
||
document.getElementById('add-review').scrollIntoView({
|
||
behavior: 'smooth'
|
||
})
|
||
},
|
||
|
||
async deleteReview(reviewId) {
|
||
if (!confirm('Вы уверены, что хотите удалить этот отзыв?')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
await apiClient.delete(`/reviews/${reviewId}`)
|
||
|
||
this.loadReviews()
|
||
this.loadStats()
|
||
|
||
this.showNotification('success', 'Успех', 'Отзыв успешно удален')
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при удалении отзыва:', error)
|
||
let errorMessage = 'Не удалось удалить отзыв'
|
||
|
||
if (error.response?.status === 403) {
|
||
errorMessage = 'Вы можете удалять только свои отзывы'
|
||
} else if (error.response?.status === 401) {
|
||
errorMessage = 'Необходимо авторизоваться'
|
||
this.handleAuthError()
|
||
} else if (error.response?.data?.message) {
|
||
errorMessage = error.response.data.message
|
||
}
|
||
|
||
this.showNotification('error', 'Ошибка', errorMessage)
|
||
}
|
||
},
|
||
|
||
resetForm() {
|
||
this.newReview = {
|
||
rating: 0,
|
||
text: '',
|
||
achievement: '',
|
||
distance: '',
|
||
improvement: '',
|
||
trainings: 0
|
||
}
|
||
this.editingReview = null
|
||
},
|
||
|
||
showNotification(type, title, text) {
|
||
// Используем существующую систему уведомлений или создаем простую альтернативу
|
||
if (this.$notify) {
|
||
this.$notify({
|
||
type: type,
|
||
title: title,
|
||
text: text
|
||
})
|
||
} else {
|
||
// Простая альтернатива, если $notify не доступен
|
||
const notification = document.createElement('div')
|
||
notification.className = `notification notification-${type}`
|
||
notification.innerHTML = `
|
||
<strong>${title}</strong>
|
||
<p>${text}</p>
|
||
`
|
||
document.body.appendChild(notification)
|
||
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
document.body.removeChild(notification)
|
||
}
|
||
}, 5000)
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
window.addEventListener('scroll', this.handleFirstInteraction, { passive: true, once: true })
|
||
window.addEventListener('click', this.handleFirstInteraction, { once: true })
|
||
window.addEventListener('touchstart', this.handleFirstInteraction, { once: true })
|
||
this.autoShowTimeout = setTimeout(() => {
|
||
if (!this.hasInteracted) {
|
||
this.showContent()
|
||
}
|
||
}, 3000)
|
||
this.checkAuth()
|
||
this.loadReviews()
|
||
this.loadStats()
|
||
},
|
||
beforeUnmount() {
|
||
// Убираем обработчики при размонтировании
|
||
window.removeEventListener('scroll', this.handleFirstInteraction)
|
||
window.removeEventListener('click', this.handleFirstInteraction)
|
||
window.removeEventListener('touchstart', this.handleFirstInteraction)
|
||
clearTimeout(this.autoShowTimeout)
|
||
},
|
||
watch: {
|
||
sortBy() {
|
||
this.currentPage = 1
|
||
this.loadReviews()
|
||
},
|
||
activeRatingFilter() {
|
||
this.currentPage = 1
|
||
this.loadReviews()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Стили остаются без изменений, как в вашем исходном файле */
|
||
/* Добавляем только стили для уведомлений */
|
||
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
z-index: 1000;
|
||
animation: slideInRight 0.3s ease;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.notification-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.notification-error {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.notification-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.notification-info {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* Остальные стили остаются такими же как в вашем файле */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
}
|
||
|
||
.spinner {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #2e8b57;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 2s linear infinite;
|
||
margin: 0 auto 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.no-reviews {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-edit,
|
||
.btn-delete {
|
||
background: none;
|
||
border: none;
|
||
color: #2e8b57;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
margin-right: 1rem;
|
||
}
|
||
|
||
.btn-delete {
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.btn-edit:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.btn-delete:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
z-index: 1000;
|
||
animation: slideInRight 0.3s ease;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.notification-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.notification-error {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.notification-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.notification-info {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* Остальные стили остаются такими же как в вашем файле */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
}
|
||
|
||
.spinner {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #2e8b57;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 2s linear infinite;
|
||
margin: 0 auto 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.no-reviews {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-edit,
|
||
.btn-delete {
|
||
background: none;
|
||
border: none;
|
||
color: #2e8b57;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
margin-right: 1rem;
|
||
}
|
||
|
||
.btn-delete {
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.btn-edit:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.btn-delete:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.loading {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
}
|
||
|
||
.spinner {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #2e8b57;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 2s linear infinite;
|
||
margin: 0 auto 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.no-reviews {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-edit,
|
||
.btn-delete {
|
||
background: none;
|
||
border: none;
|
||
color: #2e8b57;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
margin-right: 1rem;
|
||
}
|
||
|
||
.btn-delete {
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.btn-edit:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.btn-delete:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.reviews-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, #f8fff8 0%, #f0f8f0 100%);
|
||
}
|
||
|
||
/* Герой-секция */
|
||
.hero-section {
|
||
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||
color: white;
|
||
padding: 80px 0 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 3rem;
|
||
margin-bottom: 1rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.hero-subtitle {
|
||
font-size: 1.3rem;
|
||
opacity: 0.9;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.hero-stats {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 3rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2.5rem;
|
||
font-weight: 800;
|
||
color: #ffd700;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.9rem;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* Основные стили */
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 2.5rem;
|
||
color: #2e8b57;
|
||
margin-bottom: 1rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* Заголовок отзывов */
|
||
.reviews-header {
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.reviews-controls {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.sort-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.sort-controls label {
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.sort-controls select {
|
||
padding: 8px 12px;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 8px;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-controls {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: 8px 16px;
|
||
border: 2px solid #e9ecef;
|
||
background: white;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.filter-btn:hover {
|
||
border-color: #2e8b57;
|
||
}
|
||
|
||
.filter-btn.active {
|
||
background: #2e8b57;
|
||
color: white;
|
||
border-color: #2e8b57;
|
||
}
|
||
|
||
/* Сетка отзывов */
|
||
.reviews-section {
|
||
padding: 4rem 0;
|
||
}
|
||
|
||
.reviews-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||
gap: 2rem;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.review-card {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 2rem;
|
||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||
transition: all 0.3s ease;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.review-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
/* Стили по рейтингу */
|
||
.review-card.rating-5 {
|
||
border-color: #ffd700;
|
||
background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%);
|
||
}
|
||
|
||
.review-card.rating-4 {
|
||
border-color: #a8e6cf;
|
||
background: linear-gradient(135deg, #f0f8f0 0%, #e8f5e9 100%);
|
||
}
|
||
|
||
.review-card.rating-3 {
|
||
border-color: #ffd8b2;
|
||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||
}
|
||
|
||
.review-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.reviewer-info {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.reviewer-avatar {
|
||
width: 50px;
|
||
height: 50px;
|
||
background: #2e8b57;
|
||
color: white;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: 1.2rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.review-author {
|
||
color: #2e8b57;
|
||
margin: 0 0 0.3rem 0;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.reviewer-achievement {
|
||
color: #e74c3c;
|
||
font-weight: 600;
|
||
font-size: 0.9rem;
|
||
margin: 0 0 0.3rem 0;
|
||
}
|
||
|
||
.review-membership {
|
||
color: #666;
|
||
font-size: 0.8rem;
|
||
margin: 0;
|
||
}
|
||
|
||
.review-rating {
|
||
text-align: right;
|
||
}
|
||
|
||
.stars {
|
||
display: flex;
|
||
gap: 2px;
|
||
margin-bottom: 0.5rem;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.star {
|
||
color: #ddd;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.star.filled {
|
||
color: #ffd700;
|
||
}
|
||
|
||
.rating-value {
|
||
font-weight: bold;
|
||
color: #333;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.review-content {
|
||
space-y: 1rem;
|
||
}
|
||
|
||
.review-text {
|
||
line-height: 1.6;
|
||
color: #333;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.review-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.meta-items {
|
||
display: flex;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.meta-item {
|
||
background: rgba(46, 139, 87, 0.1);
|
||
color: #2e8b57;
|
||
padding: 0.3rem 0.8rem;
|
||
border-radius: 15px;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.review-date {
|
||
color: #666;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.review-actions {
|
||
border-top: 1px solid #e9ecef;
|
||
padding-top: 1rem;
|
||
}
|
||
|
||
.verified-badge {
|
||
color: #27ae60;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Пагинация */
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
margin: 3rem 0;
|
||
}
|
||
|
||
.pagination-btn {
|
||
padding: 10px 15px;
|
||
border: 2px solid #e9ecef;
|
||
background: white;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pagination-btn:hover:not(:disabled) {
|
||
border-color: #2e8b57;
|
||
background: #f8fff8;
|
||
}
|
||
|
||
.pagination-btn.active {
|
||
background: #2e8b57;
|
||
color: white;
|
||
border-color: #2e8b57;
|
||
}
|
||
|
||
.pagination-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Статистика оценок */
|
||
.reviews-stats {
|
||
background: white;
|
||
padding: 2rem;
|
||
border-radius: 15px;
|
||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.reviews-stats h3 {
|
||
color: #2e8b57;
|
||
margin-bottom: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.rating-bars {
|
||
max-width: 400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.rating-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 0.8rem;
|
||
}
|
||
|
||
.rating-label {
|
||
width: 80px;
|
||
}
|
||
|
||
.stars-small {
|
||
display: flex;
|
||
gap: 1px;
|
||
}
|
||
|
||
.star-small {
|
||
color: #ddd;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.star-small.filled {
|
||
color: #ffd700;
|
||
}
|
||
|
||
.bar-container {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: #e9ecef;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bar-fill {
|
||
height: 100%;
|
||
background: #2e8b57;
|
||
border-radius: 4px;
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
.rating-count {
|
||
width: 30px;
|
||
text-align: right;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
/* Форма отзыва */
|
||
.add-review-section {
|
||
background: white;
|
||
padding: 4rem 0;
|
||
border-top: 1px solid #e9ecef;
|
||
}
|
||
|
||
.add-review-content {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.form-header {
|
||
text-align: center;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.form-header p {
|
||
color: #666;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.review-form {
|
||
background: #f8fff8;
|
||
padding: 2rem;
|
||
border-radius: 15px;
|
||
border: 2px solid #e9ecef;
|
||
}
|
||
|
||
.auth-required {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.auth-message {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.auth-message h3 {
|
||
color: #2e8b57;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.auth-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.rating-input {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.star-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 2rem;
|
||
color: #ddd;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
padding: 0.2rem;
|
||
}
|
||
|
||
.star-btn.active {
|
||
color: #ffd700;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.star-btn:hover {
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 12px 15px;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
transition: border-color 0.3s ease;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus,
|
||
.form-group textarea:focus {
|
||
outline: none;
|
||
border-color: #2e8b57;
|
||
}
|
||
|
||
.char-counter {
|
||
text-align: right;
|
||
font-size: 0.8rem;
|
||
color: #666;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* CTA секция */
|
||
.cta-section {
|
||
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||
color: white;
|
||
padding: 80px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.cta-content h2 {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.cta-content p {
|
||
font-size: 1.2rem;
|
||
opacity: 0.9;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.success-stories-preview {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-bottom: 3rem;
|
||
max-width: 600px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.story-card {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
padding: 1.5rem;
|
||
border-radius: 10px;
|
||
backdrop-filter: blur(10px);
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
text-align: left;
|
||
}
|
||
|
||
.story-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
background: #ffd700;
|
||
color: #333;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.story-content h4 {
|
||
margin: 0 0 0.5rem 0;
|
||
color: white;
|
||
}
|
||
|
||
.story-content p {
|
||
font-size: 0.9rem;
|
||
opacity: 0.9;
|
||
margin: 0 0 0.5rem 0;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.story-achievement {
|
||
color: #ffd700;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.cta-features {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 2rem;
|
||
margin-bottom: 3rem;
|
||
max-width: 800px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.cta-feature {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
text-align: left;
|
||
}
|
||
|
||
.feature-icon {
|
||
font-size: 2.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.feature-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.feature-text strong {
|
||
font-size: 1.1rem;
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.feature-text span {
|
||
opacity: 0.8;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.cta-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.cta-contacts p {
|
||
margin-bottom: 1rem;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.contact-links {
|
||
display: flex;
|
||
gap: 1.5rem;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.contact-link {
|
||
color: #ffd700;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.contact-link:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* Кнопки */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 12px 24px;
|
||
border-radius: 50px;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
border: 2px solid transparent;
|
||
gap: 0.5rem;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #ffd700;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #e6c200;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: transparent;
|
||
color: rgb(162, 224, 198); /* Белый текст */
|
||
border-color: white; /* Белая рамка */
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: white;
|
||
color: #2e8b57; /* Зеленый текст при hover */
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-outline {
|
||
background: transparent;
|
||
color: #2e8b57;
|
||
border-color: #2e8b57;
|
||
}
|
||
|
||
.btn-outline:hover {
|
||
background: #2e8b57;
|
||
color: white;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-large {
|
||
padding: 15px 30px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
transform: none !important;
|
||
}
|
||
|
||
/* Адаптивность */
|
||
@media (max-width: 768px) {
|
||
.hero-title {
|
||
font-size: 2.2rem;
|
||
}
|
||
|
||
.hero-stats {
|
||
gap: 2rem;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.reviews-controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.sort-controls {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.filter-controls {
|
||
justify-content: center;
|
||
}
|
||
|
||
.reviews-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.form-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.review-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.review-rating {
|
||
align-self: flex-end;
|
||
}
|
||
|
||
.success-stories-preview {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.cta-features {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.cta-actions {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.auth-actions {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.hero-section {
|
||
padding: 60px 0 40px;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.container {
|
||
padding: 0 15px;
|
||
}
|
||
|
||
.review-card {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.reviewer-info {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
|
||
.reviewer-avatar {
|
||
align-self: center;
|
||
}
|
||
|
||
.review-meta {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.meta-items {
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
}
|
||
|
||
/* Анимации */
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.review-card {
|
||
animation: fadeInUp 0.6s ease;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.star-btn:hover {
|
||
animation: pulse 0.3s ease;
|
||
}
|
||
|
||
/* Задержки для анимаций карточек */
|
||
.review-card:nth-child(odd) {
|
||
animation-delay: 0.1s;
|
||
}
|
||
|
||
.review-card:nth-child(even) {
|
||
animation-delay: 0.2s;
|
||
}
|
||
|
||
/* Состояния загрузки */
|
||
.loading {
|
||
opacity: 0.6;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Улучшения доступности */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}
|
||
|
||
/* Фокус-стили */
|
||
button:focus-visible,
|
||
a:focus-visible,
|
||
select:focus-visible,
|
||
input:focus-visible,
|
||
textarea:focus-visible {
|
||
outline: 2px solid #2e8b57;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* Темная тема */
|
||
@media (prefers-color-scheme: dark) {
|
||
.reviews-page {
|
||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||
}
|
||
|
||
.review-card {
|
||
background: #2d2d2d;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.review-text {
|
||
color: #cccccc;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
background: #3d3d3d;
|
||
border-color: #555;
|
||
color: white;
|
||
}
|
||
|
||
.review-form {
|
||
background: #2d2d2d;
|
||
border-color: #555;
|
||
}
|
||
}
|
||
|
||
/* Кастомный скроллбар */
|
||
.reviews-section::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.reviews-section::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
}
|
||
|
||
.reviews-section::-webkit-scrollbar-thumb {
|
||
background: #2e8b57;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.reviews-section::-webkit-scrollbar-thumb:hover {
|
||
background: #26734a;
|
||
}
|
||
|
||
/* Утилитарные классы */
|
||
.text-center {
|
||
text-align: center;
|
||
}
|
||
|
||
.mb-2 {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.mt-2 {
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
/* Состояния валидации формы */
|
||
.form-group.error input,
|
||
.form-group.error textarea,
|
||
.form-group.error select {
|
||
border-color: #e74c3c;
|
||
}
|
||
|
||
.error-message {
|
||
color: #e74c3c;
|
||
font-size: 0.8rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
/* Уведомления */
|
||
.notification {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #27ae60;
|
||
color: white;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
z-index: 1000;
|
||
animation: slideInRight 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* Адаптация для печати */
|
||
@media print {
|
||
|
||
.hero-section,
|
||
.add-review-section,
|
||
.cta-section,
|
||
.review-actions,
|
||
.pagination {
|
||
display: none;
|
||
}
|
||
|
||
.review-card {
|
||
break-inside: avoid;
|
||
box-shadow: none;
|
||
border: 1px solid #ddd;
|
||
}
|
||
}
|
||
|
||
</style> |