diff --git a/package-lock.json b/package-lock.json index edffa32..e153e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,10 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@next/third-parties": "^14.1.0", + "embla-carousel": "^8.0.0", + "embla-carousel-auto-scroll": "^8.0.0", + "embla-carousel-autoplay": "^8.0.0", + "embla-carousel-react": "^8.0.0", "html-react-parser": "^5.1.2", "next": "^14.1.0", "react": "^18", @@ -1526,6 +1530,47 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/embla-carousel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.0.0.tgz", + "integrity": "sha512-ecixcyqS6oKD2nh5Nj5MObcgoSILWNI/GtBxkidn5ytFaCCmwVHo2SecksaQZHcARMMpIR2dWOlSIdA1LkZFUA==" + }, + "node_modules/embla-carousel-auto-scroll": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.0.0.tgz", + "integrity": "sha512-+bG79hg/BhI4enRevR6fuQdrJevfu3gfNg/S+1kGszXHUJHOOwDQne/dXOnQ12+o74XvDD5t/LG45rAOplehhQ==", + "peerDependencies": { + "embla-carousel": "8.0.0" + } + }, + "node_modules/embla-carousel-autoplay": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.0.0.tgz", + "integrity": "sha512-FWHhZULH5+ydg7fiabwQppCDoTMi8pbMC20lmVytoXn7hH2KAhXHc/8yCUb3yToqMduCN6xPKUONtgzBqz3RZg==", + "peerDependencies": { + "embla-carousel": "8.0.0" + } + }, + "node_modules/embla-carousel-react": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.0.0.tgz", + "integrity": "sha512-qT0dii8ZwoCtEIBE6ogjqU2+5IwnGfdt2teKjCzW88JRErflhlCpz8KjWnW8xoRZOP8g0clRtsMEFoAgS/elfA==", + "dependencies": { + "embla-carousel": "8.0.0", + "embla-carousel-reactive-utils": "8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0.tgz", + "integrity": "sha512-JCw0CqCXI7tbHDRogBb9PoeMLyjEC1vpN0lDOzUjmlfVgtfF+ffLaOK8bVtXVUEbNs/3guGe3NSzA5J5aYzLzw==", + "peerDependencies": { + "embla-carousel": "8.0.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/package.json b/package.json index e08dbfc..a3c8594 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@next/third-parties": "^14.1.0", + "embla-carousel": "^8.0.0", + "embla-carousel-auto-scroll": "^8.0.0", + "embla-carousel-autoplay": "^8.0.0", + "embla-carousel-react": "^8.0.0", "html-react-parser": "^5.1.2", "next": "^14.1.0", "react": "^18", diff --git a/public/Planets/ThroughTheYears/2018.svg b/public/Planets/ThroughTheYears/2018.svg new file mode 100644 index 0000000..248458d --- /dev/null +++ b/public/Planets/ThroughTheYears/2018.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/2019.svg b/public/Planets/ThroughTheYears/2019.svg new file mode 100644 index 0000000..dd82edd --- /dev/null +++ b/public/Planets/ThroughTheYears/2019.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/2020.svg b/public/Planets/ThroughTheYears/2020.svg new file mode 100644 index 0000000..ddc1676 --- /dev/null +++ b/public/Planets/ThroughTheYears/2020.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/2021.svg b/public/Planets/ThroughTheYears/2021.svg new file mode 100644 index 0000000..e1c86b7 --- /dev/null +++ b/public/Planets/ThroughTheYears/2021.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/2022.svg b/public/Planets/ThroughTheYears/2022.svg new file mode 100644 index 0000000..14ee8e9 --- /dev/null +++ b/public/Planets/ThroughTheYears/2022.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/2023.svg b/public/Planets/ThroughTheYears/2023.svg new file mode 100644 index 0000000..0600d44 --- /dev/null +++ b/public/Planets/ThroughTheYears/2023.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/Fall2016.svg b/public/Planets/ThroughTheYears/Fall2016.svg new file mode 100644 index 0000000..d8b11f1 --- /dev/null +++ b/public/Planets/ThroughTheYears/Fall2016.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/Fall2017.svg b/public/Planets/ThroughTheYears/Fall2017.svg new file mode 100644 index 0000000..4ee9f26 --- /dev/null +++ b/public/Planets/ThroughTheYears/Fall2017.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/Spring2016.svg b/public/Planets/ThroughTheYears/Spring2016.svg new file mode 100644 index 0000000..a568640 --- /dev/null +++ b/public/Planets/ThroughTheYears/Spring2016.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Planets/ThroughTheYears/Winter2017.svg b/public/Planets/ThroughTheYears/Winter2017.svg new file mode 100644 index 0000000..3ab07a8 --- /dev/null +++ b/public/Planets/ThroughTheYears/Winter2017.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/components/ThroughTheYears.tsx b/src/app/components/ThroughTheYears.tsx deleted file mode 100644 index 025c6c2..0000000 --- a/src/app/components/ThroughTheYears.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export default function ThroughTheYears() { - return ( -
- {" "} - Coming soon...{" "} -
- ); -} diff --git a/src/app/components/ThroughTheYears/EmblaCarousel.tsx b/src/app/components/ThroughTheYears/EmblaCarousel.tsx new file mode 100644 index 0000000..65d1ff4 --- /dev/null +++ b/src/app/components/ThroughTheYears/EmblaCarousel.tsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + EmblaEventType, + EmblaOptionsType, +} from "embla-carousel"; +import AutoScroll from "embla-carousel-auto-scroll"; +import styled from "styled-components"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; +import Image from "next/image"; +import Link from "next/link"; +import { DotButton, useDotButton } from "./EmblaCarouselDotButton"; +import useEmblaCarousel from "embla-carousel-react"; + +type Edition = { + url: string; + img: string; + name: string; + date: string; +}; + +type PropType = { + slides: Edition[]; + options?: EmblaOptionsType; +}; + +const Embla = styled.section` + margin: auto; + max-width: 100%; + --slide-spacing: 1rem; + --slide-size: 100%; + --slide-spacing-sm: 10rem; + --slide-size-sm: 20%; + --slide-spacing-lg: 2rem; + --slide-size-lg: calc(100% / 4); +`; + +const EmblaViewport = styled.div` + overflow: hidden; +`; + +const EmblaContainer = styled.div` + backface-visibility: hidden; + display: flex; + touch-action: pan-y; + margin-left: calc(var(--slide-spacing) * -1); + + @media (min-width: 300px) { + margin-left: calc(var(--slide-spacing-sm) * -1); + } + + @media (min-width: 750px) { + margin-left: calc(var(--slide-spacing-lg) * -1); + } +`; + +const EmblaSlide = styled.div` + min-width: 0; + flex: 0 0 var(--slide-size); + padding-left: var(--slide-spacing); + display: flex; + justify-content: center; + align-items: center; + text-align: center; + + @media (min-width: 300px) { + flex: 0 0 var(--slide-size-sm); + padding-left: var(--slide-spacing-sm); + } + + @media (min-width: 750px) { + padding-left: var(--slide-spacing-sm); + } + + @media (min-width: 1200px) { + flex: 0 0 var(--slide-size-lg); + padding-left: var(--slide-spacing-lg); + } +`; + +const EmblaControls = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin-top: 1.8rem; + + @media (max-width: 767px) { + display: none; + } +`; + +const EmblaDots = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; +`; + +const EmblaPlay = styled.button` + -webkit-appearance: none; + text-decoration: none; + cursor: url("/rocket-fire.png"), auto; + display: flex; + font-size: 1.5rem; + background-color: black; + border: none; + margin: 0px 0px 0px 10px; +`; + +const EmblaCarousel: React.FC = ({ slides, options }) => { + const [emblaRef, emblaApi] = useEmblaCarousel(options, [ + AutoScroll({ playOnInit: true, speed: 0.5 }), + ]); + const [isPlaying, setIsPlaying] = useState(false); + + const { selectedIndex, scrollSnaps, onDotButtonClick } = + useDotButton(emblaApi); + + useCallback(() => { + const autoScroll = emblaApi?.plugins()?.autoScroll; + if (!autoScroll) return; + + const options = autoScroll.options as { stopOnInteraction?: boolean }; + const stopOnInteraction = options?.stopOnInteraction ?? false; + + const reset = + typeof autoScroll.reset === "function" ? autoScroll.reset : undefined; + const stop = + typeof autoScroll.stop === "function" ? autoScroll.stop : undefined; + + if (stopOnInteraction && reset) { + reset(); + } else if (stop) { + stop(); + } + }, [emblaApi]); + + const toggleAutoplay = useCallback(() => { + const autoScroll = emblaApi?.plugins()?.autoScroll; + if (!autoScroll) return; + + if (typeof autoScroll.isPlaying === "function") { + const playOrStop = autoScroll.isPlaying() + ? (autoScroll.stop as () => void) + : (autoScroll.play as () => void); + playOrStop?.(); + } + }, [emblaApi]); + + const isMounted = useRef(false); + + useEffect(() => { + const autoScroll = emblaApi?.plugins()?.autoScroll; + if (!autoScroll) return; + + const isAutoScrollPlaying = autoScroll.isPlaying as () => boolean; + setIsPlaying(!!isAutoScrollPlaying()); + + const eventHandlers = { + "autoScroll:play": () => setIsPlaying(true), + "autoScroll:stop": () => setIsPlaying(false), + reInit: () => setIsPlaying(false), + }; + + Object.entries(eventHandlers).forEach(([event, handler]) => { + emblaApi.on(event as EmblaEventType, handler); + }); + + isMounted.current = true; + + return () => { + if (isMounted.current) { + Object.entries(eventHandlers).forEach(([event, handler]) => { + emblaApi.off(event as EmblaEventType, handler); + }); + } + }; + }, [emblaApi]); + + return ( + + + + {slides.map((edition, index) => ( + + {edition.url ? ( + + altText +

{edition.name}

+

{edition.date}

+ + ) : ( +
+ altText +

{edition.name}

+

{edition.date}

+
+ )} +
+ ))} +
+
+ + + {scrollSnaps.map((_, index) => ( + { + onDotButtonClick(index); + const autoScroll = emblaApi?.plugins()?.autoScroll; + if (autoScroll && typeof autoScroll.stop === "function") { + autoScroll.stop(); + } + }} + selectedIndex={selectedIndex === index} + /> + ))} + + + + + +
+ ); +}; + +export default EmblaCarousel; diff --git a/src/app/components/ThroughTheYears/EmblaCarouselDotButton.tsx b/src/app/components/ThroughTheYears/EmblaCarouselDotButton.tsx new file mode 100644 index 0000000..0f6159e --- /dev/null +++ b/src/app/components/ThroughTheYears/EmblaCarouselDotButton.tsx @@ -0,0 +1,96 @@ +import React, { + PropsWithChildren, + useCallback, + useEffect, + useState, +} from "react"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { EmblaCarouselType } from "embla-carousel"; +import styled from "styled-components"; + +type UseDotButtonType = { + selectedIndex: number; + scrollSnaps: number[]; + onDotButtonClick: (index: number) => void; +}; + +export const useDotButton = ( + emblaApi: EmblaCarouselType | undefined, +): UseDotButtonType => { + const [selectedIndex, setSelectedIndex] = useState(0); + const [scrollSnaps, setScrollSnaps] = useState([]); + + const onDotButtonClick = useCallback( + (index: number) => { + if (!emblaApi) return; + emblaApi.scrollTo(index); + }, + [emblaApi], + ); + + const onInit = useCallback((emblaApi: EmblaCarouselType) => { + setScrollSnaps(emblaApi.scrollSnapList()); + }, []); + + const onSelect = useCallback((emblaApi: EmblaCarouselType) => { + setSelectedIndex(emblaApi.selectedScrollSnap()); + }, []); + + useEffect(() => { + if (!emblaApi) return; + + onInit(emblaApi); + onSelect(emblaApi); + emblaApi.on("reInit", onInit); + emblaApi.on("reInit", onSelect); + emblaApi.on("select", onSelect); + }, [emblaApi, onInit, onSelect]); + + return { + selectedIndex, + scrollSnaps, + onDotButtonClick, + }; +}; + +type PropType = PropsWithChildren< + React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + > & { + selectedIndex: boolean; + } +>; + +const EmblaDot = styled.button<{ selectedIndex: boolean }>` + appearance: none; + touch-action: manipulation; + text-decoration: none; + cursor: url("/rocket-fire.png"), auto; + border: 0; + padding: 0.6rem; + margin: 0.1rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + position: relative; + user-select: none; + outline: none; + + background-color: ${(props) => { + return props.selectedIndex ? "orangered" : "slategray"; + }}; +`; + +export const DotButton: React.FC = ({ + selectedIndex, + children, + ...restProps +}) => { + return ( + + {children} + + ); +}; diff --git a/src/app/components/ThroughTheYears/ThroughTheYears.tsx b/src/app/components/ThroughTheYears/ThroughTheYears.tsx new file mode 100644 index 0000000..379492c --- /dev/null +++ b/src/app/components/ThroughTheYears/ThroughTheYears.tsx @@ -0,0 +1,86 @@ +import React from "react"; + +import { Section } from "@/app/genericComponents/General"; +import { SectionTitle } from "@/app/genericComponents/Typography"; +import EmblaCarousel from "./EmblaCarousel"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { EmblaOptionsType } from "embla-carousel"; + +export default function ThroughTheYears() { + const PREVIOUS_EDITIONS = [ + { + name: "Spring 2016", + date: "Feb 19-21", + img: "/Planets/ThroughTheYears/Spring2016.svg", + url: "https://s2016.hackupc.com/", + }, + { + name: "Fall 2016", + date: "Oct 7-9", + img: "/Planets/ThroughTheYears/Fall2016.svg", + url: "https://f2016.hackupc.com/", + }, + + { + name: "Fall 2017", + date: "Oct 13-15", + img: "/Planets/ThroughTheYears/Fall2017.svg", + url: "https://f2017.hackupc.com/", + }, + { + name: "Winter 2017", + date: "March 3-5", + img: "/Planets/ThroughTheYears/Winter2017.svg", + url: "https://w2017.hackupc.com/", + }, + { + name: "2018", + date: "Oct 19-21", + img: "/Planets/ThroughTheYears/2018.svg", + url: "https://2018.hackupc.com/", + }, + { + name: "2019", + date: "Oct 11-13", + img: "/Planets/ThroughTheYears/2019.svg", + url: "https://2019.hackupc.com/", + }, + { + name: "2020", + date: "CANCELLED", + img: "/Planets/ThroughTheYears/2020.svg", + url: "", + }, + { + name: "2021", + date: "May 14-16", + img: "/Planets/ThroughTheYears/2021.svg", + url: "https://2021.hackupc.com/", + }, + { + name: "2022", + date: "April 21-May 1", + img: "/Planets/ThroughTheYears/2022.svg", + url: "https://2022.hackupc.com/", + }, + { + name: "2023", + date: "May 12-14", + img: "/Planets/ThroughTheYears/2023.svg", + url: "https://2023.hackupc.com/", + }, + ]; + + const OPTIONS: EmblaOptionsType = { + align: "start", + loop: true, + slidesToScroll: 2, + }; + + return ( +
+ Through The Years + +
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 1e7df11..a130cac 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,11 +10,10 @@ import FAQs from "@/app/components/FAQs"; import Socials from "@/app/components/Socials"; import SponsorsAndPartners from "@/app/components/SponsorsAndPartners"; import Hackers from "@/app/components/Hackers"; -// TODO: import ThroughTheYears from "@/app/components/ThroughTheYears"; import Hero from "./components/Hero"; import Header from "@/app/components/Header"; import Footer from "@/app/components/Footer"; -import ThroughTheYears from "@/app/components/ThroughTheYears"; +import ThroughTheYears from "@/app/components/ThroughTheYears/ThroughTheYears"; import { Background } from "@/app/genericComponents/General"; export default function Home() {