From 2b70c5bfcac6cef794fe48d9fcc5d86ab67e7adf Mon Sep 17 00:00:00 2001 From: aneta88hristova-png Date: Thu, 4 Jun 2026 13:48:01 +0200 Subject: [PATCH] events --- assets/css/style.css | 20 +- .../js/controllers/bloom/eventsController.js | 214 +++++++++++++++--- assets/js/utils/eventHelpers.js | 77 +++++++ assets/js/views/events/eventsFilters.js | 54 +++++ assets/js/views/events/eventsHeader.js | 37 +++ 5 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 assets/js/utils/eventHelpers.js create mode 100644 assets/js/views/events/eventsFilters.js create mode 100644 assets/js/views/events/eventsHeader.js diff --git a/assets/css/style.css b/assets/css/style.css index ea447e4..781b041 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,13 +1,13 @@ :root { - /* Avenga Academy Brand Colors */ - --color-primary: #dd2c00; - --color-primary-dark: #b02400; - --color-secondary: #0257b4; - --color-accent: #ffa500; + /* Avenga Academy Brand Colors - Uppdaterad till Lila */ + --color-primary: #9333ea; + --color-primary-dark: #7e22ce; + --color-secondary: #4f46e5; + --color-accent: #a855f7; --color-success: #48bb78; - --color-danger: #dd2c00; - --color-warning: #ffa500; - --color-info: #0257b4; + --color-danger: #dc2626; + --color-warning: #f59e0b; + --color-info: #3b82f6; --color-text: #2d3748; --color-text-light: #718096; --color-border: #e2e8f0; @@ -78,7 +78,7 @@ body { .btn-primary:hover { transform: translateY(-2px); - box-shadow: 0 10px 20px rgba(221, 44, 0, 0.3); + box-shadow: 0 10px 20px rgba(147, 51, 234, 0.3); } .btn-secondary { @@ -195,7 +195,7 @@ body { .form-input:focus { outline: none; border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(221, 44, 0, 0.1); + box-shadow: 0 0 0 3px rgba(147, 51, 234, 0.1); } /* Modal */ diff --git a/assets/js/controllers/bloom/eventsController.js b/assets/js/controllers/bloom/eventsController.js index 2921c72..b940970 100644 --- a/assets/js/controllers/bloom/eventsController.js +++ b/assets/js/controllers/bloom/eventsController.js @@ -1,16 +1,13 @@ /** * Events Controller (Student/Alumni) * Browse upcoming career-related events and industry news. - * - * User story: "Events and industry news" - * - * Available mock service methods: - * - mockDataService.getEvents() - * - mockDataService.getEventById(id) - * - mockDataService.registerForEvent(eventId, userId) */ import authService from '../../services/authService.js'; +import mockDataService from '../../services/mockDataService.js'; import { renderAppHeader } from '../../views/appHeader.js'; +import { renderEventsHeader } from '../../views/events/eventsHeader.js'; +import { renderEventsFilters, attachFilterListeners, updateFilterButtonStyles } from '../../views/events/eventsFilters.js'; +import { formatDate, getEventTypeStyles, getEventTypeIcon, getEventTypeLabel, filterEventsByDate, showToast } from '../../utils/eventHelpers.js'; export default async function eventsController() { const app = document.getElementById('app'); @@ -21,30 +18,195 @@ export default async function eventsController() { return; } - // TODO (student task): list events, support registration, filter by type/date - // const events = await mockDataService.getEvents(); - - app.innerHTML = ` - ${renderAppHeader(user, window.location.pathname)} -
-
-
-
-

- - Events -

-

Upcoming career days, workshops, and networking events

+ let allEvents = []; + let filteredEvents = []; + let currentFilters = { + type: 'all', + dateRange: 'all' + }; + + function renderEventsGrid() { + const eventsGrid = document.getElementById('eventsGrid'); + if (!eventsGrid) return; + + if (filteredEvents.length === 0) { + eventsGrid.innerHTML = ` +
+ +

No events found

+

Try adjusting your filters or check back later for new events.

+
+ `; + return; + } + + eventsGrid.innerHTML = filteredEvents.map(event => ` +
+
+ + + ${getEventTypeLabel(event.type)} + +
+

${event.title}

+

${event.description}

+
+
+ + ${formatDate(event.date)} at ${event.time} +
+
+ + ${event.isOnline ? 'Online Event' : event.location} +
+
+ + ${event.registeredCount || 0} / ${event.maxParticipants || '∞'} registered +
+
+ +
+ `).join(''); + + document.querySelectorAll('.view-event-btn').forEach(btn => { + if (!btn.disabled) { + btn.addEventListener('click', () => { + const event = allEvents.find(e => e.id === btn.dataset.eventId); + if (event) showEventDetails(event); + }); + } + }); + } + + function showEventDetails(event) { + const modalHtml = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHtml); + + const modal = document.getElementById('eventModal'); + const closeModal = () => modal.remove(); + + document.getElementById('closeModalBtn')?.addEventListener('click', closeModal); + document.getElementById('closeModalBtn2')?.addEventListener('click', closeModal); + document.getElementById('registerFromModalBtn')?.addEventListener('click', async () => { + await handleRegistration(event.id); + closeModal(); + }); + modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); + } + + async function handleRegistration(eventId) { + try { + const result = await mockDataService.registerForEvent(eventId, user.id); + + if (result.success) { + const event = allEvents.find(e => e.id === eventId); + if (event) event.registeredCount = (event.registeredCount || 0) + 1; + showToast('Successfully registered for the event!', 'success'); + applyFiltersAndRender(); + } else if (result.reason === 'event_full') { + showToast('Sorry, this event is full!', 'error'); + } + } catch (error) { + console.error('Registration failed:', error); + showToast('Failed to register for event. Please try again.', 'error'); + } + } + + function applyFiltersAndRender() { + let filtered = [...allEvents]; + if (currentFilters.type !== 'all') { + filtered = filtered.filter(event => event.type === currentFilters.type); + } + filtered = filterEventsByDate(filtered, currentFilters.dateRange); + filteredEvents = filtered; + renderEventsGrid(); + } + + function handleFilterChange(filterType, filterValue) { + currentFilters[filterType] = filterValue; + updateFilterButtonStyles(filterType, filterValue); + applyFiltersAndRender(); + } + + try { + app.innerHTML = ` + ${renderAppHeader(user, window.location.pathname)} +
+
+
+
+
+
+
+
+ `; + + allEvents = await mockDataService.getEvents(); + filteredEvents = [...allEvents]; + + const headerContainer = document.getElementById('eventsHeader'); + const filtersContainer = document.getElementById('filtersContainer'); + + if (headerContainer) headerContainer.innerHTML = renderEventsHeader(allEvents); + if (filtersContainer) filtersContainer.innerHTML = renderEventsFilters(currentFilters, handleFilterChange); + + renderEventsGrid(); + attachFilterListeners(handleFilterChange); + + } catch (error) { + console.error('Failed to load events:', error); + app.innerHTML = ` + ${renderAppHeader(user, window.location.pathname)} +
+
- -

TODO: Events listing

-

Implement events grid, filters, and registration flow.

+ +

Failed to load events

+

Please try again later.

-
- `; + `; + } const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) logoutBtn.addEventListener('click', () => authService.logout()); diff --git a/assets/js/utils/eventHelpers.js b/assets/js/utils/eventHelpers.js new file mode 100644 index 0000000..b78616a --- /dev/null +++ b/assets/js/utils/eventHelpers.js @@ -0,0 +1,77 @@ +/** + * Event Helper Functions + */ + +export function formatDate(dateString) { + const options = { year: 'numeric', month: 'long', day: 'numeric' }; + return new Date(dateString).toLocaleDateString('en-US', options); +} + +export function getEventTypeStyles(type) { + const styles = { + 'career-day': 'bg-purple-100 text-purple-700', + 'workshop': 'bg-blue-100 text-blue-700', + 'webinar': 'bg-green-100 text-green-700', + 'networking': 'bg-yellow-100 text-yellow-700' + }; + return styles[type] || 'bg-gray-100 text-gray-700'; +} + +export function getEventTypeIcon(type) { + const icons = { + 'career-day': 'fa-briefcase', + 'workshop': 'fa-laptop-code', + 'webinar': 'fa-chalkboard-teacher', + 'networking': 'fa-handshake' + }; + return icons[type] || 'fa-calendar'; +} + +export function getEventTypeLabel(type) { + const labels = { + 'career-day': 'Career Day', + 'workshop': 'Workshop', + 'webinar': 'Webinar', + 'networking': 'Networking' + }; + return labels[type] || 'Event'; +} + +export function filterEventsByDate(events, dateRange) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + switch (dateRange) { + case 'upcoming': + return events.filter(event => new Date(event.date) >= today); + case 'this-week': + const nextWeek = new Date(today); + nextWeek.setDate(today.getDate() + 7); + return events.filter(event => { + const eventDate = new Date(event.date); + return eventDate >= today && eventDate <= nextWeek; + }); + case 'this-month': + const nextMonth = new Date(today); + nextMonth.setMonth(today.getMonth() + 1); + return events.filter(event => { + const eventDate = new Date(event.date); + return eventDate >= today && eventDate <= nextMonth; + }); + default: + return events; + } +} + +export function showToast(message, type = 'info') { + const toast = document.createElement('div'); + const colors = { + success: 'bg-green-500', + error: 'bg-red-500', + info: 'bg-blue-500' + }; + toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white z-50 fade-in ${colors[type] || colors.info}`; + toast.innerHTML = `${message}`; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 3000); +} diff --git a/assets/js/views/events/eventsFilters.js b/assets/js/views/events/eventsFilters.js new file mode 100644 index 0000000..17733ac --- /dev/null +++ b/assets/js/views/events/eventsFilters.js @@ -0,0 +1,54 @@ +/** + * Events Filters View + */ + +export function renderEventsFilters(currentFilters, onFilterChange) { + return ` +
+

Filters

+
+
+ +
+ + + + + +
+
+
+ +
+ + + + +
+
+
+
+ `; +} + +export function attachFilterListeners(onFilterChange) { + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + const filterType = btn.dataset.filterType; + const filterValue = btn.dataset.filterValue; + onFilterChange(filterType, filterValue); + }); + }); +} + +export function updateFilterButtonStyles(filterType, filterValue) { + document.querySelectorAll(`.filter-btn[data-filter-type="${filterType}"]`).forEach(btn => { + if (btn.dataset.filterValue === filterValue) { + btn.classList.add('bg-purple-600', 'text-white'); + btn.classList.remove('bg-white', 'text-gray-700', 'border-gray-300'); + } else { + btn.classList.remove('bg-purple-600', 'text-white'); + btn.classList.add('bg-white', 'text-gray-700', 'border-gray-300'); + } + }); +} diff --git a/assets/js/views/events/eventsHeader.js b/assets/js/views/events/eventsHeader.js new file mode 100644 index 0000000..65c4b39 --- /dev/null +++ b/assets/js/views/events/eventsHeader.js @@ -0,0 +1,37 @@ +/** + * Events Header View + */ + +export function renderEventsHeader(events) { + const totalEvents = events.length; + const totalRegistrations = events.reduce((sum, e) => sum + (e.registeredCount || 0), 0); + const totalWorkshops = events.filter(e => e.type === 'workshop').length; + + return ` +
+

+ + Events +

+

Upcoming career days, workshops, and networking events

+
+ +
+
+ +

${totalEvents}

+

Upcoming Events

+
+
+ +

${totalRegistrations}

+

Total Registrations

+
+
+ +

${totalWorkshops}

+

Workshops Available

+
+
+ `; +}