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
16 changes: 1 addition & 15 deletions src/app/api/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ import { getSupabaseAdmin } from '@/lib/supabase';
import { getAuthenticatedUser } from '@/lib/auth-server';
import { paymentProofExists } from '@/lib/payment-proofs';
import { registrationsClosedResponse, registrationsOpen } from '@/lib/registration-gate';
import {
isDuplicatePhoneError,
isIterSoaCollege,
isIterSoaEmail,
ITER_SOA_ERROR_MESSAGE,
PHONE_IN_USE_ERROR_MESSAGE,
} from '@/lib/validation';
import { isDuplicatePhoneError, PHONE_IN_USE_ERROR_MESSAGE } from '@/lib/validation';
import {
classifyStudentType,
createRateLimiter,
Expand Down Expand Up @@ -153,14 +147,6 @@ export async function POST(req: NextRequest) {
const input = parsed.data;
const college = studentType === 'internal' ? NIT_ROURKELA_COLLEGE : input.college;

// Exclusion rule: Students from ITER - SOA are not allowed to register
if (isIterSoaCollege(college) || isIterSoaEmail(verifiedEmail)) {
return NextResponse.json(
{ error: ITER_SOA_ERROR_MESSAGE, field: 'college' },
{ status: 403 }
);
}

const adminClient = getSupabaseAdmin(token);

// 3. Prevent duplicate registrations for the same authenticated user
Expand Down
75 changes: 61 additions & 14 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,18 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
@media (max-width:719px){.wh-card{gap:12px !important;padding:10px 12px 10px 14px !important}.wh-glyph{display:none !important}.wh-main{font-size:14px !important}.wh-sub{font-size:12px !important}}

/* ---------- sponsors ---------- */
/* Tickers: each strip holds its logos twice and slides by one copy, forever; it pauses while hovered, pressed
or holding keyboard focus, and the edges fade so logos drift in and out of view. */
@keyframes iv-sp{to{transform:translateX(-50%)}}
.sp-row{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);padding:4px 0}
.sp-strip{display:flex;width:max-content;animation:iv-sp var(--sp-dur,30s) linear infinite}
.sp-strip-rev{animation-direction:reverse}
.sp-row:hover .sp-strip,.sp-row:active .sp-strip,.sp-row:focus-within .sp-strip{animation-play-state:paused}
.sp-card{transition:border-color .3s,background-color .3s}
.sp-card:hover{border-color:rgba(20,19,18,.5) !important;background:rgba(20,19,18,.06) !important}
.sp-logo{width:100%;height:100%;object-fit:contain;filter:grayscale(1);opacity:.78;transition:filter .4s,opacity .4s}
.sp-card:hover .sp-logo,.sp-card:focus-visible .sp-logo{filter:none;opacity:1}
/* Without motion the rows stand still and scroll by hand instead, so every logo stays reachable. */
@media (prefers-reduced-motion:reduce){.sp-row{overflow-x:auto;scrollbar-width:none;-webkit-mask-image:none;mask-image:none}.sp-row::-webkit-scrollbar{display:none}.sp-strip{animation:none}}
@media (max-width:719px){.sp-title{grid-template-columns:minmax(0,1fr) !important}.sp-title-art{display:none}}
/* Past sponsors: a wall of logo cards, five across on wide screens and two on phones. A card lifts while hovered or
holding keyboard focus. */
.ps-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
.ps-card{position:relative;aspect-ratio:3 / 2;border:1px solid rgba(20,19,18,.14);transition:border-color .3s,transform .4s cubic-bezier(.25,1,.1,1),box-shadow .4s}
.ps-card:hover,.ps-card:focus-within{border-color:rgba(20,19,18,.5);transform:translateY(-3px);box-shadow:0 14px 26px -18px rgba(20,19,18,.5)}
.ps-in{position:absolute;inset:0;display:grid;place-items:center}
.ps-in:focus-visible{outline:2px solid #141312;outline-offset:3px}
.ps-logo{height:auto;max-height:64%;object-fit:contain}
@media (max-width:959px){.ps-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media (max-width:719px){.ps-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}}
@media (max-width:479px){.ps-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (prefers-reduced-motion:reduce){.ps-card:hover,.ps-card:focus-within{transform:none}}

/* ---------- home hero ---------- */
/* --d is the disc's diameter and --cy its centre. Rings, disc, orbiters, copy and astronaut all hang off the two,
Expand Down Expand Up @@ -316,6 +314,8 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
.to-planet{left:-8vw;top:calc(43% - var(--s) * 1.14)}
.to-rocket{right:5vw;top:calc(43% - var(--s) * .6 - min(9.5vh,85px));height:min(19vh,170px)}
.to-tagline{top:74%}
/* The closing fade into the mission list stays under the tagline, so the ink text never lands on the dark. */
.to-fade{height:17% !important}
}

/* ---------- worlds slider decoration (WorldsView.tsx) ---------- */
Expand Down Expand Up @@ -391,6 +391,53 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
/* Every other spark and gallery dust mote, and two of the five curtain cloud layers, sit out. */
[data-lowpower] [data-lp-skip]{display:none}

/* ---------- home gallery tunnel (HomeView.tsx) ---------- */
.t-scrim{display:none}
/* Phones: photos are capped by height too (--ar is the photo's width/height; --tx/--ty its offset from the centre), so a tall one can't outgrow the room
between the heading and the counter; the tunnel's centre and vanishing point drop into that room; and dark fades sit
behind the heading and the counter, so a photo passing beneath never covers the text. */
@media (max-width:719px){
.t-space{perspective-origin:50% 56% !important}
.t-frame{width:min(54vw,calc(28vh * var(--ar))) !important;top:calc(56% + var(--ty) * .5) !important}
.t-scrim{display:block;position:absolute;left:0;right:0;pointer-events:none}
.t-scrim-top{top:0;height:calc(4vh + 290px);background:linear-gradient(180deg,#141312 0,rgba(20,19,18,.94) 62%,rgba(20,19,18,0))}
.t-scrim-bot{bottom:0;height:200px;background:linear-gradient(0deg,#141312 0,rgba(20,19,18,.9) 45%,rgba(20,19,18,0))}
}
/* Landscape phones: too short to stack the photos between the heading and the counter, so they fly through the open
right-hand side instead, sized off the height. */
@media (max-height:499px) and (orientation:landscape){
.t-space{perspective-origin:76% 42% !important}
.t-frame{width:calc(38vh * var(--ar)) !important;left:calc(76% + var(--tx) * .3) !important;top:calc(42% + var(--ty) * .3) !important}
}

/* ---------- gallery (GalleryView.tsx) ---------- */
/* Phones: a frame sized off the width alone came out about 90-125px wide in portrait, and ran off the bottom of a
landscape screen. These fill most of the screen once the photo in focus is scaled up by the perspective (1.25x),
with Innovision#galleryTick keeping the spiral tight. Landscape phones size off the height instead. */
@media (max-width:719px){
.g-fr-l{width:min(64vw,340px) !important;height:min(42.2vw,224px) !important}
.g-fr-p{width:min(48vw,240px) !important;height:min(64vw,320px) !important}
}
@media (max-height:499px) and (orientation:landscape){
.g-fr-l{width:70vh !important;height:46vh !important}
.g-fr-p{width:42vh !important;height:56vh !important}
}

/* ---------- touch devices ---------- */
/* Phones and tablets: drop the effects that cost the most per frame while a page scrolls. Scene art's drop shadows go
(each is an extra blur pass over a big layer, every frame it moves): inline filters write theirs as
var(--no-ds, drop-shadow(...)), and opacity(1) is a no-op in its place. Low-power mode drops them everywhere. The film
grain, a full-screen layer blended over the whole page, goes too. The HUD bar and the world switcher, always on screen,
trade the frosted glass (a blur of whatever scrolls beneath, redone every frame) for a near-solid fill. */
[data-lowpower]{--no-ds:opacity(1)}
@media (pointer:coarse){
:root{--no-ds:opacity(1)}
body::after{display:none}
.hud-frost,.wn-pill{-webkit-backdrop-filter:none !important;backdrop-filter:none !important}
.hud-frost{background:rgba(236,232,223,.97) !important}
.wn-pill{background:rgba(236,232,223,.96) !important}
}

/* ---------- schedule ---------- */
/* --gut lines the rows up with the 1240px column while letting each carousel run to the screen edge. */
.sc-hero,.sc-body{--gut:max(clamp(16px,4vw,64px),(100% - 1240px) / 2)}
Expand Down
8 changes: 4 additions & 4 deletions src/components/coming-soon/ComingSoon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import styles from './ComingSoon.module.css';

type ComingSoonProps = {
title: ReactNode;
/** Second call to action, next to BACK TO INNOVISION. */
secondary: { label: string; href: string };
/** Optional second call to action, next to BACK TO INNOVISION. */
secondary?: { label: string; href: string };
};

/**
* Shared full-page teaser for flows that are built but not yet live (see src/config/launch.ts): a centred
* headline and two buttons. Used by RegistrationsOpeningSoon (/register) and MerchDroppingSoon (/merch).
* headline, a BACK TO INNOVISION button and an optional second one. Used by RegistrationsOpeningSoon (/register) and MerchDroppingSoon (/merch).
*/
export default function ComingSoon({ title, secondary }: ComingSoonProps) {
return (
Expand All @@ -20,7 +20,7 @@ export default function ComingSoon({ title, secondary }: ComingSoonProps) {
<h1 className={styles.title}>{title}</h1>
<div className={styles.actions}>
<Link href="/" className={`${styles.cta} ${styles.ctaPrimary}`}>BACK TO INNOVISION</Link>
<Link href={secondary.href} className={`${styles.cta} ${styles.ctaGhost}`}>{secondary.label}</Link>
{secondary && <Link href={secondary.href} className={`${styles.cta} ${styles.ctaGhost}`}>{secondary.label}</Link>}
</div>
</main>
);
Expand Down
5 changes: 1 addition & 4 deletions src/components/coming-soon/MerchDroppingSoon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import ComingSoon from './ComingSoon';
/** /merch while MERCH_STORE_OPEN is false (src/config/launch.ts). */
export default function MerchDroppingSoon() {
return (
<ComingSoon
title={<>Exclusive merch<br /><em>dropping soon.</em></>}
secondary={{ label: 'VIEW THE SCHEDULE', href: '/#/schedule' }}
/>
<ComingSoon title={<>Exclusive merch<br /><em>dropping soon.</em></>} />
);
}
70 changes: 15 additions & 55 deletions src/components/innovision/AuthOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
import React, { useState } from 'react';
import type { V } from './types';
import { Logo } from './icons';
import { isIterSoaCollege, isIterSoaEmail } from '@/lib/validation';

export default function AuthOverlay({ v }: { v: V }) {
const [userCollege, setUserCollege] = useState<string | null>(null);
const collegeValue = userCollege !== null ? userCollege : (v.regVals?.college || '');
const isBlockedCollege = !v.isInternal && (isIterSoaCollege(collegeValue) || isIterSoaEmail(v.regVals?.email));
return (
<div data-auth-root="" aria-hidden={v.authHidden} style={{ position: "fixed", inset: "0", zIndex: "62", visibility: "hidden", pointerEvents: "none" }}>
<div data-rift-veil="" style={{ position: "absolute", inset: "0", background: "#070605", opacity: "0" }}></div>
Expand Down Expand Up @@ -105,14 +103,7 @@ export default function AuthOverlay({ v }: { v: V }) {
{v.err.gender ? (<span role="alert" style={{ fontSize: "14px", lineHeight: "1.4", color: "oklch(0.76 0.14 35)" }}>{v.err.gender}</span>) : null}
</fieldset>
<label data-s-in="" style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: "12px", fontWeight: "700", letterSpacing: ".16em", color: "rgba(236,232,223,.86)" }}>COLLEGE NAME</span>
{isBlockedCollege && (
<span style={{ fontSize: "11px", fontWeight: "700", color: "#f87171", letterSpacing: ".06em" }}>
NOT ELIGIBLE
</span>
)}
</div>
<span style={{ fontSize: "12px", fontWeight: "700", letterSpacing: ".16em", color: "rgba(236,232,223,.86)" }}>COLLEGE NAME</span>
<input
name="college"
value={v.isInternal ? v.regVals.college : collegeValue}
Expand All @@ -126,48 +117,23 @@ export default function AuthOverlay({ v }: { v: V }) {
readOnly={v.isInternal}
autoComplete="organization"
placeholder="Full name of your institute"
aria-invalid={isBlockedCollege || !!v.err.college}
aria-invalid={!!v.err.college}
style={{
height: "54px",
padding: "0 16px",
borderRadius: "0",
background: isBlockedCollege ? "rgba(239, 68, 68, 0.08)" : v.isInternal ? "rgba(236,232,223,.08)" : "rgba(236,232,223,.04)",
background: v.isInternal ? "rgba(236,232,223,.08)" : "rgba(236,232,223,.04)",
fontSize: "16px",
color: "#ECE8DF",
outline: "none",
transition: "border-color .3s,background-color .3s",
border: isBlockedCollege ? "1.5px solid #ef4444" : `1.5px solid ${v.bc.college}`,
border: `1.5px solid ${v.bc.college}`,
cursor: v.isInternal ? "not-allowed" : "text",
}}
style-focus="border-color:oklch(0.8 0.12 85);background:rgba(236,232,223,.08)"
/>
{v.isInternal && <span style={{ fontSize: "12px", color: "oklch(0.8 0.12 85)" }}>Verified NIT Rourkela student.</span>}
{!v.isInternal && !isBlockedCollege && (
<span style={{ fontSize: "12px", color: "rgba(236,232,223,0.5)" }}>
Note: Students from ITER - SOA are not eligible to register.
</span>
)}
{isBlockedCollege && (
<div
role="alert"
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: "10px 14px",
background: "rgba(239, 68, 68, 0.14)",
border: "1px solid #ef4444",
borderRadius: "4px",
color: "#fca5a5",
fontSize: "13px",
fontWeight: 600,
lineHeight: "1.4",
}}
>
<span>Registration is not allowed for students from ITER - SOA.</span>
</div>
)}
{v.err.college && !isBlockedCollege ? (<span role="alert" style={{ fontSize: "14px", lineHeight: "1.4", color: "oklch(0.76 0.14 35)" }}>{v.err.college}</span>) : null}
{v.err.college ? (<span role="alert" style={{ fontSize: "14px", lineHeight: "1.4", color: "oklch(0.76 0.14 35)" }}>{v.err.college}</span>) : null}
</label>
<label data-s-in="" style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<span style={{ fontSize: "12px", fontWeight: "700", letterSpacing: ".16em", color: "rgba(236,232,223,.86)" }}>EMAIL</span>
Expand Down Expand Up @@ -326,7 +292,7 @@ export default function AuthOverlay({ v }: { v: V }) {
) : null}
<button
type="submit"
disabled={v.busy || (v.d.s0 !== 'none' && isBlockedCollege)}
disabled={v.busy}
onMouseEnter={v.beep}
style={{
flex: "1",
Expand All @@ -337,30 +303,24 @@ export default function AuthOverlay({ v }: { v: V }) {
minHeight: "58px",
padding: "0 28px",
border: "0",
cursor: (v.d.s0 !== 'none' && isBlockedCollege) ? "not-allowed" : "pointer",
cursor: "pointer",
fontWeight: "700",
fontSize: "15px",
letterSpacing: ".08em",
whiteSpace: "nowrap",
color: (v.d.s0 !== 'none' && isBlockedCollege) ? "#fca5a5" : "#141312",
background: (v.d.s0 !== 'none' && isBlockedCollege) ? "rgba(239, 68, 68, 0.25)" : "#ECE8DF",
opacity: (v.d.s0 !== 'none' && isBlockedCollege) ? 0.7 : v.busyO,
color: "#141312",
background: "#ECE8DF",
opacity: v.busyO,
clipPath: "polygon(12px 0,100% 0,100% calc(100% - 12px),calc(100% - 12px) 100%,0 100%,0 12px)",
transition: "background-color .4s,opacity .3s,color .3s",
}}
style-hover={(v.d.s0 !== 'none' && isBlockedCollege) ? "" : "background:oklch(0.8 0.12 85)"}
style-hover="background:oklch(0.8 0.12 85)"
style-active="transform:scale(.98)"
>
<span>
{v.d.s0 !== 'none' && isBlockedCollege
? 'NOT ELIGIBLE (ITER - SOA)'
: v.submitLbl}
</span>
{!(v.d.s0 !== 'none' && isBlockedCollege) && (
<svg width="16" height="10" viewBox="0 0 16 10" aria-hidden="true">
<path d="M11 1l4 4-4 4M15 5H0" fill="none" stroke="currentColor" strokeWidth="1.5"></path>
</svg>
)}
<span>{v.submitLbl}</span>
<svg width="16" height="10" viewBox="0 0 16 10" aria-hidden="true">
<path d="M11 1l4 4-4 4M15 5H0" fill="none" stroke="currentColor" strokeWidth="1.5"></path>
</svg>
</button>
</div>
</form>
Expand Down
Loading
Loading