Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/sponsors/EICHER__1_-removebg-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/sponsors/Encode_AI-removebg-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/sponsors/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/sponsors/sm_logo-removebg-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ CREATE TABLE events (
created_by BIGINT,
status event_status NOT NULL DEFAULT 'draft',
visibility event_visibility NOT NULL DEFAULT 'public',
registration_closed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL,
Expand All @@ -89,6 +90,7 @@ CREATE INDEX idx_events_start_time ON events(start_time);
CREATE INDEX idx_events_status ON events(status);
CREATE INDEX idx_events_created_by ON events(created_by);
CREATE INDEX idx_events_active_published ON events(status, start_time) WHERE status IN ('published', 'active');
CREATE INDEX idx_events_registration_closed ON events(registration_closed);

-- Bookings table
CREATE TABLE bookings (
Expand Down
2 changes: 2 additions & 0 deletions src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import HeroSection from 'src/views/home/HeroSection'
import SponsorsSection from 'src/views/home/SponsorsSection'

import FeaturedEvents from 'src/views/home/FeaturedEvents'
import DJHighlightSection from 'src/views/home/DJHighlightSection'
import PublicFooter from 'src/views/home/PublicFooter'
import { fetchHomeData } from 'src/store/slices/eventsSlice'

Expand Down Expand Up @@ -52,6 +53,7 @@ const Home = () => {
{/*<AboutSection />*/}

<FeaturedEvents categoryEvents={categoryEvents} loading={homeLoading && !homeData} />
<DJHighlightSection />
<SponsorsSection/>
<PublicFooter />
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Add registration_closed boolean to events table
-- When TRUE, the event's registration is closed (no new bookings allowed)

ALTER TABLE events
ADD COLUMN IF NOT EXISTS registration_closed BOOLEAN NOT NULL DEFAULT FALSE;

-- Index for quick filtering
CREATE INDEX IF NOT EXISTS idx_events_registration_closed ON events(registration_closed);
Comment on lines +4 to +8

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration adds events.registration_closed, but the repo keeps full schema snapshots in both schema.sql (root) and src/services/database/schema.sql. Those schema files currently don’t include registration_closed, so fresh DB bootstraps from schema.sql will diverge from migrated DBs. Please update both schema files to include the new column.

Copilot uses AI. Check for mistakes.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions src/services/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ CREATE TABLE events (
created_by BIGINT,
status event_status NOT NULL DEFAULT 'draft',
visibility event_visibility NOT NULL DEFAULT 'public',
registration_closed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
manager_id INTEGER,
Expand All @@ -108,6 +109,7 @@ CREATE INDEX idx_events_created_by ON events(created_by);
CREATE INDEX idx_events_active_published ON events(status, start_time) WHERE status IN ('published', 'active');
CREATE INDEX idx_events_featured ON events(featured) WHERE featured = TRUE;
CREATE INDEX idx_events_registered ON events(registered DESC);
CREATE INDEX idx_events_registration_closed ON events(registration_closed);

-- ── Bookings ───────────────────────────────────────────────────────────────────
CREATE TABLE bookings (
Expand Down
4 changes: 4 additions & 0 deletions src/services/event-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const eventService = {
e.images,
e.ticket_price,
e.registration_link,
e.registration_closed,
e.date,
d.id AS "departmentId",
d.name AS "departmentName",
Expand Down Expand Up @@ -256,6 +257,7 @@ const eventService = {
e.images,
e.ticket_price,
e.registration_link,
e.registration_closed,
e.created_at,
d.id AS "departmentId",
d.name AS "departmentName",
Expand Down Expand Up @@ -317,6 +319,7 @@ const eventService = {
e.images,
e.ticket_price,
e.registration_link,
e.registration_closed,
d.id AS "departmentId",
d.name AS "departmentName",
e.date,
Expand Down Expand Up @@ -358,6 +361,7 @@ const eventService = {
e.venue,
e.images,
e.status,
e.registration_closed,
d.name AS "departmentName"
FROM events e
LEFT JOIN departments d ON d.id = e.department_id
Expand Down
33 changes: 20 additions & 13 deletions src/store/slices/cartSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ function loadCart() {
function persistCart(items) {
if (typeof window === 'undefined') return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
// Strip runtime-only flags before persisting — they are always refreshed from DB on validateCart
const stripped = items.map(({ registrationClosed, ...rest }) => rest)
localStorage.setItem(STORAGE_KEY, JSON.stringify(stripped))
} catch {
/* quota exceeded — silently fail */
}
Expand Down Expand Up @@ -80,10 +82,10 @@ const cartSlice = createSlice({
* Sanitizes loaded data to discard any corrupted items. */
hydrateCart(state) {
const raw = loadCart()
// Sanitize: discard items with missing eventId or quantity < 1
state.items = raw.filter(
item => item && item.eventId && typeof item.quantity === 'number' && item.quantity >= 1
)
// Sanitize: discard items with missing eventId or quantity < 1; strip stale runtime flags
state.items = raw
.filter(item => item && item.eventId && typeof item.quantity === 'number' && item.quantity >= 1)
.map(({ registrationClosed, ...rest }) => rest)
state.hydrated = true
},

Expand Down Expand Up @@ -161,16 +163,21 @@ const cartSlice = createSlice({
const dbEvents = action.payload
const prevCount = state.items.length

// Update cart items with fresh DB prices and remove stale items
// Update cart items with fresh DB prices and mark unavailable items
state.items = state.items
.map(item => {
const dbEvent = dbEvents.find(e => e.id === item.eventId)
if (!dbEvent) return null // event no longer exists/published
if (!dbEvent) {
// Event no longer exists/published — mark as unavailable (maxAvailable = 0) instead of removing
return { ...item, maxAvailable: 0, registrationClosed: false }
}

const dbAvailable = Math.max(0, (dbEvent.seats || 0) - (dbEvent.registered || 0))
// Registration closed — mark it but keep in cart so user sees reason
if (dbEvent.registration_closed) {
return { ...item, title: dbEvent.title, registrationClosed: true, maxAvailable: null }
}

// Sold out — remove from cart entirely
if (dbAvailable <= 0) return null
const dbAvailable = Math.max(0, (dbEvent.seats || 0) - (dbEvent.registered || 0))

return {
...item,
Expand All @@ -179,9 +186,9 @@ const cartSlice = createSlice({
startTime: dbEvent.start_time,
venue: dbEvent.venue || item.venue,
image: dbEvent.images?.[0] || item.image,
maxAvailable: dbAvailable,
// Cap stored quantity to actual availability — no fallback to 1
quantity: Math.min(item.quantity, dbAvailable)
maxAvailable: dbAvailable || null,

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxAvailable: dbAvailable || null turns a real availability of 0 into null (treated as “unknown/unlimited” elsewhere, e.g. updateQuantity uses Infinity when maxAvailable is null). Keeping maxAvailable as a number (including 0) preserves correct quantity caps and UI disablement logic.

Suggested change
maxAvailable: dbAvailable || null,
maxAvailable: dbAvailable,

Copilot uses AI. Check for mistakes.
registrationClosed: false,
quantity: item.quantity
}
})
.filter(Boolean)
Expand Down
86 changes: 64 additions & 22 deletions src/views/cart/CartView.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function CartItem({ item, accent }) {
const dispatch = useDispatch()
const imageUrl = getItemImage(item)
const subtotal = item.ticketPrice * item.quantity
const isUnavailable = !!item.registrationClosed

return (
<Box
Expand All @@ -136,7 +137,8 @@ function CartItem({ item, accent }) {
gap: { xs: 2, md: 3 },
py: { xs: 2.5, md: 3 },
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'stretch', sm: 'flex-start' }
alignItems: { xs: 'stretch', sm: 'flex-start' },
opacity: isUnavailable ? 0.7 : 1
}}
>
{/* Image */}
Expand Down Expand Up @@ -169,18 +171,40 @@ function CartItem({ item, accent }) {
{/* Details */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 1 }}>
<Typography
variant='h6'
sx={{
fontWeight: 700,
fontSize: { xs: '1rem', md: '1.05rem' },
lineHeight: 1.3,
color: c.textPrimary,
mb: 0.5
}}
>
{item.title}
</Typography>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant='h6'
sx={{
fontWeight: 700,
fontSize: { xs: '1rem', md: '1.05rem' },
lineHeight: 1.3,
color: c.textPrimary,
mb: 0.5
}}
>
{item.title}
</Typography>
{isUnavailable && (
<Typography
variant='caption'
sx={{
display: 'inline-block',
bgcolor: alpha(c.error || '#f44336', 0.1),
color: c.error || '#f44336',
fontWeight: 700,
px: 1,
py: 0.2,
borderRadius: '4px',
fontSize: '0.72rem',
letterSpacing: 0.5,
textTransform: 'uppercase',
mb: 0.5
}}
>
Registration Closed
</Typography>
)}
</Box>

{/* Remove button */}
<IconButton
Expand Down Expand Up @@ -232,13 +256,15 @@ function CartItem({ item, accent }) {
<Typography variant='body2' sx={{ color: c.textSecondary, fontWeight: 500, fontSize: '0.85rem' }}>
{formatCurrency(item.ticketPrice)} each
</Typography>
<QuantityControl
quantity={item.quantity}
max={item.maxAvailable}
onDecrease={() => dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity - 1 }))}
onIncrease={() => dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity + 1 }))}
accent={accent}
/>
{!isUnavailable && (
<QuantityControl
quantity={item.quantity}
max={item.maxAvailable}
onDecrease={() => dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity - 1 }))}
onIncrease={() => dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity + 1 }))}
accent={accent}
/>
)}
{/* availability label removed per UX request */}
</Box>
<Typography
Expand Down Expand Up @@ -316,6 +342,7 @@ export default function CartView() {
const itemCount = useSelector(selectCartItemCount)
const subtotal = useSelector(selectCartSubtotal)
const { validating, hydrated, validationRemovedCount } = useSelector(state => state.cart)
const hasUnavailableItems = items.some(item => item.registrationClosed)

// Validate on every cart page visit.
// - Initial load: fires when hydrated flips false→true (after CartHydrator dispatches hydrateCart).
Expand All @@ -332,7 +359,7 @@ export default function CartView() {
useEffect(() => {
if (validationRemovedCount > 0) {
toast.error(
`${validationRemovedCount} sold-out event${validationRemovedCount > 1 ? 's were' : ' was'} removed from your cart.`,
`${validationRemovedCount} event${validationRemovedCount > 1 ? 's were' : ' was'} removed from your cart (no longer available).`,
{ duration: 5000, id: 'cart-removal' }
)
}
Expand Down Expand Up @@ -514,11 +541,26 @@ export default function CartView() {
</Box>

{/* Checkout CTA */}
{hasUnavailableItems && (
<Typography
variant='caption'
sx={{
display: 'block',
mt: 2,
color: c.error || '#f44336',
fontWeight: 600,
textAlign: 'center',
fontSize: '0.8rem'
}}
>
Remove unavailable events from your cart to proceed.
</Typography>
)}
<Button
variant='contained'
size='large'
fullWidth
disabled={validating}
disabled={validating || hasUnavailableItems}
endIcon={<Icon icon='tabler:arrow-right' />}
Comment on lines 559 to 564

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checkout is disabled only for hasClosedItems, but with the current validateCart behavior sold-out/unavailable items can remain in the cart and still allow “Proceed to Checkout”, leading to a failure later in checkout validation. Consider also preventing checkout when any cart item is unavailable (e.g. maxAvailable === 0 / soldOut flag) and surfacing a clear message.

Copilot uses AI. Check for mistakes.
onClick={async () => {
// Re-validate prices and availability immediately before checkout
Expand Down
Loading
Loading