Files
tp/begushiybashkir/bbvue/src/views/Reviews.vue
T
valitovgaziz 56a5a4ea50 modified: begushiybashkir/bbvue/src/views/Members.vue
modified:   begushiybashkir/bbvue/src/views/Reviews.vue
	modified:   begushiybashkir/bbvue/src/views/Training.vue
add Members page for begushiybashkir site
2025-10-02 13:31:09 +05:00

1634 lines
39 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">{{ averageRating }}</div>
<div class="stat-label">Средний рейтинг</div>
</div>
<div class="stat">
<div class="stat-number">{{ totalReviews }}</div>
<div class="stat-label">Отзывов</div>
</div>
<div class="stat">
<div class="stat-number">{{ successStories }}</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="sortReviews">
<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 class="reviews-grid">
<div
v-for="review in paginatedReviews"
: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 }}</h4>
<p class="reviewer-achievement" v-if="review.achievement">
🏆 {{ review.achievement }}
</p>
<p class="review-membership">
Участник {{ 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.date) }}</span>
</div>
<div class="review-actions" v-if="review.verified">
<span class="verified-badge"> Проверенный отзыв</span>
</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>📝 Опубликовать отзыв</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>
export default {
// eslint-disable-next-line vue/multi-word-component-names
name: 'Reviews',
data() {
return {
isAuthenticated: false, // В реальном приложении получать из store/auth
sortBy: 'newest',
activeRatingFilter: 'all',
currentPage: 1,
reviewsPerPage: 6,
submitting: false,
ratingFilters: [
{ value: 'all', label: 'Все оценки' },
{ value: '5', label: '⭐ 5 звезд' },
{ value: '4', label: '⭐ 4+ звезды' },
{ value: '3', label: '⭐ 3+ звезды' }
],
newReview: {
rating: 0,
text: '',
achievement: '',
distance: ''
},
reviews: [
{
id: 1,
author: 'Анна',
rating: 5,
text: 'Пришла в клуб с нулевым опытом бега. За год прошла путь от 0 до марафона! Тренер Загир - профессионал своего дела, всегда поддерживает и дает ценные советы. Атмосфера в клубе невероятная!',
date: '2025-01-15',
achievement: 'Первый марафон за 4:20:15',
distance: '42.2 км',
improvement: '+8 мин на 10км',
trainings: 45,
membership: '1 год',
verified: true
},
{
id: 2,
author: 'Михаил',
rating: 5,
text: 'Искал профессиональный подход к тренировкам и нашел его здесь. Индивидуальные программы, работа над техникой, регулярные соревнования. За 6 месяцев улучшил результат на полумарафоне на 25 минут!',
date: '2025-01-12',
achievement: 'Полумарафон 1:35:20',
distance: '21.1 км',
improvement: '+25 мин на 21км',
trainings: 32,
membership: '6 месяцев',
verified: true
},
{
id: 3,
author: 'Елена',
rating: 5,
text: 'Очень нравятся групповые тренировки - всегда есть с кем побегать и пообщаться. Тренер внимательно следит за техникой, что помогло избежать травм. За 3 месяца похудела на 8 кг и полюбила бег!',
date: '2025-01-10',
achievement: 'Похудение на 8 кг',
distance: '10 км',
improvement: '+12 мин на 5км',
trainings: 24,
membership: '3 месяца',
verified: true
},
{
id: 4,
author: 'Сергей',
rating: 5,
text: 'Как опытный бегун могу сказать - тренер Загир один из лучших в Уфе. Его методики действительно работают. Благодаря клубу пробежал ультрамарафон и нашел единомышленников.',
date: '2025-01-08',
achievement: 'Ультрамарафон 80 км',
distance: 'Трейл',
improvement: 'Новый уровень',
trainings: 68,
membership: '2 года',
verified: true
},
{
id: 5,
author: 'Ғаяз',
rating: 4,
text: 'Хороший клуб с сильным тренером. Немного не хватает вечерних тренировок в будни. В остальном - отличная атмосфера и профессиональный подход.',
date: '2025-01-05',
achievement: 'Марафон 3:34:33',
distance: '42.2 км',
improvement: '+15 мин на марафоне',
trainings: 52,
membership: '1.5 года',
verified: true
},
{
id: 6,
author: 'Данил',
rating: 5,
text: 'Лучшее решение - присоединиться к этому клубу! Нашел друзей, улучшил результаты, участвую в забегах. Отдельное спасибо за подготовку к трейлам - незабываемые впечатления!',
date: '2025-01-03',
achievement: 'Трейл 120 км',
distance: 'Трейл',
improvement: '+18 мин на 10км',
trainings: 41,
membership: '10 месяцев',
verified: true
},
{
id: 7,
author: 'Ильвира',
rating: 5,
text: 'Как маме двоих детей было сложно найти время для спорта. В клубе подобрали удобный график, поддержали морально. Теперь бегаю регулярно и чувствую себя прекрасно!',
date: '2024-12-28',
achievement: 'Первый полумарафон',
distance: '21.1 км',
improvement: 'С нуля до 21км',
trainings: 18,
membership: '5 месяцев',
verified: true
},
{
id: 8,
author: 'Булат',
rating: 4,
text: 'Нравится системный подход к тренировкам. Есть небольшие замечания по организации некоторых мероприятий, но в целом - хороший клуб с перспективой развития.',
date: '2024-12-25',
achievement: 'Полумарафон 1:45:48',
distance: '21.1 км',
improvement: '+10 мин на 10км',
trainings: 29,
membership: '8 месяцев',
verified: true
}
]
}
},
computed: {
totalReviews() {
return this.reviews.length
},
averageRating() {
if (this.reviews.length === 0) return 0
const sum = this.reviews.reduce((acc, review) => acc + review.rating, 0)
return (sum / this.reviews.length).toFixed(1)
},
successStories() {
return this.reviews.filter(review => review.achievement && review.rating >= 4).length
},
filteredReviews() {
let filtered = this.reviews
// Фильтрация по рейтингу
if (this.activeRatingFilter !== 'all') {
const minRating = parseInt(this.activeRatingFilter)
filtered = filtered.filter(review => review.rating >= minRating)
}
// Сортировка
switch (this.sortBy) {
case 'newest':
filtered.sort((a, b) => new Date(b.date) - new Date(a.date))
break
case 'oldest':
filtered.sort((a, b) => new Date(a.date) - new Date(b.date))
break
case 'highest':
filtered.sort((a, b) => b.rating - a.rating || new Date(b.date) - new Date(a.date))
break
case 'lowest':
filtered.sort((a, b) => a.rating - b.rating || new Date(b.date) - new Date(a.date))
break
}
return filtered
},
paginatedReviews() {
const startIndex = (this.currentPage - 1) * this.reviewsPerPage
return this.filteredReviews.slice(startIndex, startIndex + this.reviewsPerPage)
},
totalPages() {
return Math.ceil(this.filteredReviews.length / this.reviewsPerPage)
},
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: {
getInitials(name) {
return name.split(' ').map(n => n[0]).join('').toUpperCase()
},
getReviewCardClass(rating) {
return `rating-${rating}`
},
formatDate(dateString) {
const date = new Date(dateString)
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
},
sortReviews() {
this.currentPage = 1
},
filterByRating(rating) {
this.activeRatingFilter = rating
this.currentPage = 1
},
changePage(page) {
this.currentPage = page
window.scrollTo({ top: 0, behavior: 'smooth' })
},
getRatingCount(rating) {
return this.reviews.filter(review => review.rating === rating).length
},
getRatingPercentage(rating) {
const count = this.getRatingCount(rating)
return (count / this.totalReviews) * 100
},
async submitReview() {
if (!this.isFormValid) return
this.submitting = true
try {
// Имитация запроса к API
await new Promise(resolve => setTimeout(resolve, 1500))
const newReview = {
id: this.reviews.length + 1,
author: 'Вы', // В реальном приложении брать из профиля
rating: this.newReview.rating,
text: this.newReview.text,
date: new Date().toISOString().split('T')[0],
achievement: this.newReview.achievement,
distance: this.newReview.distance,
improvement: '',
trainings: 0,
membership: 'Новый участник',
verified: false
}
this.reviews.unshift(newReview)
this.resetForm()
// Показать уведомление об успехе
alert('Спасибо за ваш отзыв! После проверки модератором он будет опубликован.')
} catch (error) {
console.error('Ошибка при отправке отзыва:', error)
alert('Произошла ошибка при отправке отзыва. Попробуйте еще раз.')
} finally {
this.submitting = false
}
},
resetForm() {
this.newReview = {
rating: 0,
text: '',
achievement: '',
distance: ''
}
},
login() {
// В реальном приложении перенаправлять на страницу логина
this.isAuthenticated = true
}
},
mounted() {
// В реальном приложении проверять авторизацию
// this.checkAuth()
}
}
</script>
<style scoped>
.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: white;
border-color: white;
}
.btn-secondary:hover {
background: white;
color: #2e8b57;
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>