From 9f6b1291707a2f2a52c32d4a95eb19a65a812ad0 Mon Sep 17 00:00:00 2001 From: Janelle Tam Date: Sun, 22 Feb 2026 21:04:05 -0500 Subject: [PATCH 1/5] feat: add aceternity marquee component --- components.json | 7 +- package-lock.json | 41 ++++++- src/app/globals.css | 22 +++- src/components/ui/infinite-moving-cards.tsx | 119 ++++++++++++++++++++ 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/infinite-moving-cards.tsx diff --git a/components.json b/components.json index ffe928f..da48ccb 100644 --- a/components.json +++ b/components.json @@ -10,6 +10,7 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" -} \ No newline at end of file + "registries": { + "@aceternity": "https://ui.aceternity.com/registry/{name}.json" + } +} diff --git a/package-lock.json b/package-lock.json index 51766a4..95d3ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,6 +166,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -341,6 +342,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -466,6 +468,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -488,6 +491,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -510,6 +514,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -526,6 +531,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -542,6 +548,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -558,6 +565,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -574,6 +582,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -590,6 +599,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -606,6 +616,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -622,6 +633,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -638,6 +650,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -654,6 +667,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -670,6 +684,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -692,6 +707,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -714,6 +730,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -736,6 +753,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -758,6 +776,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -780,6 +799,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -802,6 +822,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -824,6 +845,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -846,6 +868,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -865,6 +888,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -884,6 +908,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -903,6 +928,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1932,6 +1958,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1942,6 +1969,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2012,6 +2040,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -2592,6 +2621,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2941,6 +2971,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3340,7 +3371,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -3589,6 +3621,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3762,6 +3795,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5800,6 +5834,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -6308,6 +6343,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6320,6 +6356,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7223,6 +7260,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7403,6 +7441,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/globals.css b/src/app/globals.css index 110beb1..1f19c87 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -22,7 +22,17 @@ --text-body: rgba(250, 250, 250, 0.562); --detail-medium-contrast: rgba(89, 83, 172, 0.452); + + --animate-scroll: scroll var(--animation-duration, 40s) + var(--animation-direction, forwards) linear infinite; + + @keyframes scroll { + to { + transform: translate(calc(-50% - 0.5rem)); + } + } } + /* @custom-variant dark (&:where(.dark, .dark *)); */ @custom-variant dark (&:where(.dark, .dark *)); @@ -141,7 +151,9 @@ html { background-position: bottom left; -webkit-background-clip: text; background-clip: text; - transition: background-size 0.3s ease-out, color 0.3s ease-out; + transition: + background-size 0.3s ease-out, + color 0.3s ease-out; } .register-hover:hover { @@ -285,7 +297,13 @@ html { } .cta-border-spin { - background: conic-gradient(from 90deg at 50% 50%, #04000f 0%, #dadadaa9 35%, #905aee 40%, #04000f 100%); + background: conic-gradient( + from 90deg at 50% 50%, + #04000f 0%, + #dadadaa9 35%, + #905aee 40%, + #04000f 100% + ); } .animate-sparkle-1 { diff --git a/src/components/ui/infinite-moving-cards.tsx b/src/components/ui/infinite-moving-cards.tsx new file mode 100644 index 0000000..eb3db72 --- /dev/null +++ b/src/components/ui/infinite-moving-cards.tsx @@ -0,0 +1,119 @@ +'use client' + +import { cn } from '@/lib/utils' +import React, { useEffect, useState } from 'react' + +export const InfiniteMovingCards = ({ + items, + direction = 'left', + speed = 'slow', + pauseOnHover = true, + className +}: { + items: { + quote: string + name: string + title?: string + }[] + direction?: 'left' | 'right' + speed?: 'fast' | 'normal' | 'slow' + pauseOnHover?: boolean + className?: string +}) => { + const containerRef = React.useRef(null) + const scrollerRef = React.useRef(null) + + useEffect(() => { + addAnimation() + }, []) + const [start, setStart] = useState(false) + function addAnimation () { + if (containerRef.current && scrollerRef.current) { + const scrollerContent = Array.from(scrollerRef.current.children) + + scrollerContent.forEach(item => { + const duplicatedItem = item.cloneNode(true) + if (scrollerRef.current) { + scrollerRef.current.appendChild(duplicatedItem) + } + }) + + getDirection() + getSpeed() + setStart(true) + } + } + const getDirection = () => { + if (containerRef.current) { + if (direction === 'left') { + containerRef.current.style.setProperty( + '--animation-direction', + 'forwards' + ) + } else { + containerRef.current.style.setProperty( + '--animation-direction', + 'reverse' + ) + } + } + } + const getSpeed = () => { + if (containerRef.current) { + if (speed === 'fast') { + containerRef.current.style.setProperty('--animation-duration', '20s') + } else if (speed === 'normal') { + containerRef.current.style.setProperty('--animation-duration', '40s') + } else { + containerRef.current.style.setProperty('--animation-duration', '80s') + } + } + } + return ( +
+
    + {items.map((item, idx) => ( +
  • +
    + + + {item.quote} + +
    + + + {item.name} + + {item.title && ( + + {item.title} + + )} + +
    +
    +
  • + ))} +
+
+ ) +} From 6f44f3d115638571b917207ac416335a91cf6077 Mon Sep 17 00:00:00 2001 From: Janelle Tam Date: Sun, 22 Feb 2026 21:33:30 -0500 Subject: [PATCH 2/5] feat: add testimonials data --- .../home/testimonials/testimonials.json | 61 +++++++++++++++++++ src/lib/interface.ts | 6 ++ 2 files changed, 67 insertions(+) create mode 100644 src/components/home/testimonials/testimonials.json diff --git a/src/components/home/testimonials/testimonials.json b/src/components/home/testimonials/testimonials.json new file mode 100644 index 0000000..01db2fb --- /dev/null +++ b/src/components/home/testimonials/testimonials.json @@ -0,0 +1,61 @@ +[ + { + "text": "CUSEC gave me the opportunity to connect with other people and create lifelong memories that i keep close to me. no other conference has been able to spark this much passion in me when it comes to growing, both personally and career-wise, and because of that, i was able to network with people and keep that relationship past the conference. i still keep in touch with everyone on the team post conference, and i think that says a lot about CUSEC and everyone involved in making this a reality.", + "name": "CUSEC 2026 Head Delegate" + }, + { + "text": "CUSEC has been one of the most rewarding experiences of my time at university. Over two years, I've made some of my closest friends, worked on meaningful challenges, and found a community of people who are just as curious and excited about tech as I am.", + "name": "Tyrell Haywood", + "title": "2025 Director of Technology, 2026 Director of Speakers" + }, + { + "text": "When I first joined CUSEC, I was really anxious about reaching out to professionals, I kept thinking, “I’m just a student, why would they respond?” and I was scared of rejection. Being part of the Speaker Team changed that. I learned that putting yourself out there isn’t as intimidating as it seems, and that so many people are kind and willing to say yes. CUSEC helped me grow into someone who takes initiative and isn’t afraid to start conversations that once felt impossible. What truly makes CUSEC special to me, though, is the people. At CUSEC 2024, I met two people who are now some of my best friends, and I genuinely can’t imagine my life without them. This year, I met even more amazing people, and even with such a short amount of in-person time, we grew incredibly close. Seeing everything come together and finally meeting everyone on the team was one of the best experiences I’ve had as a student, like the best kind of fever dream you never want to wake up from.", + "name": "Sirine Tarhouni", + "title": "2024 Head Delegate of McGill,\n2026 Speakers Exec" + }, + { + "text": "Being Director of Sponsorship was truly an experience I will never forget! The connections I made, the goals I accomplished, and seeing all our hard work come to life is such a wonderful feeling.", + "name": "Angie Jocson", + "title": "2024 Director of Speakers,\n2025 Director of Sponsorships" + }, + { + "text": "CUSEC 2026 was an incredible experience that pushed me to grow both professionally and personally. It challenged me to step outside my comfort zone and develop skills I had never explored before. Prior to CUSEC, I had no experience in UI/UX design. By the end of the conference, I felt confident designing interfaces and contributing meaningfully to real projects. That growth alone made the experience unforgettable.\n\nBeyond the technical skills, CUSEC exposed me to an inspiring environment filled with ambitious, growth driven individuals. I connected with talented students and professionals from across Canada, each bringing expertise from different fields. The conversations, shared ideas, and late night discussions were just as valuable as the sessions themselves. I walked away not only with knowledge, but with lasting friendships and a stronger professional network.\n\nOverall, I highly recommend CUSEC to anyone looking to grow, challenge themselves, and connect with passionate individuals. It is more than just a conference. It is a space where you can learn, build, and become part of a driven community.", + "name": "Angel Shinh", + "title": "2026 Director of UI/UX" + }, + { + "text": "Serving as Director of Technology for CUSEC 2026 was one of the most rewarding experiences of my developer journey as a student. Building and shipping a production platform with the organizing team spread across the country meant I had to balance development with real-world constraints such as deadlines, cross-team coordination, and unexpected traffic during the conference. From turning a simple meeting idea into a full-scale Scavenger Hunt feature with extensive Admin panels, to deploying fixes live at the conference, every challenge became a lesson. I genuinely believe that CUSEC has helped me grow, both as a developer and as a collaborator.", + "name": "Shrey Bhatt", + "title": "2025 Head Delegate of Seneca College,\n 2026 Director of Technology" + }, + { + "text": "CUSEC is the place where you find your community in tech, people who challenge you and lift you up. I think it is incredibly special and rare to meet so many people you connect with, but CUSEC makes that possible. We spend one year together, separated across provinces but with the same goal: to organize a national conference.\n\nThe fulfillment of being able to see the results of our hard work in January, whether in the form of delegates having hour-long discussions with speakers or students teaming up to solve our scavenger hunt riddles, makes the year-long effort so worth it!", + "name": "Janelle Tam", + "title": "2025 Speakers Exec,\n2026 Director of Events\n2027 Co-Chair" + }, + { + "text": "Being a Head Delegate for CUSEC 2025 opened up my world to the huge local computer science community in Montreal. Through my time as a Head Delegate I hosted events & info-sessions, connected with like-minded people, got a ton of hands-on experience in communications and negotations professionally, and ultimately took part in uplifting Montreal's dev community and bringing national recognition to my delegation.", + "name": "Carson Spriggs", + "title": "2025 Head Delegate of Montreal & Sponsors Exec,\n2026,Director of Logistics,\n2027 Co-Chair" + }, + { + "text": "Being on CUSEC for 2 years now (starting as a Head Delegate to joining recently as a Logistics Exec), my experiences from 2025 and 2026 were vastly different. However, what has stayed the same and is why I keep coming back (2027 fingers crossed) are the people on the team. Planning an event isn't easy, but when you are surrounded by highly motivated, passionate, and overall cool people who want to make to give back to others, idk if it makes the work easier but it certainly makes it fun. There is no experience that is as rewarding as CUSEC and I highly encourage you apply :) (who knows you might also get a role named after you)", + "name": "Franklin Ramirez", + "title": "2025 Head Delegate of University of Waterloo,\n2026 Logistics Exec,\n2027 Director of Franklin" + }, + { + "text": "My time as Director of Events taught me a tremendous amount, and many of those lessons still stay with me today. The people I met while organizing events across the City of Montreal have remained valuable connections, even years later. It was an incredibly rewarding experience, both personally and professionally. I couldn't recommend it more.", + "name": "TJ Klint", + "title": "2023 Delegate,\n2024 Head Delegate & Director of Events,\n2025 Co-Chair" + }, + { + "text": "Designing for CUSEC is so much fun, and a great learning opportunity. You get to work with nearly every team and play a key role in shaping the conference’s image for the year. Don’t worry if you’ve never designed anything, because my first experience with design tools actually started at CUSEC! You’ll gain experience designing everything from posters, to social media graphics, to logos, to merch, and there is no better feeling than seeing all that hard work come to fruition and seeing it on display. I truly recommend this role for anyone looking to try their hand at design!", + "name": "Taryn Beaupre", + "title": "2025 Director of Promotions x Tech Design,\n2026 Director of Design" + }, + { + "text": "A lot goes into CUSEC from start to finish, serving as the Director of Logistics I got to see myself how these large-scale events work. \n\nYou’re in the middle of everything, working across all teams, putting out fires, solving conflicts and issues where the stakes are high. Once CUSEC actually came around, seeing hundreds of people from across Canada come in and enjoy the event we put together was one of the most rewarding experiences I’ve had.\n\nIf you want to be apart of something cool and work with great people who push you to grow, definitely consider applying.", + "name": "Carson Spriggs", + "title": "2025 Head Delegate of Montreal & Sponsors Exec\n2026 Director of Logistics\n2027 Co-Chair" + } +] diff --git a/src/lib/interface.ts b/src/lib/interface.ts index b1fb73e..b74370f 100644 --- a/src/lib/interface.ts +++ b/src/lib/interface.ts @@ -8,6 +8,12 @@ export interface Size { height: string; // vh/vw format like "8vh" } +export interface Testimonial { + quote: string; + name: string; + title?: string; +} + export interface Stat { id: string; name: string; From 256a3db5356affbc97bcbe72e865c445a207260b Mon Sep 17 00:00:00 2001 From: Janelle Tam Date: Sun, 22 Feb 2026 23:33:21 -0500 Subject: [PATCH 3/5] feat: replace marquee with carousel, add mobile responsiveness --- components.json | 4 +- package-lock.json | 10 ++ package.json | 1 + src/app/globals.css | 9 -- src/app/page.tsx | 2 + .../home/testimonials/Testimonials.tsx | 146 ++++++++++++++++++ .../home/testimonials/testimonials.json | 43 +++--- src/components/index.tsx | 1 + src/components/ui/infinite-moving-cards.tsx | 119 -------------- 9 files changed, 180 insertions(+), 155 deletions(-) create mode 100644 src/components/home/testimonials/Testimonials.tsx delete mode 100644 src/components/ui/infinite-moving-cards.tsx diff --git a/components.json b/components.json index da48ccb..edcaef2 100644 --- a/components.json +++ b/components.json @@ -18,7 +18,5 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": { - "@aceternity": "https://ui.aceternity.com/registry/{name}.json" - } + "registries": {} } diff --git a/package-lock.json b/package-lock.json index 95d3ab2..171a7fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "cloudinary": "^2.8.0", "clsx": "^2.1.1", "embla-carousel": "^8.6.0", + "embla-carousel-auto-height": "^8.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.6", "lucide-react": "^0.539.0", @@ -3374,6 +3375,15 @@ "license": "MIT", "peer": true }, + "node_modules/embla-carousel-auto-height": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-auto-height/-/embla-carousel-auto-height-8.6.0.tgz", + "integrity": "sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/embla-carousel-react": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", diff --git a/package.json b/package.json index 6244a52..2eaaabe 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cloudinary": "^2.8.0", "clsx": "^2.1.1", "embla-carousel": "^8.6.0", + "embla-carousel-auto-height": "^8.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.6", "lucide-react": "^0.539.0", diff --git a/src/app/globals.css b/src/app/globals.css index 1f19c87..d36dae0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -22,15 +22,6 @@ --text-body: rgba(250, 250, 250, 0.562); --detail-medium-contrast: rgba(89, 83, 172, 0.452); - - --animate-scroll: scroll var(--animation-duration, 40s) - var(--animation-direction, forwards) linear infinite; - - @keyframes scroll { - to { - transform: translate(calc(-50% - 0.5rem)); - } - } } /* @custom-variant dark (&:where(.dark, .dark *)); */ diff --git a/src/app/page.tsx b/src/app/page.tsx index 1d835f3..154f355 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,6 +12,7 @@ import { Footer, FAQ, Gallery, + Testimonials, } from "@/components"; const Home = () => { @@ -32,6 +33,7 @@ const Home = () => { + diff --git a/src/components/home/testimonials/Testimonials.tsx b/src/components/home/testimonials/Testimonials.tsx new file mode 100644 index 0000000..2ac8a0c --- /dev/null +++ b/src/components/home/testimonials/Testimonials.tsx @@ -0,0 +1,146 @@ +'use client' + +import { useState, useEffect, useCallback, useRef } from 'react' +import Image from 'next/image' +import { motion } from 'framer-motion' +import useEmblaCarousel from 'embla-carousel-react' +import AutoHeight from 'embla-carousel-auto-height' +import type { EmblaPluginType } from 'embla-carousel' +import { + PrevButton, + NextButton, + usePrevNextButtons +} from '../gallery/embla/EmblaCarouselArrowButtons' +import { useDotButton } from '../gallery/embla/EmblaCarouselDotButtons' +import { cn } from '@/lib/utils' +import testimonials from './testimonials.json' +import '../../../app/embla.css' + +const CARD_BASE = + 'rounded-2xl border-4 border-white/10 backdrop-blur-xs px-8 py-8 md:px-12 md:py-10 flex flex-col gap-6 transition-all duration-300 cursor-default' +const CARD_ACTIVE = + 'bg-light-mode/10 hover:scale-[1.010] hover:shadow-[0_0_26px_rgba(245,240,233,0.1)]' +const CARD_INACTIVE = 'bg-light-mode/5 opacity-50' + +const KOI_STYLE = { + bottom: '-23vw', + x: '-200%', + rotate: 80, + height: '35vw', + minHeight: '140px', + minWidth: '140px', + width: '15vw', + maxHeight: '520px', + maxWidth: '520px' +} + +const Testimonials: React.FC = () => { + const [plugins, setPlugins] = useState([]) + + useEffect(() => { + if (window.matchMedia('(max-width: 767px)').matches) { + setPlugins([AutoHeight()]) + } + }, []) + + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: 'center' }, plugins) + const { selectedIndex } = useDotButton(emblaApi) + const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = + usePrevNextButtons(emblaApi) + + // Track viewport height via ResizeObserver so nav buttons follow the + // AutoHeight animation in real-time (top: 50% doesn't trigger CSS transitions) + const viewportElRef = useRef(null) + const [btnTop, setBtnTop] = useState(undefined) + + const setViewportRef = useCallback( + (node: HTMLDivElement | null) => { + viewportElRef.current = node + emblaRef(node) + }, + [emblaRef] + ) + + useEffect(() => { + const el = viewportElRef.current + if (!el) return + setBtnTop(el.offsetHeight / 2) + const ro = new ResizeObserver(() => setBtnTop(el.offsetHeight / 2)) + ro.observe(el) + return () => ro.disconnect() + }, [emblaApi]) + + return ( +
+
+
+

+ Why Join The Team +

+
+ +
+ {/* edge fade overlays */} +
+
+ + {/* nav buttons — top set via JS so they track the AutoHeight animation */} +
+ +
+
+ +
+ +
+
+ {testimonials.map((item, idx) => ( +
+
+

+ {item.quote} +

+
+ {item.name} + {item.title && ( + + {item.title} + + )} +
+
+
+ ))} +
+
+
+
+ + {/* decorative koi for desktop only */} + +
+ Koi +
+
+
+ ) +} + +export default Testimonials diff --git a/src/components/home/testimonials/testimonials.json b/src/components/home/testimonials/testimonials.json index 01db2fb..edd2603 100644 --- a/src/components/home/testimonials/testimonials.json +++ b/src/components/home/testimonials/testimonials.json @@ -1,61 +1,56 @@ [ { - "text": "CUSEC gave me the opportunity to connect with other people and create lifelong memories that i keep close to me. no other conference has been able to spark this much passion in me when it comes to growing, both personally and career-wise, and because of that, i was able to network with people and keep that relationship past the conference. i still keep in touch with everyone on the team post conference, and i think that says a lot about CUSEC and everyone involved in making this a reality.", + "quote": "CUSEC gave me the opportunity to connect with other people and create lifelong memories that i keep close to me. no other conference has been able to spark this much passion in me when it comes to growing, both personally and career-wise, and because of that, i was able to network with people and keep that relationship past the conference. i still keep in touch with everyone on the team post conference, and i think that says a lot about CUSEC and everyone involved in making this a reality.", "name": "CUSEC 2026 Head Delegate" }, { - "text": "CUSEC has been one of the most rewarding experiences of my time at university. Over two years, I've made some of my closest friends, worked on meaningful challenges, and found a community of people who are just as curious and excited about tech as I am.", + "quote": "CUSEC has been one of the most rewarding experiences of my time at university. Over two years, I've made some of my closest friends, worked on meaningful challenges, and found a community of people who are just as curious and excited about tech as I am.", "name": "Tyrell Haywood", "title": "2025 Director of Technology, 2026 Director of Speakers" }, { - "text": "When I first joined CUSEC, I was really anxious about reaching out to professionals, I kept thinking, “I’m just a student, why would they respond?” and I was scared of rejection. Being part of the Speaker Team changed that. I learned that putting yourself out there isn’t as intimidating as it seems, and that so many people are kind and willing to say yes. CUSEC helped me grow into someone who takes initiative and isn’t afraid to start conversations that once felt impossible. What truly makes CUSEC special to me, though, is the people. At CUSEC 2024, I met two people who are now some of my best friends, and I genuinely can’t imagine my life without them. This year, I met even more amazing people, and even with such a short amount of in-person time, we grew incredibly close. Seeing everything come together and finally meeting everyone on the team was one of the best experiences I’ve had as a student, like the best kind of fever dream you never want to wake up from.", + "quote": "When I first joined CUSEC, I was really anxious about reaching out to professionals, I kept thinking, “I’m just a student, why would they respond?” and I was scared of rejection. Being part of the Speaker Team changed that. I learned that putting yourself out there isn’t as intimidating as it seems, and that so many people are kind and willing to say yes. CUSEC helped me grow into someone who takes initiative and isn’t afraid to start conversations that once felt impossible. What truly makes CUSEC special to me, though, is the people. At CUSEC 2024, I met two people who are now some of my best friends, and I genuinely can’t imagine my life without them. This year, I met even more amazing people, and even with such a short amount of in-person time, we grew incredibly close. Seeing everything come together and finally meeting everyone on the team was one of the best experiences I’ve had as a student, like the best kind of fever dream you never want to wake up from.", "name": "Sirine Tarhouni", - "title": "2024 Head Delegate of McGill,\n2026 Speakers Exec" + "title": "2024 Head Delegate of McGill, 2026 Speakers Exec" }, { - "text": "Being Director of Sponsorship was truly an experience I will never forget! The connections I made, the goals I accomplished, and seeing all our hard work come to life is such a wonderful feeling.", + "quote": "Being Director of Sponsorship was truly an experience I will never forget! The connections I made, the goals I accomplished, and seeing all our hard work come to life is such a wonderful feeling.", "name": "Angie Jocson", - "title": "2024 Director of Speakers,\n2025 Director of Sponsorships" + "title": "2024 Director of Speakers, 2025 Director of Sponsorships" }, { - "text": "CUSEC 2026 was an incredible experience that pushed me to grow both professionally and personally. It challenged me to step outside my comfort zone and develop skills I had never explored before. Prior to CUSEC, I had no experience in UI/UX design. By the end of the conference, I felt confident designing interfaces and contributing meaningfully to real projects. That growth alone made the experience unforgettable.\n\nBeyond the technical skills, CUSEC exposed me to an inspiring environment filled with ambitious, growth driven individuals. I connected with talented students and professionals from across Canada, each bringing expertise from different fields. The conversations, shared ideas, and late night discussions were just as valuable as the sessions themselves. I walked away not only with knowledge, but with lasting friendships and a stronger professional network.\n\nOverall, I highly recommend CUSEC to anyone looking to grow, challenge themselves, and connect with passionate individuals. It is more than just a conference. It is a space where you can learn, build, and become part of a driven community.", + "quote": "Prior to CUSEC, I had no experience in UI/UX design. By the end of the conference, I felt confident designing interfaces and contributing meaningfully to real projects. That growth alone made the experience unforgettable. Beyond the technical skills, CUSEC exposed me to an inspiring environment filled with ambitious, growth driven individuals. I connected with talented students and professionals from across Canada, each bringing expertise from different fields. The conversations, shared ideas, and late night discussions were just as valuable as the sessions themselves. I walked away not only with knowledge, but with lasting friendships and a stronger professional network. Overall, I highly recommend CUSEC to anyone looking to grow, challenge themselves, and connect with passionate individuals. It is more than just a conference. It is a space where you can learn, build, and become part of a driven community.", "name": "Angel Shinh", "title": "2026 Director of UI/UX" }, { - "text": "Serving as Director of Technology for CUSEC 2026 was one of the most rewarding experiences of my developer journey as a student. Building and shipping a production platform with the organizing team spread across the country meant I had to balance development with real-world constraints such as deadlines, cross-team coordination, and unexpected traffic during the conference. From turning a simple meeting idea into a full-scale Scavenger Hunt feature with extensive Admin panels, to deploying fixes live at the conference, every challenge became a lesson. I genuinely believe that CUSEC has helped me grow, both as a developer and as a collaborator.", + "quote": "Serving as Director of Technology for CUSEC 2026 was one of the most rewarding experiences of my developer journey as a student. Building and shipping a production platform with the organizing team spread across the country meant I had to balance development with real-world constraints such as deadlines, cross-team coordination, and unexpected traffic during the conference. From turning a simple meeting idea into a full-scale Scavenger Hunt feature with extensive Admin panels, to deploying fixes live at the conference, every challenge became a lesson. I genuinely believe that CUSEC has helped me grow, both as a developer and as a collaborator.", "name": "Shrey Bhatt", - "title": "2025 Head Delegate of Seneca College,\n 2026 Director of Technology" + "title": "2025 Head Delegate of Seneca College, 2026 Director of Technology" }, { - "text": "CUSEC is the place where you find your community in tech, people who challenge you and lift you up. I think it is incredibly special and rare to meet so many people you connect with, but CUSEC makes that possible. We spend one year together, separated across provinces but with the same goal: to organize a national conference.\n\nThe fulfillment of being able to see the results of our hard work in January, whether in the form of delegates having hour-long discussions with speakers or students teaming up to solve our scavenger hunt riddles, makes the year-long effort so worth it!", + "quote": "CUSEC is the place where you find your community in tech, people who challenge you and lift you up. I think it is incredibly special and rare to meet so many people you connect with, but CUSEC makes that possible. We spend one year together, separated across provinces but with the same goal: to organize a national conference.The fulfillment of being able to see the results of our hard work in January, whether in the form of delegates having hour-long discussions with speakers or students teaming up to solve our scavenger hunt riddles, makes the year-long effort so worth it!", "name": "Janelle Tam", - "title": "2025 Speakers Exec,\n2026 Director of Events\n2027 Co-Chair" + "title": "2025 Speakers Exec, 2026 Director of Events, 2027 Co-Chair" }, { - "text": "Being a Head Delegate for CUSEC 2025 opened up my world to the huge local computer science community in Montreal. Through my time as a Head Delegate I hosted events & info-sessions, connected with like-minded people, got a ton of hands-on experience in communications and negotations professionally, and ultimately took part in uplifting Montreal's dev community and bringing national recognition to my delegation.", - "name": "Carson Spriggs", - "title": "2025 Head Delegate of Montreal & Sponsors Exec,\n2026,Director of Logistics,\n2027 Co-Chair" - }, - { - "text": "Being on CUSEC for 2 years now (starting as a Head Delegate to joining recently as a Logistics Exec), my experiences from 2025 and 2026 were vastly different. However, what has stayed the same and is why I keep coming back (2027 fingers crossed) are the people on the team. Planning an event isn't easy, but when you are surrounded by highly motivated, passionate, and overall cool people who want to make to give back to others, idk if it makes the work easier but it certainly makes it fun. There is no experience that is as rewarding as CUSEC and I highly encourage you apply :) (who knows you might also get a role named after you)", + "quote": "Being on CUSEC for 2 years now (starting as a Head Delegate to joining recently as a Logistics Exec), my experiences from 2025 and 2026 were vastly different. However, what has stayed the same and is why I keep coming back (2027 fingers crossed) are the people on the team. Planning an event isn't easy, but when you are surrounded by highly motivated, passionate, and overall cool people who want to make to give back to others, idk if it makes the work easier but it certainly makes it fun. There is no experience that is as rewarding as CUSEC and I highly encourage you apply :) (who knows you might also get a role named after you)", "name": "Franklin Ramirez", - "title": "2025 Head Delegate of University of Waterloo,\n2026 Logistics Exec,\n2027 Director of Franklin" + "title": "2025 Head Delegate of University of Waterloo, 2026 Logistics Exec, 2027 Director of Franklin" }, { - "text": "My time as Director of Events taught me a tremendous amount, and many of those lessons still stay with me today. The people I met while organizing events across the City of Montreal have remained valuable connections, even years later. It was an incredibly rewarding experience, both personally and professionally. I couldn't recommend it more.", + "quote": "My time as Director of Events taught me a tremendous amount, and many of those lessons still stay with me today. The people I met while organizing events across the City of Montreal have remained valuable connections, even years later. It was an incredibly rewarding experience, both personally and professionally. I couldn't recommend it more.", "name": "TJ Klint", - "title": "2023 Delegate,\n2024 Head Delegate & Director of Events,\n2025 Co-Chair" + "title": "2023 Delegate, 2024 Head Delegate & Director of Events, 2025 Co-Chair" }, { - "text": "Designing for CUSEC is so much fun, and a great learning opportunity. You get to work with nearly every team and play a key role in shaping the conference’s image for the year. Don’t worry if you’ve never designed anything, because my first experience with design tools actually started at CUSEC! You’ll gain experience designing everything from posters, to social media graphics, to logos, to merch, and there is no better feeling than seeing all that hard work come to fruition and seeing it on display. I truly recommend this role for anyone looking to try their hand at design!", + "quote": "Designing for CUSEC is so much fun, and a great learning opportunity. You get to work with nearly every team and play a key role in shaping the conference’s image for the year. Don’t worry if you’ve never designed anything, because my first experience with design tools actually started at CUSEC! You’ll gain experience designing everything from posters, to social media graphics, to logos, to merch, and there is no better feeling than seeing all that hard work come to fruition and seeing it on display. I truly recommend this role for anyone looking to try their hand at design!", "name": "Taryn Beaupre", - "title": "2025 Director of Promotions x Tech Design,\n2026 Director of Design" + "title": "2025 Director of Promotions x Tech Design, 2026 Director of Design" }, { - "text": "A lot goes into CUSEC from start to finish, serving as the Director of Logistics I got to see myself how these large-scale events work. \n\nYou’re in the middle of everything, working across all teams, putting out fires, solving conflicts and issues where the stakes are high. Once CUSEC actually came around, seeing hundreds of people from across Canada come in and enjoy the event we put together was one of the most rewarding experiences I’ve had.\n\nIf you want to be apart of something cool and work with great people who push you to grow, definitely consider applying.", + "quote": "A lot goes into CUSEC from start to finish, serving as the Director of Logistics I got to see myself how these large-scale events work. \nYou’re in the middle of everything, working across all teams, putting out fires, solving conflicts and issues where the stakes are high. Once CUSEC actually came around, seeing hundreds of people from across Canada come in and enjoy the event we put together was one of the most rewarding experiences I’ve had.\n\nIf you want to be apart of something cool and work with great people who push you to grow, definitely consider applying.", "name": "Carson Spriggs", - "title": "2025 Head Delegate of Montreal & Sponsors Exec\n2026 Director of Logistics\n2027 Co-Chair" + "title": "2025 Head Delegate of Montreal & Sponsors Exec, 2026 Director of Logistics, 2027 Co-Chair" } ] diff --git a/src/components/index.tsx b/src/components/index.tsx index be96484..f152d26 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -11,6 +11,7 @@ export { default as LoadingScreen } from "./general/LoadingScreen"; export { default as SmoothFollower } from "./general/SmoothFollower"; export { default as Gallery } from "./home/gallery/Gallery"; export { default as FAQ } from "./home/faq/FAQ"; +export { default as Testimonials } from "./home/testimonials/Testimonials"; export { default as CodeOfConduct } from "./code-of-conduct/CodeOfConduct"; export { default as PrivacyPolicy } from "./privacy-policy/PrivacyPolicy"; export { default as Schedule } from "./schedule/Schedule"; diff --git a/src/components/ui/infinite-moving-cards.tsx b/src/components/ui/infinite-moving-cards.tsx deleted file mode 100644 index eb3db72..0000000 --- a/src/components/ui/infinite-moving-cards.tsx +++ /dev/null @@ -1,119 +0,0 @@ -'use client' - -import { cn } from '@/lib/utils' -import React, { useEffect, useState } from 'react' - -export const InfiniteMovingCards = ({ - items, - direction = 'left', - speed = 'slow', - pauseOnHover = true, - className -}: { - items: { - quote: string - name: string - title?: string - }[] - direction?: 'left' | 'right' - speed?: 'fast' | 'normal' | 'slow' - pauseOnHover?: boolean - className?: string -}) => { - const containerRef = React.useRef(null) - const scrollerRef = React.useRef(null) - - useEffect(() => { - addAnimation() - }, []) - const [start, setStart] = useState(false) - function addAnimation () { - if (containerRef.current && scrollerRef.current) { - const scrollerContent = Array.from(scrollerRef.current.children) - - scrollerContent.forEach(item => { - const duplicatedItem = item.cloneNode(true) - if (scrollerRef.current) { - scrollerRef.current.appendChild(duplicatedItem) - } - }) - - getDirection() - getSpeed() - setStart(true) - } - } - const getDirection = () => { - if (containerRef.current) { - if (direction === 'left') { - containerRef.current.style.setProperty( - '--animation-direction', - 'forwards' - ) - } else { - containerRef.current.style.setProperty( - '--animation-direction', - 'reverse' - ) - } - } - } - const getSpeed = () => { - if (containerRef.current) { - if (speed === 'fast') { - containerRef.current.style.setProperty('--animation-duration', '20s') - } else if (speed === 'normal') { - containerRef.current.style.setProperty('--animation-duration', '40s') - } else { - containerRef.current.style.setProperty('--animation-duration', '80s') - } - } - } - return ( -
-
    - {items.map((item, idx) => ( -
  • -
    - - - {item.quote} - -
    - - - {item.name} - - {item.title && ( - - {item.title} - - )} - -
    -
    -
  • - ))} -
-
- ) -} From 5dc157816b306047a55f71f89914dd6a2e6dc6e5 Mon Sep 17 00:00:00 2001 From: Janelle Tam Date: Sun, 22 Feb 2026 23:43:42 -0500 Subject: [PATCH 4/5] chore: reset globals.css and components.json to version on main (to fully revert aceternity install). clean up package-lock --- components.json | 5 ++--- package-lock.json | 4 ++-- src/app/globals.css | 13 ++----------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/components.json b/components.json index edcaef2..ffe928f 100644 --- a/components.json +++ b/components.json @@ -10,7 +10,6 @@ "cssVariables": true, "prefix": "" }, - "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -18,5 +17,5 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} -} + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 171a7fe..c821e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "embla-carousel-auto-height": "^8.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.6", + "html5-qrcode": "^2.3.8", "lucide-react": "^0.539.0", "mongodb": "^6.17.0", "mongoose": "^8.16.4", @@ -34,6 +35,7 @@ "react-dom": "^18.0.0", "react-icons": "^5.5.0", "react-zxing": "^1.1.3", + "sharp": "^0.34.5", "tailwind-merge": "^3.3.1" }, "devDependencies": { @@ -45,9 +47,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.3", - "html5-qrcode": "^2.3.8", "serwist": "^9.4.2", - "sharp": "^0.34.5", "tailwindcss": "^4", "tw-animate-css": "^1.3.7", "typescript": "^5" diff --git a/src/app/globals.css b/src/app/globals.css index d36dae0..110beb1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -23,7 +23,6 @@ --text-body: rgba(250, 250, 250, 0.562); --detail-medium-contrast: rgba(89, 83, 172, 0.452); } - /* @custom-variant dark (&:where(.dark, .dark *)); */ @custom-variant dark (&:where(.dark, .dark *)); @@ -142,9 +141,7 @@ html { background-position: bottom left; -webkit-background-clip: text; background-clip: text; - transition: - background-size 0.3s ease-out, - color 0.3s ease-out; + transition: background-size 0.3s ease-out, color 0.3s ease-out; } .register-hover:hover { @@ -288,13 +285,7 @@ html { } .cta-border-spin { - background: conic-gradient( - from 90deg at 50% 50%, - #04000f 0%, - #dadadaa9 35%, - #905aee 40%, - #04000f 100% - ); + background: conic-gradient(from 90deg at 50% 50%, #04000f 0%, #dadadaa9 35%, #905aee 40%, #04000f 100%); } .animate-sparkle-1 { From 1b06a671ba2c81517f4a6e5368819c2e885b11bc Mon Sep 17 00:00:00 2001 From: Janelle Tam Date: Sun, 22 Feb 2026 23:54:46 -0500 Subject: [PATCH 5/5] refactor: remove unused testimonial interface + remove nav button animation logic since it is barely noticeable and add complexity to the code --- .../home/testimonials/Testimonials.tsx | 94 +++++++++---------- src/lib/interface.ts | 6 -- 2 files changed, 45 insertions(+), 55 deletions(-) diff --git a/src/components/home/testimonials/Testimonials.tsx b/src/components/home/testimonials/Testimonials.tsx index 2ac8a0c..736c2a3 100644 --- a/src/components/home/testimonials/Testimonials.tsx +++ b/src/components/home/testimonials/Testimonials.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback, useRef } from 'react' +import { useState, useEffect } from 'react' import Image from 'next/image' import { motion } from 'framer-motion' import useEmblaCarousel from 'embla-carousel-react' @@ -35,40 +35,24 @@ const KOI_STYLE = { } const Testimonials: React.FC = () => { + // autoheight for carousel only needed on mobile, since desktop cards are wide enough that heights are similar const [plugins, setPlugins] = useState([]) - useEffect(() => { - if (window.matchMedia('(max-width: 767px)').matches) { + if (window.matchMedia('(max-width: 767px)').matches) setPlugins([AutoHeight()]) - } }, []) - const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: 'center' }, plugins) - const { selectedIndex } = useDotButton(emblaApi) - const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = - usePrevNextButtons(emblaApi) - - // Track viewport height via ResizeObserver so nav buttons follow the - // AutoHeight animation in real-time (top: 50% doesn't trigger CSS transitions) - const viewportElRef = useRef(null) - const [btnTop, setBtnTop] = useState(undefined) - - const setViewportRef = useCallback( - (node: HTMLDivElement | null) => { - viewportElRef.current = node - emblaRef(node) - }, - [emblaRef] + const [emblaRef, emblaApi] = useEmblaCarousel( + { loop: true, align: 'center' }, + plugins ) - - useEffect(() => { - const el = viewportElRef.current - if (!el) return - setBtnTop(el.offsetHeight / 2) - const ro = new ResizeObserver(() => setBtnTop(el.offsetHeight / 2)) - ro.observe(el) - return () => ro.disconnect() - }, [emblaApi]) + const { selectedIndex } = useDotButton(emblaApi) + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick + } = usePrevNextButtons(emblaApi) return (
@@ -87,33 +71,40 @@ const Testimonials: React.FC = () => {
- {/* nav buttons — top set via JS so they track the AutoHeight animation */} -
- + {/* nav buttons */} +
+
-
- +
+
-
+
{testimonials.map((item, idx) => ( -
-
+
+

{item.quote}

- {item.name} + + {item.name} + {item.title && ( {item.title} @@ -128,7 +119,7 @@ const Testimonials: React.FC = () => {
- {/* decorative koi for desktop only */} + {/* decorative koi — desktop only, bobs on a 3s loop */} { transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }} >
- Koi + Koi
diff --git a/src/lib/interface.ts b/src/lib/interface.ts index b74370f..b1fb73e 100644 --- a/src/lib/interface.ts +++ b/src/lib/interface.ts @@ -8,12 +8,6 @@ export interface Size { height: string; // vh/vw format like "8vh" } -export interface Testimonial { - quote: string; - name: string; - title?: string; -} - export interface Stat { id: string; name: string;