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:
2025-10-25 05:50:36 +05:00
parent 21487660d6
commit 1c774ce80c
5 changed files with 465 additions and 102 deletions
+10 -1
View File
@@ -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;
-1
View File
@@ -120,7 +120,6 @@ const router = createRouter({
name: 'Events',
component: () => import('../views/Events.vue')
},
// Можно добавить маршрут для детальной страницы события
{
path: '/events/:id',
name: 'EventDetails',
+36 -30
View File
@@ -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
}
+414 -68
View File
@@ -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;
+5 -2
View File
@@ -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: {