From 936190fbdfeee90e737f4c3b731119d4600801ea Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Fri, 14 Aug 2026 22:57:26 +0530 Subject: [PATCH 1/3] starting of team page --- src/components/TeamMemberCard.tsx | 151 ++++++++++++++ src/components/layout/Header.tsx | 1 + src/lib/supabase/actions/storage.actions.ts | 48 ++++- src/lib/supabase/actions/team.actions.ts | 14 ++ src/pages/team/data/team_details.tsx | 60 ++++++ src/pages/team/index.tsx | 205 ++++++++++++++++++++ 6 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 src/components/TeamMemberCard.tsx create mode 100644 src/pages/team/data/team_details.tsx create mode 100644 src/pages/team/index.tsx diff --git a/src/components/TeamMemberCard.tsx b/src/components/TeamMemberCard.tsx new file mode 100644 index 0000000..ec41302 --- /dev/null +++ b/src/components/TeamMemberCard.tsx @@ -0,0 +1,151 @@ +"use client"; + +import Image from "next/image"; +import { useState } from "react"; + +import { Linkedin } from "lucide-react"; + +interface TeamMember { + name: string; + role: string; + description: string; + location?: string; + image: string; + socials?: { + twitter?: string; + linkedin?: string; + website?: string; + }; +} + +interface TeamMemberCardProps { + member: TeamMember; +} + +export default function TeamMemberCard({ + member, +}: TeamMemberCardProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className={` + overflow-hidden + rounded-[24px] + bg-[#f0f1f3] + p-2.5 + transition-all + duration-500 + ${isHovered ? "-translate-y-1 shadow-xl" : ""} + `} + > + {/* IMAGE */} +
+ {member.name} + +
+
+ + {/* INFO */} +
+ + {/* ALWAYS VISIBLE */} +

+ {member.name} +

+ +

+ {member.role} +

+ + {/* HOVER CONTENT */} +
+ {/* DESCRIPTION */} +

+ {member.description} +

+ + {/* BOTTOM */} +
+ + {/* LOCATION */} + {member.location && ( +
+ + + + + + {member.location} +
+ )} + + {member.socials?.linkedin && ( + + + +)} + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 139291c..535be82 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -58,6 +58,7 @@ const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => { const navigation = [ { name: "Home", path: "/" }, + { name: "Team", path: "/team"}, { name: "Projects", path: "/project" }, { name: "Activities", path: "/activities" }, { name: "Events", path: "/events" }, diff --git a/src/lib/supabase/actions/storage.actions.ts b/src/lib/supabase/actions/storage.actions.ts index 8678821..2584c1b 100644 --- a/src/lib/supabase/actions/storage.actions.ts +++ b/src/lib/supabase/actions/storage.actions.ts @@ -1,8 +1,11 @@ import { base64ToBlob, HTMLToMarkdown } from "@/lib/utils"; import { client } from "../supabase"; +const STORAGE_BUCKET = "media"; +const SIGNED_URL_TTL_SECONDS = 60 * 60; + export const getFileNames = async (folder: string) => { - const { data, error } = await client.storage.from("media").list(folder); + const { data, error } = await client.storage.from(STORAGE_BUCKET).list(folder); if (error) { console.log(error); @@ -18,7 +21,7 @@ export const getImagesFromFolder = async (folder: string) => { for (let i = 0; i < files.length; i++) { - const { data } = client.storage.from("media").getPublicUrl(`${folder}/${files[i]}`); + const { data } = client.storage.from(STORAGE_BUCKET).getPublicUrl(`${folder}/${files[i]}`); publicUrls.push(data.publicUrl); } @@ -33,7 +36,7 @@ export const uploadImage = async (folder: string, name: string, fileData: string const blob = base64ToBlob(base64String, contentType) const filePath = `${folder}/${name}`; - const { data, error } = await client.storage.from("media").upload(filePath, blob, { contentType, upsert: true }); + const { data, error } = await client.storage.from(STORAGE_BUCKET).upload(filePath, blob, { contentType, upsert: true }); if (error) console.log(error); @@ -41,7 +44,7 @@ export const uploadImage = async (folder: string, name: string, fileData: string }; export const deleteImage = async (paths: string[]) => { - const { data, error } = await client.storage.from("media").remove(paths); + const { data, error } = await client.storage.from(STORAGE_BUCKET).remove(paths); if (error) { console.log(error); @@ -53,7 +56,7 @@ export const deleteImage = async (paths: string[]) => { }; export const getMarkdownFile = async (fileName: string, type: string) => { - const { data, error } = await client.storage.from("media").download(`markdown/${type}/${fileName.split(".")[0]}/${fileName}`); + const { data, error } = await client.storage.from(STORAGE_BUCKET).download(`markdown/${type}/${fileName.split(".")[0]}/${fileName}`); if (error) { @@ -72,23 +75,48 @@ export const uploadMarkdownFile = async (fileNameWithExtension: string, type: st type: contentType }); - await client.storage.from("media").upload(`markdown/${type}/${fileNameWithExtension.split(".")[0]}/${fileNameWithExtension}`, file, { + await client.storage.from(STORAGE_BUCKET).upload(`markdown/${type}/${fileNameWithExtension.split(".")[0]}/${fileNameWithExtension}`, file, { contentType, upsert: true }); }; export const getMarkdownPublicURL = (fileName: string, folder: string) => { - const { data } = client.storage.from("media").getPublicUrl(`markdown/${folder}/${fileName.split(".")[0]}/${fileName}`); + const { data } = client.storage.from(STORAGE_BUCKET).getPublicUrl(`markdown/${folder}/${fileName.split(".")[0]}/${fileName}`); return data.publicUrl; }; +export const getStorageImageUrl = async (path: string) => { + const cleanPath = path.trim().replace(/^\/+/, ""); + + if (!cleanPath) { + return null; + } + + // If DB already stores a complete public/signed URL, use it directly. + if (/^https?:\/\//i.test(path.trim())) { + return path.trim(); + } + + const { data: signedData, error: signedError } = await client + .storage + .from(STORAGE_BUCKET) + .createSignedUrl(cleanPath, SIGNED_URL_TTL_SECONDS); + + if (!signedError && signedData?.signedUrl) { + return signedData.signedUrl; + } + + const { data: publicData } = client.storage.from(STORAGE_BUCKET).getPublicUrl(cleanPath); + return publicData.publicUrl || null; +}; + export const getAllFiles = async (path: string) => { let allFiles: string[] = []; const { data: list, error } = await client .storage - .from("media") + .from(STORAGE_BUCKET) .list(path, { limit: 1000 }); if (error) { @@ -116,7 +144,7 @@ export const deleteMarkdownFolder = async (folder: string, type: string) => { const filesToDelete = await getAllFiles(`markdown/${type}/${folder}`); if (filesToDelete.length > 0) { - const { data, error } = await client.storage.from("media").remove(filesToDelete); + const { data, error } = await client.storage.from(STORAGE_BUCKET).remove(filesToDelete); if (error) { console.log(error); @@ -132,7 +160,7 @@ export const deleteMarkdownFolder = async (folder: string, type: string) => { }; export const deleteMarkdownFile = async (fileNameWithExtension: string, type: string) => { - const { data, error } = await client.storage.from("media").remove([`markdown/${type}/${fileNameWithExtension.split(".")[0]}/${fileNameWithExtension}`]); + const { data, error } = await client.storage.from(STORAGE_BUCKET).remove([`markdown/${type}/${fileNameWithExtension.split(".")[0]}/${fileNameWithExtension}`]); if (error) { console.log(error); diff --git a/src/lib/supabase/actions/team.actions.ts b/src/lib/supabase/actions/team.actions.ts index 7639b26..cc49add 100644 --- a/src/lib/supabase/actions/team.actions.ts +++ b/src/lib/supabase/actions/team.actions.ts @@ -3,6 +3,20 @@ import { client } from "../supabase"; import { deleteImage, uploadImage } from "./storage.actions"; import { urlToBase64 } from "@/lib/utils"; + +export const getTeamImages = async () => { + const { data, error } = await client + .from("team") + .select("name, image"); + + if (error) { + console.error("Failed to fetch team images:", error); + return []; + } + + return data as { name: string; image: string }[]; +}; + export const getTeamMembers = async () => { const { data, error } = await client.from("team").select("*"); if (error) console.log(error); diff --git a/src/pages/team/data/team_details.tsx b/src/pages/team/data/team_details.tsx new file mode 100644 index 0000000..0a034d5 --- /dev/null +++ b/src/pages/team/data/team_details.tsx @@ -0,0 +1,60 @@ +export type TeamMemberDetails = { + role: string; + description: string; + location?: string; + socials?: { + twitter?: string; + linkedin?: string; + website?: string; + }; +}; + +export const teamDetails: Record = { + "Kartavya ": { + role: "Faculty Advisor", + description: + "Guides the team with his experience, mentorship and vision while helping us build innovative robotics projects.", + location: "ELECTRONICS AND COMMUNICATION", + socials: { + linkedin: "#", + }, + }, + + "Shreyas Vij": { + role: "Technical Lead", + description: + "Works on everything expect robotics ", + location: "ELECTRONICS AND COMMUNICATION", + socials: { + linkedin: "https://www.linkedin.com/in/shreyas-vij-07b541328/", + }, + }, + "Dhairya Sood": { + role: "Add role here", + description: "Works on robotics and ROS. Reliable hotspot provider. Password- 1234567889", + location: "ELECTRONICS AND COMMUNICATION", + socials: { + linkedin: "https://www.linkedin.com/in/dhairya-sood/", + }, + }, + + + "Manav Kohli": { + role: "Add role here", + description: "Works on robotics and this site. Joined ACM. Made one joke. Became a former member. ", + location: "ELECTRICAL", + socials: { + linkedin: "https://www.linkedin.com/in/manav-kohli/", + }, + }, + + "Ansh Agnihotry": { + role: "Add role here", + description: "Works on robotics, embedded systems. Trying to guess his password? Just start with Hitler", + location: "PRODUCTION", + socials: { + linkedin: "https://www.linkedin.com/in/ansh-agnihotry-b12145204/", + }, + }, + +}; \ No newline at end of file diff --git a/src/pages/team/index.tsx b/src/pages/team/index.tsx new file mode 100644 index 0000000..4b35d92 --- /dev/null +++ b/src/pages/team/index.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +import { getStorageImageUrl } from "@/lib/supabase/actions/storage.actions"; +import { getTeamMembers } from "@/lib/supabase/actions/team.actions"; + +import { teamDetails } from "./data/team_details"; + +import TeamMemberCard from "../../components/TeamMemberCard"; + +type TeamMember = { + id: string; + name: string; + role: string; + description?: string; + location?: string; + image: string; + socials?: { + twitter?: string; + linkedin?: string; + website?: string; + }; +}; + +export default function TeamPage() { + const [teamMembers, setTeamMembers] = useState([]); + const [resolvedImageUrls, setResolvedImageUrls] = useState< + Record + >({}); + + const [loadingMembers, setLoadingMembers] = useState(true); + const [loadingImages, setLoadingImages] = useState(true); + + /* + * Add information from team_details.ts to the Supabase members. + * + * Supabase provides: + * - name + * - role + * - image + * + * team_details.ts provides: + * - description + * - location + * - socials + */ + const membersWithDetails = useMemo(() => { + return teamMembers.map((member) => { + const details = teamDetails[member.name]; + + return { + ...member, + + description: + member.description ?? + details?.description ?? + "Core member of the Robotics Society team.", + + location: + member.location ?? + details?.location, + + socials: { + ...details?.socials, + ...member.socials, + }, + }; + }); + }, [teamMembers]); + + /* + * Fetch team members from Supabase + */ + useEffect(() => { + let isActive = true; + + const fetchMembers = async () => { + setLoadingMembers(true); + + try { + const data = await getTeamMembers(); + + if (!isActive) { + return; + } + + const formattedMembers: TeamMember[] = data.map( + (member: { + id: string; + firstName: string; + lastName: string; + role: string; + image: string; + }) => ({ + id: member.id, + name: `${member.firstName} ${member.lastName}`.trim(), + role: member.role, + image: member.image, + }) + ); + + setTeamMembers(formattedMembers); + } catch (error) { + if (!isActive) { + return; + } + + console.error("Failed to fetch team members:", error); + setTeamMembers([]); + } finally { + if (isActive) { + setLoadingMembers(false); + } + } + }; + + void fetchMembers(); + + return () => { + isActive = false; + }; + }, []); + + /* + * Resolve Supabase Storage image URLs + */ + useEffect(() => { + let isActive = true; + + const fetchImageUrls = async () => { + if (membersWithDetails.length === 0) { + setResolvedImageUrls({}); + setLoadingImages(false); + return; + } + + setLoadingImages(true); + + const urlPairs = await Promise.all( + membersWithDetails.map(async (member) => { + const url = await getStorageImageUrl(member.image || ""); + + return [member.id, url] as const; + }) + ); + + if (!isActive) { + return; + } + + setResolvedImageUrls(Object.fromEntries(urlPairs)); + setLoadingImages(false); + }; + + void fetchImageUrls(); + + return () => { + isActive = false; + }; + }, [membersWithDetails]); + + return ( +
+ + {/* ================= HEADER ================= */} + +
+

+ Our People +

+ +

+ Meet the Team +

+ +

+ Meet the people behind the ideas, technology and innovation. +

+
+ + {/* ================= TEAM GRID ================= */} + +
+
+ {membersWithDetails.map((member) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file From 70c081f1a8c1dabb5b0022b896146fa75d89bcf1 Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Mon, 17 Aug 2026 16:05:43 +0530 Subject: [PATCH 2/3] change --- src/components/TeamMemberCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TeamMemberCard.tsx b/src/components/TeamMemberCard.tsx index ec41302..2a2a9c4 100644 --- a/src/components/TeamMemberCard.tsx +++ b/src/components/TeamMemberCard.tsx @@ -52,7 +52,7 @@ export default function TeamMemberCard({ transition-all duration-700 ease-in-out - ${isHovered ? "grayscale-0 scale-105" : "grayscale"} + `} sizes=" (max-width: 640px) 100vw, From 0b40bb60fbd5599951eeb708870912f2769b17b7 Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Tue, 18 Aug 2026 17:15:45 +0530 Subject: [PATCH 3/3] made changes removed greyscale and update the the name --- src/components/TeamMemberCard.tsx | 59 ++++++++++++++------- src/pages/team/data/team_details.tsx | 45 ++++++++++++---- src/pages/team/index.tsx | 79 ++++++++++++++-------------- 3 files changed, 114 insertions(+), 69 deletions(-) diff --git a/src/components/TeamMemberCard.tsx b/src/components/TeamMemberCard.tsx index 2a2a9c4..96695e4 100644 --- a/src/components/TeamMemberCard.tsx +++ b/src/components/TeamMemberCard.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import { useState } from "react"; -import { Linkedin } from "lucide-react"; +import { Linkedin, Github } from "lucide-react"; interface TeamMember { name: string; @@ -14,7 +14,7 @@ interface TeamMember { socials?: { twitter?: string; linkedin?: string; - website?: string; + github?: string; }; } @@ -125,23 +125,44 @@ export default function TeamMemberCard({
)} - {member.socials?.linkedin && ( - - - -)} + {/* SOCIAL ICONS */} +
+ {member.socials?.github && ( + + + + )} + + {member.socials?.linkedin && ( + + + + )} +
diff --git a/src/pages/team/data/team_details.tsx b/src/pages/team/data/team_details.tsx index 0a034d5..f13ec19 100644 --- a/src/pages/team/data/team_details.tsx +++ b/src/pages/team/data/team_details.tsx @@ -5,56 +5,79 @@ export type TeamMemberDetails = { socials?: { twitter?: string; linkedin?: string; - website?: string; + github?: string; + }; }; +// Controls the exact order cards render in on the Team page. +// Add a name here to place it — anyone not listed falls to the end, +// sorted alphabetically. +export const teamOrder: string[] = [ + "Kartavya Bang", + "Shreyas Vij", + "Dhairya Sood", + "Manav Kohli", + "Ansh Agnihotry", +]; + export const teamDetails: Record = { - "Kartavya ": { + "Kartavya Bang": { role: "Faculty Advisor", description: "Guides the team with his experience, mentorship and vision while helping us build innovative robotics projects.", location: "ELECTRONICS AND COMMUNICATION", socials: { - linkedin: "#", + linkedin: "https://www.linkedin.com/in/kartavya-bang-0b5ba9288/", + github: "http://github.com/BangKartavya", + }, }, "Shreyas Vij": { role: "Technical Lead", - description: - "Works on everything expect robotics ", + description: "Works on everything expect robotics", location: "ELECTRONICS AND COMMUNICATION", socials: { linkedin: "https://www.linkedin.com/in/shreyas-vij-07b541328/", + github: "https://github.com/ShreyasVij", + }, }, - "Dhairya Sood": { + + "Dhairya Sood": { role: "Add role here", - description: "Works on robotics and ROS. Reliable hotspot provider. Password- 1234567889", + description: + "Works on robotics and ROS. Reliable hotspot provider. Password- 1234567889", location: "ELECTRONICS AND COMMUNICATION", socials: { linkedin: "https://www.linkedin.com/in/dhairya-sood/", + github: "https://github.com/DhairyaSood", + }, }, - "Manav Kohli": { role: "Add role here", - description: "Works on robotics and this site. Joined ACM. Made one joke. Became a former member. ", + description: + "Works on robotics and this site. His jokes may not be funny, but they may have funny consequences.", location: "ELECTRICAL", socials: { linkedin: "https://www.linkedin.com/in/manav-kohli/", + github: "https://github.com/Mysterious-Wizard", + }, }, "Ansh Agnihotry": { role: "Add role here", - description: "Works on robotics, embedded systems. Trying to guess his password? Just start with Hitler", + description: + "Works on robotics, embedded systems. Trying to guess his password? Just start with Hitler", location: "PRODUCTION", socials: { linkedin: "https://www.linkedin.com/in/ansh-agnihotry-b12145204/", + github: "https://github.com/anshagnihotry258", + }, }, - }; \ No newline at end of file diff --git a/src/pages/team/index.tsx b/src/pages/team/index.tsx index 4b35d92..0616bea 100644 --- a/src/pages/team/index.tsx +++ b/src/pages/team/index.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { getStorageImageUrl } from "@/lib/supabase/actions/storage.actions"; import { getTeamMembers } from "@/lib/supabase/actions/team.actions"; -import { teamDetails } from "./data/team_details"; +import { teamDetails, teamOrder } from "./data/team_details"; import TeamMemberCard from "../../components/TeamMemberCard"; @@ -25,48 +25,45 @@ type TeamMember = { export default function TeamPage() { const [teamMembers, setTeamMembers] = useState([]); - const [resolvedImageUrls, setResolvedImageUrls] = useState< - Record - >({}); + const [resolvedImageUrls, setResolvedImageUrls] = useState>({}); const [loadingMembers, setLoadingMembers] = useState(true); const [loadingImages, setLoadingImages] = useState(true); /* - * Add information from team_details.ts to the Supabase members. + * Merge Supabase rows with team_details.ts, then apply explicit order. * - * Supabase provides: - * - name - * - role - * - image - * - * team_details.ts provides: - * - description - * - location - * - socials + * Supabase provides: name, role (job title, e.g. "Website Subhead"), image + * team_details.ts provides: description, location, socials */ const membersWithDetails = useMemo(() => { - return teamMembers.map((member) => { + const merged = teamMembers.map((member) => { const details = teamDetails[member.name]; return { ...member, - + role: member.role, // straight from Supabase `role` column description: - member.description ?? - details?.description ?? - "Core member of the Robotics Society team.", - - location: - member.location ?? - details?.location, - + details?.description ?? "Core member of the Robotics Society team.", + location: details?.location, socials: { ...details?.socials, ...member.socials, }, }; }); + + return merged.sort((a, b) => { + const ai = teamOrder.indexOf(a.name); + const bi = teamOrder.indexOf(b.name); + + // Anyone not in teamOrder sorts after everyone who is, + // then alphabetically among themselves. + if (ai === -1 && bi === -1) return a.name.localeCompare(b.name); + if (ai === -1) return 1; + if (bi === -1) return -1; + return ai - bi; + }); }, [teamMembers]); /* @@ -86,19 +83,21 @@ export default function TeamPage() { } const formattedMembers: TeamMember[] = data.map( - (member: { - id: string; - firstName: string; - lastName: string; - role: string; - image: string; - }) => ({ - id: member.id, - name: `${member.firstName} ${member.lastName}`.trim(), - role: member.role, - image: member.image, - }) - ); + (member: { + id: string; + firstName: string; + lastName: string; + role: string; + image: string; + }) => ({ + id: member.id, + name: `${member.firstName} ${member.lastName}` + .replace(/\s+/g, " ") + .trim(), + role: member.role, + image: member.image, + }) +); setTeamMembers(formattedMembers); } catch (error) { @@ -162,7 +161,6 @@ export default function TeamPage() { return (
- {/* ================= HEADER ================= */}
@@ -195,7 +193,10 @@ export default function TeamPage() { {membersWithDetails.map((member) => ( ))}