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
|
<button
|
||||||
v-if="showActions && isRegistered"
|
v-if="showActions && isRegistered"
|
||||||
class="btn btn-danger btn-sm"
|
class="btn btn-danger btn-sm"
|
||||||
@click="$emit('cancel', event.registration_id)"
|
@click="$emit('cancel', getRegistrationId())"
|
||||||
>
|
>
|
||||||
❌ Отменить
|
❌ Отменить
|
||||||
</button>
|
</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) => {
|
const formatDate = (dateString) => {
|
||||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -154,6 +161,7 @@ export default {
|
|||||||
isFull,
|
isFull,
|
||||||
eventTypeLabel,
|
eventTypeLabel,
|
||||||
imageStyle,
|
imageStyle,
|
||||||
|
getRegistrationId,
|
||||||
formatDate,
|
formatDate,
|
||||||
truncateDescription
|
truncateDescription
|
||||||
}
|
}
|
||||||
@@ -162,6 +170,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Стили из предыдущего ответа */
|
||||||
.event-card {
|
.event-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ const router = createRouter({
|
|||||||
name: 'Events',
|
name: 'Events',
|
||||||
component: () => import('../views/Events.vue')
|
component: () => import('../views/Events.vue')
|
||||||
},
|
},
|
||||||
// Можно добавить маршрут для детальной страницы события
|
|
||||||
{
|
{
|
||||||
path: '/events/:id',
|
path: '/events/:id',
|
||||||
name: 'EventDetails',
|
name: 'EventDetails',
|
||||||
|
|||||||
@@ -11,33 +11,37 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
// Getters
|
// Getters - добавляем проверки на null/undefined
|
||||||
const upcomingEvents = computed(() =>
|
const upcomingEvents = computed(() => {
|
||||||
events.value
|
if (!events.value || !Array.isArray(events.value)) return []
|
||||||
.filter(event => new Date(event.date) > new Date())
|
return events.value
|
||||||
|
.filter(event => event && new Date(event.date) > new Date())
|
||||||
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||||
)
|
})
|
||||||
|
|
||||||
const pastEvents = computed(() =>
|
const pastEvents = computed(() => {
|
||||||
events.value
|
if (!events.value || !Array.isArray(events.value)) return []
|
||||||
.filter(event => new Date(event.date) <= new Date())
|
return events.value
|
||||||
|
.filter(event => event && new Date(event.date) <= new Date())
|
||||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||||
)
|
})
|
||||||
|
|
||||||
const registeredEvents = computed(() =>
|
const registeredEvents = computed(() => {
|
||||||
userRegistrations.value
|
if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
|
||||||
.filter(reg => reg.status === 'confirmed')
|
return userRegistrations.value
|
||||||
|
.filter(reg => reg && reg.status === 'confirmed')
|
||||||
.map(reg => ({
|
.map(reg => ({
|
||||||
...reg.event,
|
...reg.event,
|
||||||
registration_status: reg.status,
|
registration_status: reg.status,
|
||||||
registration_id: reg.id,
|
registration_id: reg.id,
|
||||||
result_time: reg.result_time
|
result_time: reg.result_time
|
||||||
}))
|
}))
|
||||||
)
|
})
|
||||||
|
|
||||||
const pendingRegistrations = computed(() =>
|
const pendingRegistrations = computed(() => {
|
||||||
userRegistrations.value.filter(reg => reg.status === 'pending')
|
if (!userRegistrations.value || !Array.isArray(userRegistrations.value)) return []
|
||||||
)
|
return userRegistrations.value.filter(reg => reg && reg.status === 'pending')
|
||||||
|
})
|
||||||
|
|
||||||
const eventTypes = {
|
const eventTypes = {
|
||||||
'race': 'Забег',
|
'race': 'Забег',
|
||||||
@@ -52,8 +56,8 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
try {
|
try {
|
||||||
const queryString = new URLSearchParams(params).toString()
|
const queryString = new URLSearchParams(params).toString()
|
||||||
const response = await apiClient.get(`/events?${queryString}`)
|
const response = await apiClient.get(`/events?${queryString}`)
|
||||||
events.value = response.data
|
events.value = response.data || []
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: events.value }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Events endpoint not available, using mock data', error)
|
console.warn('Events endpoint not available, using mock data', error)
|
||||||
events.value = generateMockEvents()
|
events.value = generateMockEvents()
|
||||||
@@ -66,10 +70,10 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
return withLoading({ loading, error }, async () => {
|
return withLoading({ loading, error }, async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/events/upcoming')
|
const response = await apiClient.get('/events/upcoming')
|
||||||
events.value = response.data
|
events.value = response.data || []
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: events.value }
|
||||||
} catch (error) {
|
} 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 }
|
return { success: true, data: upcomingEvents.value }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,8 +86,8 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
eventDetails.value = response.data
|
eventDetails.value = response.data
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: response.data }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Event details endpoint not available', error)
|
console.warn('Event details endpoint not available' + 'error = ' + error)
|
||||||
eventDetails.value = events.value.find(event => event.id === parseInt(eventId)) || null
|
eventDetails.value = events.value.find(event => event && event.id === parseInt(eventId)) || null
|
||||||
return { success: true, data: eventDetails.value }
|
return { success: true, data: eventDetails.value }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -93,8 +97,8 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
return withLoading({ loading, error }, async () => {
|
return withLoading({ loading, error }, async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/events/my/registrations')
|
const response = await apiClient.get('/events/my/registrations')
|
||||||
userRegistrations.value = response.data
|
userRegistrations.value = response.data || []
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: userRegistrations.value }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('User registrations endpoint not available, using mock data', error)
|
console.warn('User registrations endpoint not available, using mock data', error)
|
||||||
userRegistrations.value = generateMockRegistrations()
|
userRegistrations.value = generateMockRegistrations()
|
||||||
@@ -140,13 +144,15 @@ export const useEventsStore = defineStore('events', () => {
|
|||||||
const response = await apiClient.get(`/events/${eventId}/availability`)
|
const response = await apiClient.get(`/events/${eventId}/availability`)
|
||||||
return { success: true, data: response.data }
|
return { success: true, data: response.data }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.warn('Event availability endpoint not available' + 'error = ' + error)
|
||||||
console.warn('Event availability endpoint not available')
|
const event = events.value.find(e => e && e.id === parseInt(eventId))
|
||||||
const event = events.value.find(e => e.id === parseInt(eventId))
|
|
||||||
const available = event &&
|
const available = event &&
|
||||||
event.registration_open &&
|
event.registration_open &&
|
||||||
(event.max_participants === 0 || event.participants_count < event.max_participants)
|
(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 () => {
|
return withLoading({ loading, error }, async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.put(`/events/${eventId}`, eventData)
|
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) {
|
if (index !== -1) {
|
||||||
events.value[index] = response.data
|
events.value[index] = response.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,131 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page events-page">
|
<div class="page events-page">
|
||||||
<!-- ... остальной код без изменений ... -->
|
<div class="page-header">
|
||||||
|
<button class="btn btn-back" @click="$router.go(-1)">← Назад</button>
|
||||||
|
<h1>📅 События</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Предстоящие события -->
|
<div class="events-controls">
|
||||||
<div v-if="activeTab === 'upcoming'" class="events-section">
|
<div class="tabs">
|
||||||
<h2>🔮 Предстоящие события</h2>
|
<button
|
||||||
<div v-if="filteredUpcomingEvents.length === 0" class="empty-state">
|
class="tab-button"
|
||||||
<p>Нет предстоящих событий</p>
|
:class="{ active: activeTab === 'upcoming' }"
|
||||||
<button class="btn btn-primary" @click="refreshEvents">
|
@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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="events-grid">
|
|
||||||
<EventCard
|
<div class="filters">
|
||||||
v-for="event in filteredUpcomingEvents"
|
<select v-model="typeFilter" @change="applyFilters">
|
||||||
:key="event.id"
|
<option value="">Все типы</option>
|
||||||
:event="event"
|
<option value="race">Забеги</option>
|
||||||
:user-registrations="userRegistrations"
|
<option value="training">Тренировки</option>
|
||||||
@register="handleRegister"
|
<option value="social">Встречи</option>
|
||||||
@view-details="viewEventDetails"
|
<option value="workshop">Семинары</option>
|
||||||
/>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div v-if="showRegistrationModal" class="modal-overlay" @click="closeRegistrationModal">
|
||||||
@@ -112,15 +214,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useEventsStore } from '../stores/events'
|
import { useEventsStore } from '../stores/event'
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
// Импортируем компоненты как default
|
||||||
import EventCard from '../components/EventCard.vue'
|
import EventCard from '../components/EventCard.vue'
|
||||||
|
import RegistrationCard from '../components/RegistrationCard.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// eslint-disable-next-line vue/multi-word-component-names
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
name: 'Events',
|
name: 'Events',
|
||||||
components: {
|
components: {
|
||||||
EventCard,
|
EventCard,
|
||||||
|
RegistrationCard
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const eventsStore = useEventsStore()
|
const eventsStore = useEventsStore()
|
||||||
@@ -135,13 +241,13 @@ export default {
|
|||||||
const availabilityCheck = ref(null)
|
const availabilityCheck = ref(null)
|
||||||
const showSuccessNotification = ref(false)
|
const showSuccessNotification = ref(false)
|
||||||
|
|
||||||
// Computed
|
// Computed - добавляем безопасные проверки
|
||||||
const filteredUpcomingEvents = computed(() => {
|
const filteredUpcomingEvents = computed(() => {
|
||||||
let events = eventsStore.upcomingEvents
|
let events = eventsStore.upcomingEvents || []
|
||||||
if (typeFilter.value) {
|
if (typeFilter.value && events) {
|
||||||
events = events.filter(event => event.type === typeFilter.value)
|
events = events.filter(event => event && event.type === typeFilter.value)
|
||||||
}
|
}
|
||||||
return events
|
return events || []
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch для проверки доступности при выборе события
|
// Watch для проверки доступности при выборе события
|
||||||
@@ -158,10 +264,14 @@ export default {
|
|||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const loadEventsData = async () => {
|
const loadEventsData = async () => {
|
||||||
await Promise.all([
|
try {
|
||||||
eventsStore.fetchEvents(),
|
await Promise.all([
|
||||||
eventsStore.fetchUserRegistrations()
|
eventsStore.fetchEvents(),
|
||||||
])
|
eventsStore.fetchUserRegistrations()
|
||||||
|
])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading events data:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshEvents = async () => {
|
const refreshEvents = async () => {
|
||||||
@@ -173,14 +283,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRegister = async (event) => {
|
const handleRegister = async (event) => {
|
||||||
|
if (!event) return
|
||||||
|
|
||||||
selectedEvent.value = event
|
selectedEvent.value = event
|
||||||
registrationNotes.value = ''
|
registrationNotes.value = ''
|
||||||
showRegistrationModal.value = true
|
showRegistrationModal.value = true
|
||||||
|
|
||||||
// Проверяем доступность
|
// Проверяем доступность
|
||||||
const result = await eventsStore.checkEventAvailability(event.id)
|
try {
|
||||||
if (result.success) {
|
const result = await eventsStore.checkEventAvailability(event.id)
|
||||||
availabilityCheck.value = result.data
|
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
|
if (!selectedEvent.value || !availabilityCheck.value?.available) return
|
||||||
|
|
||||||
registering.value = true
|
registering.value = true
|
||||||
const result = await eventsStore.registerForEvent(
|
try {
|
||||||
selectedEvent.value.id,
|
const result = await eventsStore.registerForEvent(
|
||||||
registrationNotes.value
|
selectedEvent.value.id,
|
||||||
)
|
registrationNotes.value
|
||||||
registering.value = false
|
)
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
closeRegistrationModal()
|
|
||||||
showSuccessNotification.value = true
|
|
||||||
// Автоматически скрываем уведомление через 5 секунд
|
|
||||||
setTimeout(() => {
|
|
||||||
showSuccessNotification.value = false
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Переключаемся на вкладку "Мои регистрации"
|
if (result.success) {
|
||||||
activeTab.value = 'my'
|
closeRegistrationModal()
|
||||||
} else {
|
showSuccessNotification.value = true
|
||||||
alert('Ошибка при регистрации: ' + result.error)
|
// Автоматически скрываем уведомление через 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) => {
|
const handleCancelRegistration = async (registrationId) => {
|
||||||
if (confirm('Вы уверены, что хотите отменить регистрацию?')) {
|
if (confirm('Вы уверены, что хотите отменить регистрацию?')) {
|
||||||
const result = await eventsStore.cancelRegistration(registrationId)
|
try {
|
||||||
if (result.success) {
|
const result = await eventsStore.cancelRegistration(registrationId)
|
||||||
// Показываем уведомление об отмене
|
if (result.success) {
|
||||||
alert('Регистрация успешно отменена')
|
// Показываем уведомление об отмене
|
||||||
} else {
|
alert('Регистрация успешно отменена')
|
||||||
alert('Ошибка при отмене регистрации: ' + result.error)
|
} 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 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) {
|
if (event) {
|
||||||
alert(`Детали события: ${event.title}\n\n${event.description}`)
|
alert(`Детали события: ${event.title}\n\n${event.description}`)
|
||||||
}
|
}
|
||||||
@@ -240,13 +367,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
return new Date(dateString).toLocaleDateString('ru-RU', {
|
if (!dateString) return ''
|
||||||
day: 'numeric',
|
try {
|
||||||
month: 'long',
|
return new Date(dateString).toLocaleDateString('ru-RU', {
|
||||||
year: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
month: 'long',
|
||||||
minute: '2-digit'
|
year: 'numeric',
|
||||||
})
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Date formatting error:', error)
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -256,11 +389,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
loading: computed(() => eventsStore.loading),
|
loading: computed(() => eventsStore.loading),
|
||||||
error: computed(() => eventsStore.error),
|
error: computed(() => eventsStore.error),
|
||||||
upcomingEvents: computed(() => eventsStore.upcomingEvents),
|
upcomingEvents: computed(() => eventsStore.upcomingEvents || []),
|
||||||
pastEvents: computed(() => eventsStore.pastEvents),
|
pastEvents: computed(() => eventsStore.pastEvents || []),
|
||||||
registeredEvents: computed(() => eventsStore.registeredEvents),
|
registeredEvents: computed(() => eventsStore.registeredEvents || []),
|
||||||
pendingRegistrations: computed(() => eventsStore.pendingRegistrations),
|
pendingRegistrations: computed(() => eventsStore.pendingRegistrations || []),
|
||||||
userRegistrations: computed(() => eventsStore.userRegistrations),
|
userRegistrations: computed(() => eventsStore.userRegistrations || []),
|
||||||
activeTab,
|
activeTab,
|
||||||
typeFilter,
|
typeFilter,
|
||||||
showRegistrationModal,
|
showRegistrationModal,
|
||||||
@@ -285,7 +418,134 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -318,6 +578,19 @@ export default {
|
|||||||
color: #333;
|
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 {
|
.event-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -357,6 +630,32 @@ export default {
|
|||||||
border-radius: 6px;
|
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 {
|
.char-count {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -377,6 +676,12 @@ export default {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
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) {
|
@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 {
|
.notification {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
|
|||||||
@@ -192,7 +192,7 @@
|
|||||||
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -214,7 +214,10 @@ export default {
|
|||||||
avatarLoadError: false,
|
avatarLoadError: false,
|
||||||
personalBests: [],
|
personalBests: [],
|
||||||
upcomingEvents: [],
|
upcomingEvents: [],
|
||||||
currentTrainingPlan: null
|
currentTrainingPlan: null,
|
||||||
|
hasInteracted: false,
|
||||||
|
isContentVisible: false,
|
||||||
|
autoShowTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
Reference in New Issue
Block a user