Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ad97543
FED KIIT Clone in Next.js
Krishna-Das20 Jul 30, 2026
5fc7ec3
home page
rajatava06 Jul 30, 2026
84ba12e
Fix: redirect after sign-in on the auth pages
Krishna-Das20 Jul 30, 2026
2d3a5fa
Move the post-sign-in redirect into the auth components
Krishna-Das20 Jul 30, 2026
596c5f6
Fix the redirect after sign-in
Krishna-Das20 Jul 30, 2026
9a1073e
Halve the client bundle and fix the SSR hydration errors
Krishna-Das20 Jul 31, 2026
67ef1e9
Fix the remaining SSR hydration errors, and add an audit that finds them
Krishna-Das20 Jul 31, 2026
ec0cb96
Fix the admin calendar, and restore Express parity for forms and events
Krishna-Das20 Jul 31, 2026
ff0861a
Revamp UI system and add certificate APIs
hardikguptaofficialgit Aug 2, 2026
a0e5c24
Merge branch 'nextjs-migration' into page/home
rajatava06 Aug 2, 2026
e57e4c2
Restore and commit all local modifications and files after merge
rajatava06 Aug 2, 2026
9d7f5cd
fix qr payload
nyxb1t Aug 3, 2026
02e5573
duplicate handling
nyxb1t Aug 3, 2026
53a9934
attendance qr issues fixed
nyxb1t Aug 3, 2026
4ac0762
Home Page Revamped with Navbar
rajatava06 Aug 3, 2026
865d1cb
Send 4-digit OTPs again, so they fit the four boxes on screen
Krishna-Das20 Aug 3, 2026
185f719
Restore the global button reset, and put the Chatbot back on every route
Krishna-Das20 Aug 4, 2026
69235b2
Fix the password reset, and two more request-body contract breaks
Krishna-Das20 Aug 4, 2026
ce82826
Fix useSearchParams destructuring, which crashed two pages
Krishna-Das20 Aug 4, 2026
1d6712f
Add a health endpoint for production monitoring
Krishna-Das20 Aug 4, 2026
c668eed
Rebuild team management against the Express controllers
Krishna-Das20 Aug 4, 2026
d651ca1
Fix the useSearchParams destructuring that crashed the team page
Krishna-Das20 Aug 4, 2026
7dae203
Only build invite links from a trusted origin
Krishna-Das20 Aug 4, 2026
98848cb
Carry team invite links through sign-in and sign-up
Krishna-Das20 Aug 4, 2026
baa9966
Tell people when an invite link's auto-join fails
Krishna-Das20 Aug 4, 2026
8d86210
Fix four team-mutation defects found by running them
Krishna-Das20 Aug 4, 2026
fbced77
Distinguish renaming a team from replacing one
Krishna-Das20 Aug 4, 2026
0dc6c0b
Merge upstream/main
Krishna-Das20 Aug 4, 2026
27e16af
Merge upstream/beta
Krishna-Das20 Aug 4, 2026
8227eb3
Regenerate the Prisma client automatically when the schema changes
Krishna-Das20 Aug 4, 2026
29a732f
Fix Google sign-in, which rejected every request
Krishna-Das20 Aug 4, 2026
e15ebb9
parallax effect and footer updated
rajatava06 Aug 4, 2026
43e389b
Fixed all the comments above namely, image dimensions in one file, ro…
Krishna-Das20 Aug 4, 2026
c33998f
Merge remote-tracking branch 'upstream/main'
Krishna-Das20 Aug 5, 2026
05ab596
home and events page updated
rajatava06 Aug 6, 2026
15c2aff
updated
hardikguptaofficialgit Aug 6, 2026
32389c8
Merge branch 'page/home' into main accepting all incoming changes
rajatava06 Aug 6, 2026
eabb69a
Merge branch 'nextjs-migration' from hardikguptaofficial accepting al…
rajatava06 Aug 6, 2026
a2a6843
Home Page revamped
rajatava06 Aug 6, 2026
cb79def
Merge attendance QR fixes from nyxb1t (#1)
Krishna-Das20 Aug 6, 2026
d48a880
Fix the attendance QR flow against the real API contract
Krishna-Das20 Aug 6, 2026
8843cab
Restrict attendance scanning to ADMIN
Krishna-Das20 Aug 6, 2026
f1f512b
Merge the Home and Navbar revamp from rajatava06 (#2)
Krishna-Das20 Aug 6, 2026
1113243
Merge upstream/beta
Krishna-Das20 Aug 6, 2026
b5ff6a4
Keep each component's stylesheet with its component
Krishna-Das20 Aug 6, 2026
9ad106e
Resolve every unresolved CSS-module class reference
Krishna-Das20 Aug 6, 2026
e52293a
Remove the hardcoded Builder.io CDN key from event image fallbacks
Krishna-Das20 Aug 6, 2026
a8ccb5a
Bring in the Footer, Events page and EventCard revamp
Krishna-Das20 Aug 6, 2026
b82acd5
Remove the bundled sample data and read everything from the backend
Krishna-Das20 Aug 6, 2026
2194906
Honour the admin ranking on the home page, and smooth the navbar and pin
Krishna-Das20 Aug 6, 2026
9e796c2
Reserve navbar space page-wide, and show the signed-in avatar
Krishna-Das20 Aug 6, 2026
67f7e10
Stop reserving the navbar's height twice on the Events page
Krishna-Das20 Aug 6, 2026
f07cc1a
Add a link payment mode, payment proof, and an admin view for both
Krishna-Das20 Aug 7, 2026
981cb44
Turn the event detail and registration views into real pages
Krishna-Das20 Aug 7, 2026
77130f3
Bound the analytics modal's height and let it scroll its own content
Krishna-Das20 Aug 7, 2026
bc15ea4
Fix the analytics modal's layout and put payment proof in the attenda…
Krishna-Das20 Aug 7, 2026
ce6996c
Limit the poster stretch to the featured card
Krishna-Das20 Aug 7, 2026
c6f9693
Size the spotlight against the viewport and fix the form on phones
Krishna-Das20 Aug 7, 2026
7967b6a
Give the session one expiry so the Login button cannot deadlock
Krishna-Das20 Aug 7, 2026
f1416e2
Merge upstream/main
Krishna-Das20 Aug 9, 2026
057195a
Fix event links, the related-event lock, certificates and mobile imag…
Krishna-Das20 Aug 9, 2026
44b2ec5
Give the admin card buttons a full touch target on mobile
Krishna-Das20 Aug 9, 2026
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
50 changes: 32 additions & 18 deletions app/api/certificate/sendCertificatesAndEvents/route.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
import { sendCertificatesAndEvents } from "@/lib/services/certificates";
import { prisma } from "@/lib/db";
import { body, expressError, handle, json } from "@/lib/api/express";
import { getCurrentUser, isAdmin } from "@/lib/auth/access";

/**
* POST /api/certificate/sendCertificatesAndEvents
* Port of controllers/certificate/eventCertificateController.js — admin only.
* Port of controllers/certificate/eventCertificateController.js.
*
* Recipients who already hold a certificate for the event are skipped rather
* than issued a duplicate, so re-running after a partial failure is safe.
* The name is misleading and comes from the Express backend: this sends
* nothing. It looks up the certificates already issued to one email address and
* returns each one alongside the event it belongs to, which is what the "My
* Events" tab uses to put a View link on each past event.
*
* It was previously implemented here as the certificate *issuing* endpoint —
* admin-only, taking `{ eventId, recipients }` and returning `{ data }`. Nothing
* called it that way; the only caller is the participant profile, which sends
* `{ email }` and reads `certandevent`. So every participant got a 403, their
* certificate list stayed empty, and the View button had no link to follow.
*
* Access: you may read your own certificates; admins may read anyone's. The
* Express route had no auth middleware at all, which let anyone enumerate
* another member's certificates by guessing their address.
*/
export async function POST(request: Request) {
return handle(async () => {
const user = await getCurrentUser();
if (!user) return expressError(401, "Token is required");
if (!isAdmin(user)) return expressError(403, "Unauthorized");

const b = await body<{
eventId?: string;
recipients?: Array<{ email: string; fieldValues?: Record<string, string> }>;
emails?: string[];
}>(request);
const b = await body<{ email?: string }>(request);
const email = (b.email ?? "").toLowerCase().trim();
if (!email) return expressError(400, "Email is required");

// Accepts either a rich recipient list or a plain array of addresses.
const recipients =
b.recipients ??
(b.emails ?? []).map((email) => ({ email, fieldValues: {} }));
if (email !== user.email.toLowerCase() && !isAdmin(user)) {
return expressError(403, "Unauthorized");
}

const data = await sendCertificatesAndEvents({
eventId: b.eventId ?? "",
recipients,
const issued = await prisma.issuedCertificates.findMany({
where: { email },
include: { event: true },
});

return json({ success: true, message: "Certificates processed", data });
// Shape kept identical to Express: [{ cert, event }], with rows whose event
// has since been deleted dropped rather than returned as null.
const certandevent = issued
.filter((row) => row.event)
.map(({ event, ...cert }) => ({ cert, event }));

return json({ certandevent });
});
}
2 changes: 1 addition & 1 deletion src/authentication/Login/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const Login = () => {
};

return (
<div>
<div className={style.page}>
<div className={style.container}>
<Link href={"/"}>
<div className={style.ArrowBackIcon}>
Expand Down
13 changes: 13 additions & 0 deletions src/authentication/Login/styles/Login.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
// The page shell inside `.authpage`.
//
// `.authpage` is a column flex container with `align-items: center`, so its
// child is sized shrink-to-fit on the cross axis. This wrapper had no width of
// its own, collapsed to its content, and `.container`'s `width: 100%` then
// resolved against that — the card rendered ~257px wide on a laptop and never
// reached its 480px max-width.
.page {
width: 100%;
display: flex;
justify-content: center;
}

.container {
position: relative;
z-index: 10;
Expand Down
100 changes: 47 additions & 53 deletions src/components/EventCard/EventCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ import { Blurhash } from "react-blurhash";
import { Alert, MicroLoading } from "../../microInteraction";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { isPrerequisiteMet } from "../../utils/prerequisite";
import { cdn } from "../../utils/cloudinary";

const EventCard = (props) => {
const {
data,
onOpen,
type,
modalpath,
showShareButton = true,
showRegisterButton = true,
additionalContent,
onEdit,
onDelete,
enableEdit,
isLoading,
isRegisteredInRelatedEvents,
eventName,
variant = "default",
} = props;
Expand All @@ -38,7 +38,6 @@ const EventCard = (props) => {
const authCtx = useContext(AuthContext);
const [isOpen, setOpen] = useState(false);
const [isQRModalOpen, setQRModalOpen] = useState(false);
const [isHovered, setisHovered] = useState(false);
const [remainingTime, setRemainingTime] = useState("");
const [btnTxt, setBtnTxt] = useState("Register Now");
const router = useRouter();
Expand Down Expand Up @@ -97,6 +96,10 @@ const EventCard = (props) => {
}
};

// Per-event, not per-page: this card's own prerequisite against this
// visitor's own registrations.
const prerequisiteMet = isPrerequisiteMet(info, authCtx.user?.regForm);

const dayWithSuffix = day + getOrdinalSuffix(day);
const month = date.toLocaleDateString("en-GB", { month: "long" });
const year = date.getFullYear();
Expand Down Expand Up @@ -162,54 +165,33 @@ const EventCard = (props) => {
}, [info.isRegistrationClosed, remainingTime]);

useEffect(() => {
if (authCtx.isLoggedIn && authCtx.user.regForm) {
if (isRegisteredInRelatedEvents) {
if (data?.info?.relatedEvent === "null") {
if (authCtx.user.regForm.includes(data.id)) {
setBtnTxt("Already Registered");
}
} else {
if (authCtx.user.regForm.includes(data.id)) {
setBtnTxt("Already Registered");
} else {
if (remainingTime) {
setBtnTxt(remainingTime);
} else if (data?.info?.isRegistrationClosed) {
setBtnTxt("Closed");
} else {
setBtnTxt("Register Now");
}
}
}
} else {
if (data?.info?.relatedEvent === "null") {
if (authCtx.user.regForm.includes(data.id)) {
setBtnTxt("Already Registered");
} else {
if (remainingTime) {
setBtnTxt(remainingTime);
} else if (data?.info?.isRegistrationClosed) {
setBtnTxt("Closed");
} else {
setBtnTxt("Register Now");
}
}
} else {
if (authCtx.user.access === "USER") {
if (data?.info?.isRegistrationClosed) {
setBtnTxt("Closed");
} else {
setBtnTxt("Locked");
}
}
}
}
if (!authCtx.isLoggedIn || !authCtx.user.regForm) return;

const openState = () => {
if (remainingTime) return remainingTime;
if (data?.info?.isRegistrationClosed) return "Closed";
return "Register Now";
};

if (authCtx.user.regForm.includes(data.id)) {
setBtnTxt("Already Registered");
return;
}

// Locked until this event's own prerequisite is met. Admins are exempt so
// they can still open a gated form to check it.
if (!prerequisiteMet && authCtx.user.access === "USER") {
setBtnTxt(data?.info?.isRegistrationClosed ? "Closed" : "Locked");
return;
}

setBtnTxt(openState());
}, [
authCtx.isLoggedIn,
authCtx.user.regForm,
authCtx.user.access,
data,
isRegisteredInRelatedEvents,
prerequisiteMet,
remainingTime,
]);

Expand Down Expand Up @@ -264,7 +246,7 @@ const EventCard = (props) => {
) {
setAlert({
type: "info",
message: `You need to register for ${eventName} first`,
message: `You need to register for ${eventName || "the required event"} first`,
position: "bottom-right",
duration: 3000,
});
Expand Down Expand Up @@ -324,7 +306,16 @@ const EventCard = (props) => {
}
};

const detailsHref = modalpath + data.id;
// Every event -- upcoming, past, or viewed from the admin panel -- is served
// by the single /Events/[eventId] route, so the card builds its own link
// rather than trusting the caller.
//
// This used to be `modalpath + data.id`, from when `modalpath` named a modal
// and was never navigated to. Turning the title into a real <Link> made those
// strings live URLs, and three of the four callers were passing paths that
// have no route: "/pastEvents/", "/Events/pastEvents/" and "/profile/Events/".
// Every past-event card on the site 404'd as a result.
const detailsHref = `/Events/${data.id}`;
// Built from the origin, not from the current href - appending the id to
// whatever page you happen to be on produced links like /Events/x/y.
const shareUrl =
Expand Down Expand Up @@ -423,8 +414,6 @@ const EventCard = (props) => {
return (
<article
className={`${style.card} ${variant === "featured" ? style.featured : ""}`}
onMouseEnter={() => setisHovered(true)}
onMouseLeave={() => setisHovered(false)}
>
{variant === "featured" && (
<div className={style.techPattern} aria-hidden="true">
Expand Down Expand Up @@ -536,11 +525,12 @@ const EventCard = (props) => {
{/* Cropped from the bottom rather than the centre — see `.image` in the
stylesheet for why. */}
<img
src={info.eventImg}
src={cdn(info.eventImg, variant === "featured" ? 1000 : 700)}
className={style.image}
style={{ opacity: imageLoaded ? 1 : 0 }}
alt=""
loading="lazy"
decoding="async"
onLoad={() => setImageLoaded(true)}
/>
<span className={style.badge} data-tone={status.tone}>
Expand Down Expand Up @@ -625,7 +615,12 @@ const EventCard = (props) => {
</div>
</div>

{enableEdit && isHovered && authCtx.user.access === "ADMIN" && (
{/* Always rendered. It used to be gated on an `isHovered` state, which no
touch device ever sets — so admins on a phone could not reach Edit,
Delete or Analytics at all. The fade-in on hover now lives in CSS,
behind `@media (hover: hover)`, so pointer devices keep the reveal and
touch devices simply always see the bar. */}
{enableEdit && authCtx.user.access === "ADMIN" && (
<div className={style.adminBar}>
<button
type="button"
Expand Down Expand Up @@ -683,7 +678,6 @@ EventCard.propTypes = {
data: PropTypes.object.isRequired,
onOpen: PropTypes.func,
type: PropTypes.string.isRequired,
modalpath: PropTypes.string.isRequired,
customStyles: PropTypes.object,
showShareButton: PropTypes.bool,
showRegisterButton: PropTypes.bool,
Expand Down
33 changes: 33 additions & 0 deletions src/components/EventCard/styles/EventCard.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,46 @@ $poster-stretch: 1.19;
right: 12px;
z-index: 2;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
padding: 6px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
background-color: rgba(0, 0, 0, 0.82);
}

// The reveal-on-hover only applies where hovering is possible. A touch screen
// matches `hover: none`, so the bar stays visible there instead of being
// unreachable — which is what happened when this was a JS `isHovered` flag.
@media (hover: hover) and (pointer: fine) {
.adminBar {
opacity: 0;
transition: opacity 160ms var(--ease);
}

.card:hover .adminBar,
.adminBar:focus-within {
opacity: 1;
}
}

// The bar sits over the poster; on a narrow card the three buttons need room
// to wrap rather than run off the edge. They also become the primary way an
// admin reaches Analytics on a phone, so they get a full touch target — they
// measured 32px tall, under the usual 44px minimum.
@media (max-width: 768px) {
.adminBar {
max-width: calc(100% - 24px);
}

.adminBtn {
min-height: 44px;
padding-inline: 14px;
font-size: 0.8125rem;
}
}

.adminBtn {
display: inline-flex;
align-items: center;
Expand Down
9 changes: 8 additions & 1 deletion src/components/HeroGallery/HeroGallery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import styles from "./styles/HeroGallery.module.scss";
import { cdn } from "../../utils/cloudinary";

const INTERVAL_MS = 3200;

Expand Down Expand Up @@ -69,10 +70,16 @@ function HeroGallery({ images }) {
}
aria-current={offset === 0 ? "true" : undefined}
>
{/* Every slide is mounted at once so the carousel can animate
between them, so all ten originals were downloaded and
decoded up front — several of them 4000-6000px wide for a
box under 400px. */}
<img
src={image.image}
src={cdn(image.image, 700)}
alt={image.title || `Gallery photo ${index + 1}`}
draggable={false}
loading={Math.abs(offset) <= 1 ? "eager" : "lazy"}
decoding="async"
/>
</button>
);
Expand Down
3 changes: 2 additions & 1 deletion src/features/Modals/Event/LiveEventPopup/LiveEventPopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import axios from "axios";
import styles from "./styles/LiveEventPopup.module.scss";
import { api } from "../../../../services";
import { cdn } from "../../../../utils/cloudinary";

const LiveEventPopup = () => {
const [isVisible, setIsVisible] = useState(false);
Expand Down Expand Up @@ -76,7 +77,7 @@ const LiveEventPopup = () => {
×
</button>
<a href="/Events">
<img src={eventImage} alt="Event" className={styles.popupContent} />
<img src={cdn(eventImage, 600)} alt="Event" className={styles.popupContent} loading="lazy" decoding="async" />
</a>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/sections/Home/Events/EventsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
import Link from "next/link";
import { api } from "../../../services";
import styles from "./styles/EventsSection.module.scss";
import { cdn } from "../../../utils/cloudinary";

/** Shown when an event carries no image of its own. */
const EVENT_BANNER_FALLBACK = "/fedkiit-logo.png";
Expand Down Expand Up @@ -135,7 +136,7 @@ export default function HomeEventsSection() {
<div className={styles.eventCard}>
{/* Top Image Banner with Tag Overlay */}
<div className={styles.cardImageWrapper}>
<img src={ev.banner} alt={ev.title} className={styles.cardImage} />
<img src={cdn(ev.banner, 700)} alt={ev.title} className={styles.cardImage} loading="lazy" decoding="async" />
<span className={styles.tagBadge}>{ev.tag}</span>
</div>

Expand Down
Loading
Loading