modified: main_dc/BB/bbvue/src/components/EventCard.vue
modified: main_dc/BB/bbvue/src/router/index.js modified: main_dc/BB/bbvue/src/stores/event.js modified: main_dc/BB/bbvue/src/views/Events.vue modified: main_dc/BB/bbvue/src/views/Profile.vue add events page and event store and eventcard modal
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
<button
|
||||
v-if="showActions && isRegistered"
|
||||
class="btn btn-danger btn-sm"
|
||||
@click="$emit('cancel', event.registration_id)"
|
||||
@click="$emit('cancel', getRegistrationId())"
|
||||
>
|
||||
❌ Отменить
|
||||
</button>
|
||||
@@ -132,6 +132,13 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
const getRegistrationId = () => {
|
||||
const registration = props.userRegistrations.find(reg =>
|
||||
reg.event_id === props.event.id
|
||||
)
|
||||
return registration?.id
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
@@ -154,6 +161,7 @@ export default {
|
||||
isFull,
|
||||
eventTypeLabel,
|
||||
imageStyle,
|
||||
getRegistrationId,
|
||||
formatDate,
|
||||
truncateDescription
|
||||
}
|
||||
@@ -162,6 +170,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили из предыдущего ответа */
|
||||
.event-card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
|
||||
@@ -120,7 +120,6 @@ const router = createRouter({
|
||||
name: 'Events',
|
||||
component: () => import('../views/Events.vue')
|
||||
},
|
||||
// Можно добавить маршрут для детальной страницы события
|
||||
{
|
||||
path: '/events/:id',
|
||||
name: 'EventDetails',
|
||||
|
||||
@@ -11,33 +11,37 @@ export const useEventsStore = defineStore('events', () => {
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// Getters
|
||||
const upcomingEvents = computed(() =>
|
||||
events.value
|
||||
.filter(event => new Date(event.date) > new Date())
|
||||
// Getters - добавляем проверки на null/undefined
|
||||
const upcomingEvents = computed(() => {
|
||||
if (!events.value || !Array.isArray(events.value)) return []
|
||||
return events.value
|
||||
.filter(event => event && new Date(event.date) > new Date())
|
||||
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||
)
|
||||
})
|
||||
|
||||
const pastEvents = computed(() =>
|
||||
events.value
|
||||
.filter(event => new Date(event.date) <= new Date())
|
||||
const pastEvents = computed(() => {
|
||||
if (!events.value || !Array.isArray(events.value)) return []
|
||||
return events.value
|
||||
.filter(event => event && new Date(event.date) <= new Date())
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
)
|
||||
})
|
||||
|
||||
const registeredEvents = computed(() =>
|
||||
userRegistrations.value
|
||||
.filter(reg => reg.status === 'confirmed')
|
||||
const registeredEvents = computed(() => {
|
||||
if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
|
||||
return userRegistrations.value
|
||||
.filter(reg => reg && reg.status === 'confirmed')
|
||||
.map(reg => ({
|
||||
...reg.event,
|
||||
registration_status: reg.status,
|
||||
registration_id: reg.id,
|
||||
result_time: reg.result_time
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
const pendingRegistrations = computed(() =>
|
||||
userRegistrations.value.filter(reg => reg.status === 'pending')
|
||||
)
|
||||
const pendingRegistrations = computed(() => {
|
||||
if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
|
||||
return userRegistrations.value.filter(reg => reg && reg.status === 'pending')
|
||||
})
|
||||
|
||||
const eventTypes = {
|
||||
'race': 'Забег',
|
||||
@@ -52,8 +56,8 @@ export const useEventsStore = defineStore('events', () => {
|
||||
try {
|
||||
const queryString = new URLSearchParams(params).toString()
|
||||
const response = await apiClient.get(`/events?${queryString}`)
|
||||
events.value = response.data
|
||||
return { success: true, data: response.data }
|
||||
events.value = response.data || []
|
||||
return { success: true, data: events.value }
|
||||
} catch (error) {
|
||||
console.warn('Events endpoint not available, using mock data', error)
|
||||
events.value = generateMockEvents()
|
||||
@@ -66,10 +70,10 @@ export const useEventsStore = defineStore('events', () => {
|
||||
return withLoading({ loading, error }, async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/events/upcoming')
|
||||
events.value = response.data
|
||||
return { success: true, data: response.data }
|
||||
events.value = response.data || []
|
||||
return { success: true, data: events.value }
|
||||
} catch (error) {
|
||||
console.warn('Upcoming events endpoint not available', error)
|
||||
console.warn('Upcoming events endpoint not available' + 'error = ' + error)
|
||||
return { success: true, data: upcomingEvents.value }
|
||||
}
|
||||
})
|
||||
@@ -82,8 +86,8 @@ export const useEventsStore = defineStore('events', () => {
|
||||
eventDetails.value = response.data
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
console.warn('Event details endpoint not available', error)
|
||||
eventDetails.value = events.value.find(event => event.id === parseInt(eventId)) || null
|
||||
console.warn('Event details endpoint not available' + 'error = ' + error)
|
||||
eventDetails.value = events.value.find(event => event && event.id === parseInt(eventId)) || null
|
||||
return { success: true, data: eventDetails.value }
|
||||
}
|
||||
})
|
||||
@@ -93,8 +97,8 @@ export const useEventsStore = defineStore('events', () => {
|
||||
return withLoading({ loading, error }, async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/events/my/registrations')
|
||||
userRegistrations.value = response.data
|
||||
return { success: true, data: response.data }
|
||||
userRegistrations.value = response.data || []
|
||||
return { success: true, data: userRegistrations.value }
|
||||
} catch (error) {
|
||||
console.warn('User registrations endpoint not available, using mock data', error)
|
||||
userRegistrations.value = generateMockRegistrations()
|
||||
@@ -140,13 +144,15 @@ export const useEventsStore = defineStore('events', () => {
|
||||
const response = await apiClient.get(`/events/${eventId}/availability`)
|
||||
return { success: true, data: response.data }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.warn('Event availability endpoint not available')
|
||||
const event = events.value.find(e => e.id === parseInt(eventId))
|
||||
console.warn('Event availability endpoint not available' + 'error = ' + error)
|
||||
const event = events.value.find(e => e && e.id === parseInt(eventId))
|
||||
const available = event &&
|
||||
event.registration_open &&
|
||||
(event.max_participants === 0 || event.participants_count < event.max_participants)
|
||||
return { success: true, data: { available, remaining_spots: event.max_participants - event.participants_count } }
|
||||
return { success: true, data: {
|
||||
available,
|
||||
remaining_spots: event ? event.max_participants - event.participants_count : 0
|
||||
} }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -167,7 +173,7 @@ export const useEventsStore = defineStore('events', () => {
|
||||
return withLoading({ loading, error }, async () => {
|
||||
try {
|
||||
const response = await apiClient.put(`/events/${eventId}`, eventData)
|
||||
const index = events.value.findIndex(event => event.id === parseInt(eventId))
|
||||
const index = events.value.findIndex(event => event && event.id === parseInt(eventId))
|
||||
if (index !== -1) {
|
||||
events.value[index] = response.data
|
||||
}
|
||||
|
||||
@@ -1,29 +1,131 @@
|
||||
<template>
|
||||
<div class="page events-page">
|
||||
<!-- ... остальной код без изменений ... -->
|
||||
<div class="page-header">
|
||||
<button class="btn btn-back" @click="$router.go(-1)">← Назад</button>
|
||||
<h1>📅 События</h1>
|
||||
</div>
|
||||
|
||||
<!-- Предстоящие события -->
|
||||
<div v-if="activeTab === 'upcoming'" class="events-section">
|
||||
<h2>🔮 Предстоящие события</h2>
|
||||
<div v-if="filteredUpcomingEvents.length === 0" class="empty-state">
|
||||
<p>Нет предстоящих событий</p>
|
||||
<button class="btn btn-primary" @click="refreshEvents">
|
||||
🔄 Обновить
|
||||
<div class="events-controls">
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'upcoming' }"
|
||||
@click="activeTab = 'upcoming'"
|
||||
>
|
||||
🔮 Предстоящие
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'my' }"
|
||||
@click="activeTab = 'my'"
|
||||
>
|
||||
🎫 Мои регистрации
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'past' }"
|
||||
@click="activeTab = 'past'"
|
||||
>
|
||||
📚 Прошедшие
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="events-grid">
|
||||
<EventCard
|
||||
v-for="event in filteredUpcomingEvents"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
:user-registrations="userRegistrations"
|
||||
@register="handleRegister"
|
||||
@view-details="viewEventDetails"
|
||||
/>
|
||||
|
||||
<div class="filters">
|
||||
<select v-model="typeFilter" @change="applyFilters">
|
||||
<option value="">Все типы</option>
|
||||
<option value="race">Забеги</option>
|
||||
<option value="training">Тренировки</option>
|
||||
<option value="social">Встречи</option>
|
||||
<option value="workshop">Семинары</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ... остальной код без изменений ... -->
|
||||
<div v-if="loading" class="loading">Загрузка событий...</div>
|
||||
|
||||
<div v-else class="events-content">
|
||||
<!-- Предстоящие события -->
|
||||
<div v-if="activeTab === 'upcoming'" class="events-section">
|
||||
<h2>🔮 Предстоящие события</h2>
|
||||
<div v-if="!filteredUpcomingEvents || filteredUpcomingEvents.length === 0" class="empty-state">
|
||||
<p>Нет предстоящих событий</p>
|
||||
<button class="btn btn-primary" @click="refreshEvents">
|
||||
🔄 Обновить
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="events-grid">
|
||||
<EventCard
|
||||
v-for="event in filteredUpcomingEvents"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
:user-registrations="userRegistrations"
|
||||
@register="handleRegister"
|
||||
@view-details="viewEventDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Мои регистрации -->
|
||||
<div v-if="activeTab === 'my'" class="events-section">
|
||||
<h2>🎫 Мои регистрации</h2>
|
||||
<div v-if="(!registeredEvents || registeredEvents.length === 0) && (!pendingRegistrations || pendingRegistrations.length === 0)" class="empty-state">
|
||||
<p>У вас нет зарегистрированных событий</p>
|
||||
<button class="btn btn-primary" @click="activeTab = 'upcoming'">
|
||||
📅 Посмотреть события
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="pendingRegistrations && pendingRegistrations.length > 0" class="registrations-section">
|
||||
<h3>⏳ Ожидают подтверждения</h3>
|
||||
<div class="registrations-list">
|
||||
<RegistrationCard
|
||||
v-for="registration in pendingRegistrations"
|
||||
:key="registration.id"
|
||||
:registration="registration"
|
||||
@cancel="handleCancelRegistration"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="registeredEvents && registeredEvents.length > 0" class="registrations-section">
|
||||
<h3>✅ Подтвержденные участия</h3>
|
||||
<div class="events-grid">
|
||||
<EventCard
|
||||
v-for="event in registeredEvents"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
:user-registrations="userRegistrations"
|
||||
:show-actions="true"
|
||||
@cancel="handleCancelRegistration"
|
||||
@view-details="viewEventDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Прошедшие события -->
|
||||
<div v-if="activeTab === 'past'" class="events-section">
|
||||
<h2>📚 Прошедшие события</h2>
|
||||
<div v-if="!pastEvents || pastEvents.length === 0" class="empty-state">
|
||||
<p>Нет прошедших событий</p>
|
||||
</div>
|
||||
<div v-else class="events-grid">
|
||||
<EventCard
|
||||
v-for="event in pastEvents"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
:user-registrations="userRegistrations"
|
||||
:show-actions="false"
|
||||
@view-details="viewEventDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
<button class="btn-retry" @click="loadEventsData">Попробовать снова</button>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно регистрации -->
|
||||
<div v-if="showRegistrationModal" class="modal-overlay" @click="closeRegistrationModal">
|
||||
@@ -112,15 +214,19 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useEventsStore } from '../stores/events'
|
||||
import { useEventsStore } from '../stores/event'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
|
||||
// Импортируем компоненты как default
|
||||
import EventCard from '../components/EventCard.vue'
|
||||
import RegistrationCard from '../components/RegistrationCard.vue'
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/multi-word-component-names
|
||||
name: 'Events',
|
||||
components: {
|
||||
EventCard,
|
||||
RegistrationCard
|
||||
},
|
||||
setup() {
|
||||
const eventsStore = useEventsStore()
|
||||
@@ -135,13 +241,13 @@ export default {
|
||||
const availabilityCheck = ref(null)
|
||||
const showSuccessNotification = ref(false)
|
||||
|
||||
// Computed
|
||||
// Computed - добавляем безопасные проверки
|
||||
const filteredUpcomingEvents = computed(() => {
|
||||
let events = eventsStore.upcomingEvents
|
||||
if (typeFilter.value) {
|
||||
events = events.filter(event => event.type === typeFilter.value)
|
||||
let events = eventsStore.upcomingEvents || []
|
||||
if (typeFilter.value && events) {
|
||||
events = events.filter(event => event && event.type === typeFilter.value)
|
||||
}
|
||||
return events
|
||||
return events || []
|
||||
})
|
||||
|
||||
// Watch для проверки доступности при выборе события
|
||||
@@ -158,10 +264,14 @@ export default {
|
||||
|
||||
// Методы
|
||||
const loadEventsData = async () => {
|
||||
await Promise.all([
|
||||
eventsStore.fetchEvents(),
|
||||
eventsStore.fetchUserRegistrations()
|
||||
])
|
||||
try {
|
||||
await Promise.all([
|
||||
eventsStore.fetchEvents(),
|
||||
eventsStore.fetchUserRegistrations()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('Error loading events data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshEvents = async () => {
|
||||
@@ -173,14 +283,20 @@ export default {
|
||||
}
|
||||
|
||||
const handleRegister = async (event) => {
|
||||
if (!event) return
|
||||
|
||||
selectedEvent.value = event
|
||||
registrationNotes.value = ''
|
||||
showRegistrationModal.value = true
|
||||
|
||||
// Проверяем доступность
|
||||
const result = await eventsStore.checkEventAvailability(event.id)
|
||||
if (result.success) {
|
||||
availabilityCheck.value = result.data
|
||||
try {
|
||||
const result = await eventsStore.checkEventAvailability(event.id)
|
||||
if (result.success) {
|
||||
availabilityCheck.value = result.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking availability:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,35 +304,46 @@ export default {
|
||||
if (!selectedEvent.value || !availabilityCheck.value?.available) return
|
||||
|
||||
registering.value = true
|
||||
const result = await eventsStore.registerForEvent(
|
||||
selectedEvent.value.id,
|
||||
registrationNotes.value
|
||||
)
|
||||
registering.value = false
|
||||
|
||||
if (result.success) {
|
||||
closeRegistrationModal()
|
||||
showSuccessNotification.value = true
|
||||
// Автоматически скрываем уведомление через 5 секунд
|
||||
setTimeout(() => {
|
||||
showSuccessNotification.value = false
|
||||
}, 5000)
|
||||
try {
|
||||
const result = await eventsStore.registerForEvent(
|
||||
selectedEvent.value.id,
|
||||
registrationNotes.value
|
||||
)
|
||||
|
||||
// Переключаемся на вкладку "Мои регистрации"
|
||||
activeTab.value = 'my'
|
||||
} else {
|
||||
alert('Ошибка при регистрации: ' + result.error)
|
||||
if (result.success) {
|
||||
closeRegistrationModal()
|
||||
showSuccessNotification.value = true
|
||||
// Автоматически скрываем уведомление через 5 секунд
|
||||
setTimeout(() => {
|
||||
showSuccessNotification.value = false
|
||||
}, 5000)
|
||||
|
||||
// Переключаемся на вкладку "Мои регистрации"
|
||||
activeTab.value = 'my'
|
||||
} else {
|
||||
alert('Ошибка при регистрации: ' + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
alert('Ошибка при регистрации: ' + error.message)
|
||||
} finally {
|
||||
registering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelRegistration = async (registrationId) => {
|
||||
if (confirm('Вы уверены, что хотите отменить регистрацию?')) {
|
||||
const result = await eventsStore.cancelRegistration(registrationId)
|
||||
if (result.success) {
|
||||
// Показываем уведомление об отмене
|
||||
alert('Регистрация успешно отменена')
|
||||
} else {
|
||||
alert('Ошибка при отмене регистрации: ' + result.error)
|
||||
try {
|
||||
const result = await eventsStore.cancelRegistration(registrationId)
|
||||
if (result.success) {
|
||||
// Показываем уведомление об отмене
|
||||
alert('Регистрация успешно отменена')
|
||||
} else {
|
||||
alert('Ошибка при отмене регистрации: ' + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Cancel registration error:', error)
|
||||
alert('Ошибка при отмене регистрации: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +351,7 @@ export default {
|
||||
const viewEventDetails = (eventId) => {
|
||||
// Переход на страницу деталей события
|
||||
// Можно реализовать позже отдельную страницу
|
||||
const event = eventsStore.events.find(e => e.id === parseInt(eventId))
|
||||
const event = eventsStore.events.find(e => e && e.id === parseInt(eventId))
|
||||
if (event) {
|
||||
alert(`Детали события: ${event.title}\n\n${event.description}`)
|
||||
}
|
||||
@@ -240,13 +367,19 @@ export default {
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Date formatting error:', error)
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -256,11 +389,11 @@ export default {
|
||||
return {
|
||||
loading: computed(() => eventsStore.loading),
|
||||
error: computed(() => eventsStore.error),
|
||||
upcomingEvents: computed(() => eventsStore.upcomingEvents),
|
||||
pastEvents: computed(() => eventsStore.pastEvents),
|
||||
registeredEvents: computed(() => eventsStore.registeredEvents),
|
||||
pendingRegistrations: computed(() => eventsStore.pendingRegistrations),
|
||||
userRegistrations: computed(() => eventsStore.userRegistrations),
|
||||
upcomingEvents: computed(() => eventsStore.upcomingEvents || []),
|
||||
pastEvents: computed(() => eventsStore.pastEvents || []),
|
||||
registeredEvents: computed(() => eventsStore.registeredEvents || []),
|
||||
pendingRegistrations: computed(() => eventsStore.pendingRegistrations || []),
|
||||
userRegistrations: computed(() => eventsStore.userRegistrations || []),
|
||||
activeTab,
|
||||
typeFilter,
|
||||
showRegistrationModal,
|
||||
@@ -285,7 +418,134 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ... существующие стили ... */
|
||||
/* Стили остаются без изменений */
|
||||
.events-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.events-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 0.25rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
color: #2e8b57;
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.filters select {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.events-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.events-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.events-section h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.registrations-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.registrations-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.registrations-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
@@ -318,6 +578,19 @@ export default {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.event-preview {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid #2e8b57;
|
||||
}
|
||||
|
||||
.event-preview h4 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -357,6 +630,32 @@ export default {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #2e8b57;
|
||||
box-shadow: 0 0 0 2px rgba(46, 139, 87, 0.2);
|
||||
}
|
||||
|
||||
.char-count {
|
||||
text-align: right;
|
||||
font-size: 0.8rem;
|
||||
@@ -377,6 +676,12 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
@@ -411,8 +716,49 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-retry {
|
||||
background: #2e8b57;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.btn-retry:hover {
|
||||
background: #26734d;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.events-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.filters {
|
||||
order: 1;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.notification {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -214,7 +214,10 @@ export default {
|
||||
avatarLoadError: false,
|
||||
personalBests: [],
|
||||
upcomingEvents: [],
|
||||
currentTrainingPlan: null
|
||||
currentTrainingPlan: null,
|
||||
hasInteracted: false,
|
||||
isContentVisible: false,
|
||||
autoShowTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
Reference in New Issue
Block a user