diff --git a/src/client/components/programs/BoardPicture.tsx b/src/client/components/programs/BoardPicture.tsx index f70bac7c..010726d1 100644 --- a/src/client/components/programs/BoardPicture.tsx +++ b/src/client/components/programs/BoardPicture.tsx @@ -10,7 +10,7 @@ const BoardPicture = () => { {/* Text Overlay */}
-

+

We offer a variety of programs that provide members of all experience levels with opportunities to network, socialize, and develop technical and leadership skills. Read on to learn more about our Intern Program, Engineering Team, Web Team, and Intramural sports league.

diff --git a/src/client/components/programs/Carousel.tsx b/src/client/components/programs/Carousel.tsx new file mode 100644 index 00000000..6efc397f --- /dev/null +++ b/src/client/components/programs/Carousel.tsx @@ -0,0 +1,153 @@ +import { cn } from "@/shared/utils"; +import type { EmblaCarouselType, EmblaEventType, EmblaOptionsType } from "embla-carousel"; +import useEmblaCarousel from "embla-carousel-react"; +import React, { useCallback, useEffect, useRef } from "react"; +import { NextButton, PrevButton, usePrevNextButtons } from "./CarouselArrows"; +import ProgramImages from "./ProgramImages"; +import Testimonials from "./Testimonials"; + +const TWEEN_FACTOR_BASE = 0.52; + +const numberWithinRange = (number: number, min: number, max: number): number => Math.min(Math.max(number, min), max); + +type PropType = { + options?: EmblaOptionsType; + purpose: string; + prog: string; +}; + +const TestimonialCarousel: React.FC = ({ prog, purpose }) => { + let slides; + if (purpose == "Testimonials") { + slides = Testimonials.find((t) => t.program === prog)?.testimonials ?? []; + } else { + slides = ProgramImages.find((i) => i.program === prog)?.images ?? []; + } + + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }); + const tweenFactor = useRef(0); + const tweenNodes = useRef>([]); + + const { nextBtnDisabled, onNextButtonClick, onPrevButtonClick, prevBtnDisabled } = usePrevNextButtons(emblaApi); + + const setTweenNodes = useCallback((emblaApi: EmblaCarouselType): void => { + tweenNodes.current = emblaApi.slideNodes().map((slideNode) => { + return slideNode.querySelector(".embla__slide__image") as HTMLElement; + }); + }, []); + + const setTweenFactor = useCallback((emblaApi: EmblaCarouselType) => { + tweenFactor.current = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length; + }, []); + + const tweenScale = useCallback((emblaApi: EmblaCarouselType, eventName?: EmblaEventType) => { + const engine = emblaApi.internalEngine(); + const scrollProgress = emblaApi.scrollProgress(); + const slidesInView = emblaApi.slidesInView(); + const isScrollEvent = eventName === "scroll"; + + emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => { + let diffToTarget = scrollSnap - scrollProgress; + const slidesInSnap = engine.slideRegistry[snapIndex]; + + slidesInSnap.forEach((slideIndex) => { + if (isScrollEvent && !slidesInView.includes(slideIndex)) return; + + if (engine.options.loop) { + engine.slideLooper.loopPoints.forEach((loopItem) => { + const target = loopItem.target(); + + if (slideIndex === loopItem.index && target !== 0) { + const sign = Math.sign(target); + + if (sign === -1) { + diffToTarget = scrollSnap - (1 + scrollProgress); + } + if (sign === 1) { + diffToTarget = scrollSnap + (1 - scrollProgress); + } + } + }); + } + + const tweenValue = 1.25 - Math.abs(diffToTarget * tweenFactor.current); + const scale = numberWithinRange(tweenValue, 0, 1).toString(); + const tweenNode = tweenNodes.current[slideIndex]; + tweenNode.style.transform = `scale(${scale})`; + }); + }); + }, []); + + useEffect(() => { + if (!emblaApi) return; + + setTweenNodes(emblaApi); + setTweenFactor(emblaApi); + tweenScale(emblaApi); + + emblaApi.on("reInit", setTweenNodes).on("reInit", setTweenFactor).on("reInit", tweenScale).on("scroll", tweenScale).on("slideFocus", tweenScale); + }, [emblaApi, tweenScale]); + + return ( +
+ {purpose === "Testimonials" ? ( + <> + +
+ + ) : null} + +
+
+ {slides.map((slide, index) => ( +
+
+
+ {/* If slide element is not a string, carousel is for testimonials. If it is, carousel is for images */} + {typeof slide != "string" ? ( + {`Image`} + ) : ( + {`Image`} + )} + {typeof slide != "string" ? ( +
+

+ {slide.name} +

+

{slide.position}

+

+ "{slide.quote}" +

+
+ ) : null} +
+
+
+ ))} +
+
+ {purpose === "Testimonials" ? ( + <> +
+ + + ) : ( +
+
+ + +
+
+ )} +
+ ); +}; + +export default TestimonialCarousel; diff --git a/src/client/components/programs/CarouselArrows.tsx b/src/client/components/programs/CarouselArrows.tsx new file mode 100644 index 00000000..b1475e09 --- /dev/null +++ b/src/client/components/programs/CarouselArrows.tsx @@ -0,0 +1,84 @@ +import type { EmblaCarouselType } from "embla-carousel"; +import type { ComponentPropsWithRef } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import "@components/home/embla.css"; + +type UsePrevNextButtonsType = { + prevBtnDisabled: boolean; + nextBtnDisabled: boolean; + onPrevButtonClick: () => void; + onNextButtonClick: () => void; +}; + +export const usePrevNextButtons = ( + emblaApi: EmblaCarouselType | undefined, + onButtonClick?: (emblaApi: EmblaCarouselType) => void, +): UsePrevNextButtonsType => { + const [prevBtnDisabled, setPrevBtnDisabled] = useState(true); + const [nextBtnDisabled, setNextBtnDisabled] = useState(true); + + const onPrevButtonClick = useCallback(() => { + if (!emblaApi) return; + emblaApi.scrollPrev(); + if (onButtonClick) onButtonClick(emblaApi); + }, [emblaApi, onButtonClick]); + + const onNextButtonClick = useCallback(() => { + if (!emblaApi) return; + emblaApi.scrollNext(); + if (onButtonClick) onButtonClick(emblaApi); + }, [emblaApi, onButtonClick]); + + const onSelect = useCallback((emblaApi: EmblaCarouselType) => { + setPrevBtnDisabled(!emblaApi.canScrollPrev()); + setNextBtnDisabled(!emblaApi.canScrollNext()); + }, []); + + useEffect(() => { + if (!emblaApi) return; + + onSelect(emblaApi); + emblaApi.on("reInit", onSelect).on("select", onSelect); + }, [emblaApi, onSelect]); + + return { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + }; +}; + +type PropType = ComponentPropsWithRef<"button">; + +export const PrevButton: React.FC = (props) => { + const { children, ...restProps } = props; + + return ( + + ); +}; + +export const NextButton: React.FC = (props) => { + const { children, ...restProps } = props; + + return ( + + ); +}; diff --git a/src/client/components/programs/FAQCard.tsx b/src/client/components/programs/FAQCard.tsx index 1bd2e878..52871588 100644 --- a/src/client/components/programs/FAQCard.tsx +++ b/src/client/components/programs/FAQCard.tsx @@ -42,7 +42,7 @@ const FAQItem: React.FC = ({ answer, question }) => { const FAQ: React.FC = ({ faqData }) => { return ( -
+
{faqData.map((item, index) => ( ))} diff --git a/src/client/components/programs/GoalCard.tsx b/src/client/components/programs/GoalCard.tsx index 660a59df..270f1266 100644 --- a/src/client/components/programs/GoalCard.tsx +++ b/src/client/components/programs/GoalCard.tsx @@ -17,7 +17,7 @@ const GoalCard = ({ color, text }: GoalCardProps) => { }, )} > -

{text}

+

{text}

); diff --git a/src/client/components/programs/InfoCard.tsx b/src/client/components/programs/InfoCard.tsx index 2ac570d2..6493d439 100644 --- a/src/client/components/programs/InfoCard.tsx +++ b/src/client/components/programs/InfoCard.tsx @@ -6,7 +6,7 @@ interface SimpleCardProps { const InfoCard: React.FC = ({ text }) => { return ( -
+
{/* Main Card with Black Border and Green Shadow */}
{/* Text Content */} diff --git a/src/client/components/programs/ProgramCard.tsx b/src/client/components/programs/ProgramCard.tsx index cafa9bd3..7fafa680 100644 --- a/src/client/components/programs/ProgramCard.tsx +++ b/src/client/components/programs/ProgramCard.tsx @@ -18,11 +18,11 @@ const ProgramCard: React.FC = ({ image, link, text }) => { {/* Text Content */}
-

{text}

+

{text}

{/* Learn More Button */} - diff --git a/src/client/components/programs/ProgramImages.ts b/src/client/components/programs/ProgramImages.ts new file mode 100644 index 00000000..b2b393b8 --- /dev/null +++ b/src/client/components/programs/ProgramImages.ts @@ -0,0 +1,25 @@ +import InternsPic from "@assets/interns/SaseInterns.png"; +import SETPic from "@assets/set/SaseSet.png"; +import SportsPic from "@assets/sports/SaseSports.png"; +import WebDevPic from "@assets/webdev/WebDevTeam.jpg"; + +const ProgramImages = [ + { + program: "Interns", + images: [InternsPic, InternsPic, InternsPic, InternsPic], + }, + { + program: "SET", + images: [SETPic, SETPic, SETPic, SETPic], + }, + { + program: "Web Dev", + images: [WebDevPic, WebDevPic, WebDevPic, WebDevPic], + }, + { + program: "Sports", + images: [SportsPic, SportsPic, SportsPic, SportsPic], + }, +]; + +export default ProgramImages; diff --git a/src/client/components/programs/TestimonialCard.tsx b/src/client/components/programs/TestimonialCard.tsx deleted file mode 100644 index be237334..00000000 --- a/src/client/components/programs/TestimonialCard.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { imageUrls } from "@/client/assets/imageUrls"; -import React from "react"; - -interface TestimonialCardProps { - image: string; - text: string; - name: string; - title: string; -} - -const TestimonialCard: React.FC = ({ image, name, text, title }) => { - return ( -
- {/* Image Section */} -
- {name} - {/* Decorative Icon */} - Decorative star -
- - {/* Text Section */} -
-

“{text}”

-

- {name}, {title} -

-
-
- ); -}; - -export default TestimonialCard; diff --git a/src/client/components/programs/Testimonials.ts b/src/client/components/programs/Testimonials.ts new file mode 100644 index 00000000..a6696014 --- /dev/null +++ b/src/client/components/programs/Testimonials.ts @@ -0,0 +1,110 @@ +//import { imageUrls } from "@assets/imageUrls"; +import BryantCao from "@assets/interns/InternTestimonial.png"; +import SET2024 from "@assets/set/SetProject.png"; +import CameronMcMullen from "@assets/sports/SportsTestimonial.jpeg"; + +const Testimonials = [ + { + program: "Interns", + testimonials: [ + { + name: "Bryant Cao", + position: "2024 Intern", + quote: + "I really enjoyed my time with SASE Interns as it was a great mixture of fun and general skill building. I got to connect with other SASE members, get more involved with SASE, and improve a variety of skills. But there was also tons of times where I got to joke around making Interns never feel like a burden.", + image: BryantCao, + }, + { + name: "Bryant Cao", + position: "2024 Intern", + quote: + "I really enjoyed my time with SASE Interns as it was a great mixture of fun and general skill building. I got to connect with other SASE members, get more involved with SASE, and improve a variety of skills. But there was also tons of times where I got to joke around making Interns never feel like a burden.", + image: BryantCao, + }, + { + name: "Bryant Cao", + position: "2024 Intern", + quote: + "I really enjoyed my time with SASE Interns as it was a great mixture of fun and general skill building. I got to connect with other SASE members, get more involved with SASE, and improve a variety of skills. But there was also tons of times where I got to joke around making Interns never feel like a burden.", + image: BryantCao, + }, + { + name: "Bryant Cao", + position: "2024 Intern", + quote: + "I really enjoyed my time with SASE Interns as it was a great mixture of fun and general skill building. I got to connect with other SASE members, get more involved with SASE, and improve a variety of skills. But there was also tons of times where I got to joke around making Interns never feel like a burden.", + image: BryantCao, + }, + ], + }, + { + program: "SET", + testimonials: [ + { + name: "SET Bro", + position: "2023 Project", + quote: + "During SET's first semester, SET successfully developed a campus cleaner robot designed to autonomously identify and pick up trash 🤖. This innovative project not only helped keep our campus clean but also provided valuable experience in robotics, programming, and teamwork.", + image: SET2024, + }, + { + name: "SET Dumpy", + position: "2023-2024 Project", + quote: + "A campus cleaner robot designed to autonomously identify and pick up trash 🤖. This innovative project keeps our campus clean and provided valuable experience in robotics, programming, and teamwork.", + image: SET2024, + }, + { + name: "2025 Project Name", + position: "2024-2025 Project", + quote: "Coming soon...", + image: SET2024, + }, + { + name: "2026 Project Name", + position: "2025-2026 Project", + quote: "Coming soon...", + image: SET2024, + }, + ], + }, + { + program: "Web Dev", + testimonials: [], + }, + { + program: "Sports", + testimonials: [ + { + name: "Cameron McMullen", + position: "SASE Sports Member", + quote: + "SASE sports is a fun way to connect with people while being active! I’m really glad I got to join and make friends while getting to play soccer again, and everyone is really nice to play with! We get to joke around and break a swear together, and I’ll cherish the memories and friends I made far beyond college.", + image: CameronMcMullen, + }, + { + name: "Cameron McMullen", + position: "SASE Sports Member", + quote: + "SASE sports is a fun way to connect with people while being active! I’m really glad I got to join and make friends while getting to play soccer again, and everyone is really nice to play with! We get to joke around and break a swear together, and I’ll cherish the memories and friends I made far beyond college.", + image: CameronMcMullen, + }, + { + name: "Cameron McMullen", + position: "SASE Sports Member", + quote: + "SASE sports is a fun way to connect with people while being active! I’m really glad I got to join and make friends while getting to play soccer again, and everyone is really nice to play with! We get to joke around and break a swear together, and I’ll cherish the memories and friends I made far beyond college.", + image: CameronMcMullen, + }, + { + name: "Cameron McMullen", + position: "SASE Sports Member", + quote: + "SASE sports is a fun way to connect with people while being active! I’m really glad I got to join and make friends while getting to play soccer again, and everyone is really nice to play with! We get to joke around and break a swear together, and I’ll cherish the memories and friends I made far beyond college.", + image: CameronMcMullen, + }, + ], + }, +]; + +export default Testimonials; diff --git a/src/client/routes/interns.tsx b/src/client/routes/interns.tsx index 23e249c0..b7c10c6d 100644 --- a/src/client/routes/interns.tsx +++ b/src/client/routes/interns.tsx @@ -1,12 +1,10 @@ -import InternTestimonial from "@assets/interns/InternTestimonial.png"; -import InternsPic from "@assets/interns/SaseInterns.png"; +import Carousel from "@/client/components/programs/Carousel"; +import { imageUrls } from "@assets/imageUrls"; import FAQ from "@components/programs/FAQCard"; +import { faqData } from "@components/programs/faqInterns"; import GoalCard from "@components/programs/GoalCard"; import InfoCard from "@components/programs/InfoCard"; -import TestimonialCard from "@components/programs/TestimonialCard"; import { createFileRoute } from "@tanstack/react-router"; -import { imageUrls } from "../assets/imageUrls"; -import { faqData } from "../components/programs/faqInterns"; import { seo } from "../utils/seo"; export const Route = createFileRoute("/interns")({ @@ -20,12 +18,12 @@ export const Route = createFileRoute("/interns")({ component: () => { return ( -
+
{/* Green Line and Text in Row */}
-

+

SASE
INTERNS @@ -43,27 +41,14 @@ export const Route = createFileRoute("/interns")({ } />

- {/* Placeholder Image */}
-
- Placeholder -
-
+ +

Testimonials

- -
+ +

Goals & Outcomes

@@ -72,7 +57,7 @@ export const Route = createFileRoute("/interns")({
-
+

FAQs

diff --git a/src/client/routes/programs.tsx b/src/client/routes/programs.tsx index df5e50a5..144a4425 100644 --- a/src/client/routes/programs.tsx +++ b/src/client/routes/programs.tsx @@ -10,16 +10,16 @@ import React from "react"; export const Route = createFileRoute("/programs")({ component: () => { return ( -
+
-

PROGRAMS

+

PROGRAMS

-

SASE Interns

+

SASE Interns

-

Engineering Team (SET)

+

Engineering Team (SET)

-

Web Development Team

+

Web Development Team

-

SASE Sports

+

SASE Sports

- {/* Image Section */}
-
- SET Image -
+

Past Projects

- +

Goals & Outcomes

diff --git a/src/client/routes/sports.tsx b/src/client/routes/sports.tsx index 42c1d66d..122d4c80 100644 --- a/src/client/routes/sports.tsx +++ b/src/client/routes/sports.tsx @@ -1,11 +1,9 @@ -import SportsPic from "@assets/sports/SaseSports.png"; -import SportsTestimony from "@assets/sports/SportsTestimonial.jpeg"; import FAQ from "@components/programs/FAQCard"; import GoalCard from "@components/programs/GoalCard"; import InfoCard from "@components/programs/InfoCard"; -import TestimonialCard from "@components/programs/TestimonialCard"; import { createFileRoute } from "@tanstack/react-router"; import { imageUrls } from "../assets/imageUrls"; +import Carousel from "../components/programs/Carousel"; import { faqData } from "../components/programs/faqSports"; import { seo } from "../utils/seo"; @@ -41,26 +39,13 @@ export const Route = createFileRoute("/sports")({ } />
- {/* Placeholder Image */}
-
- SaseSports -
+

Testimonials

- +

Goals & Outcomes

diff --git a/src/client/routes/webdev.tsx b/src/client/routes/webdev.tsx index 85768e98..2a923447 100644 --- a/src/client/routes/webdev.tsx +++ b/src/client/routes/webdev.tsx @@ -2,9 +2,9 @@ import BackendLead from "@assets/webdev/BackendLead.jpeg"; import FrontEndLead from "@assets/webdev/FrontendLead.png"; import FullStackLead from "@assets/webdev/FullStackLead2.jpg"; import UIUXLead from "@assets/webdev/UIUXLead.jpg"; -import TeamPhoto from "@assets/webdev/WebDevTeam.jpg"; import WebmasterChair from "@assets/webdev/WebmasterChair.jpeg"; import MemberCard from "@components/home/MemberCard"; +import Carousel from "@components/programs/Carousel"; import FAQ from "@components/programs/FAQCard"; import { faqData } from "@components/programs/faqWebdev"; import GoalCard from "@components/programs/GoalCard"; @@ -47,16 +47,8 @@ export const Route = createFileRoute("/webdev")({ } />
- {/* Placeholder Image */}
-
- Placeholder -
+

Leadership