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. + + )} + ) : event.registration_link ? ( + ) : ( + <> + + )} + ) : event.registration_link ? ( // Registration Link Button (Desktop) + ) : event.registration_link ? ( + ) : event.registration_link ? ( + + )} + + + + + ) +} diff --git a/src/views/home/SponsorsSection.js b/src/views/home/SponsorsSection.js index 5c15d07..1971432 100644 --- a/src/views/home/SponsorsSection.js +++ b/src/views/home/SponsorsSection.js @@ -15,6 +15,18 @@ const SPONSOR_CATEGORIES = [ { name: 'HDFC Bank', src: '/sponsors/hdfc.png' } ] }, + { + category: 'Powered By', + logos: [ + { name: 'Eicher', src: '/sponsors/EICHER__1_-removebg-preview.png' } + ] + }, + { + category: 'Nutrition Partner', + logos: [ + { name: 'Sanchi', src: '/sponsors/SANCHI__1__page-0001-removebg-preview.png' } + ] + }, { category: 'Community Partners', logos: [ @@ -27,6 +39,15 @@ const SPONSOR_CATEGORIES = [ logos: [ { name: 'Indore Talk', src: '/sponsors/indoretalkWhite.png' } ] + }, + { + category: 'Our Proud Sponsors', + logos: [ + { name: 'Pisarv', src: '/sponsors/WhatsApp_Image_2026-04-03_at_13.02.15-removebg-preview.png' }, + { name: 'Universal Informatics', src: '/sponsors/Universal_Logo-removebg-preview.png' }, + { name: 'Study Metro', src: '/sponsors/sm_logo-removebg-preview.png' }, + { name: 'Encode AI', src: '/sponsors/image.png' } + ] } ] @@ -75,8 +96,9 @@ export default function SponsorsSection() { // Cycle through categories: Community Partners stays for 2.4s (shows both), others for 2.4s each useEffect(() => { const currentCategory = SPONSOR_CATEGORIES[currentCategoryIndex] - // Community Partners (index 1) shows both logos at once, so only 1 "iteration" - const itemCount = currentCategory.category === 'Community Partners' ? 1 : currentCategory.logos.length + // Community Partners and Our Proud Sponsors show all logos at once + const showAll = currentCategory.category === 'Community Partners' || currentCategory.category === 'Our Proud Sponsors' + const itemCount = showAll ? 1 : currentCategory.logos.length const totalTime = itemCount * 2400 const categoryTimer = setInterval(() => { @@ -90,8 +112,9 @@ export default function SponsorsSection() { // Cycle through logos within current category (skip for Community Partners) useEffect(() => { const currentCategory = SPONSOR_CATEGORIES[currentCategoryIndex] - // Don't cycle through Community Partners - show both at once - if (currentCategory.category === 'Community Partners' || currentCategory.logos.length <= 1) return + // Don't cycle through categories that show all logos at once + const showAll = currentCategory.category === 'Community Partners' || currentCategory.category === 'Our Proud Sponsors' + if (showAll || currentCategory.logos.length <= 1) return const logoTimer = setInterval(() => { setCurrentLogoIndex(prev => (prev + 1) % currentCategory.logos.length) @@ -200,8 +223,8 @@ export default function SponsorsSection() { {/* Animated Logo Display */} - {currentCategory.category === 'Community Partners' ? ( - // Show both Community Partner logos side by side + {(currentCategory.category === 'Community Partners' || currentCategory.category === 'Our Proud Sponsors') ? ( + // Show all logos side by side