Skip to content

Commit

Permalink
Programs Carousels (#244)
Browse files Browse the repository at this point in the history
* carousels for images and testimonials on programs pages, also adjusted fonts on pages to match rest of website

* formatting fix

* im confused
  • Loading branch information
stephanietfong authored Feb 20, 2025
1 parent 8ad25e7 commit 991a5cf
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 115 deletions.
2 changes: 1 addition & 1 deletion src/client/components/programs/BoardPicture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const BoardPicture = () => {
{/* Text Overlay */}
<div className="absolute inset-0 flex items-end justify-center p-4">
<div className="w-5/6 rounded-b-xl bg-gradient-to-t from-black to-transparent p-4">
<p className="text-lg text-white">
<p className="font-redhat text-lg text-white">
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.
</p>
Expand Down
153 changes: 153 additions & 0 deletions src/client/components/programs/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -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<PropType> = ({ 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<Array<HTMLElement>>([]);

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 (
<div
className={cn(
{
"flex items-center justify-center": purpose === "Testimonials",
},
`relative m-auto pb-8`,
)}
>
{purpose === "Testimonials" ? (
<>
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
<div className="absolute left-0 top-0 z-10 ml-[8%] h-full w-16 bg-gradient-to-r from-white to-transparent" />
</>
) : null}

<div className="overflow-hidden" ref={emblaRef}>
<div className="flex touch-pan-y touch-pinch-zoom">
{slides.map((slide, index) => (
<div className="flex min-w-0 flex-[0_0_100%] items-center justify-center [transform:translate3d(0,0,0)] md:flex-[0_0_50%]" key={index}>
<div className="embla__slide__image rounded-2xl bg-gradient-to-r from-saseBlue via-[#7DC242] to-saseGreen p-[4px]">
<div className="embla__slide__image group relative flex items-center justify-center hover:cursor-pointer">
{/* If slide element is not a string, carousel is for testimonials. If it is, carousel is for images */}
{typeof slide != "string" ? (
<img src={slide.image} alt={`Image`} className="aspect-auto rounded-xl" />
) : (
<img src={slide} alt={`Image`} className="aspect-auto rounded-xl" />
)}
{typeof slide != "string" ? (
<div className="absolute inset-0 flex flex-col items-center justify-end rounded-xl bg-saseGray/60 hover:bg-saseGray/90">
<p className="absolute pb-10 font-redhat text-xl font-semibold opacity-100 transition duration-300 group-hover:opacity-0">
{slide.name}
</p>
<p className="absolute pb-4 font-redhat text-lg opacity-100 transition duration-300 group-hover:opacity-0">{slide.position}</p>
<p className="flex h-0 w-full items-center justify-center overflow-hidden px-4 text-center font-redhat text-lg font-medium text-black opacity-0 transition-all duration-700 ease-in-out group-hover:h-full group-hover:translate-y-0 group-hover:opacity-100 md:text-sm lg:text-base">
"{slide.quote}"
</p>
</div>
) : null}
</div>
</div>
</div>
))}
</div>
</div>
{purpose === "Testimonials" ? (
<>
<div className="absolute right-0 top-0 z-10 mr-[8%] h-full w-16 bg-gradient-to-l from-white to-transparent" />
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</>
) : (
<div className="mt-4 grid justify-center">
<div className="grid grid-cols-[1fr,1fr] items-center gap-2">
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</div>
</div>
)}
</div>
);
};

export default TestimonialCarousel;
84 changes: 84 additions & 0 deletions src/client/components/programs/CarouselArrows.tsx
Original file line number Diff line number Diff line change
@@ -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<PropType> = (props) => {
const { children, ...restProps } = props;

return (
<button className="embla__button embla__button--prev text-black" type="button" {...restProps}>
<svg className="h-[35%] w-[35%]" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
/>
</svg>
{children}
</button>
);
};

export const NextButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props;

return (
<button className="embla__button embla__button--next text-black" type="button" {...restProps}>
<svg className="h-[35%] w-[35%]" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
/>
</svg>
{children}
</button>
);
};
2 changes: 1 addition & 1 deletion src/client/components/programs/FAQCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const FAQItem: React.FC<FAQItemProps> = ({ answer, question }) => {

const FAQ: React.FC<FAQProps> = ({ faqData }) => {
return (
<div className="w-full p-8">
<div className="w-full p-8 font-redhat">
{faqData.map((item, index) => (
<FAQItem key={index} question={item.question} answer={item.answer} />
))}
Expand Down
2 changes: 1 addition & 1 deletion src/client/components/programs/GoalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const GoalCard = ({ color, text }: GoalCardProps) => {
},
)}
>
<p className="text-2xl text-black">{text}</p>
<p className="font-redhat text-2xl text-black">{text}</p>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/client/components/programs/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface SimpleCardProps {

const InfoCard: React.FC<SimpleCardProps> = ({ text }) => {
return (
<div className="relative mx-auto mb-14 w-full max-w-4xl p-5">
<div className="relative mx-auto mb-14 w-full max-w-4xl p-5 font-redhat">
{/* Main Card with Black Border and Green Shadow */}
<div className="relative flex flex-col items-center rounded-3xl border-2 border-black bg-gray-100 p-12 shadow-[24px_24px_0px_#7DC242]">
{/* Text Content */}
Expand Down
4 changes: 2 additions & 2 deletions src/client/components/programs/ProgramCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ const ProgramCard: React.FC<ProgramCardProps> = ({ image, link, text }) => {

{/* Text Content */}
<div className="flex flex-col justify-between">
<p className="mb-4 text-xl text-black">{text}</p>
<p className="mb-4 font-redhat text-xl text-black">{text}</p>

{/* Learn More Button */}
<a href={link} target="_blank" rel="noopener noreferrer">
<button className="mt-4 w-40 rounded-full bg-saseBlueLight py-2 text-center text-lg italic text-white transition duration-300 hover:scale-105 hover:bg-saseBlue focus:outline-none focus:ring-2 focus:ring-blue-500">
<button className="mt-4 w-40 rounded-full bg-saseBlueLight py-2 text-center font-redhat text-lg italic text-white transition duration-300 hover:scale-105 hover:bg-saseBlue focus:outline-none focus:ring-2 focus:ring-blue-500">
LEARN MORE
</button>
</a>
Expand Down
25 changes: 25 additions & 0 deletions src/client/components/programs/ProgramImages.ts
Original file line number Diff line number Diff line change
@@ -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;
32 changes: 0 additions & 32 deletions src/client/components/programs/TestimonialCard.tsx

This file was deleted.

Loading

0 comments on commit 991a5cf

Please sign in to comment.