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 ? (
+
+
+ {edition.name}
+ {edition.date}
+
+ ) : (
+
+
+
{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 (
+
+ );
+}
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() {