diff --git a/client/package-lock.json b/client/package-lock.json index 4e2c7c6..2b2379b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "@stripe/react-stripe-js": "^6.3.0", "@stripe/stripe-js": "^9.5.0", - "axios": "^1.16.0", "@tailwindcss/vite": "^4.3.0", + "axios": "^1.16.0", + "framer-motion": "^12.39.0", "lucide-react": "^1.11.0", + "motion": "^12.39.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", @@ -1329,29 +1331,6 @@ "win32" ] }, - "node_modules/@stripe/react-stripe-js": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.3.0.tgz", - "integrity": "sha512-N1FTRNCMKySElDz1lAsf/m6Oy5vcl6LRVXcW29t0Y3U3HYOAqCBlk6nuDsR2x7SAuaXkVCjnpCqrNbA/7l74jg==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@stripe/stripe-js": ">=9.3.1 <10.0.0", - "react": ">=16.8.0 <20.0.0", - "react-dom": ">=16.8.0 <20.0.0" - } - }, - "node_modules/@stripe/stripe-js": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.5.0.tgz", - "integrity": "sha512-dTQWkJRw5lhcQipPuw6qZRBK2zY5eWWZ1Srw9mSjhIXSLdsNYO3uaIV+YRMkI0/tB/D7yQdHYStrcZrbeHI5Jg==", - "license": "MIT", - "engines": { - "node": ">=12.16" - } - }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -3739,6 +3718,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.39.0.tgz", + "integrity": "sha512-+vnLfzrv0MzjLzNl+nvNvR7jdg3q4cxxjz/YvzfifHl0TREtL00cs1RoMTxs+1PzLiEqZGV6gYsBY0oEAYZ24w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.39.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -4555,6 +4561,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5033,18 +5040,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5200,6 +5195,47 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/motion": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.39.0.tgz", + "integrity": "sha512-H4a+Ze+a9j+/NTla5ezfb/g9vmIOxC+viDj++NGDZyTZkdRKjiOz3kSv6TalRWM8ZmD2y/CfC6TkQc97ybyqSA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.39.0.tgz", + "integrity": "sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5559,17 +5595,6 @@ "babel-runtime": "6.26.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5655,12 +5680,6 @@ "react": "^19.2.4" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react-hot-toast": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", @@ -5678,6 +5697,12 @@ "react-dom": ">=16" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -6467,6 +6492,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", diff --git a/client/package.json b/client/package.json index d91e0ea..685916b 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,11 @@ "preview": "vite preview" }, "dependencies": { - "@stripe/react-stripe-js": "^6.3.0", - "@stripe/stripe-js": "^9.5.0", "@tailwindcss/vite": "^4.3.0", + "framer-motion": "^12.39.0", "axios": "^1.16.0", "lucide-react": "^1.11.0", + "motion": "^12.39.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", diff --git a/client/src/components/ExecCard.tsx b/client/src/components/ExecCard.tsx index 8b82a36..e297899 100644 --- a/client/src/components/ExecCard.tsx +++ b/client/src/components/ExecCard.tsx @@ -1,4 +1,5 @@ import "../style/common.css"; +import "../style/image_block/ImageBlock.css"; import { Pencil, Trash2 } from "lucide-react"; import api from "../api"; @@ -25,72 +26,60 @@ interface ExecProps { interface ExecCardProps { role: "admin" | "user"; onDelete: () => void; + onOpen: () => void; } const ExecCard: React.FC = ({ role, onDelete, + onOpen, id, imageURL, displayName, execRole, - description, - fullName, - ethnicity, - degree, - mbti, - fact, - sponsor, - greenFlag, - redFlag, - emojis, }) => { return (
-
- {displayName} - {role === "admin" && ( - - )} + - )} -
-

{displayName}

-
-

- Meet our {execRole}, {displayName}! -

-

{description}

-
    -
  • Full Name: {fullName}
  • - {ethnicity !== "" &&
  • Ethnicity: {ethnicity}
  • } - {degree !== "" &&
  • Degree: {degree}
  • } -
  • MBTI: {mbti}
  • -
  • Fun Fact: {fact}
  • -
  • Favourite KAC Sponsor: {sponsor}
  • -
  • Green Flag ✅: {greenFlag}
  • -
  • Red Flag 🚩: {redFlag}
  • - {emojis !== "" &&
  • Fav Emojis: {emojis}
  • } -
+
+
+ {displayName} + {role === "admin" && ( + + )} + + {role === "admin" && ( + + )} +
+ +
+

{execRole}

+

{displayName}

+
); diff --git a/client/src/components/Executives.tsx b/client/src/components/Executives.tsx index 79d0e3f..c2d570d 100644 --- a/client/src/components/Executives.tsx +++ b/client/src/components/Executives.tsx @@ -1,7 +1,7 @@ import "../style/common.css"; import "../style/about.css"; -import { useEffect, useState } from "react"; +import { useMemo, useEffect, useState } from "react"; import NewExecModal from "./NewExecModal"; @@ -13,6 +13,7 @@ interface Executive { imageURL: string; displayName: string; execRole: string; + roleGroup?: string; description: string; fullName: string; ethnicity: string; @@ -25,9 +26,54 @@ interface Executive { emojis: string; } +interface RoleGroup { + id: string; + label: string; +} + +const ROLE_GROUPS: RoleGroup[] = [ + { + id: "president", + label: "PRESIDENTS:", + }, + { + id: "admin", + label: "ADMIN:", + }, + { + id: "events", + label: "EVENTS:", + }, + { + id: "public-relations", + label: "PUBLIC RELATIONS:", + }, + { + id: "marketing", + label: "MARKETING:", + }, + { + id: "aesir-representative", + label: "AESIR REPRESENTATIVE:", + }, + { + id: "past-exec", + label: "OUR PAST EXECS:", + }, +]; + +const normaliseRoleKey = (value?: string) => + (value || "") + .trim() + .toLowerCase() + .replace(/[\s_-]+/g, " "); + +const EXEC_IMG = "src/images/exec-placeholder.png"; + const Executives = () => { const [execs, setExecs] = useState(null); const [loading, setLoading] = useState(true); + const [selectedExec, setSelectedExec] = useState(null); const loadExecs = async () => { try { @@ -45,6 +91,45 @@ const Executives = () => { loadExecs(); }, []); + const groupedExecs = useMemo(() => { + if (!execs) return [] as Array<[RoleGroup, Executive[]]>; + + const groupedMap = new Map(); + + for (const exec of execs) { + const normalisedRoleGroup = normaliseRoleKey(exec.roleGroup).replaceAll( + " ", + "-" + ); + const groupId = normalisedRoleGroup || "other"; + + if (!groupedMap.has(groupId)) groupedMap.set(groupId, []); + groupedMap.get(groupId)!.push(exec); + } + + const predefined = ROLE_GROUPS.map((group): [RoleGroup, Executive[]] => [ + group, + [...(groupedMap.get(group.id) || [])], + ]).filter(([, roleExecs]) => roleExecs.length > 0); + + const knownGroupIds = new Set(ROLE_GROUPS.map((group) => group.id)); + const customGroups = Array.from(groupedMap.entries()) + .filter( + ([groupId, roleExecs]) => + !knownGroupIds.has(groupId) && roleExecs.length + ) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([groupId, roleExecs]): [RoleGroup, Executive[]] => [ + { + id: groupId, + label: groupId.replace(/[-_]/g, " ").toUpperCase(), + }, + [...roleExecs], + ]); + + return [...predefined, ...customGroups]; + }, [execs]); + if (loading) { return (
{ return (
- {execs.map((exec) => { - return ( - ( +
+

{group.label}

+ +
+ {roleExecs.map((exec) => ( + setSelectedExec(exec)} + /> + ))} +
+
+ ))} + + {selectedExec && ( +
+ + +
+
+ {selectedExec.displayName} +
+ +
+

{selectedExec.execRole}

+

+ {selectedExec.displayName} +

+

+ {selectedExec.description} +

+
+
+
+
+ )} +
); diff --git a/client/src/components/ImageSlider.tsx b/client/src/components/ImageSlider.tsx new file mode 100644 index 0000000..b8a1231 --- /dev/null +++ b/client/src/components/ImageSlider.tsx @@ -0,0 +1,88 @@ +import { useState } from "react"; +import { motion, type Variants } from "framer-motion"; +import { ImageBlock } from "./ImageBlock/ImageBlock"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +interface ImageSliderProps { + pageKeys: string[]; +} + +const ImageSlider = ({ pageKeys }: ImageSliderProps) => { + const [positionIndexes, setPositionIndexes] = useState( + pageKeys.map((_, i) => i) + ); + + const total = pageKeys.length; + + const handleNext = () => { + setPositionIndexes((prevIndexes) => + prevIndexes.map((prevIndex) => (prevIndex + 1) % total) + ); + }; + + const handleBack = () => { + setPositionIndexes((prevIndexes) => + prevIndexes.map((prevIndex) => (prevIndex + total - 1) % total) + ); + }; + + const positions: string[] = ["center", "left", "right"]; + + const imageVariants: Variants = { + center: { x: "0%", y: "24%", scale: 1.1, zIndex: 4 }, + left: { x: "-90%", y: "-20%", scale: 0.6, zIndex: 3 }, + right: { x: "90%", y: "-20%", scale: 0.6, zIndex: 3 }, + }; + + return ( +
+ {pageKeys.map((pageKey, index) => { + return ( + +
+
+ +
+

+ {pageKey} +

+
+
+ ); + })} + + +
+ ); +}; + +export default ImageSlider; diff --git a/client/src/components/NewExecModal.tsx b/client/src/components/NewExecModal.tsx index a5c46f8..48b5baa 100644 --- a/client/src/components/NewExecModal.tsx +++ b/client/src/components/NewExecModal.tsx @@ -19,7 +19,7 @@ export default function Modal({}: ModalProps) { }; return ( -
+