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
32 changes: 30 additions & 2 deletions src/app/api/admin/registrations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ async function withReferredVia(supabase: SupabaseClient, registrations: Record<s
};
}

/**
* reviewed_by_name for every row: the name (or email) of the staff member who approved or rejected it, null while
* pending. Admins only: the Registration-Team list does not say who reviewed what.
*/
async function withReviewer(supabase: SupabaseClient, registrations: Record<string, unknown>[]) {
const ids = [...new Set(registrations.map((r) => r.reviewed_by).filter((v): v is string => typeof v === 'string'))];
const reviewers = await rowsById(supabase, 'profiles', 'id, full_name, email', ids);
if (reviewers.error) return { error: reviewers.error, registrations };
const reviewerById = new Map(reviewers.rows.map((p) => [p.id, p]));
return {
error: null,
registrations: registrations.map((r) => {
const reviewer = reviewerById.get(r.reviewed_by as string);
return { ...r, reviewed_by_name: reviewer ? reviewer.full_name?.trim() || reviewer.email || null : null };
}),
};
}

// GET all registrations (with optional filtering)
export async function GET(req: NextRequest) {
try {
Expand All @@ -97,7 +115,7 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: auth.error }, { status: auth.status });
}

const { supabase } = auth;
const { supabase, role } = auth;
const { searchParams } = new URL(req.url);
const status = searchParams.get('status');
const studentType = searchParams.get('student_type');
Expand Down Expand Up @@ -175,7 +193,17 @@ export async function GET(req: NextRequest) {
console.error('/api/admin/registrations attempts lookup error:', counted.error);
return NextResponse.json({ error: 'Database operation failed.' }, { status: 500 });
}
return NextResponse.json({ success: true, registrations: counted.registrations });
if (role !== 'admin') {
// Who reviewed a registration is for admins only.
const anonymous = counted.registrations.map((r) => ({ ...r, reviewed_by: null }));
return NextResponse.json({ success: true, registrations: anonymous });
}
const reviewed = await withReviewer(supabase, counted.registrations);
if (reviewed.error) {
console.error('/api/admin/registrations reviewer lookup error:', reviewed.error);
return NextResponse.json({ error: 'Database operation failed.' }, { status: 500 });
}
return NextResponse.json({ success: true, registrations: reviewed.registrations });
} catch (err: unknown) {
console.error('/api/admin/registrations error:', err);
const message = 'Failed to fetch registrations';
Expand Down
2 changes: 2 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;o
.hero-kicker{--ls:.42em;gap:.55em;font-size:clamp(11px,1.05vw,16px);letter-spacing:var(--ls)}
/* Second kicker line: a little smaller and lighter, so NIT ROURKELA leads. --ls is in em, so the centring pad shrinks with it. */
.hero-kicker-sub{font-size:.88em;font-weight:700;opacity:.78}
/* Third kicker line: PRESENTS, smaller and quieter again, a hand-off from the kicker to the title. */
.hero-kicker-presents{font-size:max(.78em,8px);font-weight:500;opacity:.66}
.hero-title{margin:clamp(10px,2vh,22px) 0 clamp(8px,1.4vh,16px);font-size:clamp(36px,12.4vw,224px)}
.hero-sub{--ls:.32em;gap:18px;font-size:clamp(12px,1.35vw,21px);letter-spacing:var(--ls)}
.hero-sub-rule{width:clamp(24px,4vw,64px);flex:none}
Expand Down
2 changes: 1 addition & 1 deletion src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export const size = OG_SIZE;
export const contentType = 'image/png';

export default function OpengraphImage() {
return ogCard({ kicker: 'NIT ROURKELA', kickerSub: 'STUDENT ACTIVITY CENTRE PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
return ogCard({ kicker: 'NIT ROURKELA', kickerSub: 'STUDENT ACTIVITY CENTRE', kickerTail: 'PRESENTS', title: 'INNOVISION', subtitle: '2026 · THE CELESTIAL ODYSSEY' });
}
40 changes: 36 additions & 4 deletions src/components/innovision/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,15 @@ export default function AdminDashboard({
// Update local state
setRegistrations((prev) =>
prev.map((r) =>
r.id === reg.id ? { ...r, status: newStatus, rejection_reason: newStatus === 'rejected' ? reason ?? null : null } : r
r.id === reg.id
? {
...r,
status: newStatus,
rejection_reason: newStatus === 'rejected' ? reason ?? null : null,
reviewed_at: new Date().toISOString(),
...(isAdmin ? { reviewed_by_name: currentUser.full_name?.trim() || currentUser.email } : {}),
}
: r
)
);
setSeatsKey((k) => k + 1);
Expand Down Expand Up @@ -362,6 +370,8 @@ export default function AdminDashboard({
'Referred Via Code',
'Referred By',
'Registered At',
// Who reviewed a registration is for admins only.
...(isAdmin ? ['Reviewed By', 'Reviewed At'] : []),
];

const rows = registrations.map((r) => [
Expand All @@ -384,6 +394,7 @@ export default function AdminDashboard({
r.referred_via ? formatReferralCode(r.referred_via.code) : '',
r.referred_via?.name ?? '',
r.created_at,
...(isAdmin ? [r.reviewed_by_name ?? '', r.status === 'pending' ? '' : r.reviewed_at ?? ''] : []),
].map(csvCell));

const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n');
Expand Down Expand Up @@ -1286,7 +1297,7 @@ export default function AdminDashboard({
background: 'rgba(16,15,14,0.6)',
}}
>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', minWidth: '980px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', minWidth: isAdmin ? '1120px' : '980px' }}>
<thead>
<tr style={{ background: 'rgba(236,232,223,0.06)', borderBottom: '1px solid rgba(236,232,223,0.12)' }}>
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>REG ID</th>
Expand All @@ -1296,19 +1307,22 @@ export default function AdminDashboard({
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>PROOFS</th>
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>FEE / UTR</th>
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>STATUS</th>
{isAdmin && (
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>REVIEWED BY</th>
)}
<th style={{ padding: '14px 18px', fontSize: '11px', fontWeight: 700, letterSpacing: '.14em', color: 'rgba(236,232,223,0.6)' }}>ACTION</th>
</tr>
</thead>
<tbody>
{loadingRegs ? (
<tr>
<td colSpan={8} style={{ padding: '40px', textAlign: 'center', color: 'rgba(236,232,223,0.6)' }}>
<td colSpan={isAdmin ? 9 : 8} style={{ padding: '40px', textAlign: 'center', color: 'rgba(236,232,223,0.6)' }}>
Loading registrations from database...
</td>
</tr>
) : registrations.length === 0 ? (
<tr>
<td colSpan={8} style={{ padding: '48px 24px', textAlign: 'center' }}>
<td colSpan={isAdmin ? 9 : 8} style={{ padding: '48px 24px', textAlign: 'center' }}>
<div style={{ color: '#ECE8DF', fontSize: '15px', fontWeight: 700, marginBottom: '8px' }}>
No event registrations submitted yet.
</div>
Expand Down Expand Up @@ -1506,6 +1520,24 @@ export default function AdminDashboard({
)}
</td>

{/* Reviewed By (admins only) */}
{isAdmin && (
<td style={{ padding: '14px 18px' }}>
{!isPending && reg.reviewed_by_name ? (
<>
<div style={{ fontSize: '13px', color: '#ECE8DF' }}>{reg.reviewed_by_name}</div>
{reg.reviewed_at && (
<div style={{ marginTop: '4px', fontSize: '11px', color: 'rgba(236,232,223,0.5)' }}>
{new Date(reg.reviewed_at).toLocaleString()}
</div>
)}
</>
) : (
<span style={{ color: 'rgba(236,232,223,0.4)' }}>—</span>
)}
</td>
)}

{/* Action (Approve / Reject) */}
<td style={{ padding: '14px 18px' }}>
{isPending ? (
Expand Down
5 changes: 3 additions & 2 deletions src/components/innovision/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ export default function HomeView({ v }: { v: V }) {
</div>
<div className="hero-copy" style={{ display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", pointerEvents: "none" }}>
{/* Wide screens: display contents, so copy and buttons share one centred column. Stacked screens: a box the size of the disc.
The page's one heading is the whole visible line-up: NIT ROURKELA / STUDENT ACTIVITY CENTRE PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
The page's one heading is the whole visible line-up: NIT ROURKELA / STUDENT ACTIVITY CENTRE / PRESENTS / INNOVISION / 2026 · THE CELESTIAL ODYSSEY.
Its parts are spans (phrasing content); the spaces between them are not rendered between flex items but keep the words apart in the text. */}
<h1 aria-label="NIT Rourkela Student Activity Centre presents Innovision 2026: The Celestial Odyssey" className="hero-copy-disc" style={{ margin: "0", fontSize: "inherit", fontWeight: "inherit" }}>
<span data-h-kicker="" className="hero-kicker" style={{ display: "flex", flexDirection: "column", alignItems: "center", color: "#ECE8DF", fontFamily: "var(--font-sans)", fontWeight: "700", whiteSpace: "nowrap" }}>
<span className="hero-track">NIT ROURKELA</span>{" "}
<span className="hero-track hero-kicker-sub">STUDENT ACTIVITY CENTRE PRESENTS</span>
<span className="hero-track hero-kicker-sub">STUDENT ACTIVITY CENTRE</span>{" "}
<span className="hero-track hero-kicker-presents">PRESENTS</span>
</span>{" "}
<span className="hero-title" style={{ display: "flex", fontFamily: "var(--font-display)", fontWeight: "400", lineHeight: ".95", letterSpacing: ".01em", color: "#fff", mixBlendMode: "difference", whiteSpace: "nowrap" }}>
{v.heroChars.map((c, cI) => (
Expand Down
3 changes: 2 additions & 1 deletion src/components/innovision/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export default function Loader({ v }: { v: V }) {
<div data-l-fade="" style={{ overflow: "hidden" }}>
<div data-l-line="" style={{ fontSize: "12px", fontWeight: "500", letterSpacing: ".34em" }}>
NIT ROURKELA
<span style={{ display: "block", marginTop: "6px", fontSize: "min(11px, 3.2vw)", fontWeight: "700", letterSpacing: ".24em", opacity: ".75" }}>STUDENT ACTIVITY CENTRE PRESENTS</span>
<span style={{ display: "block", marginTop: "6px", fontSize: "min(11px, 3.2vw)", fontWeight: "700", letterSpacing: ".24em", opacity: ".75" }}>STUDENT ACTIVITY CENTRE</span>
<span style={{ display: "block", marginTop: "5px", fontSize: "max(8px, min(9px, 2.6vw))", fontWeight: "500", letterSpacing: ".24em", opacity: ".6" }}>PRESENTS</span>
</div>
</div>
<div data-l-fade="" style={{ overflow: "hidden" }}>
Expand Down
6 changes: 4 additions & 2 deletions src/lib/og-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ export const OG_SIZE = { width: OG_IMAGE.width, height: OG_IMAGE.height };

/**
* @param kicker small caps line above the title, e.g. 'NIT ROURKELA'.
* @param kickerSub optional smaller second kicker line, e.g. 'STUDENT ACTIVITY CENTRE PRESENTS'.
* @param kickerSub optional smaller second kicker line, e.g. 'STUDENT ACTIVITY CENTRE'.
* @param kickerTail optional smallest third kicker line, e.g. 'PRESENTS'.
* @param title the big serif line; anything longer than INNOVISION is set smaller and may wrap onto two lines.
* @param subtitle the gold line under the title.
*/
export async function ogCard({ kicker, kickerSub, title, subtitle }: { kicker: string; kickerSub?: string; title: string; subtitle: string }) {
export async function ogCard({ kicker, kickerSub, kickerTail, title, subtitle }: { kicker: string; kickerSub?: string; kickerTail?: string; title: string; subtitle: string }) {
const [serif, starfield, logo] = await Promise.all([
readFile(join(process.cwd(), 'src/assets/fonts/DMSerifDisplay-Regular.ttf')),
readFile(join(process.cwd(), 'public/assets/starfield.svg')),
Expand All @@ -44,6 +45,7 @@ export async function ogCard({ kicker, kickerSub, title, subtitle }: { kicker: s
<img src={`data:image/png;base64,${logo.toString('base64')}`} alt="" width={84} height={84} style={{ marginBottom: 18 }} />
<div style={{ color: BONE, fontSize: 20, letterSpacing: 6, fontWeight: 700 }}>{kicker}</div>
{kickerSub && <div style={{ color: BONE, opacity: 0.78, fontSize: 17, letterSpacing: 4, fontWeight: 700, marginTop: 8 }}>{kickerSub}</div>}
{kickerTail && <div style={{ color: BONE, opacity: 0.66, fontSize: 14, letterSpacing: 4, fontWeight: 500, marginTop: 7 }}>{kickerTail}</div>}
<div
style={{
display: 'flex', justifyContent: 'center', maxWidth: 470, textAlign: 'center', color: '#fff', fontFamily: 'DM Serif Display',
Expand Down
2 changes: 2 additions & 0 deletions src/lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface Registration {
attempt?: number;
/** Not a column: added by GET /api/admin/registrations, why the user's previous registration was rejected. */
previous_rejection_reason?: RejectionReason | null;
/** Not a column: added by GET /api/admin/registrations for admins only, who approved or rejected it. */
reviewed_by_name?: string | null;
created_at?: string;
updated_at?: string;
}
Expand Down
Loading