Skip to content
Snippets Groups Projects
Commit 54db27d6 authored by Ben Davison's avatar Ben Davison
Browse files

Merge

parents 07181610 b1a80f50
Branches bd00438
No related tags found
1 merge request!24Merge reviews and recommendations
Showing
with 477 additions and 171 deletions
......@@ -2,4 +2,11 @@ Icon by <a class="link_pro" href="https://freeicons.io/regular-life-icons/search
<a href="https://www.flaticon.com/free-icons/filter" title="filter icons">Filter icons created by Laura Reen - Flaticon</a>
Icon by <a class="link_pro" href="https://freeicons.io/essential-web-2/user-ciecle-round-account-person-icon-40275">BECRIS</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/material-icons-social-2/notifications-icon-16784">icon king1</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/software-and-solution-icons-2/bars-icon-13346">Anu Rocks</a> on <a href="https://freeicons.io">freeicons.io</a>
\ No newline at end of file
Icon by <a class="link_pro" href="https://freeicons.io/software-and-solution-icons-2/bars-icon-13346">Anu Rocks</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/web-application-v.1-3/pin-web-app-gps-location-marker-icon-104645">ColourCreatype</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/user-and-profile-icons/garbage-user-profile-icon-6317">Raj Dev</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/essential-icons/arrow-up-newsformat-calendar-icon-6037">Free Preloaders</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/business-and-online-icons/edit-icon-icon-4">Raj Dev</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/basic-symbol/bin-remove-garbage-recycle-trash-delete-icon-38493">MD Badsha Meah</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/essentials/cross-mark-false-wrong-close-icon-441095">Hilmy Abiyyu Asad</a> on <a href="https://freeicons.io">freeicons.io</a>
Icon by <a class="link_pro" href="https://freeicons.io/bullets-and-check-icons/check-ok-yes-tick-icon-2920">icon king1</a> on <a href="https://freeicons.io">freeicons.io</a>
\ No newline at end of file
<?xml version="1.0" ?><svg enable-background="new 0 0 500 500" id="Layer_1" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><path fill="rgb(211 212 237)" d="M250,291.6c-52.8,0-95.8-43-95.8-95.8s43-95.8,95.8-95.8s95.8,43,95.8,95.8S302.8,291.6,250,291.6z M250,127.3 c-37.7,0-68.4,30.7-68.4,68.4s30.7,68.4,68.4,68.4s68.4-30.7,68.4-68.4S287.7,127.3,250,127.3z"/></g><g><path fill="rgb(211 212 237)" d="M386.9,401.1h-27.4c0-60.4-49.1-109.5-109.5-109.5s-109.5,49.1-109.5,109.5h-27.4c0-75.5,61.4-136.9,136.9-136.9 S386.9,325.6,386.9,401.1z"/></g></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.cls-1{fill:none;stroke:rgb(211 212 237);stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><title>68.calendar</title><g id="_68.calendar" data-name="68.calendar"><rect class="cls-1" x="1" y="3" width="22" height="20" rx="3" ry="3"/><line class="cls-1" x1="1" y1="9" x2="23" y2="9"/><line class="cls-1" x1="12" y1="5" x2="12" y2="1"/><line class="cls-1" x1="6" y1="5" x2="6" y2="1"/><line class="cls-1" x1="18" y1="5" x2="18" y2="1"/><line class="cls-1" x1="5" y1="14" x2="7" y2="14"/><line class="cls-1" x1="11" y1="14" x2="13" y2="14"/><line class="cls-1" x1="17" y1="14" x2="19" y2="14"/><line class="cls-1" x1="5" y1="18" x2="7" y2="18"/><line class="cls-1" x1="11" y1="18" x2="13" y2="18"/><line class="cls-1" x1="17" y1="18" x2="19" y2="18"/></g></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>Cross</title><path fill="rgb(211 212 237)" d="M286.17,256,420.42,121.75a21.33,21.33,0,1,0-30.17-30.17L256,225.83,121.75,91.58a21.33,21.33,0,1,0-30.17,30.17L225.83,256,91.58,390.25a21.33,21.33,0,1,0,30.17,30.17L256,286.17,390.25,420.42a21.33,21.33,0,1,0,30.17-30.17Z"/></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><defs><style>.cls-1{fill:rgb(211 212 237);}.cls-2{fill:rgb(211 212 237);}</style></defs><title>x</title><path class="cls-1" d="M17.66229,21.88486,63.3847,30.82574l45.72241,8.94088a1.559,1.559,0,0,0,1.82788-1.22994A10.15176,10.15176,0,0,0,102.9192,26.6239l-15.172-2.96684.79656-4.07352A11.10952,11.10952,0,0,0,79.7827,6.56318L57.33412,2.17343A11.1096,11.1096,0,0,0,44.31375,10.9345L43.51718,15.008l-15.172-2.96685A10.15176,10.15176,0,0,0,16.43235,20.057a1.559,1.559,0,0,0,1.22994,1.82788ZM60.0674,9.82374,74.369,12.62036a8.2641,8.2641,0,0,1,6.5245,9.69647h0l-15.2613-2.9843L50.37093,16.34825h0A8.2641,8.2641,0,0,1,60.0674,9.82374Z"/><path class="cls-2" d="M110.58839,47.36161H17.41161a1.559,1.559,0,0,0-1.55785,1.55785v5.90918c0,.85949,16.14275,61.05238,16.14275,61.05238a11.08149,11.08149,0,0,0,11.03938,10.153H84.96412A11.08149,11.08149,0,0,0,96.0035,115.881s16.14275-60.19289,16.14275-61.05238V48.91946A1.559,1.559,0,0,0,110.58839,47.36161Zm-61.934,64.2194a2.60793,2.60793,0,0,1-3.19666-1.84821c-4.44239-16.61345-8.95983-33.53068-11.91535-44.72956a2.61069,2.61069,0,1,1,5.04851-1.33243c2.95407,11.19159,7.47077,28.10409,11.911,44.71353A2.61043,2.61043,0,0,1,48.65435,111.581Zm17.95316-2.52243a2.61095,2.61095,0,0,1-5.22189,0V64.337a2.61095,2.61095,0,0,1,5.22189,0ZM94.45735,65.00325C91.3685,76.70879,86.46715,95.05644,82.542,109.73317a2.61073,2.61073,0,1,1-5.04414-1.34918c3.9237-14.67272,8.8236-33.01491,11.911-44.71316a2.61069,2.61069,0,1,1,5.04851,1.33243Z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="rgb(211 212 237)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34"></path><polygon points="18 2 22 6 12 16 8 16 8 12 18 2"></polygon></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 2.5C21.11 2.5 12.25 11.85 12.25 23.35C12.25 34.35 30.02 59.79 30.77 60.86C30.9096 61.0561 31.0941 61.2159 31.3081 61.3262C31.522 61.4365 31.7593 61.4941 32 61.4941C32.2407 61.4941 32.478 61.4365 32.6919 61.3262C32.9059 61.2159 33.0904 61.0561 33.23 60.86C34 59.79 51.75 34.39 51.75 23.35C51.75 11.85 42.89 2.5 32 2.5ZM22.77 23.35C23.27 11.12 40.77 11.12 41.23 23.35C40.73 35.59 23.27 35.58 22.77 23.35Z" fill="rgb(211 212 237)"/>
</svg>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 80.588 61.158" style="enable-background:new 0 0 80.588 61.158;" xml:space="preserve">
<path style="fill:rgb(211 212 237)" d="M29.658,61.157c-1.238,0-2.427-0.491-3.305-1.369L1.37,34.808c-1.826-1.825-1.826-4.785,0-6.611
c1.825-1.826,4.786-1.827,6.611,0l21.485,21.481L72.426,1.561c1.719-1.924,4.674-2.094,6.601-0.374
c1.926,1.72,2.094,4.675,0.374,6.601L33.145,59.595c-0.856,0.959-2.07,1.523-3.355,1.56C29.746,61.156,29.702,61.157,29.658,61.157z
"/>
</svg>
.displayCard {
width: 100%;
padding: 15px;
margin-bottom: 10px;
background: rgba(179, 185, 253, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(158, 162, 204, 0.3);
z-index: 1;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
transition: 0.25s ease-in-out;
&Clickable {
@extend .displayCard;
cursor: pointer;
&:hover {
background: rgba(179, 185, 253, 0.3);
}
&:active {
background: rgba(179, 185, 253, 0.5);
transition: 0.15s ease-in-out;
}
}
.displayCardHeader {
width: 100%;
height: 16px;
display: flex;
justify-content: space-between;
.eventTitle {
width: 80%;
&Link {
text-decoration: underline;
color: rgb(255, 235, 235);
cursor: pointer;
transition: 0.15s ease-in-out;
&:hover {
color: rgb(252, 166, 166);
}
&:active {
transition: 0.1s;
color: #e66f5c;
}
}
}
.location {
width: 20%;
position: relative;
bottom: 15px;
text-overflow: ellipsis !important;
.locationIcon {
height: 30px;
width: 25px;
margin-right: 5px;
position: relative;
top: 5px;
}
.locationText {
width: 17%;
position: fixed;
top: 15px;
overflow: hidden;
white-space: nowrap !important;
text-overflow: ellipsis !important;
}
}
}
.displayCardBody {
width: 100%;
display: flex;
margin-top: 10px;
margin-bottom: 10px;
.eventInfo {
width: 70%;
.calendar {
height: 20px;
width: 20px;
margin-top: 8px;
margin-right: 15px;
position: relative;
top: 2px;
}
.date {
position: relative;
bottom: 3px;
}
.attendance {
height: 30px;
width: 30px;
margin-right: 5px;
position: relative;
top: 2px;
right: 4.5px;
}
.attendanceCount {
position: relative;
bottom: 7px;
}
}
.buttonContainer {
width: 30%;
display: flex;
align-items: center;
justify-content: flex-end;
.confirmCancelContainer {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
align-items: flex-end;
.confirmCancelText {
align-self: flex-start;
position: relative;
margin-bottom: 5px;
}
.confirmCancelButtonContainer {
width: 100%;
display: flex;
gap: 15px;
justify-content: flex-start;
}
}
.displayCardButton {
background-color: rgba(255, 255, 255, 0.3);
height: 40px;
width: 40px;
border: none;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
float: right;
margin-right: 15px;
cursor: pointer;
transition: 0.1s ease-in-out;
&:hover {
background-color: rgba(255, 255, 255, 0.4);
}
&:active {
background-color: rgba(208, 230, 230, 0.55);
}
}
.confirmButton {
margin: 0;
padding: 2px 15px;
height: 35px;
width: 40%;
display: flex;
color: rgba(239, 211, 207, 0.75);
background-color: rgba(255, 255, 255, 0.15);
border: none;
border-radius: 60px;
transition: 0.15s;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.buttonIcon {
width: 24px;
height: 24px;
position: relative;
top: 2px;
right: 3px;
}
.tickIcon {
@extend .buttonIcon;
width: 20px;
height: 20px;
}
}
.Edit {
&:hover {
background-color: rgba(115, 187, 253, 0.80);
}
&:active {
background-color: rgba(115, 187, 253, 0.50);
}
}
.Delete {
&:hover {
background-color: rgba(255, 0, 0, 0.70);
}
&:active {
background-color: rgba(255, 36, 36, 0.50);
}
}
}
}
.displayCardFooter{
width: 100%;
.tag {
padding: 2px 10px;
margin-right: 10px;
color: #fff;
background-color: rgba(115, 186, 253, 0.90);
border: none;
border-radius: 60px;
overflow: auto;
}
}
}
\ No newline at end of file
import { useNavigate } from 'react-router-dom';
import styles from './DisplayCard.module.scss';
import { Event, isEventPassed } from '../../pages/EventManagement/EventManagement';
import { Booking } from '../../pages/BookingManagement/BookingManagement';
import { useState } from 'react';
interface displayCard {
event: Event,
booking?: Booking,
browsing?: boolean,
pastEndDate?: boolean,
handleOnClick?: (event: Event) => void,
handleEdit?: (event: Event) => void,
// Defined for booking or event
handleDelete?: (Id: string) => void,
}
function DisplayCard({event, booking, handleOnClick, handleEdit, handleDelete}: displayCard) {
const [showCancelConfirm, setShowCancelConfirm] = useState<boolean>(false);
const navigate = useNavigate();
const getIdType = () => {
return booking ? booking.id : event.id;
}
const eventLinkOnClick = (event: Event) => {
navigate(`/event/${event.id}`, { state: { event } });
};
const onDeleteConfirm = () => {
setShowCancelConfirm(false)
if(handleDelete) handleDelete(getIdType());
}
return(
<div key={getIdType()} className={handleOnClick ? styles.displayCardClickable : styles.displayCard} onClick={handleOnClick ? () => handleOnClick(event) : () => {}}>
<div className={styles.displayCardHeader}>
<strong className={!handleOnClick ? styles.eventTitleLink : styles.eventTitle} onClick={!handleOnClick ? () => eventLinkOnClick(event) : () => {}}>{event.name}</strong>
{/*
{booking ?
<span className= {
isEventPassed(event.end)
? (styles.pastLabel || "pastLabel")
: (styles.upcomingLabel || "upcomingLabel")
}>
{isEventPassed(event.end) ? 'Past' : 'Upcoming'}
</span> : event.venue}
*** Reminder to replace this with filters in each components to see upcoming or past events
*/}
<div className={styles.location} title={ event.venue.length > 12 ? event.venue : "" }>
<img className={styles.locationIcon} src="/location.svg"/>
<span className={styles.locationText}>{ event.venue }</span>
</div>
</div>
<div className={styles.displayCardBody}>
<span className={styles.eventInfo}>
<img className={styles.calendar} src="/calendar.svg"/>
<span className={styles.date}>{new Date(event.start).toLocaleString()} - {new Date(event.end).toLocaleString()}</span>
<br />
<img className={styles.attendance} src="/attendance.svg"/>
<span className={styles.attendanceCount}>{event.attendeeCount} / {event.capacity} attendees</span>
</span>
<div className={styles.buttonContainer}>
{ handleEdit && !isEventPassed(event.end) && !showCancelConfirm &&
<button className={styles.displayCardButton + " " + styles.Edit} onClick={() => handleEdit(event)}>
<img src="/edit.svg"/>
</button>
}
{ handleDelete && !isEventPassed(event.end) &&
(showCancelConfirm ? (
<div className={styles.confirmCancelContainer}>
<span className={styles.confirmCancelText}>Are you sure?</span>
<div className={styles.confirmCancelButtonContainer}>
<button className={styles.confirmButton + " " + styles.Delete} onClick={() => onDeleteConfirm()}>
<img className={styles.tickIcon} src="/tick.svg"/>
<span>Yes, Delete</span>
</button>
<button className={styles.confirmButton} onClick={() => setShowCancelConfirm(false)}>
<img className={styles.buttonIcon} src="/cross.svg"/>
<span>No, Keep</span>
</button>
</div>
</div>
) : (
<button
className={styles.displayCardButton + " " + styles.Delete}
onClick={() => setShowCancelConfirm(true)}
>
<img className={styles.deleteIcon} src="/delete.svg"/>
</button>
)
)}
</div>
</div>
<div className={styles.displayCardFooter}>
{!booking && event.tags && event.tags.length > 0 &&
<div className={styles.tags}>
{event.tags.map((tag) => (
<span className={styles.tag}>{tag}</span>
))}
</div>
}
{booking &&
<div className={styles.bookingInfo}>
<small>Booking ID: {booking.id}</small>
<br/>
<small>Booked on: {new Date(booking.created_at).toLocaleDateString()}</small>
</div>
}
</div>
</div>
);
}
export default DisplayCard;
\ No newline at end of file
......@@ -3,13 +3,14 @@
top: 0px;
left: 0px;
width: 100%;
z-index: 1;
}
.header {
//background-color: #0e121e;
color: #efd3cf;
background-color: #000000;
border-bottom: 2px solid rgb(211 212 237);
background-color: #0e1016;
border-bottom: 2px solid rgb(44, 44, 53);
display: flex;
position: relative;
z-index: 2;
......@@ -55,7 +56,7 @@
position: relative;
float: right;
margin-right: 15px;
Cursor: pointer;
cursor: pointer;
transition: 0.1s ease-in-out;
&:hover {
background-color: rgba(255, 255, 255, 0.4);
......
......@@ -3,7 +3,7 @@ import styles from './SearchBar.module.scss';
function SearchBar() {
// Need to add query sanitisation
let query = "query";
let query = "";
const navigate = useNavigate();
const searchButtonClick = () => {
......
......@@ -3,6 +3,7 @@
height: 100%;
padding: 10px;
margin-top: 80px;
position: relative;
z-index: 0;
box-sizing: border-box;
}
\ No newline at end of file
......@@ -102,7 +102,7 @@ function App() {
<Route path="/" element={<LandingPage />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/browse/:query" element={<BrowsingPage />} />
<Route path="/browse/:query?" element={<BrowsingPage />} />
<Route path="/event/:eventId" element={<Events />} />
<Route path="/BookingManagement" element={<BookingManagement />} />
</Routes>
......
.browseLink {
cursor: pointer;
text-decoration: underline;
transition: 0.15s ease-in-out;
&:hover {
color: rgb(252, 166, 166);
}
&:active {
transition: 0.1s;
color: #e66f5c;
}
}
\ No newline at end of file
import { useContext, useEffect, useState } from 'react';
import styles from './BookingManagement.module.scss';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { SessionContext } from '../App/App';
import { getAuthHeaders, handleAuthError } from '../../../utils/auth';
import { Event } from '../../pages/EventManagement/EventManagement';
import DisplayCard from '../../components/DisplayCard/DisplayCard';
type Event = {
id: string;
name: string;
description: string;
start: string;
end: string;
venue: string;
capacity: number;
tags?: string[];
created_by?: string;
};
interface Booking {
export interface Booking {
id: string;
event_id: string;
user_id: string;
created_at: string;
upcoming?: string;
event?: Event; // For displaying event details with booking
}
......@@ -29,7 +20,6 @@ function BookingManagement() {
const [bookings, setBookings] = useState<Booking[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [showCancelConfirm, setShowCancelConfirm] = useState<string | null>(null);
const [bookingInProgress, setBookingInProgress] = useState<boolean>(false);
const [bookingSuccess, setBookingSuccess] = useState<boolean>(false);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
......@@ -101,6 +91,26 @@ function BookingManagement() {
}
}));
const eventsWithAttendeesCounts = await Promise.all(bookingsWithEvents.map(async (booking: Booking) => {
if(booking.event) {
try {
const bookingsRes = await fetch(`http://localhost:8000/events/bookings?event_id=${booking.event.id}`, { method: "GET", headers: getAuthHeaders() });
const bookingsData = await bookingsRes.json();
return {
...booking.event,
attendeeCount: bookingsData.length ? bookingsData.length : 0,
};
} catch (err) {
console.error(`Failed to fetch bookings for event ${booking.event.id}`, err);
return {...booking.event, attendeeCount: 0 };
}
}
}));
bookingsWithEvents.map((booking: Booking) => {
booking.event = eventsWithAttendeesCounts.find(event => booking?.event?.id === event?.id);
});
setBookings(bookingsWithEvents);
} catch (err: any) {
console.error('Error in fetchBookings:', err);
......@@ -265,7 +275,6 @@ function BookingManagement() {
// Remove the booking from state
setBookings(bookings.filter(booking => booking.id !== bookingId));
setShowCancelConfirm(null);
} catch (err: any) {
console.error('Error in handleCancelBooking:', err);
setError(err.message);
......@@ -277,6 +286,11 @@ function BookingManagement() {
return new Date(end) < new Date();
};
const browseEvents = () => {
const query = "";
navigate(`/browse/${query}`);
}
// Load bookings when component mounts
useEffect(() => {
fetchBookings();
......@@ -329,117 +343,24 @@ function BookingManagement() {
{!loading && bookings.length === 0 && !bookingInProgress && !statusMessage && (
<div className={styles.emptyState || "emptyState"}>
<p>You don't have any event bookings yet.</p>
<Link to="/events" className={styles.browseLink || "browseLink"}>Browse Available Events</Link>
<span onClick={browseEvents} className={styles.browseLink}>Browse Available Events</span>
</div>
)}
{/* Bookings list */}
{bookings.length > 0 && (
<ul className={styles.bookingsList || "bookingsList"}>
<div className={styles.bookingsList || "bookingsList"}>
{bookings.map((booking: Booking) => (
<li key={booking.id} className={styles.bookingItem || "bookingItem"}>
{booking.event ? (
<>
<div className={styles.bookingContent || "bookingContent"}>
<div className={styles.eventHeader || "eventHeader"}>
<Link to={`/events/${booking.event_id}`} className={styles.eventLink || "eventLink"}>
<strong>{booking.event.name}</strong>
</Link>
<span className={
isEventPassed(booking.event.end)
? (styles.pastLabel || "pastLabel")
: (styles.upcomingLabel || "upcomingLabel")
}>
{isEventPassed(booking.event.end) ? 'Past' : 'Upcoming'}
</span>
</div>
<div className={styles.venueTime || "venueTime"}>
<div>
<i className={styles.venueIcon || "venueIcon"}></i> {booking.event.venue}
</div>
<div>
<i className={styles.timeIcon || "timeIcon"}></i>
{new Date(booking.event.start).toLocaleString()} - {new Date(booking.event.end).toLocaleString()}
</div>
</div>
<div className={styles.bookingMeta || "bookingMeta"}>
<small>Booking ID: {booking.id}</small>
<small>Booked on: {new Date(booking.created_at).toLocaleDateString()}</small>
</div>
</div>
<div className={styles.bookingActions || "bookingActions"}>
{!isEventPassed(booking.event.end) && (
showCancelConfirm === booking.id ? (
<div className={styles.confirmCancel || "confirmCancel"}>
<p>Are you sure?</p>
<div>
<button
className={styles.confirmButton || "confirmButton"}
onClick={() => handleCancelBooking(booking.id)}
>
Yes, Cancel
</button>
<button
className={styles.cancelButton || "cancelButton"}
onClick={() => setShowCancelConfirm(null)}
>
No, Keep
</button>
</div>
</div>
) : (
<button
className={styles.cancelBookingButton || "cancelBookingButton"}
onClick={() => setShowCancelConfirm(booking.id)}
>
Cancel Booking
</button>
)
)}
</div>
</>
) : (
<div className={styles.errorCard || "errorCard"}>
<p>Unable to load event details for booking {booking.id}</p>
<button
className={styles.cancelBookingButton || "cancelBookingButton"}
onClick={() => setShowCancelConfirm(booking.id)}
>
Cancel Booking
</button>
{showCancelConfirm === booking.id && (
<div className={styles.confirmCancel || "confirmCancel"}>
<p>Are you sure?</p>
<div>
<button
className={styles.confirmButton || "confirmButton"}
onClick={() => handleCancelBooking(booking.id)}
>
Yes, Cancel
</button>
<button
className={styles.cancelButton || "cancelButton"}
onClick={() => setShowCancelConfirm(null)}
>
No, Keep
</button>
</div>
</div>
)}
</div>
)}
</li>
booking.event &&
<DisplayCard event={booking.event} booking={booking} handleDelete={handleCancelBooking}></DisplayCard>
))}
</ul>
</div>
)}
{/* Debugging information - remove in production */}
<div style={{marginTop: '30px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '5px'}}>
<div style={{marginTop: '30px', padding: '10px', backgroundColor: '#2f3544', borderRadius: '5px'}}>
<h3>Component State</h3>
<pre style={{backgroundColor: '#e0e0e0', padding: '10px', overflow: 'auto'}}>
<pre style={{backgroundColor: '#1b1f2a', padding: '10px', overflow: 'auto'}}>
{JSON.stringify({
navigationState: {
action,
......
.pageContent {
width: 80vw;
margin: auto;
padding-top: 10vh;
}
\ No newline at end of file
import { useEffect, useState } from 'react';
import styles from './BrowsingPage.module.scss';
import { useNavigate } from 'react-router-dom';
import { getAuthHeaders, handleAuthError } from '../../../utils/auth';
import DisplayCard from '../../components/DisplayCard/DisplayCard';
import { Event } from '../../pages/EventManagement/EventManagement';
// Define the Event type for better type safety
interface Event {
id: string;
name: string;
start: Date;
end: Date;
venue: string;
capacity: number;
}
function BrowsingPage() {
// Replace context with local state for events
......@@ -35,12 +29,26 @@ function BrowsingPage() {
const data = await response.json();
// Transform dates from strings to Date objects
const processedEvents = data.map((event: any) => ({
const eventsWithDates = data.map((event: any) => ({
...event,
start: new Date(event.start),
end: new Date(event.end)
}));
const processedEvents = await Promise.all(eventsWithDates.map(async (event: Event) => {
try {
const bookingsRes = await fetch(`http://localhost:8000/events/bookings?event_id=${event.id}`, { method: "GET", headers: getAuthHeaders() });
const bookingsData = await bookingsRes.json();
return {
...event,
attendeeCount: bookingsData.length ? bookingsData.length : 0,
};
} catch (err) {
console.error(`Failed to fetch bookings for event ${event.id}`, err);
return { ...event, attendeeCount: 0 };
}
}));
setEvents(processedEvents);
setError(null);
} catch (err: any) {
......@@ -59,29 +67,17 @@ function BrowsingPage() {
};
return (
<div>
<div className={styles.pageContent}>
{isLoading && <p>Loading events...</p>}
{error && <p className={styles.errorMessage}>{error}</p>}
<div className={styles.pageContent}>
{isLoading && <p>Loading events...</p>}
{error && <p className={styles.errorMessage}>{error}</p>}
{!isLoading && !error && events.length === 0 && (
<p>No events found.</p>
)}
{!isLoading && !error && events.length === 0 && (
<p>No events found.</p>
)}
{events.map((event) => (
<div
onClick={() => eventOnClick(event)}
key={event.id}
className={styles.eventCard}
>
<p className={styles.eventName}>{event.name}</p>
<p>Start: {event.start.toLocaleString()}</p>
<p>End: {event.end.toLocaleString()}</p>
<p>Venue: {event.venue}</p>
<p>Capacity: {event.capacity}</p>
</div>
))}
</div>
{events.map((event) => (
<DisplayCard event={event} handleOnClick={eventOnClick}></DisplayCard>
))}
</div>
);
}
......
.container {
max-width: 800px;
margin: auto;
padding: 20px;
z-index: 0;
}
.eventList {
list-style: none;
margin-top: 20px;
padding: 0;
}
.eventItem {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background: #f8f8f8;
display: flex;
justify-content: space-between;
align-items: center;
}
.actions button {
margin-left: 0.5rem;
}
.formSection {
margin: 20px 0;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment