From 2e7dca6dae76f0cbe1dd08caa8ae4380e37aaab2 Mon Sep 17 00:00:00 2001 From: anishalle Date: Tue, 7 Apr 2026 12:04:49 -0500 Subject: [PATCH 1/7] feat: constellations --- .gitignore | 3 + app/components/navbar/Navbar.tsx | 5 +- app/components/teams/Teams.tsx | 782 +++++++++++++++++++- app/components/teams/constellationLayout.ts | 238 ++++++ app/components/teams/sceneConfig.ts | 96 +++ app/data/constellation-templates.json | 237 ++++++ app/data/officer-teams.json | 277 +++++++ app/globals.css | 25 + package-lock.json | 15 +- 9 files changed, 1647 insertions(+), 31 deletions(-) create mode 100644 app/components/teams/constellationLayout.ts create mode 100644 app/components/teams/sceneConfig.ts create mode 100644 app/data/constellation-templates.json create mode 100644 app/data/officer-teams.json diff --git a/.gitignore b/.gitignore index d4e444b..9d243ae 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # claude .claude + +# local agent workspace +/.codex diff --git a/app/components/navbar/Navbar.tsx b/app/components/navbar/Navbar.tsx index 071924a..ba4ac21 100644 --- a/app/components/navbar/Navbar.tsx +++ b/app/components/navbar/Navbar.tsx @@ -57,13 +57,14 @@ export default function Navbar() { - {link.label} + {link.label} + ))} Gallery diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index cdba6bd..89224c3 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -1,22 +1,774 @@ -const teams = ["Marketing", "Tech"]; +"use client"; + +import type { CSSProperties } from "react"; +import { useEffect, useRef, useState } from "react"; +import { useIsMobile } from "@/app/hooks/useIsMobile"; +import { usePrefersReducedMotion } from "@/app/hooks/usePrefersReducedMotion"; +import { + ORDERED_OFFICER_TEAMS, + resolveConstellationLayout, + type OfficerTeam, + type ResolvedConstellationLayout, +} from "./constellationLayout"; +import { + getDesktopConstellationBox, + type ConstellationBox, + TEAM_CLUSTER_BOX, + TEAMS_BACKGROUND_STARS, + TEAMS_COPY, + TEAMS_LAYOUT, + TEAMS_SCROLL, +} from "./sceneConfig"; + +type ActiveNodeState = { + teamId: string; + personId: string; +} | null; + +function getInitials(name: string) { + const segments = name + .trim() + .split(/\s+/) + .filter(Boolean); + + return segments + .slice(0, 2) + .map((segment) => segment[0]?.toUpperCase() ?? "") + .join(""); +} + +function getMemberCountLabel(memberCount: number) { + return `${memberCount} MEMBERS`; +} + +function getTeamOpacity(activeTeamId: string | null, teamId: string) { + if (!activeTeamId || activeTeamId === teamId) { + return 1; + } + + return 0.25; +} + +function getTooltipPlacement( + nodeX: number, + nodeY: number, + box: ConstellationBox, +) { + let horizontal = "left-1/2 -translate-x-1/2"; + + if (nodeX < box.width * 0.18) { + horizontal = "left-0 translate-x-0"; + } else if (nodeX > box.width * 0.82) { + horizontal = "right-0 translate-x-0"; + } + + const vertical = + nodeY < box.height * 0.34 ? "top-full mt-4" : "bottom-full mb-4"; + + return `${vertical} ${horizontal}`; +} + +function areBoxesEqual(left: ConstellationBox, right: ConstellationBox) { + return ( + left.width === right.width && + left.height === right.height && + left.padding === right.padding && + left.verticalBias === right.verticalBias && + left.leadNodeSize === right.leadNodeSize && + left.nodeSize === right.nodeSize + ); +} + +function TeamConstellation({ + layout, + box, + activeTeamId, + setActiveTeamId, + activeNode, + openNode, + clearTooltipClose, + scheduleTooltipClose, + interactive, + showCaption = true, +}: { + layout: ResolvedConstellationLayout; + box: ConstellationBox; + activeTeamId: string | null; + setActiveTeamId: (teamId: string | null) => void; + activeNode: ActiveNodeState; + openNode: (teamId: string, personId: string) => void; + clearTooltipClose: () => void; + scheduleTooltipClose: () => void; + interactive: boolean; + showCaption?: boolean; +}) { + const teamOpacity = getTeamOpacity(activeTeamId, layout.team.id); + const memberCountLabel = getMemberCountLabel(layout.nodes.length); + const graphBounds = layout.nodes.reduce( + (bounds, node) => { + const radius = (node.isLead ? box.leadNodeSize : box.nodeSize) / 2 + 6; + + return { + minX: Math.min(bounds.minX, node.renderX - radius), + maxX: Math.max(bounds.maxX, node.renderX + radius), + }; + }, + { + minX: Number.POSITIVE_INFINITY, + maxX: Number.NEGATIVE_INFINITY, + }, + ); + const graphWidth = graphBounds.maxX - graphBounds.minX; + const captionWidth = 240; + const layoutWidth = Math.max(graphWidth + 16, captionWidth); + const graphOffsetX = (layoutWidth - graphWidth) / 2 - graphBounds.minX; + const shiftedNodes = layout.nodes.map((node) => ({ + ...node, + shiftedX: node.renderX + graphOffsetX, + })); + const nodeMap = new Map(shiftedNodes.map((node) => [node.id, node])); + + const trimEdge = (fromId: string, toId: string) => { + const fromNode = nodeMap.get(fromId); + const toNode = nodeMap.get(toId); + + if (!fromNode || !toNode) { + return null; + } + + const dx = toNode.renderX - fromNode.renderX; + const dy = toNode.renderY - fromNode.renderY; + const distance = Math.hypot(dx, dy) || 1; + const fromRadius = + (fromNode.isLead ? box.leadNodeSize : box.nodeSize) / 2 + 3; + const toRadius = (toNode.isLead ? box.leadNodeSize : box.nodeSize) / 2 + 3; + + return { + x1: fromNode.shiftedX + (dx / distance) * fromRadius, + y1: fromNode.renderY + (dy / distance) * fromRadius, + x2: toNode.shiftedX - (dx / distance) * toRadius, + y2: toNode.renderY - (dy / distance) * toRadius, + }; + }; + + return ( +
{ + clearTooltipClose(); + setActiveTeamId(layout.team.id); + } + : undefined + } + onMouseLeave={interactive ? scheduleTooltipClose : undefined} + > +
+ + + {shiftedNodes.map((node) => { + const isActive = + activeNode?.teamId === layout.team.id && + activeNode.personId === node.person.id; + const tooltipPlacement = getTooltipPlacement( + node.shiftedX, + node.renderY, + { ...box, width: layoutWidth }, + ); + const nodePositionStyle: CSSProperties = { + left: `${node.shiftedX}px`, + top: `${node.renderY}px`, + zIndex: isActive ? 30 : node.isLead ? 12 : 8, + }; + const nodeButtonStyle: CSSProperties = { + width: node.isLead ? `${box.leadNodeSize}px` : `${box.nodeSize}px`, + height: node.isLead ? `${box.leadNodeSize}px` : `${box.nodeSize}px`, + boxShadow: node.isLead + ? "0 0 0 8px rgba(243, 22, 103, 0.08)" + : "none", + }; + const nodeLabelStyle: CSSProperties = { + fontSize: node.isLead + ? `${Math.round(box.leadNodeSize * 0.42)}px` + : `${Math.round(box.nodeSize * 0.46)}px`, + }; + + return ( +
+ {isActive ? ( +
{ + clearTooltipClose(); + setActiveTeamId(layout.team.id); + }} + onMouseLeave={scheduleTooltipClose} + onFocusCapture={() => { + clearTooltipClose(); + setActiveTeamId(layout.team.id); + }} + onBlurCapture={(event) => { + const relatedTarget = event.relatedTarget; + + if ( + relatedTarget instanceof Node && + event.currentTarget.contains(relatedTarget) + ) { + return; + } + + scheduleTooltipClose(); + }} + > +

+ {node.person.name} +

+

+ {node.person.role} +

+ + LinkedIn + +
+ ) : null} + + +
+ ); + })} +
+ + {showCaption ? ( +
+

+ {layout.template.name} +

+

+ {layout.team.label} +

+

+ {memberCountLabel} +

+
+ ) : null} +
+ ); +} + +function MobileTeamCard({ + layout, + box, + activeNode, + setActiveTeamId, + openNode, + clearTooltipClose, + scheduleTooltipClose, +}: { + layout: ResolvedConstellationLayout; + box: ConstellationBox; + activeNode: ActiveNodeState; + setActiveTeamId: (teamId: string | null) => void; + openNode: (teamId: string, personId: string) => void; + clearTooltipClose: () => void; + scheduleTooltipClose: () => void; +}) { + const memberCountLabel = getMemberCountLabel(layout.nodes.length); + + return ( +
+

+ HACKUTD +

+

+ {layout.template.name} +

+

{layout.team.label}

+

+ {memberCountLabel} +

+
+ +
+
+ ); +} + +function buildLayouts(teams: OfficerTeam[], box: ConstellationBox) { + return teams.map((team) => + resolveConstellationLayout( + team, + box.width, + box.height, + box.padding, + box.verticalBias, + ), + ); +} + +function TeamSeparator() { + return ( + + ); +} export default function Teams() { + const sectionRef = useRef(null); + const trackViewportRef = useRef(null); + const trackRef = useRef(null); + const tooltipCloseTimeoutRef = useRef(null); + const isMobile = useIsMobile(); + const prefersReducedMotion = usePrefersReducedMotion(); + const [desktopBox, setDesktopBox] = useState( + TEAM_CLUSTER_BOX.desktop, + ); + const [activeTeamId, setActiveTeamId] = useState(null); + const [activeNode, setActiveNode] = useState(null); + + useEffect(() => { + if (isMobile) { + return; + } + + const trackViewport = trackViewportRef.current; + + if (!trackViewport) { + return; + } + + const updateDesktopBox = () => { + const nextBox = getDesktopConstellationBox( + trackViewport.offsetWidth, + window.innerHeight, + ); + + setDesktopBox((currentBox) => + areBoxesEqual(currentBox, nextBox) ? currentBox : nextBox, + ); + }; + + updateDesktopBox(); + + const resizeObserver = new ResizeObserver(updateDesktopBox); + resizeObserver.observe(trackViewport); + window.addEventListener("resize", updateDesktopBox); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener("resize", updateDesktopBox); + }; + }, [isMobile]); + + useEffect(() => { + if (isMobile || prefersReducedMotion) { + return; + } + + const section = sectionRef.current; + const trackViewport = trackViewportRef.current; + const track = trackRef.current; + + if (!section || !trackViewport || !track) { + return; + } + + let frame = 0; + let currentX = 0; + let targetX = 0; + let maxTranslate = 0; + + const clamp = (value: number, min: number, max: number) => + Math.min(Math.max(value, min), max); + + const updateTarget = () => { + const scrollableDistance = Math.max( + section.offsetHeight - window.innerHeight, + 1, + ); + const progress = clamp( + (window.scrollY - section.offsetTop) / scrollableDistance, + 0, + 1, + ); + targetX = progress * maxTranslate; + + if (progress <= 0.001 || progress >= 0.999) { + currentX = targetX; + track.style.transform = `translate3d(${-currentX}px, 0, 0)`; + return; + } + + queueRender(); + }; + + const updateMetrics = () => { + maxTranslate = Math.max(track.scrollWidth - trackViewport.offsetWidth, 0); + updateTarget(); + }; + + const renderTrack = () => { + currentX += (targetX - currentX) * TEAMS_SCROLL.smoothing; + + if (Math.abs(targetX - currentX) < 0.12) { + currentX = targetX; + } + + track.style.transform = `translate3d(${-currentX}px, 0, 0)`; + + if (currentX !== targetX) { + frame = window.requestAnimationFrame(renderTrack); + return; + } + + frame = 0; + }; + + const queueRender = () => { + if (frame !== 0) { + return; + } + + frame = window.requestAnimationFrame(renderTrack); + }; + + updateMetrics(); + + const resizeObserver = new ResizeObserver(updateMetrics); + resizeObserver.observe(trackViewport); + resizeObserver.observe(track); + window.addEventListener("scroll", updateTarget, { passive: true }); + window.addEventListener("resize", updateMetrics); + + return () => { + if (frame !== 0) { + window.cancelAnimationFrame(frame); + } + + resizeObserver.disconnect(); + window.removeEventListener("scroll", updateTarget); + window.removeEventListener("resize", updateMetrics); + track.style.transform = ""; + }; + }, [desktopBox.height, desktopBox.width, isMobile, prefersReducedMotion]); + + useEffect(() => { + return () => { + if (tooltipCloseTimeoutRef.current !== null) { + window.clearTimeout(tooltipCloseTimeoutRef.current); + } + }; + }, []); + + const clearTooltipClose = () => { + if (tooltipCloseTimeoutRef.current === null) { + return; + } + + window.clearTimeout(tooltipCloseTimeoutRef.current); + tooltipCloseTimeoutRef.current = null; + }; + + const scheduleTooltipClose = () => { + clearTooltipClose(); + tooltipCloseTimeoutRef.current = window.setTimeout(() => { + setActiveNode(null); + setActiveTeamId(null); + tooltipCloseTimeoutRef.current = null; + }, TEAMS_SCROLL.tooltipCloseDelayMs); + }; + + const openNode = (teamId: string, personId: string) => { + clearTooltipClose(); + setActiveTeamId(teamId); + setActiveNode({ teamId, personId }); + }; + + const desktopLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, desktopBox); + const mobileLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, TEAM_CLUSTER_BOX.mobile); + + if (isMobile) { + return ( +
+ + +
+

+ {TEAMS_COPY.eyebrow} +

+

+ {TEAMS_COPY.heading[0]} +
+ {TEAMS_COPY.heading[1]} +

+ +
+ {mobileLayouts.map((layout) => ( + + ))} +
+
+
+ ); + } + + if (prefersReducedMotion) { + return ( +
+ + +
+

+ {TEAMS_COPY.eyebrow} +

+

+ {TEAMS_COPY.heading[0]} +
+ {TEAMS_COPY.heading[1]} +

+ +
+ {desktopLayouts.map((layout) => ( +
+ +
+ ))} +
+
+
+ ); + } + return ( -
-

- The -
- Constellation -

- -
- {teams.map((team) => ( -
-

{team}

- {/* Team member nodes will go here */} -
+
+
+
); diff --git a/app/components/teams/constellationLayout.ts b/app/components/teams/constellationLayout.ts new file mode 100644 index 0000000..1bfd354 --- /dev/null +++ b/app/components/teams/constellationLayout.ts @@ -0,0 +1,238 @@ +import constellationTemplatesData from "@/app/data/constellation-templates.json"; +import officerTeamsData from "@/app/data/officer-teams.json"; + +export type OfficerMember = { + id: string; + name: string; + role: string; + linkedinUrl: string; +}; + +export type OfficerTeam = { + id: string; + label: string; + order: number; + lead: OfficerMember; + members: OfficerMember[]; +}; + +export type ConstellationNode = { + id: string; + x: number; + y: number; +}; + +export type ConstellationEdge = { + fromId: string; + toId: string; +}; + +export type ConstellationAppendRule = { + fromNodeId: string; + toNodeId: string; + stepX: number; + stepY: number; +}; + +export type ConstellationTemplate = { + id: string; + name: string; + starCount: number; + leadNodeId: string; + nodes: ConstellationNode[]; + edges: ConstellationEdge[]; + append: ConstellationAppendRule; +}; + +export type ResolvedOfficerNode = ConstellationNode & { + person: OfficerMember; + isLead: boolean; + isOverflow: boolean; +}; + +export type RenderedConstellationEdge = { + fromId: string; + toId: string; + from: { x: number; y: number }; + to: { x: number; y: number }; +}; + +export type RenderedOfficerNode = ResolvedOfficerNode & { + renderX: number; + renderY: number; +}; + +export type ResolvedConstellationLayout = { + team: OfficerTeam; + template: ConstellationTemplate; + nodes: RenderedOfficerNode[]; + edges: RenderedConstellationEdge[]; +}; + +const officerTeams = officerTeamsData as OfficerTeam[]; +const constellationTemplates = constellationTemplatesData as ConstellationTemplate[]; + +export const ORDERED_OFFICER_TEAMS = officerTeams + .slice() + .sort((left, right) => left.order - right.order); + +function hashString(input: string) { + let hash = 0; + + for (const character of input) { + hash = (hash * 31 + character.charCodeAt(0)) >>> 0; + } + + return hash; +} + +function pickTemplate(teamId: string, peopleCount: number) { + const availableCounts = Array.from( + new Set(constellationTemplates.map((template) => template.starCount)), + ).sort((left, right) => left - right); + + const chosenCount = + availableCounts.filter((count) => count <= peopleCount).at(-1) ?? + availableCounts[0]; + const candidates = constellationTemplates.filter( + (template) => template.starCount === chosenCount, + ); + + return candidates[hashString(teamId) % candidates.length]; +} + +function getAssignmentOrder(template: ConstellationTemplate) { + return [ + template.leadNodeId, + ...template.nodes + .map((node) => node.id) + .filter((nodeId) => nodeId !== template.leadNodeId), + ]; +} + +function fitNodesToBox( + nodes: ResolvedOfficerNode[], + width: number, + height: number, + padding: number, + verticalBias: number, +) { + const minX = Math.min(...nodes.map((node) => node.x)); + const maxX = Math.max(...nodes.map((node) => node.x)); + const minY = Math.min(...nodes.map((node) => node.y)); + const maxY = Math.max(...nodes.map((node) => node.y)); + const rawWidth = Math.max(maxX - minX, 0.01); + const rawHeight = Math.max(maxY - minY, 0.01); + const scale = Math.min( + (width - padding * 2) / rawWidth, + (height - padding * 2) / rawHeight, + ); + const contentWidth = rawWidth * scale; + const contentHeight = rawHeight * scale; + const offsetX = (width - contentWidth) / 2; + const centeredOffsetY = (height - contentHeight) / 2; + const offsetY = Math.min( + centeredOffsetY + verticalBias, + height - contentHeight - padding * 0.4, + ); + + return nodes.map((node) => ({ + ...node, + renderX: (node.x - minX) * scale + offsetX, + renderY: (node.y - minY) * scale + offsetY, + })); +} + +export function resolveConstellationLayout( + team: OfficerTeam, + width: number, + height: number, + padding: number, + verticalBias = 0, +): ResolvedConstellationLayout { + const people = [team.lead, ...team.members]; + const template = pickTemplate(team.id, people.length); + const assignmentOrder = getAssignmentOrder(template); + const selectedNodeIds = assignmentOrder.slice( + 0, + Math.min(people.length, template.nodes.length), + ); + const selectedNodeIdSet = new Set(selectedNodeIds); + const resolvedNodes: ResolvedOfficerNode[] = template.nodes + .filter((node) => selectedNodeIdSet.has(node.id)) + .map((node) => ({ + ...node, + person: + people[ + selectedNodeIds.findIndex((selectedNodeId) => selectedNodeId === node.id) + ], + isLead: node.id === template.leadNodeId, + isOverflow: false, + })); + const resolvedEdges: ConstellationEdge[] = template.edges.filter( + (edge) => + selectedNodeIdSet.has(edge.fromId) && selectedNodeIdSet.has(edge.toId), + ); + + if (people.length > template.nodes.length) { + const anchorNode = template.nodes.find( + (node) => node.id === template.append.toNodeId, + ); + + if (anchorNode) { + let previousNodeId = anchorNode.id; + + for ( + let overflowIndex = 0; + overflowIndex < people.length - template.nodes.length; + overflowIndex += 1 + ) { + const person = people[template.nodes.length + overflowIndex]; + const overflowNodeId = `overflow-${overflowIndex + 1}`; + resolvedNodes.push({ + id: overflowNodeId, + x: anchorNode.x + template.append.stepX * (overflowIndex + 1), + y: anchorNode.y + template.append.stepY * (overflowIndex + 1), + person, + isLead: false, + isOverflow: true, + }); + resolvedEdges.push({ + fromId: previousNodeId, + toId: overflowNodeId, + }); + previousNodeId = overflowNodeId; + } + } + } + + const renderedNodes = fitNodesToBox( + resolvedNodes, + width, + height, + padding, + verticalBias, + ); + const nodeMap = new Map( + renderedNodes.map((node) => [node.id, { x: node.renderX, y: node.renderY }]), + ); + const renderedEdges = resolvedEdges + .map((edge) => { + const from = nodeMap.get(edge.fromId); + const to = nodeMap.get(edge.toId); + + if (!from || !to) { + return null; + } + + return { fromId: edge.fromId, toId: edge.toId, from, to }; + }) + .filter((edge): edge is RenderedConstellationEdge => edge !== null); + + return { + team, + template, + nodes: renderedNodes, + edges: renderedEdges, + }; +} diff --git a/app/components/teams/sceneConfig.ts b/app/components/teams/sceneConfig.ts new file mode 100644 index 0000000..2e5475f --- /dev/null +++ b/app/components/teams/sceneConfig.ts @@ -0,0 +1,96 @@ +export type AmbientStar = { + id: number; + top: number; + left: number; + size: number; + opacity: number; +}; + +export type ConstellationBox = { + width: number; + height: number; + padding: number; + verticalBias: number; + leadNodeSize: number; + nodeSize: number; +}; + +export const TEAMS_COPY = { + eyebrow: "HACKUTD", + heading: ["The", "Constellation"], +} as const; + +export const TEAMS_LAYOUT = { + desktopSectionMinHeight: "min-h-[560vh]", + mobileSectionPadding: "px-5 py-24 sm:px-6", + desktopViewportHeight: "h-[100svh] md:h-screen", + desktopContainer: "mx-auto flex h-full w-full max-w-[1800px] items-center gap-8 px-5 md:px-8 lg:gap-10 lg:px-12", + introWidth: "w-[240px] shrink-0 lg:w-[300px]", + desktopTrackViewport: "relative min-w-0 flex-1 overflow-x-hidden overflow-y-visible", +} as const; + +export const TEAMS_SCROLL = { + smoothing: 0.22, + desktopGap: 10, + desktopTrailingSpace: 180, + desktopPeekWidth: 150, + separatorWidth: 170, + firstConstellationOffset: 320, + tooltipCloseDelayMs: 140, +} as const; + +export const TEAM_CLUSTER_BOX = { + desktop: { + width: 980, + height: 380, + padding: 42, + verticalBias: 22, + leadNodeSize: 72, + nodeSize: 50, + }, + mobile: { + width: 272, + height: 208, + padding: 24, + verticalBias: 10, + leadNodeSize: 52, + nodeSize: 38, + }, +} as const; + +function clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +export function getDesktopConstellationBox( + trackViewportWidth: number, + viewportHeight: number, +): ConstellationBox { + return { + width: clamp(trackViewportWidth * 0.78, 760, 1240), + height: clamp(viewportHeight * 0.5, 340, 500), + padding: clamp(trackViewportWidth * 0.028, 34, 52), + verticalBias: 22, + leadNodeSize: 72, + nodeSize: 50, + }; +} + +function createAmbientStars(count: number): AmbientStar[] { + let seed = 71; + + const next = () => { + seed = (seed * 48271) % 2147483647; + return seed / 2147483647; + }; + + return Array.from({ length: count }, (_, index) => ({ + id: index, + top: 8 + next() * 82, + left: 2 + next() * 96, + size: 1 + next() * 2.8, + opacity: 0.16 + next() * 0.4, + })); +} + +export const TEAMS_BACKGROUND_STARS = createAmbientStars(34); diff --git a/app/data/constellation-templates.json b/app/data/constellation-templates.json new file mode 100644 index 0000000..3d43977 --- /dev/null +++ b/app/data/constellation-templates.json @@ -0,0 +1,237 @@ +[ + { + "id": "cassiopeia", + "name": "Cassiopeia", + "starCount": 5, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.48, "y": 0.12 }, + { "id": "n1", "x": 0.16, "y": 0.3 }, + { "id": "n2", "x": 0.04, "y": 0.66 }, + { "id": "n3", "x": 0.7, "y": 0.44 }, + { "id": "n4", "x": 0.96, "y": 0.72 } + ], + "edges": [ + { "fromId": "n1", "toId": "n0" }, + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" } + ], + "append": { + "fromNodeId": "n3", + "toNodeId": "n4", + "stepX": 0.16, + "stepY": 0.08 + } + }, + { + "id": "lyra", + "name": "Lyra", + "starCount": 5, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.64, "y": 0.1 }, + { "id": "n1", "x": 0.28, "y": 0.32 }, + { "id": "n2", "x": 0.68, "y": 0.46 }, + { "id": "n3", "x": 0.3, "y": 0.76 }, + { "id": "n4", "x": 0.78, "y": 0.88 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n2", "toId": "n4" } + ], + "append": { + "fromNodeId": "n2", + "toNodeId": "n4", + "stepX": 0.12, + "stepY": 0.1 + } + }, + { + "id": "delphinus", + "name": "Delphinus", + "starCount": 5, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.52, "y": 0.1 }, + { "id": "n1", "x": 0.18, "y": 0.4 }, + { "id": "n2", "x": 0.5, "y": 0.5 }, + { "id": "n3", "x": 0.82, "y": 0.42 }, + { "id": "n4", "x": 0.64, "y": 0.86 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n0" }, + { "fromId": "n2", "toId": "n4" } + ], + "append": { + "fromNodeId": "n2", + "toNodeId": "n4", + "stepX": 0.08, + "stepY": 0.16 + } + }, + { + "id": "orion", + "name": "Orion", + "starCount": 7, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.24, "y": 0.1 }, + { "id": "n1", "x": 0.76, "y": 0.18 }, + { "id": "n2", "x": 0.4, "y": 0.36 }, + { "id": "n3", "x": 0.5, "y": 0.48 }, + { "id": "n4", "x": 0.6, "y": 0.58 }, + { "id": "n5", "x": 0.28, "y": 0.82 }, + { "id": "n6", "x": 0.78, "y": 0.9 } + ], + "edges": [ + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n1", "toId": "n4" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n2", "toId": "n5" }, + { "fromId": "n4", "toId": "n6" } + ], + "append": { + "fromNodeId": "n4", + "toNodeId": "n6", + "stepX": 0.12, + "stepY": 0.12 + } + }, + { + "id": "aquila", + "name": "Aquila", + "starCount": 8, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.5, "y": 0.08 }, + { "id": "n1", "x": 0.16, "y": 0.24 }, + { "id": "n2", "x": 0.34, "y": 0.34 }, + { "id": "n3", "x": 0.66, "y": 0.38 }, + { "id": "n4", "x": 0.86, "y": 0.24 }, + { "id": "n5", "x": 0.42, "y": 0.58 }, + { "id": "n6", "x": 0.56, "y": 0.72 }, + { "id": "n7", "x": 0.68, "y": 0.92 } + ], + "edges": [ + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n0" }, + { "fromId": "n0", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n2", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n6", "toId": "n7" }, + { "fromId": "n3", "toId": "n6" } + ], + "append": { + "fromNodeId": "n6", + "toNodeId": "n7", + "stepX": 0.1, + "stepY": 0.14 + } + }, + { + "id": "cepheus", + "name": "Cepheus", + "starCount": 8, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.56, "y": 0.08 }, + { "id": "n1", "x": 0.28, "y": 0.28 }, + { "id": "n2", "x": 0.76, "y": 0.28 }, + { "id": "n3", "x": 0.12, "y": 0.54 }, + { "id": "n4", "x": 0.68, "y": 0.56 }, + { "id": "n5", "x": 0.3, "y": 0.82 }, + { "id": "n6", "x": 0.58, "y": 0.84 }, + { "id": "n7", "x": 0.94, "y": 0.58 } + ], + "edges": [ + { "fromId": "n1", "toId": "n0" }, + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n1", "toId": "n3" }, + { "fromId": "n3", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n6", "toId": "n4" }, + { "fromId": "n4", "toId": "n2" }, + { "fromId": "n4", "toId": "n7" } + ], + "append": { + "fromNodeId": "n4", + "toNodeId": "n7", + "stepX": 0.14, + "stepY": 0.04 + } + }, + { + "id": "cygnus", + "name": "Cygnus", + "starCount": 9, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.5, "y": 0.06 }, + { "id": "n1", "x": 0.5, "y": 0.24 }, + { "id": "n2", "x": 0.5, "y": 0.44 }, + { "id": "n3", "x": 0.5, "y": 0.68 }, + { "id": "n4", "x": 0.5, "y": 0.92 }, + { "id": "n5", "x": 0.18, "y": 0.42 }, + { "id": "n6", "x": 0.32, "y": 0.5 }, + { "id": "n7", "x": 0.68, "y": 0.5 }, + { "id": "n8", "x": 0.82, "y": 0.42 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n6", "toId": "n2" }, + { "fromId": "n2", "toId": "n7" }, + { "fromId": "n7", "toId": "n8" } + ], + "append": { + "fromNodeId": "n3", + "toNodeId": "n4", + "stepX": 0, + "stepY": 0.14 + } + }, + { + "id": "andromeda", + "name": "Andromeda", + "starCount": 9, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.18, "y": 0.18 }, + { "id": "n1", "x": 0.34, "y": 0.3 }, + { "id": "n2", "x": 0.5, "y": 0.42 }, + { "id": "n3", "x": 0.68, "y": 0.54 }, + { "id": "n4", "x": 0.88, "y": 0.68 }, + { "id": "n5", "x": 0.32, "y": 0.56 }, + { "id": "n6", "x": 0.44, "y": 0.72 }, + { "id": "n7", "x": 0.62, "y": 0.84 }, + { "id": "n8", "x": 0.82, "y": 0.92 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n1", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n6", "toId": "n7" }, + { "fromId": "n7", "toId": "n8" } + ], + "append": { + "fromNodeId": "n7", + "toNodeId": "n8", + "stepX": 0.12, + "stepY": 0.08 + } + } +] diff --git a/app/data/officer-teams.json b/app/data/officer-teams.json new file mode 100644 index 0000000..3a8fbcb --- /dev/null +++ b/app/data/officer-teams.json @@ -0,0 +1,277 @@ +[ + { + "id": "marketing", + "label": "Marketing", + "order": 0, + "lead": { + "id": "marketing-lead", + "name": "Olivia Tran", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/olivia-tran-hackutd" + }, + "members": [ + { + "id": "marketing-1", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd" + }, + { + "id": "marketing-2", + "name": "Ethan Nguyen", + "role": "Brand Design", + "linkedinUrl": "https://www.linkedin.com/in/ethan-nguyen-hackutd" + }, + { + "id": "marketing-3", + "name": "Sofia Ramirez", + "role": "Social Media", + "linkedinUrl": "https://www.linkedin.com/in/sofia-ramirez-hackutd" + }, + { + "id": "marketing-4", + "name": "Jordan Kim", + "role": "Photo + Video", + "linkedinUrl": "https://www.linkedin.com/in/jordan-kim-hackutd" + }, + { + "id": "marketing-5", + "name": "Avery Brooks", + "role": "Copywriting", + "linkedinUrl": "https://www.linkedin.com/in/avery-brooks-hackutd" + } + ] + }, + { + "id": "tech", + "label": "Tech", + "order": 1, + "lead": { + "id": "tech-lead", + "name": "Noah Chen", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/noah-chen-hackutd" + }, + "members": [ + { + "id": "tech-1", + "name": "Aiden Williams", + "role": "Platform Engineering", + "linkedinUrl": "https://www.linkedin.com/in/aiden-williams-hackutd" + }, + { + "id": "tech-2", + "name": "Priya Shah", + "role": "Infra + DevOps", + "linkedinUrl": "https://www.linkedin.com/in/priya-shah-hackutd" + }, + { + "id": "tech-3", + "name": "Lucas Martin", + "role": "Registration Systems", + "linkedinUrl": "https://www.linkedin.com/in/lucas-martin-hackutd" + }, + { + "id": "tech-4", + "name": "Grace Lee", + "role": "Website Experience", + "linkedinUrl": "https://www.linkedin.com/in/grace-lee-hackutd" + }, + { + "id": "tech-5", + "name": "Mateo Garcia", + "role": "Internal Tooling", + "linkedinUrl": "https://www.linkedin.com/in/mateo-garcia-hackutd" + }, + { + "id": "tech-6", + "name": "Chloe Nguyen", + "role": "Security + Reliability", + "linkedinUrl": "https://www.linkedin.com/in/chloe-nguyen-hackutd" + }, + { + "id": "tech-7", + "name": "Arjun Mehta", + "role": "Automation", + "linkedinUrl": "https://www.linkedin.com/in/arjun-mehta-hackutd" + } + ] + }, + { + "id": "industry", + "label": "Industry", + "order": 2, + "lead": { + "id": "industry-lead", + "name": "Leah Johnson", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/leah-johnson-hackutd" + }, + "members": [ + { + "id": "industry-1", + "name": "Ryan Park", + "role": "Sponsor Outreach", + "linkedinUrl": "https://www.linkedin.com/in/ryan-park-hackutd" + }, + { + "id": "industry-2", + "name": "Anika Singh", + "role": "Partner Success", + "linkedinUrl": "https://www.linkedin.com/in/anika-singh-hackutd" + }, + { + "id": "industry-3", + "name": "Daniel Scott", + "role": "Relationships", + "linkedinUrl": "https://www.linkedin.com/in/daniel-scott-hackutd" + }, + { + "id": "industry-4", + "name": "Harper Wilson", + "role": "Prospecting", + "linkedinUrl": "https://www.linkedin.com/in/harper-wilson-hackutd" + }, + { + "id": "industry-5", + "name": "Kevin Flores", + "role": "Deliverables", + "linkedinUrl": "https://www.linkedin.com/in/kevin-flores-hackutd" + }, + { + "id": "industry-6", + "name": "Isabella Moore", + "role": "Stewardship", + "linkedinUrl": "https://www.linkedin.com/in/isabella-moore-hackutd" + }, + { + "id": "industry-7", + "name": "Nathan Davis", + "role": "Sponsorship Ops", + "linkedinUrl": "https://www.linkedin.com/in/nathan-davis-hackutd" + }, + { + "id": "industry-8", + "name": "Mila Brown", + "role": "Activation Planning", + "linkedinUrl": "https://www.linkedin.com/in/mila-brown-hackutd" + } + ] + }, + { + "id": "finance", + "label": "Finance", + "order": 3, + "lead": { + "id": "finance-lead", + "name": "Elena Martinez", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/elena-martinez-hackutd" + }, + "members": [ + { + "id": "finance-1", + "name": "Caleb Adams", + "role": "Budgeting", + "linkedinUrl": "https://www.linkedin.com/in/caleb-adams-hackutd" + }, + { + "id": "finance-2", + "name": "Sarah Murphy", + "role": "Procurement", + "linkedinUrl": "https://www.linkedin.com/in/sarah-murphy-hackutd" + }, + { + "id": "finance-3", + "name": "Jason Perez", + "role": "Reimbursements", + "linkedinUrl": "https://www.linkedin.com/in/jason-perez-hackutd" + }, + { + "id": "finance-4", + "name": "Emma Torres", + "role": "Forecasting", + "linkedinUrl": "https://www.linkedin.com/in/emma-torres-hackutd" + }, + { + "id": "finance-5", + "name": "Benjamin Cox", + "role": "Vendor Payments", + "linkedinUrl": "https://www.linkedin.com/in/benjamin-cox-hackutd" + }, + { + "id": "finance-6", + "name": "Lily Foster", + "role": "Ops Accounting", + "linkedinUrl": "https://www.linkedin.com/in/lily-foster-hackutd" + } + ] + }, + { + "id": "experience", + "label": "Experience", + "order": 4, + "lead": { + "id": "experience-lead", + "name": "Marcus Rivera", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/marcus-rivera-hackutd" + }, + "members": [ + { + "id": "experience-1", + "name": "Zoe Hernandez", + "role": "Operations", + "linkedinUrl": "https://www.linkedin.com/in/zoe-hernandez-hackutd" + }, + { + "id": "experience-2", + "name": "Adam Walker", + "role": "Volunteer Coordination", + "linkedinUrl": "https://www.linkedin.com/in/adam-walker-hackutd" + }, + { + "id": "experience-3", + "name": "Nina Robinson", + "role": "Check-In Flow", + "linkedinUrl": "https://www.linkedin.com/in/nina-robinson-hackutd" + }, + { + "id": "experience-4", + "name": "Tyler Hall", + "role": "Venue Logistics", + "linkedinUrl": "https://www.linkedin.com/in/tyler-hall-hackutd" + }, + { + "id": "experience-5", + "name": "Claire Lewis", + "role": "Swag + Merch", + "linkedinUrl": "https://www.linkedin.com/in/claire-lewis-hackutd" + }, + { + "id": "experience-6", + "name": "Isaac Young", + "role": "Food + Hospitality", + "linkedinUrl": "https://www.linkedin.com/in/isaac-young-hackutd" + }, + { + "id": "experience-7", + "name": "Julia King", + "role": "Community Care", + "linkedinUrl": "https://www.linkedin.com/in/julia-king-hackutd" + }, + { + "id": "experience-8", + "name": "Henry Wright", + "role": "Room Flow", + "linkedinUrl": "https://www.linkedin.com/in/henry-wright-hackutd" + }, + { + "id": "experience-9", + "name": "Stella Green", + "role": "Late-Night Programming", + "linkedinUrl": "https://www.linkedin.com/in/stella-green-hackutd" + } + ] + } +] diff --git a/app/globals.css b/app/globals.css index 8b2cfb1..e7cb4cf 100644 --- a/app/globals.css +++ b/app/globals.css @@ -40,6 +40,23 @@ body { } } +@keyframes constellation-lead-pulse { + 0%, + 100% { + transform: scale(1); + box-shadow: + 0 0 0 4px rgba(243, 22, 103, 0.16), + 0 0 16px rgba(243, 22, 103, 0.42); + } + + 50% { + transform: scale(1.12); + box-shadow: + 0 0 0 8px rgba(243, 22, 103, 0.08), + 0 0 30px rgba(243, 22, 103, 0.68); + } +} + .hero-star { box-shadow: 0 0 8px rgba(255, 162, 31, 0.45); clip-path: polygon( @@ -55,10 +72,18 @@ body { animation: hero-star-twinkle 4s ease-in-out infinite; } +.constellation-lead { + animation: constellation-lead-pulse 4.8s ease-in-out infinite; +} + @media (prefers-reduced-motion: reduce) { .hero-star { animation: none; } + + .constellation-lead { + animation: none; + } } @media (max-width: 767px) { diff --git a/package-lock.json b/package-lock.json index 8f84a5d..4b315b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1574,7 +1573,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1634,7 +1632,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -2160,7 +2157,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2504,7 +2500,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3073,7 +3068,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3259,7 +3253,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3838,8 +3831,7 @@ "version": "3.14.2", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license.", - "peer": true + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/has-bigints": { "version": "1.1.0", @@ -5464,7 +5456,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5474,7 +5465,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6163,7 +6153,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6326,7 +6315,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6602,7 +6590,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 6f1e8a0dffcd46de4d0461a1ae3da439f3ddd628 Mon Sep 17 00:00:00 2001 From: Sreevasan Date: Thu, 9 Apr 2026 18:57:03 -0500 Subject: [PATCH 2/7] adjuted the constellation formatting --- app/components/teams/Teams.tsx | 35 +++++++---------------------- app/components/teams/sceneConfig.ts | 20 +++++++++-------- app/data/officer-teams.json | 23 +++++++++++++++++-- app/data/teams.ts | 1 - 4 files changed, 40 insertions(+), 39 deletions(-) delete mode 100644 app/data/teams.ts diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index 89224c3..72b8154 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -119,9 +119,7 @@ function TeamConstellation({ }, ); const graphWidth = graphBounds.maxX - graphBounds.minX; - const captionWidth = 240; - const layoutWidth = Math.max(graphWidth + 16, captionWidth); - const graphOffsetX = (layoutWidth - graphWidth) / 2 - graphBounds.minX; + const graphOffsetX = (box.width - graphWidth) / 2 - graphBounds.minX; const shiftedNodes = layout.nodes.map((node) => ({ ...node, shiftedX: node.renderX + graphOffsetX, @@ -154,7 +152,7 @@ function TeamConstellation({ return (
{ @@ -167,12 +165,12 @@ function TeamConstellation({ >
- ); -} export default function Teams() { const sectionRef = useRef(null); @@ -603,7 +585,7 @@ export default function Teams() {

{TEAMS_COPY.eyebrow}

-

+

{TEAMS_COPY.heading[0]}
{TEAMS_COPY.heading[1]} @@ -658,7 +640,7 @@ export default function Teams() {

{TEAMS_COPY.eyebrow}

-

+

{TEAMS_COPY.heading[0]}
{TEAMS_COPY.heading[1]} @@ -722,7 +704,7 @@ export default function Teams() {

{TEAMS_COPY.eyebrow}

-

+

{TEAMS_COPY.heading[0]}
{TEAMS_COPY.heading[1]} @@ -763,7 +745,6 @@ export default function Teams() { scheduleTooltipClose={scheduleTooltipClose} interactive /> - {index < desktopLayouts.length - 1 ? : null}

))}
diff --git a/app/components/teams/sceneConfig.ts b/app/components/teams/sceneConfig.ts index 2e5475f..032d900 100644 --- a/app/components/teams/sceneConfig.ts +++ b/app/components/teams/sceneConfig.ts @@ -24,8 +24,10 @@ export const TEAMS_LAYOUT = { desktopSectionMinHeight: "min-h-[560vh]", mobileSectionPadding: "px-5 py-24 sm:px-6", desktopViewportHeight: "h-[100svh] md:h-screen", - desktopContainer: "mx-auto flex h-full w-full max-w-[1800px] items-center gap-8 px-5 md:px-8 lg:gap-10 lg:px-12", + desktopContainer: "mx-auto flex h-full w-full max-w-[1800px] items-start pt-28 gap-8 px-5 md:px-8 lg:gap-10 lg:px-12", introWidth: "w-[240px] shrink-0 lg:w-[300px]", + desktopHeading: "mt-6 text-3xl font-semibold leading-none text-foreground lg:text-4xl", + mobileHeading: "mt-5 text-2xl font-semibold leading-none text-foreground sm:text-3xl", desktopTrackViewport: "relative min-w-0 flex-1 overflow-x-hidden overflow-y-visible", } as const; @@ -34,8 +36,8 @@ export const TEAMS_SCROLL = { desktopGap: 10, desktopTrailingSpace: 180, desktopPeekWidth: 150, - separatorWidth: 170, - firstConstellationOffset: 320, + separatorWidth: 380, + firstConstellationOffset: 160, tooltipCloseDelayMs: 140, } as const; @@ -67,12 +69,12 @@ export function getDesktopConstellationBox( viewportHeight: number, ): ConstellationBox { return { - width: clamp(trackViewportWidth * 0.78, 760, 1240), - height: clamp(viewportHeight * 0.5, 340, 500), - padding: clamp(trackViewportWidth * 0.028, 34, 52), - verticalBias: 22, - leadNodeSize: 72, - nodeSize: 50, + width: clamp(trackViewportWidth * 0.95, 820, 1380), + height: clamp(viewportHeight * 0.66, 420, 600), + padding: clamp(trackViewportWidth * 0.028, 60, 80), + verticalBias: 50, + leadNodeSize: 74, + nodeSize: 56, }; } diff --git a/app/data/officer-teams.json b/app/data/officer-teams.json index 3a8fbcb..8dccb41 100644 --- a/app/data/officer-teams.json +++ b/app/data/officer-teams.json @@ -158,10 +158,29 @@ } ] }, + { + "id": "logistics", + "label": "Logistics", + "order": 4, + "lead": { + "id": "logistics-lead", + "name": "TBD", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/" + }, + "members": [ + { "id": "logistics-1", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, + { "id": "logistics-2", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, + { "id": "logistics-3", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, + { "id": "logistics-4", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, + { "id": "logistics-5", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, + { "id": "logistics-6", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" } + ] + }, { "id": "finance", "label": "Finance", - "order": 3, + "order": 5, "lead": { "id": "finance-lead", "name": "Elena Martinez", @@ -210,7 +229,7 @@ { "id": "experience", "label": "Experience", - "order": 4, + "order": 3, "lead": { "id": "experience-lead", "name": "Marcus Rivera", diff --git a/app/data/teams.ts b/app/data/teams.ts deleted file mode 100644 index 3e62e81..0000000 --- a/app/data/teams.ts +++ /dev/null @@ -1 +0,0 @@ -export const teams = ["Marketing", "Tech"]; From 798e3b596a2b9af8f5275dcfba3f5da684d2e615 Mon Sep 17 00:00:00 2001 From: Sreevasan Date: Thu, 9 Apr 2026 22:22:18 -0500 Subject: [PATCH 3/7] adjusted nodes to have a popup --- app/components/teams/Teams.tsx | 43 ++- app/components/teams/constellationLayout.ts | 1 + app/data/officer-teams.json | 299 +++++--------------- 3 files changed, 98 insertions(+), 245 deletions(-) diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index 72b8154..b7cd379 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -229,7 +229,7 @@ function TeamConstellation({ > {isActive ? (
{ clearTooltipClose(); setActiveTeamId(layout.team.id); @@ -252,20 +252,41 @@ function TeamConstellation({ scheduleTooltipClose(); }} > -

+ +

{node.person.name}

-

+

{node.person.role}

- - LinkedIn - + {node.person.quote ? ( +

+ “{node.person.quote}” +

+ ) : null} +
) : null} diff --git a/app/components/teams/constellationLayout.ts b/app/components/teams/constellationLayout.ts index 1bfd354..cda2501 100644 --- a/app/components/teams/constellationLayout.ts +++ b/app/components/teams/constellationLayout.ts @@ -6,6 +6,7 @@ export type OfficerMember = { name: string; role: string; linkedinUrl: string; + quote?: string; }; export type OfficerTeam = { diff --git a/app/data/officer-teams.json b/app/data/officer-teams.json index 8dccb41..9807d75 100644 --- a/app/data/officer-teams.json +++ b/app/data/officer-teams.json @@ -7,39 +7,15 @@ "id": "marketing-lead", "name": "Olivia Tran", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/olivia-tran-hackutd" + "linkedinUrl": "https://www.linkedin.com/in/olivia-tran-hackutd", + "quote": "Building the story behind HackUTD one campaign at a time." }, "members": [ - { - "id": "marketing-1", - "name": "Maya Patel", - "role": "Content Strategy", - "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd" - }, - { - "id": "marketing-2", - "name": "Ethan Nguyen", - "role": "Brand Design", - "linkedinUrl": "https://www.linkedin.com/in/ethan-nguyen-hackutd" - }, - { - "id": "marketing-3", - "name": "Sofia Ramirez", - "role": "Social Media", - "linkedinUrl": "https://www.linkedin.com/in/sofia-ramirez-hackutd" - }, - { - "id": "marketing-4", - "name": "Jordan Kim", - "role": "Photo + Video", - "linkedinUrl": "https://www.linkedin.com/in/jordan-kim-hackutd" - }, - { - "id": "marketing-5", - "name": "Avery Brooks", - "role": "Copywriting", - "linkedinUrl": "https://www.linkedin.com/in/avery-brooks-hackutd" - } + { "id": "marketing-1", "name": "Maya Patel", "role": "Content Strategy", "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", "quote": "Good content makes people feel something." }, + { "id": "marketing-2", "name": "Ethan Nguyen", "role": "Brand Design", "linkedinUrl": "https://www.linkedin.com/in/ethan-nguyen-hackutd", "quote": "Design is the silent ambassador of your brand." }, + { "id": "marketing-3", "name": "Sofia Ramirez", "role": "Social Media", "linkedinUrl": "https://www.linkedin.com/in/sofia-ramirez-hackutd", "quote": "Every post is a chance to spark curiosity." }, + { "id": "marketing-4", "name": "Jordan Kim", "role": "Photo + Video", "linkedinUrl": "https://www.linkedin.com/in/jordan-kim-hackutd", "quote": "A frame can capture a feeling words can't." }, + { "id": "marketing-5", "name": "Avery Brooks", "role": "Copywriting", "linkedinUrl": "https://www.linkedin.com/in/avery-brooks-hackutd", "quote": "Words are the architecture of ideas." } ] }, { @@ -50,51 +26,17 @@ "id": "tech-lead", "name": "Noah Chen", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/noah-chen-hackutd" + "linkedinUrl": "https://www.linkedin.com/in/noah-chen-hackutd", + "quote": "The best infrastructure is the kind nobody notices." }, "members": [ - { - "id": "tech-1", - "name": "Aiden Williams", - "role": "Platform Engineering", - "linkedinUrl": "https://www.linkedin.com/in/aiden-williams-hackutd" - }, - { - "id": "tech-2", - "name": "Priya Shah", - "role": "Infra + DevOps", - "linkedinUrl": "https://www.linkedin.com/in/priya-shah-hackutd" - }, - { - "id": "tech-3", - "name": "Lucas Martin", - "role": "Registration Systems", - "linkedinUrl": "https://www.linkedin.com/in/lucas-martin-hackutd" - }, - { - "id": "tech-4", - "name": "Grace Lee", - "role": "Website Experience", - "linkedinUrl": "https://www.linkedin.com/in/grace-lee-hackutd" - }, - { - "id": "tech-5", - "name": "Mateo Garcia", - "role": "Internal Tooling", - "linkedinUrl": "https://www.linkedin.com/in/mateo-garcia-hackutd" - }, - { - "id": "tech-6", - "name": "Chloe Nguyen", - "role": "Security + Reliability", - "linkedinUrl": "https://www.linkedin.com/in/chloe-nguyen-hackutd" - }, - { - "id": "tech-7", - "name": "Arjun Mehta", - "role": "Automation", - "linkedinUrl": "https://www.linkedin.com/in/arjun-mehta-hackutd" - } + { "id": "tech-1", "name": "Aiden Williams", "role": "Platform Engineering", "linkedinUrl": "https://www.linkedin.com/in/aiden-williams-hackutd", "quote": "Scalability isn't an afterthought — it's a mindset." }, + { "id": "tech-2", "name": "Priya Shah", "role": "Infra + DevOps", "linkedinUrl": "https://www.linkedin.com/in/priya-shah-hackutd", "quote": "Automate the boring so humans can do the interesting." }, + { "id": "tech-3", "name": "Lucas Martin", "role": "Registration Systems", "linkedinUrl": "https://www.linkedin.com/in/lucas-martin-hackutd", "quote": "A smooth check-in sets the tone for everything after." }, + { "id": "tech-4", "name": "Grace Lee", "role": "Website Experience", "linkedinUrl": "https://www.linkedin.com/in/grace-lee-hackutd", "quote": "The website is every hacker's first impression of us." }, + { "id": "tech-5", "name": "Mateo Garcia", "role": "Internal Tooling", "linkedinUrl": "https://www.linkedin.com/in/mateo-garcia-hackutd", "quote": "Tools that work quietly keep teams moving fast." }, + { "id": "tech-6", "name": "Chloe Nguyen", "role": "Security + Reliability", "linkedinUrl": "https://www.linkedin.com/in/chloe-nguyen-hackutd", "quote": "Security is a feature, not a fix." }, + { "id": "tech-7", "name": "Arjun Mehta", "role": "Automation", "linkedinUrl": "https://www.linkedin.com/in/arjun-mehta-hackutd", "quote": "If you do it twice, automate it." } ] }, { @@ -105,57 +47,41 @@ "id": "industry-lead", "name": "Leah Johnson", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/leah-johnson-hackutd" + "linkedinUrl": "https://www.linkedin.com/in/leah-johnson-hackutd", + "quote": "Great partnerships start with a genuine conversation." }, "members": [ - { - "id": "industry-1", - "name": "Ryan Park", - "role": "Sponsor Outreach", - "linkedinUrl": "https://www.linkedin.com/in/ryan-park-hackutd" - }, - { - "id": "industry-2", - "name": "Anika Singh", - "role": "Partner Success", - "linkedinUrl": "https://www.linkedin.com/in/anika-singh-hackutd" - }, - { - "id": "industry-3", - "name": "Daniel Scott", - "role": "Relationships", - "linkedinUrl": "https://www.linkedin.com/in/daniel-scott-hackutd" - }, - { - "id": "industry-4", - "name": "Harper Wilson", - "role": "Prospecting", - "linkedinUrl": "https://www.linkedin.com/in/harper-wilson-hackutd" - }, - { - "id": "industry-5", - "name": "Kevin Flores", - "role": "Deliverables", - "linkedinUrl": "https://www.linkedin.com/in/kevin-flores-hackutd" - }, - { - "id": "industry-6", - "name": "Isabella Moore", - "role": "Stewardship", - "linkedinUrl": "https://www.linkedin.com/in/isabella-moore-hackutd" - }, - { - "id": "industry-7", - "name": "Nathan Davis", - "role": "Sponsorship Ops", - "linkedinUrl": "https://www.linkedin.com/in/nathan-davis-hackutd" - }, - { - "id": "industry-8", - "name": "Mila Brown", - "role": "Activation Planning", - "linkedinUrl": "https://www.linkedin.com/in/mila-brown-hackutd" - } + { "id": "industry-1", "name": "Ryan Park", "role": "Sponsor Outreach", "linkedinUrl": "https://www.linkedin.com/in/ryan-park-hackutd", "quote": "Every cold email is a door waiting to open." }, + { "id": "industry-2", "name": "Anika Singh", "role": "Partner Success", "linkedinUrl": "https://www.linkedin.com/in/anika-singh-hackutd", "quote": "Success is when sponsors come back year after year." }, + { "id": "industry-3", "name": "Daniel Scott", "role": "Relationships", "linkedinUrl": "https://www.linkedin.com/in/daniel-scott-hackutd", "quote": "Long-term relationships outlast any single event." }, + { "id": "industry-4", "name": "Harper Wilson", "role": "Prospecting", "linkedinUrl": "https://www.linkedin.com/in/harper-wilson-hackutd", "quote": "Find the companies that believe in what we build." }, + { "id": "industry-5", "name": "Kevin Flores", "role": "Deliverables", "linkedinUrl": "https://www.linkedin.com/in/kevin-flores-hackutd", "quote": "Under-promise, over-deliver — every single time." }, + { "id": "industry-6", "name": "Isabella Moore", "role": "Stewardship", "linkedinUrl": "https://www.linkedin.com/in/isabella-moore-hackutd", "quote": "Gratitude is how you turn a sponsor into a partner." }, + { "id": "industry-7", "name": "Nathan Davis", "role": "Sponsorship Ops", "linkedinUrl": "https://www.linkedin.com/in/nathan-davis-hackutd", "quote": "The details are what make sponsors feel valued." }, + { "id": "industry-8", "name": "Mila Brown", "role": "Activation Planning", "linkedinUrl": "https://www.linkedin.com/in/mila-brown-hackutd", "quote": "A great activation is one hackers actually remember." } + ] + }, + { + "id": "experience", + "label": "Experience", + "order": 3, + "lead": { + "id": "experience-lead", + "name": "Marcus Rivera", + "role": "Team Lead", + "linkedinUrl": "https://www.linkedin.com/in/marcus-rivera-hackutd", + "quote": "The best hackathon is the one people can't stop talking about." + }, + "members": [ + { "id": "experience-1", "name": "Zoe Hernandez", "role": "Operations", "linkedinUrl": "https://www.linkedin.com/in/zoe-hernandez-hackutd", "quote": "Chaos is just a plan that hasn't been written yet." }, + { "id": "experience-2", "name": "Adam Walker", "role": "Volunteer Coordination", "linkedinUrl": "https://www.linkedin.com/in/adam-walker-hackutd", "quote": "Volunteers are the heartbeat of every event." }, + { "id": "experience-3", "name": "Nina Robinson", "role": "Check-In Flow", "linkedinUrl": "https://www.linkedin.com/in/nina-robinson-hackutd", "quote": "First impressions are made at the door." }, + { "id": "experience-4", "name": "Tyler Hall", "role": "Venue Logistics", "linkedinUrl": "https://www.linkedin.com/in/tyler-hall-hackutd", "quote": "The space shapes the experience." }, + { "id": "experience-5", "name": "Claire Lewis", "role": "Swag + Merch", "linkedinUrl": "https://www.linkedin.com/in/claire-lewis-hackutd", "quote": "Good swag is something you actually want to keep." }, + { "id": "experience-6", "name": "Isaac Young", "role": "Food + Hospitality", "linkedinUrl": "https://www.linkedin.com/in/isaac-young-hackutd", "quote": "Feed the hackers, fuel the ideas." }, + { "id": "experience-7", "name": "Julia King", "role": "Community Care", "linkedinUrl": "https://www.linkedin.com/in/julia-king-hackutd", "quote": "Everyone deserves to feel welcome here." }, + { "id": "experience-8", "name": "Henry Wright", "role": "Room Flow", "linkedinUrl": "https://www.linkedin.com/in/henry-wright-hackutd", "quote": "A room that works is one people don't think about." }, + { "id": "experience-9", "name": "Stella Green", "role": "Late-Night Programming", "linkedinUrl": "https://www.linkedin.com/in/stella-green-hackutd", "quote": "2am is when the best ideas happen." } ] }, { @@ -166,15 +92,16 @@ "id": "logistics-lead", "name": "TBD", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/" + "linkedinUrl": "https://www.linkedin.com/in/", + "quote": "Coming soon." }, "members": [ - { "id": "logistics-1", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, - { "id": "logistics-2", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, - { "id": "logistics-3", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, - { "id": "logistics-4", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, - { "id": "logistics-5", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" }, - { "id": "logistics-6", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/" } + { "id": "logistics-1", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." }, + { "id": "logistics-2", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." }, + { "id": "logistics-3", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." }, + { "id": "logistics-4", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." }, + { "id": "logistics-5", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." }, + { "id": "logistics-6", "name": "TBD", "role": "TBD", "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon." } ] }, { @@ -185,112 +112,16 @@ "id": "finance-lead", "name": "Elena Martinez", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/elena-martinez-hackutd" - }, - "members": [ - { - "id": "finance-1", - "name": "Caleb Adams", - "role": "Budgeting", - "linkedinUrl": "https://www.linkedin.com/in/caleb-adams-hackutd" - }, - { - "id": "finance-2", - "name": "Sarah Murphy", - "role": "Procurement", - "linkedinUrl": "https://www.linkedin.com/in/sarah-murphy-hackutd" - }, - { - "id": "finance-3", - "name": "Jason Perez", - "role": "Reimbursements", - "linkedinUrl": "https://www.linkedin.com/in/jason-perez-hackutd" - }, - { - "id": "finance-4", - "name": "Emma Torres", - "role": "Forecasting", - "linkedinUrl": "https://www.linkedin.com/in/emma-torres-hackutd" - }, - { - "id": "finance-5", - "name": "Benjamin Cox", - "role": "Vendor Payments", - "linkedinUrl": "https://www.linkedin.com/in/benjamin-cox-hackutd" - }, - { - "id": "finance-6", - "name": "Lily Foster", - "role": "Ops Accounting", - "linkedinUrl": "https://www.linkedin.com/in/lily-foster-hackutd" - } - ] - }, - { - "id": "experience", - "label": "Experience", - "order": 3, - "lead": { - "id": "experience-lead", - "name": "Marcus Rivera", - "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/marcus-rivera-hackutd" + "linkedinUrl": "https://www.linkedin.com/in/elena-martinez-hackutd", + "quote": "Every dollar we spend is a decision about our priorities." }, "members": [ - { - "id": "experience-1", - "name": "Zoe Hernandez", - "role": "Operations", - "linkedinUrl": "https://www.linkedin.com/in/zoe-hernandez-hackutd" - }, - { - "id": "experience-2", - "name": "Adam Walker", - "role": "Volunteer Coordination", - "linkedinUrl": "https://www.linkedin.com/in/adam-walker-hackutd" - }, - { - "id": "experience-3", - "name": "Nina Robinson", - "role": "Check-In Flow", - "linkedinUrl": "https://www.linkedin.com/in/nina-robinson-hackutd" - }, - { - "id": "experience-4", - "name": "Tyler Hall", - "role": "Venue Logistics", - "linkedinUrl": "https://www.linkedin.com/in/tyler-hall-hackutd" - }, - { - "id": "experience-5", - "name": "Claire Lewis", - "role": "Swag + Merch", - "linkedinUrl": "https://www.linkedin.com/in/claire-lewis-hackutd" - }, - { - "id": "experience-6", - "name": "Isaac Young", - "role": "Food + Hospitality", - "linkedinUrl": "https://www.linkedin.com/in/isaac-young-hackutd" - }, - { - "id": "experience-7", - "name": "Julia King", - "role": "Community Care", - "linkedinUrl": "https://www.linkedin.com/in/julia-king-hackutd" - }, - { - "id": "experience-8", - "name": "Henry Wright", - "role": "Room Flow", - "linkedinUrl": "https://www.linkedin.com/in/henry-wright-hackutd" - }, - { - "id": "experience-9", - "name": "Stella Green", - "role": "Late-Night Programming", - "linkedinUrl": "https://www.linkedin.com/in/stella-green-hackutd" - } + { "id": "finance-1", "name": "Caleb Adams", "role": "Budgeting", "linkedinUrl": "https://www.linkedin.com/in/caleb-adams-hackutd", "quote": "A budget is your vision translated into numbers." }, + { "id": "finance-2", "name": "Sarah Murphy", "role": "Procurement", "linkedinUrl": "https://www.linkedin.com/in/sarah-murphy-hackutd", "quote": "Getting the right things at the right price is an art." }, + { "id": "finance-3", "name": "Jason Perez", "role": "Reimbursements", "linkedinUrl": "https://www.linkedin.com/in/jason-perez-hackutd", "quote": "Fast reimbursements keep teams happy and moving." }, + { "id": "finance-4", "name": "Emma Torres", "role": "Forecasting", "linkedinUrl": "https://www.linkedin.com/in/emma-torres-hackutd", "quote": "Good forecasts mean no surprises on event day." }, + { "id": "finance-5", "name": "Benjamin Cox", "role": "Vendor Payments", "linkedinUrl": "https://www.linkedin.com/in/benjamin-cox-hackutd", "quote": "Paying on time is how you build trust with vendors." }, + { "id": "finance-6", "name": "Lily Foster", "role": "Ops Accounting", "linkedinUrl": "https://www.linkedin.com/in/lily-foster-hackutd", "quote": "Clarity in the books means clarity in the mission." } ] } ] From 15ff6f3127dcc5462e80cbcb0780003b40f8b514 Mon Sep 17 00:00:00 2001 From: Sreevasan Date: Sat, 11 Apr 2026 17:12:50 -0500 Subject: [PATCH 4/7] added a description for each team --- app/components/teams/Teams.tsx | 64 +++- app/components/teams/constellationLayout.ts | 2 + app/components/teams/sceneConfig.ts | 6 +- app/data/officer-teams.json | 405 +++++++++++++++++--- 4 files changed, 415 insertions(+), 62 deletions(-) diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index b7cd379..bba5a6d 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -1,6 +1,7 @@ "use client"; import type { CSSProperties } from "react"; +import Image from "next/image"; import { useEffect, useRef, useState } from "react"; import { useIsMobile } from "@/app/hooks/useIsMobile"; import { usePrefersReducedMotion } from "@/app/hooks/usePrefersReducedMotion"; @@ -263,12 +264,29 @@ function TeamConstellation({ > ✕ -

- {node.person.name} -

-

- {node.person.role} -

+
+ {node.person.imageUrl ? ( + {node.person.name} + ) : ( +
+ {getInitials(node.person.name)} +
+ )} +
+

+ {node.person.name} +

+

+ {node.person.role} +

+
+
{node.person.quote ? (

“{node.person.quote}” @@ -407,8 +425,12 @@ export default function Teams() { const trackViewportRef = useRef(null); const trackRef = useRef(null); const tooltipCloseTimeoutRef = useRef(null); + const descTransitionRef = useRef(null); + const activeTeamIndexRef = useRef(0); const isMobile = useIsMobile(); const prefersReducedMotion = usePrefersReducedMotion(); + const [displayedTeamIndex, setDisplayedTeamIndex] = useState(0); + const [descVisible, setDescVisible] = useState(true); const [desktopBox, setDesktopBox] = useState( TEAM_CLUSTER_BOX.desktop, ); @@ -482,6 +504,27 @@ export default function Teams() { ); targetX = progress * maxTranslate; + if (maxTranslate > 0) { + const slotWidth = track.scrollWidth / ORDERED_OFFICER_TEAMS.length; + const nextIndex = clamp( + Math.round((progress * maxTranslate) / slotWidth), + 0, + ORDERED_OFFICER_TEAMS.length - 1, + ); + if (nextIndex !== activeTeamIndexRef.current) { + activeTeamIndexRef.current = nextIndex; + setDescVisible(false); + if (descTransitionRef.current !== null) { + window.clearTimeout(descTransitionRef.current); + } + descTransitionRef.current = window.setTimeout(() => { + setDisplayedTeamIndex(nextIndex); + setDescVisible(true); + descTransitionRef.current = null; + }, 200); + } + } + if (progress <= 0.001 || progress >= 0.999) { currentX = targetX; track.style.transform = `translate3d(${-currentX}px, 0, 0)`; @@ -546,6 +589,9 @@ export default function Teams() { if (tooltipCloseTimeoutRef.current !== null) { window.clearTimeout(tooltipCloseTimeoutRef.current); } + if (descTransitionRef.current !== null) { + window.clearTimeout(descTransitionRef.current); + } }; }, []); @@ -730,6 +776,12 @@ export default function Teams() {
{TEAMS_COPY.heading[1]} +

+ {ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.description ?? ""} +

Date: Wed, 15 Apr 2026 20:21:07 -0500 Subject: [PATCH 5/7] adjusted sizing for mobile and android devices as well --- app/components/teams/Teams.tsx | 269 +++++++++++++++++++++++----- app/components/teams/sceneConfig.ts | 16 +- app/hooks/useIsAndroid.ts | 20 +++ 3 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 app/hooks/useIsAndroid.ts diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index bba5a6d..82775d9 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -3,6 +3,7 @@ import type { CSSProperties } from "react"; import Image from "next/image"; import { useEffect, useRef, useState } from "react"; +import { useIsAndroid } from "@/app/hooks/useIsAndroid"; import { useIsMobile } from "@/app/hooks/useIsMobile"; import { usePrefersReducedMotion } from "@/app/hooks/usePrefersReducedMotion"; import { @@ -378,7 +379,7 @@ function MobileTeamCard({ const memberCountLabel = getMemberCountLabel(layout.nodes.length); return ( -
+

HACKUTD

@@ -422,18 +423,22 @@ function buildLayouts(teams: OfficerTeam[], box: ConstellationBox) { export default function Teams() { const sectionRef = useRef(null); + const mobileSectionRef = useRef(null); + const mobileTrackRef = useRef(null); const trackViewportRef = useRef(null); const trackRef = useRef(null); const tooltipCloseTimeoutRef = useRef(null); const descTransitionRef = useRef(null); const activeTeamIndexRef = useRef(0); const isMobile = useIsMobile(); + const isAndroid = useIsAndroid(); const prefersReducedMotion = usePrefersReducedMotion(); const [displayedTeamIndex, setDisplayedTeamIndex] = useState(0); const [descVisible, setDescVisible] = useState(true); const [desktopBox, setDesktopBox] = useState( TEAM_CLUSTER_BOX.desktop, ); + const [mobileBox, setMobileBox] = useState(TEAM_CLUSTER_BOX.mobile); const [activeTeamId, setActiveTeamId] = useState(null); const [activeNode, setActiveNode] = useState(null); @@ -471,6 +476,47 @@ export default function Teams() { }; }, [isMobile]); + useEffect(() => { + if (!isMobile) return; + + const update = () => { + const w = window.innerWidth; + const h = window.innerHeight; + + if (isAndroid) { + // Android Chrome reserves space for system nav bar and address bar. + // Use a more conservative height fraction and clamp node sizes so + // constellations stay fully visible on common 360–412 px wide screens. + const clampedW = Math.min(w, 412); + const nodeSize = Math.round(Math.min(44 + (clampedW - 360) * 0.06, 50)); + const leadNodeSize = Math.round(nodeSize * 1.3); + setMobileBox({ + width: w, + // 0.46 leaves room for address bar + bottom nav (≈ 80–100 px combined) + height: Math.round(h * 0.46), + padding: Math.round(w * 0.1), + verticalBias: 12, + leadNodeSize, + nodeSize, + }); + } else { + // iOS Safari — 100svh already accounts for browser chrome + setMobileBox({ + width: w, + height: Math.round(h * 0.54), + padding: Math.round(w * 0.09), + verticalBias: 18, + leadNodeSize: 68, + nodeSize: 52, + }); + } + }; + + update(); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, [isMobile, isAndroid]); + useEffect(() => { if (isMobile || prefersReducedMotion) { return; @@ -584,6 +630,81 @@ export default function Teams() { }; }, [desktopBox.height, desktopBox.width, isMobile, prefersReducedMotion]); + useEffect(() => { + if (!isMobile || prefersReducedMotion) { + return; + } + + const section = mobileSectionRef.current; + const track = mobileTrackRef.current; + + if (!section || !track) { + return; + } + + let frame = 0; + let currentX = 0; + let targetX = 0; + + const clamp = (value: number, min: number, max: number) => + Math.min(Math.max(value, min), max); + + const updateTarget = () => { + const maxTranslate = Math.max(track.scrollWidth - window.innerWidth, 0); + const scrollableDistance = Math.max( + section.offsetHeight - window.innerHeight, + 1, + ); + const progress = clamp( + (window.scrollY - section.offsetTop) / scrollableDistance, + 0, + 1, + ); + targetX = progress * maxTranslate; + + if (progress <= 0.001 || progress >= 0.999) { + currentX = targetX; + track.style.transform = `translate3d(${-currentX}px, 0, 0)`; + return; + } + + queueRender(); + }; + + const renderTrack = () => { + currentX += (targetX - currentX) * TEAMS_SCROLL.smoothing; + + if (Math.abs(targetX - currentX) < 0.12) { + currentX = targetX; + } + + track.style.transform = `translate3d(${-currentX}px, 0, 0)`; + + if (currentX !== targetX) { + frame = window.requestAnimationFrame(renderTrack); + return; + } + + frame = 0; + }; + + const queueRender = () => { + if (frame !== 0) return; + frame = window.requestAnimationFrame(renderTrack); + }; + + window.addEventListener("scroll", updateTarget, { passive: true }); + window.addEventListener("resize", updateTarget); + updateTarget(); + + return () => { + if (frame !== 0) window.cancelAnimationFrame(frame); + window.removeEventListener("scroll", updateTarget); + window.removeEventListener("resize", updateTarget); + track.style.transform = ""; + }; + }, [isMobile, prefersReducedMotion]); + useEffect(() => { return () => { if (tooltipCloseTimeoutRef.current !== null) { @@ -620,57 +741,117 @@ export default function Teams() { }; const desktopLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, desktopBox); - const mobileLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, TEAM_CLUSTER_BOX.mobile); + const mobileLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, mobileBox); if (isMobile) { + if (prefersReducedMotion) { + return ( +
+ +
+

+ {TEAMS_COPY.eyebrow} +

+

+ {TEAMS_COPY.heading[0]} +
+ {TEAMS_COPY.heading[1]} +

+
+ {mobileLayouts.map((layout) => ( + + ))} +
+
+
+ ); + } + return (
- +
+ -
-

- {TEAMS_COPY.eyebrow} -

-

- {TEAMS_COPY.heading[0]} -
- {TEAMS_COPY.heading[1]} -

+
+
+

+ {TEAMS_COPY.eyebrow} +

+

+ {TEAMS_COPY.heading[0]} +
+ {TEAMS_COPY.heading[1]} +

+
-
- {mobileLayouts.map((layout) => ( - - ))} +
+
+ {mobileLayouts.map((layout) => ( + + ))} +
+
diff --git a/app/components/teams/sceneConfig.ts b/app/components/teams/sceneConfig.ts index 0730d93..fab8775 100644 --- a/app/components/teams/sceneConfig.ts +++ b/app/components/teams/sceneConfig.ts @@ -22,7 +22,11 @@ export const TEAMS_COPY = { export const TEAMS_LAYOUT = { desktopSectionMinHeight: "min-h-[560vh]", + mobileSectionMinHeight: "min-h-[420vh]", + mobileSectionMinHeightAndroid: "min-h-[480vh]", mobileSectionPadding: "px-5 py-24 sm:px-6", + mobileViewportHeight: "h-[100svh]", + mobileViewportHeightAndroid: "h-[100dvh]", desktopViewportHeight: "h-[100svh] md:h-screen", desktopContainer: "mx-auto flex h-full w-full max-w-[1800px] items-start pt-28 gap-8 px-5 md:px-8 lg:gap-10 lg:px-12", introWidth: "w-[320px] shrink-0 lg:w-[400px]", @@ -51,12 +55,12 @@ export const TEAM_CLUSTER_BOX = { nodeSize: 50, }, mobile: { - width: 272, - height: 208, - padding: 24, - verticalBias: 10, - leadNodeSize: 52, - nodeSize: 38, + width: 310, + height: 260, + padding: 30, + verticalBias: 14, + leadNodeSize: 62, + nodeSize: 46, }, } as const; diff --git a/app/hooks/useIsAndroid.ts b/app/hooks/useIsAndroid.ts new file mode 100644 index 0000000..40b8be3 --- /dev/null +++ b/app/hooks/useIsAndroid.ts @@ -0,0 +1,20 @@ +"use client"; + +import { useSyncExternalStore } from "react"; + +function getSnapshot() { + return /android/i.test(navigator.userAgent); +} + +function getServerSnapshot() { + return false; +} + +// No-op subscribe: UA string never changes after mount. +function subscribe(callback: () => void) { + return () => {}; +} + +export function useIsAndroid() { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); +} From 3537f62561a2dc0f0493fae0c6948b496c76196e Mon Sep 17 00:00:00 2001 From: Sreevasan Date: Wed, 15 Apr 2026 21:45:11 -0500 Subject: [PATCH 6/7] Added a group photo section and adjusted contellations --- app/components/teams/Teams.tsx | 75 +++----- app/components/teams/constellationLayout.ts | 11 +- app/data/constellation-templates.json | 177 +++++++++++++++++++ app/data/officer-teams.json | 181 +++++++++----------- 4 files changed, 293 insertions(+), 151 deletions(-) diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index 82775d9..dcfc6e4 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -359,54 +359,6 @@ function TeamConstellation({ ); } -function MobileTeamCard({ - layout, - box, - activeNode, - setActiveTeamId, - openNode, - clearTooltipClose, - scheduleTooltipClose, -}: { - layout: ResolvedConstellationLayout; - box: ConstellationBox; - activeNode: ActiveNodeState; - setActiveTeamId: (teamId: string | null) => void; - openNode: (teamId: string, personId: string) => void; - clearTooltipClose: () => void; - scheduleTooltipClose: () => void; -}) { - const memberCountLabel = getMemberCountLabel(layout.nodes.length); - - return ( -
-

- HACKUTD -

-

- {layout.template.name} -

-

{layout.team.label}

-

- {memberCountLabel} -

-
- -
-
- ); -} function buildLayouts(teams: OfficerTeam[], box: ConstellationBox) { return teams.map((team) => @@ -944,7 +896,6 @@ export default function Teams() { /> ); })} -
@@ -963,6 +914,32 @@ export default function Teams() { > {ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.description ?? ""}

+ +
+ {ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.groupPhotoUrl ? ( + {`${ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.label} + ) : ( +
+

+ Group photo coming soon +

+
+ )} +
t.id === templateId); + if (explicit) return explicit; + } + const availableCounts = Array.from( new Set(constellationTemplates.map((template) => template.starCount)), ).sort((left, right) => left - right); @@ -154,7 +161,7 @@ export function resolveConstellationLayout( verticalBias = 0, ): ResolvedConstellationLayout { const people = [team.lead, ...team.members]; - const template = pickTemplate(team.id, people.length); + const template = pickTemplate(team.id, people.length, team.templateId); const assignmentOrder = getAssignmentOrder(template); const selectedNodeIds = assignmentOrder.slice( 0, diff --git a/app/data/constellation-templates.json b/app/data/constellation-templates.json index 3d43977..e8830d4 100644 --- a/app/data/constellation-templates.json +++ b/app/data/constellation-templates.json @@ -233,5 +233,182 @@ "stepX": 0.12, "stepY": 0.08 } + }, + { + "id": "perseus", + "name": "Perseus", + "starCount": 7, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.46, "y": 0.46 }, + { "id": "n1", "x": 0.10, "y": 0.14 }, + { "id": "n2", "x": 0.28, "y": 0.20 }, + { "id": "n3", "x": 0.50, "y": 0.14 }, + { "id": "n4", "x": 0.70, "y": 0.22 }, + { "id": "n5", "x": 0.78, "y": 0.54 }, + { "id": "n6", "x": 0.64, "y": 0.80 } + ], + "edges": [ + { "fromId": "n1", "toId": "n2" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n2", "toId": "n0" }, + { "fromId": "n4", "toId": "n0" }, + { "fromId": "n0", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" } + ], + "append": { + "fromNodeId": "n5", + "toNodeId": "n6", + "stepX": -0.10, + "stepY": 0.14 + } + }, + { + "id": "bootes", + "name": "Boötes", + "starCount": 7, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.50, "y": 0.90 }, + { "id": "n1", "x": 0.50, "y": 0.60 }, + { "id": "n2", "x": 0.30, "y": 0.76 }, + { "id": "n3", "x": 0.28, "y": 0.28 }, + { "id": "n4", "x": 0.50, "y": 0.08 }, + { "id": "n5", "x": 0.72, "y": 0.28 }, + { "id": "n6", "x": 0.12, "y": 0.48 } + ], + "edges": [ + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n2", "toId": "n1" }, + { "fromId": "n1", "toId": "n0" }, + { "fromId": "n1", "toId": "n3" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n4", "toId": "n5" }, + { "fromId": "n5", "toId": "n1" }, + { "fromId": "n3", "toId": "n6" } + ], + "append": { + "fromNodeId": "n3", + "toNodeId": "n6", + "stepX": -0.12, + "stepY": 0.10 + } + }, + { + "id": "castor", + "name": "Castor System", + "starCount": 6, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.38, "y": 0.14 }, + { "id": "n1", "x": 0.58, "y": 0.22 }, + { "id": "n2", "x": 0.14, "y": 0.50 }, + { "id": "n3", "x": 0.34, "y": 0.62 }, + { "id": "n4", "x": 0.66, "y": 0.72 }, + { "id": "n5", "x": 0.84, "y": 0.58 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n2", "toId": "n3" }, + { "fromId": "n4", "toId": "n5" }, + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n1", "toId": "n4" }, + { "fromId": "n3", "toId": "n4" } + ], + "append": { + "fromNodeId": "n4", + "toNodeId": "n5", + "stepX": 0.12, + "stepY": -0.10 + } + }, + { + "id": "cetus", + "name": "Cetus", + "starCount": 14, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.18, "y": 0.10 }, + { "id": "n1", "x": 0.06, "y": 0.26 }, + { "id": "n2", "x": 0.24, "y": 0.36 }, + { "id": "n3", "x": 0.38, "y": 0.22 }, + { "id": "n4", "x": 0.44, "y": 0.36 }, + { "id": "n5", "x": 0.54, "y": 0.46 }, + { "id": "n6", "x": 0.62, "y": 0.28 }, + { "id": "n7", "x": 0.76, "y": 0.38 }, + { "id": "n8", "x": 0.88, "y": 0.52 }, + { "id": "n9", "x": 0.78, "y": 0.66 }, + { "id": "n10", "x": 0.60, "y": 0.72 }, + { "id": "n11", "x": 0.48, "y": 0.80 }, + { "id": "n12", "x": 0.36, "y": 0.90 }, + { "id": "n13", "x": 0.52, "y": 0.94 } + ], + "edges": [ + { "fromId": "n1", "toId": "n0" }, + { "fromId": "n0", "toId": "n3" }, + { "fromId": "n3", "toId": "n2" }, + { "fromId": "n2", "toId": "n1" }, + { "fromId": "n3", "toId": "n4" }, + { "fromId": "n4", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n6", "toId": "n7" }, + { "fromId": "n7", "toId": "n8" }, + { "fromId": "n8", "toId": "n9" }, + { "fromId": "n9", "toId": "n10" }, + { "fromId": "n5", "toId": "n10" }, + { "fromId": "n10", "toId": "n11" }, + { "fromId": "n11", "toId": "n12" }, + { "fromId": "n12", "toId": "n13" } + ], + "append": { + "fromNodeId": "n12", + "toNodeId": "n13", + "stepX": 0.10, + "stepY": 0.08 + } + }, + { + "id": "ophiuchus", + "name": "Ophiuchus", + "starCount": 13, + "leadNodeId": "n0", + "nodes": [ + { "id": "n0", "x": 0.50, "y": 0.06 }, + { "id": "n1", "x": 0.72, "y": 0.20 }, + { "id": "n2", "x": 0.28, "y": 0.18 }, + { "id": "n3", "x": 0.68, "y": 0.40 }, + { "id": "n4", "x": 0.32, "y": 0.38 }, + { "id": "n5", "x": 0.50, "y": 0.50 }, + { "id": "n6", "x": 0.64, "y": 0.62 }, + { "id": "n7", "x": 0.36, "y": 0.60 }, + { "id": "n8", "x": 0.74, "y": 0.74 }, + { "id": "n9", "x": 0.24, "y": 0.74 }, + { "id": "n10", "x": 0.78, "y": 0.88 }, + { "id": "n11", "x": 0.18, "y": 0.88 }, + { "id": "n12", "x": 0.50, "y": 0.94 } + ], + "edges": [ + { "fromId": "n0", "toId": "n1" }, + { "fromId": "n0", "toId": "n2" }, + { "fromId": "n1", "toId": "n3" }, + { "fromId": "n2", "toId": "n4" }, + { "fromId": "n3", "toId": "n5" }, + { "fromId": "n4", "toId": "n5" }, + { "fromId": "n5", "toId": "n6" }, + { "fromId": "n5", "toId": "n7" }, + { "fromId": "n6", "toId": "n8" }, + { "fromId": "n7", "toId": "n9" }, + { "fromId": "n8", "toId": "n10" }, + { "fromId": "n9", "toId": "n11" }, + { "fromId": "n10", "toId": "n12" }, + { "fromId": "n11", "toId": "n12" } + ], + "append": { + "fromNodeId": "n10", + "toNodeId": "n12", + "stepX": 0.0, + "stepY": 0.10 + } } ] diff --git a/app/data/officer-teams.json b/app/data/officer-teams.json index e161889..b13fc9b 100644 --- a/app/data/officer-teams.json +++ b/app/data/officer-teams.json @@ -3,11 +3,12 @@ "id": "marketing", "label": "Marketing", "order": 0, + "templateId": "cetus", "lead": { "id": "marketing-lead", - "name": "Olivia Tran", + "name": "Dhivyesh Prithiviraj", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/olivia-tran-hackutd", + "linkedinUrl": "https://www.linkedin.com/in/dhivyeshprithiviraj/", "quote": "Building the story behind HackUTD one campaign at a time.", "imageUrl": "" }, @@ -51,6 +52,70 @@ "linkedinUrl": "https://www.linkedin.com/in/avery-brooks-hackutd", "quote": "Words are the architecture of ideas.", "imageUrl": "" + }, + { + "id": "marketing-6", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-7", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-8", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-9", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-10", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-11", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-12", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" + }, + { + "id": "marketing-13", + "name": "Maya Patel", + "role": "Content Strategy", + "linkedinUrl": "https://www.linkedin.com/in/maya-patel-hackutd", + "quote": "Good content makes people feel something.", + "imageUrl": "" } ], "description": "Crafting the visual identity and story of HackUTD — from social campaigns to brand design, they make sure the world knows we exist." @@ -59,11 +124,12 @@ "id": "tech", "label": "Tech", "order": 1, + "templateId": "orion", "lead": { "id": "tech-lead", - "name": "Noah Chen", + "name": "Caleb Bae", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/noah-chen-hackutd", + "linkedinUrl": "https://www.linkedin.com/in/baecal000/", "quote": "The best infrastructure is the kind nobody notices.", "imageUrl": "" }, @@ -115,14 +181,6 @@ "linkedinUrl": "https://www.linkedin.com/in/chloe-nguyen-hackutd", "quote": "Security is a feature, not a fix.", "imageUrl": "" - }, - { - "id": "tech-7", - "name": "Arjun Mehta", - "role": "Automation", - "linkedinUrl": "https://www.linkedin.com/in/arjun-mehta-hackutd", - "quote": "If you do it twice, automate it.", - "imageUrl": "" } ], "description": "Building the platform that powers HackUTD — registration, infrastructure, and every line of code that keeps 1,000+ hackers online." @@ -131,11 +189,12 @@ "id": "industry", "label": "Industry", "order": 2, + "templateId": "perseus", "lead": { "id": "industry-lead", - "name": "Leah Johnson", + "name": "Sachi Hansalia", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/leah-johnson-hackutd", + "linkedinUrl": "https://www.linkedin.com/in/sachi-hansalia/", "quote": "Great partnerships start with a genuine conversation.", "imageUrl": "" }, @@ -187,22 +246,6 @@ "linkedinUrl": "https://www.linkedin.com/in/isabella-moore-hackutd", "quote": "Gratitude is how you turn a sponsor into a partner.", "imageUrl": "" - }, - { - "id": "industry-7", - "name": "Nathan Davis", - "role": "Sponsorship Ops", - "linkedinUrl": "https://www.linkedin.com/in/nathan-davis-hackutd", - "quote": "The details are what make sponsors feel valued.", - "imageUrl": "" - }, - { - "id": "industry-8", - "name": "Mila Brown", - "role": "Activation Planning", - "linkedinUrl": "https://www.linkedin.com/in/mila-brown-hackutd", - "quote": "A great activation is one hackers actually remember.", - "imageUrl": "" } ], "description": "The bridge between HackUTD and the companies that believe in what we build. They turn cold outreach into lasting partnerships." @@ -211,11 +254,12 @@ "id": "experience", "label": "Experience", "order": 3, + "templateId": "bootes", "lead": { "id": "experience-lead", - "name": "Marcus Rivera", + "name": "Liana Forster", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/marcus-rivera-hackutd", + "linkedinUrl": "https://www.linkedin.com/in/liana-forster/", "quote": "The best hackathon is the one people can't stop talking about.", "imageUrl": "" }, @@ -267,30 +311,6 @@ "linkedinUrl": "https://www.linkedin.com/in/isaac-young-hackutd", "quote": "Feed the hackers, fuel the ideas.", "imageUrl": "" - }, - { - "id": "experience-7", - "name": "Julia King", - "role": "Community Care", - "linkedinUrl": "https://www.linkedin.com/in/julia-king-hackutd", - "quote": "Everyone deserves to feel welcome here.", - "imageUrl": "" - }, - { - "id": "experience-8", - "name": "Henry Wright", - "role": "Room Flow", - "linkedinUrl": "https://www.linkedin.com/in/henry-wright-hackutd", - "quote": "A room that works is one people don't think about.", - "imageUrl": "" - }, - { - "id": "experience-9", - "name": "Stella Green", - "role": "Late-Night Programming", - "linkedinUrl": "https://www.linkedin.com/in/stella-green-hackutd", - "quote": "2am is when the best ideas happen.", - "imageUrl": "" } ], "description": "Turning 24 hours into an unforgettable experience — from the moment hackers walk in to the moment they leave inspired." @@ -299,11 +319,12 @@ "id": "logistics", "label": "Logistics", "order": 4, + "templateId": "castor", "lead": { "id": "logistics-lead", - "name": "TBD", + "name": "Sofia Thomas", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/", + "linkedinUrl": "https://www.linkedin.com/in/sofia-thomas-82055832b/", "quote": "Coming soon.", "imageUrl": "" }, @@ -347,14 +368,6 @@ "linkedinUrl": "https://www.linkedin.com/in/", "quote": "Coming soon.", "imageUrl": "" - }, - { - "id": "logistics-6", - "name": "TBD", - "role": "TBD", - "linkedinUrl": "https://www.linkedin.com/in/", - "quote": "Coming soon.", - "imageUrl": "" } ], "description": "The backbone of the event. They handle the details nobody sees so that everything everyone does see runs flawlessly." @@ -365,9 +378,9 @@ "order": 5, "lead": { "id": "finance-lead", - "name": "Elena Martinez", + "name": "Aatish Bommisetty", "role": "Team Lead", - "linkedinUrl": "https://www.linkedin.com/in/elena-martinez-hackutd", + "linkedinUrl": "https://www.linkedin.com/in/aatishbommisetty/", "quote": "Every dollar we spend is a decision about our priorities.", "imageUrl": "" }, @@ -387,38 +400,6 @@ "linkedinUrl": "https://www.linkedin.com/in/sarah-murphy-hackutd", "quote": "Getting the right things at the right price is an art.", "imageUrl": "" - }, - { - "id": "finance-3", - "name": "Jason Perez", - "role": "Reimbursements", - "linkedinUrl": "https://www.linkedin.com/in/jason-perez-hackutd", - "quote": "Fast reimbursements keep teams happy and moving.", - "imageUrl": "" - }, - { - "id": "finance-4", - "name": "Emma Torres", - "role": "Forecasting", - "linkedinUrl": "https://www.linkedin.com/in/emma-torres-hackutd", - "quote": "Good forecasts mean no surprises on event day.", - "imageUrl": "" - }, - { - "id": "finance-5", - "name": "Benjamin Cox", - "role": "Vendor Payments", - "linkedinUrl": "https://www.linkedin.com/in/benjamin-cox-hackutd", - "quote": "Paying on time is how you build trust with vendors.", - "imageUrl": "" - }, - { - "id": "finance-6", - "name": "Lily Foster", - "role": "Ops Accounting", - "linkedinUrl": "https://www.linkedin.com/in/lily-foster-hackutd", - "quote": "Clarity in the books means clarity in the mission.", - "imageUrl": "" } ], "description": "Keeping HackUTD financially sound — from budget planning to vendor payments, they make sure every dollar serves a purpose." From 069809b11ddff130ff1661ceebe43dc99865b9ae Mon Sep 17 00:00:00 2001 From: Sreevasan Date: Thu, 23 Apr 2026 00:39:36 -0500 Subject: [PATCH 7/7] added mobile photo and description --- app/components/teams/Teams.tsx | 210 ++++++++++++++++++++------------- 1 file changed, 125 insertions(+), 85 deletions(-) diff --git a/app/components/teams/Teams.tsx b/app/components/teams/Teams.tsx index dcfc6e4..cc9f89e 100644 --- a/app/components/teams/Teams.tsx +++ b/app/components/teams/Teams.tsx @@ -436,30 +436,27 @@ export default function Teams() { const h = window.innerHeight; if (isAndroid) { - // Android Chrome reserves space for system nav bar and address bar. - // Use a more conservative height fraction and clamp node sizes so - // constellations stay fully visible on common 360–412 px wide screens. const clampedW = Math.min(w, 412); - const nodeSize = Math.round(Math.min(44 + (clampedW - 360) * 0.06, 50)); + const nodeSize = Math.round(Math.min(38 + (clampedW - 360) * 0.05, 44)); const leadNodeSize = Math.round(nodeSize * 1.3); setMobileBox({ width: w, - // 0.46 leaves room for address bar + bottom nav (≈ 80–100 px combined) - height: Math.round(h * 0.46), - padding: Math.round(w * 0.1), + // reduced from 0.46 to leave room for photo + description panel above + height: Math.round(h * 0.38), + padding: Math.round(w * 0.08), verticalBias: 12, leadNodeSize, nodeSize, }); } else { - // iOS Safari — 100svh already accounts for browser chrome setMobileBox({ width: w, - height: Math.round(h * 0.54), - padding: Math.round(w * 0.09), - verticalBias: 18, - leadNodeSize: 68, - nodeSize: 52, + // reduced from 0.54 to leave room for photo + description panel above + height: Math.round(h * 0.40), + padding: Math.round(w * 0.07), + verticalBias: 16, + leadNodeSize: 58, + nodeSize: 44, }); } }; @@ -470,27 +467,22 @@ export default function Teams() { }, [isMobile, isAndroid]); useEffect(() => { - if (isMobile || prefersReducedMotion) { - return; - } + if (!isMobile || prefersReducedMotion) return; - const section = sectionRef.current; - const trackViewport = trackViewportRef.current; - const track = trackRef.current; + const section = mobileSectionRef.current; + const track = mobileTrackRef.current; - if (!section || !trackViewport || !track) { - return; - } + if (!section || !track) return; let frame = 0; let currentX = 0; let targetX = 0; - let maxTranslate = 0; const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); const updateTarget = () => { + const maxTranslate = Math.max(track.scrollWidth - window.innerWidth, 0); const scrollableDistance = Math.max( section.offsetHeight - window.innerHeight, 1, @@ -532,11 +524,6 @@ export default function Teams() { queueRender(); }; - const updateMetrics = () => { - maxTranslate = Math.max(track.scrollWidth - trackViewport.offsetWidth, 0); - updateTarget(); - }; - const renderTrack = () => { currentX += (targetX - currentX) * TEAMS_SCROLL.smoothing; @@ -555,54 +542,44 @@ export default function Teams() { }; const queueRender = () => { - if (frame !== 0) { - return; - } - + if (frame !== 0) return; frame = window.requestAnimationFrame(renderTrack); }; - updateMetrics(); - - const resizeObserver = new ResizeObserver(updateMetrics); - resizeObserver.observe(trackViewport); - resizeObserver.observe(track); window.addEventListener("scroll", updateTarget, { passive: true }); - window.addEventListener("resize", updateMetrics); + window.addEventListener("resize", updateTarget); + updateTarget(); return () => { - if (frame !== 0) { - window.cancelAnimationFrame(frame); - } - - resizeObserver.disconnect(); + if (frame !== 0) window.cancelAnimationFrame(frame); window.removeEventListener("scroll", updateTarget); - window.removeEventListener("resize", updateMetrics); + window.removeEventListener("resize", updateTarget); track.style.transform = ""; }; - }, [desktopBox.height, desktopBox.width, isMobile, prefersReducedMotion]); + }, [isMobile, prefersReducedMotion]); useEffect(() => { - if (!isMobile || prefersReducedMotion) { + if (isMobile || prefersReducedMotion) { return; } - const section = mobileSectionRef.current; - const track = mobileTrackRef.current; + const section = sectionRef.current; + const trackViewport = trackViewportRef.current; + const track = trackRef.current; - if (!section || !track) { + if (!section || !trackViewport || !track) { return; } let frame = 0; let currentX = 0; let targetX = 0; + let maxTranslate = 0; const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); const updateTarget = () => { - const maxTranslate = Math.max(track.scrollWidth - window.innerWidth, 0); const scrollableDistance = Math.max( section.offsetHeight - window.innerHeight, 1, @@ -614,6 +591,27 @@ export default function Teams() { ); targetX = progress * maxTranslate; + if (maxTranslate > 0) { + const slotWidth = track.scrollWidth / ORDERED_OFFICER_TEAMS.length; + const nextIndex = clamp( + Math.round((progress * maxTranslate) / slotWidth), + 0, + ORDERED_OFFICER_TEAMS.length - 1, + ); + if (nextIndex !== activeTeamIndexRef.current) { + activeTeamIndexRef.current = nextIndex; + setDescVisible(false); + if (descTransitionRef.current !== null) { + window.clearTimeout(descTransitionRef.current); + } + descTransitionRef.current = window.setTimeout(() => { + setDisplayedTeamIndex(nextIndex); + setDescVisible(true); + descTransitionRef.current = null; + }, 200); + } + } + if (progress <= 0.001 || progress >= 0.999) { currentX = targetX; track.style.transform = `translate3d(${-currentX}px, 0, 0)`; @@ -623,6 +621,11 @@ export default function Teams() { queueRender(); }; + const updateMetrics = () => { + maxTranslate = Math.max(track.scrollWidth - trackViewport.offsetWidth, 0); + updateTarget(); + }; + const renderTrack = () => { currentX += (targetX - currentX) * TEAMS_SCROLL.smoothing; @@ -641,21 +644,32 @@ export default function Teams() { }; const queueRender = () => { - if (frame !== 0) return; + if (frame !== 0) { + return; + } + frame = window.requestAnimationFrame(renderTrack); }; + updateMetrics(); + + const resizeObserver = new ResizeObserver(updateMetrics); + resizeObserver.observe(trackViewport); + resizeObserver.observe(track); window.addEventListener("scroll", updateTarget, { passive: true }); - window.addEventListener("resize", updateTarget); - updateTarget(); + window.addEventListener("resize", updateMetrics); return () => { - if (frame !== 0) window.cancelAnimationFrame(frame); + if (frame !== 0) { + window.cancelAnimationFrame(frame); + } + + resizeObserver.disconnect(); window.removeEventListener("scroll", updateTarget); - window.removeEventListener("resize", updateTarget); + window.removeEventListener("resize", updateMetrics); track.style.transform = ""; }; - }, [isMobile, prefersReducedMotion]); + }, [desktopBox.height, desktopBox.width, isMobile, prefersReducedMotion]); useEffect(() => { return () => { @@ -696,26 +710,63 @@ export default function Teams() { const mobileLayouts = buildLayouts(ORDERED_OFFICER_TEAMS, mobileBox); if (isMobile) { + const mobileStars = ( + + ); + + const mobilePhotoPanel = ( +
+ {ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.groupPhotoUrl ? ( + {`${ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.label} + ) : ( +
+

+ Group photo coming soon +

+
+ )} + {ORDERED_OFFICER_TEAMS[displayedTeamIndex]?.description ? ( +

+ {ORDERED_OFFICER_TEAMS[displayedTeamIndex]!.description} +

+ ) : null} +
+ ); + if (prefersReducedMotion) { return (
- + {mobileStars}

{TEAMS_COPY.eyebrow} @@ -754,20 +805,7 @@ export default function Teams() { className={`relative bg-background ${isAndroid ? TEAMS_LAYOUT.mobileSectionMinHeightAndroid : TEAMS_LAYOUT.mobileSectionMinHeight}`} >

- + {mobileStars}
@@ -781,7 +819,9 @@ export default function Teams() {
-
+ {mobilePhotoPanel} + +