diff --git a/public/sponsors/EICHER__1_-removebg-preview.png b/public/sponsors/EICHER__1_-removebg-preview.png
new file mode 100644
index 0000000..083a0ee
Binary files /dev/null and b/public/sponsors/EICHER__1_-removebg-preview.png differ
diff --git a/public/sponsors/Encode_AI-removebg-preview.png b/public/sponsors/Encode_AI-removebg-preview.png
new file mode 100644
index 0000000..07693c7
Binary files /dev/null and b/public/sponsors/Encode_AI-removebg-preview.png differ
diff --git a/public/sponsors/SANCHI__1__page-0001-removebg-preview.png b/public/sponsors/SANCHI__1__page-0001-removebg-preview.png
new file mode 100644
index 0000000..bf8db8b
Binary files /dev/null and b/public/sponsors/SANCHI__1__page-0001-removebg-preview.png differ
diff --git a/public/sponsors/Universal_Logo-removebg-preview.png b/public/sponsors/Universal_Logo-removebg-preview.png
new file mode 100644
index 0000000..17cad98
Binary files /dev/null and b/public/sponsors/Universal_Logo-removebg-preview.png differ
diff --git a/public/sponsors/WhatsApp_Image_2026-04-03_at_13.02.15-removebg-preview.png b/public/sponsors/WhatsApp_Image_2026-04-03_at_13.02.15-removebg-preview.png
new file mode 100644
index 0000000..be5fb54
Binary files /dev/null and b/public/sponsors/WhatsApp_Image_2026-04-03_at_13.02.15-removebg-preview.png differ
diff --git a/public/sponsors/image.png b/public/sponsors/image.png
new file mode 100644
index 0000000..af11438
Binary files /dev/null and b/public/sponsors/image.png differ
diff --git a/public/sponsors/sm_logo-removebg-preview.png b/public/sponsors/sm_logo-removebg-preview.png
new file mode 100644
index 0000000..ab491b8
Binary files /dev/null and b/public/sponsors/sm_logo-removebg-preview.png differ
diff --git a/schema.sql b/schema.sql
index 22df9b7..576da60 100644
--- a/schema.sql
+++ b/schema.sql
@@ -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,
@@ -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 (
diff --git a/src/pages/index.js b/src/pages/index.js
index cb1244d..8cd6f88 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -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'
@@ -52,6 +53,7 @@ const Home = () => {
{/**/}
+
diff --git a/src/services/database/migrations/009_add_registration_closed.sql b/src/services/database/migrations/009_add_registration_closed.sql
new file mode 100644
index 0000000..024a475
--- /dev/null
+++ b/src/services/database/migrations/009_add_registration_closed.sql
@@ -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);
diff --git a/src/services/database/schema.sql b/src/services/database/schema.sql
index e66e36b..812c1fb 100644
--- a/src/services/database/schema.sql
+++ b/src/services/database/schema.sql
@@ -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,
@@ -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 (
diff --git a/src/services/event-service.js b/src/services/event-service.js
index b14abb2..b0d55e7 100644
--- a/src/services/event-service.js
+++ b/src/services/event-service.js
@@ -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",
@@ -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",
@@ -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,
@@ -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
diff --git a/src/store/slices/cartSlice.js b/src/store/slices/cartSlice.js
index 1df4d9e..2a10d65 100644
--- a/src/store/slices/cartSlice.js
+++ b/src/store/slices/cartSlice.js
@@ -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 */
}
@@ -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
},
@@ -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,
@@ -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,
+ registrationClosed: false,
+ quantity: item.quantity
}
})
.filter(Boolean)
diff --git a/src/views/cart/CartView.js b/src/views/cart/CartView.js
index ddec236..f94ec24 100644
--- a/src/views/cart/CartView.js
+++ b/src/views/cart/CartView.js
@@ -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 (
{/* Image */}
@@ -169,18 +171,40 @@ function CartItem({ item, accent }) {
{/* Details */}
-
- {item.title}
-
+
+
+ {item.title}
+
+ {isUnavailable && (
+
+ Registration Closed
+
+ )}
+
{/* Remove button */}
{formatCurrency(item.ticketPrice)} each
- dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity - 1 }))}
- onIncrease={() => dispatch(updateQuantity({ eventId: item.eventId, quantity: item.quantity + 1 }))}
- accent={accent}
- />
+ {!isUnavailable && (
+ 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 */}
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).
@@ -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' }
)
}
@@ -514,11 +541,26 @@ export default function CartView() {
{/* Checkout CTA */}
+ {hasUnavailableItems && (
+
+ Remove unavailable events from your cart to proceed.
+
+ )}
}
onClick={async () => {
// Re-validate prices and availability immediately before checkout
diff --git a/src/views/events/EventDetailView.js b/src/views/events/EventDetailView.js
index 05d9278..d00622f 100644
--- a/src/views/events/EventDetailView.js
+++ b/src/views/events/EventDetailView.js
@@ -32,7 +32,8 @@ function formatDate(iso) {
weekday: 'long',
day: 'numeric',
month: 'long',
- year: 'numeric'
+ year: 'numeric',
+ timeZone: 'Asia/Kolkata'
})
}
@@ -41,7 +42,8 @@ function formatTime(iso) {
return new Date(iso).toLocaleTimeString('en-IN', {
hour: '2-digit',
minute: '2-digit',
- hour12: true
+ hour12: true,
+ timeZone: 'Asia/Kolkata'
})
}
@@ -233,6 +235,7 @@ export default function EventDetailView() {
const currentImage = allImages[activeImageIndex] || null
const hasGallery = allImages.length > 1
const isOver = event.start_time ? new Date(event.start_time).getTime() <= Date.now() : false
+ const isRegClosed = !!event.registration_closed
const details = event.details || {}
const hasPrizes = details.prize && typeof details.prize === 'object' && Object.keys(details.prize).length > 0
@@ -497,7 +500,25 @@ export default function EventDetailView() {
{/* ── Mobile inline action buttons ── */}
{isMobile && (
- {event.registration_link ? (
+ {isRegClosed ? (
+
+ ) : event.registration_link ? (