From f72521bcf0da5afc03a0f90d5462dc89229ae39d Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Mon, 26 Oct 2026 16:27:59 +0530 Subject: [PATCH 1/2] Show admins who reviewed each registration Admins get a REVIEWED BY column (reviewer name and review time) in the registrations table and two matching CSV columns. The list API looks up reviewer names only for admins and blanks reviewed_by for the Registration-Team, so that role never sees who reviewed what. Co-Authored-By: Claude Opus 5.5 --- src/app/api/admin/registrations/route.ts | 32 +++++++++++++++- src/components/innovision/AdminDashboard.tsx | 40 ++++++++++++++++++-- src/lib/supabase.ts | 2 + 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/app/api/admin/registrations/route.ts b/src/app/api/admin/registrations/route.ts index f3d6b17..bd49135 100644 --- a/src/app/api/admin/registrations/route.ts +++ b/src/app/api/admin/registrations/route.ts @@ -89,6 +89,24 @@ async function withReferredVia(supabase: SupabaseClient, registrations: Record[]) { + 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 { @@ -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'); @@ -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'; diff --git a/src/components/innovision/AdminDashboard.tsx b/src/components/innovision/AdminDashboard.tsx index 2405a9e..7ffb21c 100644 --- a/src/components/innovision/AdminDashboard.tsx +++ b/src/components/innovision/AdminDashboard.tsx @@ -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); @@ -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) => [ @@ -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'); @@ -1286,7 +1297,7 @@ export default function AdminDashboard({ background: 'rgba(16,15,14,0.6)', }} > - +
@@ -1296,19 +1307,22 @@ export default function AdminDashboard({ + {isAdmin && ( + + )} {loadingRegs ? ( - ) : registrations.length === 0 ? ( - + {/* Reviewed By (admins only) */} + {isAdmin && ( + + )} + {/* Action (Approve / Reject) */}
REG IDPROOFS FEE / UTR STATUSREVIEWED BYACTION
+ Loading registrations from database...
+
No event registrations submitted yet.
@@ -1506,6 +1520,24 @@ export default function AdminDashboard({ )}
+ {!isPending && reg.reviewed_by_name ? ( + <> +
{reg.reviewed_by_name}
+ {reg.reviewed_at && ( +
+ {new Date(reg.reviewed_at).toLocaleString()} +
+ )} + + ) : ( + — + )} +
{isPending ? ( diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 2b6153b..b9eabfa 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -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; } From 2c5182e2214b7f2c7961e5b4d8d7b7b35bb80e6b Mon Sep 17 00:00:00 2001 From: Krish Agarwal Date: Mon, 26 Oct 2026 17:29:50 +0530 Subject: [PATCH 2/2] Put PRESENTS on its own line under Student Activity Centre The homepage hero, the loader and the share image now read NIT ROURKELA / STUDENT ACTIVITY CENTRE / PRESENTS, each line a step smaller and quieter than the one above, with an 8px floor on PRESENTS. Co-Authored-By: Claude Opus 5.5 --- src/app/globals.css | 2 ++ src/app/opengraph-image.tsx | 2 +- src/components/innovision/HomeView.tsx | 5 +++-- src/components/innovision/Loader.tsx | 3 ++- src/lib/og-card.tsx | 6 ++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index b8d552f..708dc4a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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} diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index 280411c..27df341 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -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' }); } diff --git a/src/components/innovision/HomeView.tsx b/src/components/innovision/HomeView.tsx index 96e0926..5f8fba2 100644 --- a/src/components/innovision/HomeView.tsx +++ b/src/components/innovision/HomeView.tsx @@ -55,12 +55,13 @@ export default function HomeView({ v }: { v: V }) {
{/* 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. */}

NIT ROURKELA{" "} - STUDENT ACTIVITY CENTRE PRESENTS + STUDENT ACTIVITY CENTRE{" "} + PRESENTS {" "} {v.heroChars.map((c, cI) => ( diff --git a/src/components/innovision/Loader.tsx b/src/components/innovision/Loader.tsx index e85b8ad..16c5ed0 100644 --- a/src/components/innovision/Loader.tsx +++ b/src/components/innovision/Loader.tsx @@ -64,7 +64,8 @@ export default function Loader({ v }: { v: V }) {
NIT ROURKELA - STUDENT ACTIVITY CENTRE PRESENTS + STUDENT ACTIVITY CENTRE + PRESENTS
diff --git a/src/lib/og-card.tsx b/src/lib/og-card.tsx index b9e171c..f73004b 100644 --- a/src/lib/og-card.tsx +++ b/src/lib/og-card.tsx @@ -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')), @@ -44,6 +45,7 @@ export async function ogCard({ kicker, kickerSub, title, subtitle }: { kicker: s
{kicker}
{kickerSub &&
{kickerSub}
} + {kickerTail &&
{kickerTail}
}