From cb42c4f3391bebe6c01a0b4553814d1854e562d5 Mon Sep 17 00:00:00 2001 From: henryssims <134118548+henryssims@users.noreply.github.com> Date: Sun, 14 Apr 2024 23:45:51 -0400 Subject: [PATCH] finished basic outline of image carousel --- package-lock.json | 61 +++++++++ package.json | 2 + src/components/About.js | 10 +- src/components/EmblaCarousel.js | 73 ++++++++++ src/components/EmblaCarouselArrowButtons.js | 78 +++++++++++ src/components/EmblaCarouselDotButton.js | 49 +++++++ src/pages/index.js | 2 +- src/styles/embla.css | 144 ++++++++++++++++++++ 8 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 src/components/EmblaCarousel.js create mode 100644 src/components/EmblaCarouselArrowButtons.js create mode 100644 src/components/EmblaCarouselDotButton.js create mode 100644 src/styles/embla.css diff --git a/package-lock.json b/package-lock.json index 469245b..5858e01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "dependencies": { "babel-plugin-styled-components": "^1.12.0", + "embla-carousel-autoplay": "^8.0.2", + "embla-carousel-react": "^8.0.2", "gatsby": "^3.11.1", "gatsby-plugin-image": "^1.11.0", "gatsby-plugin-manifest": "^3.11.0", @@ -7024,6 +7026,39 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.82.tgz", "integrity": "sha512-Ks+ANzLoIrFDUOJdjxYMH6CMKB8UQo5modAwvSZTxgF+vEs/U7G5IbWFUp6dS4klPkTDVdxbORuk8xAXXhMsWw==" }, + "node_modules/embla-carousel": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.0.2.tgz", + "integrity": "sha512-bogsDO8xosuh/l3PxIvA5AMl3+BnRVAse9sDW/60amzj4MbGS5re4WH5eVEXiuH8G1/3G7QUAX2QNr3Yx8z5rA==" + }, + "node_modules/embla-carousel-autoplay": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.0.2.tgz", + "integrity": "sha512-31lBigAkHeI4k1767uYcr9Gm2y12mzLpVsi+QRxIWQT8J4AtRoHLOhd8YJFiXf22DbI/j78+IaeJK4lZRlFxUw==", + "peerDependencies": { + "embla-carousel": "8.0.2" + } + }, + "node_modules/embla-carousel-react": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.0.2.tgz", + "integrity": "sha512-RHe1GKLulOW8EDN+cJgbFbVVfRXcaLT2/89dyVw3ONGgVpZjD19wB87I1LUZ1aCzOSrTccx0PFSQanK4OOfGPA==", + "dependencies": { + "embla-carousel": "8.0.2", + "embla-carousel-reactive-utils": "8.0.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.2.tgz", + "integrity": "sha512-nLZqDkQdO0hvOP49/dUwjkkepMnUXgIzhyRuDjwGqswpB4Ibnc5M+w7rSQQAM+uMj0cPaXnYOTlv8XD7I/zVNw==", + "peerDependencies": { + "embla-carousel": "8.0.2" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -29077,6 +29112,32 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.82.tgz", "integrity": "sha512-Ks+ANzLoIrFDUOJdjxYMH6CMKB8UQo5modAwvSZTxgF+vEs/U7G5IbWFUp6dS4klPkTDVdxbORuk8xAXXhMsWw==" }, + "embla-carousel": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.0.2.tgz", + "integrity": "sha512-bogsDO8xosuh/l3PxIvA5AMl3+BnRVAse9sDW/60amzj4MbGS5re4WH5eVEXiuH8G1/3G7QUAX2QNr3Yx8z5rA==" + }, + "embla-carousel-autoplay": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.0.2.tgz", + "integrity": "sha512-31lBigAkHeI4k1767uYcr9Gm2y12mzLpVsi+QRxIWQT8J4AtRoHLOhd8YJFiXf22DbI/j78+IaeJK4lZRlFxUw==", + "requires": {} + }, + "embla-carousel-react": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.0.2.tgz", + "integrity": "sha512-RHe1GKLulOW8EDN+cJgbFbVVfRXcaLT2/89dyVw3ONGgVpZjD19wB87I1LUZ1aCzOSrTccx0PFSQanK4OOfGPA==", + "requires": { + "embla-carousel": "8.0.2", + "embla-carousel-reactive-utils": "8.0.2" + } + }, + "embla-carousel-reactive-utils": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.2.tgz", + "integrity": "sha512-nLZqDkQdO0hvOP49/dUwjkkepMnUXgIzhyRuDjwGqswpB4Ibnc5M+w7rSQQAM+uMj0cPaXnYOTlv8XD7I/zVNw==", + "requires": {} + }, "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 d6596ae..4a7c02a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "babel-plugin-styled-components": "^1.12.0", + "embla-carousel-autoplay": "^8.0.2", + "embla-carousel-react": "^8.0.2", "gatsby": "^3.11.1", "gatsby-plugin-image": "^1.11.0", "gatsby-plugin-manifest": "^3.11.0", diff --git a/src/components/About.js b/src/components/About.js index eefdad6..7b6fb92 100644 --- a/src/components/About.js +++ b/src/components/About.js @@ -8,6 +8,12 @@ import goal2 from "../images/goal2.png" import goal3 from "../images/goal3.png" import gbmImage from "../images/gallery/gbm-2022-02-20.jpg" +import EmblaCarousel from './EmblaCarousel' +import '../styles/embla.css' + +const OPTIONS = { loop: true } +const SLIDES = [1, 2, 3, 4, 5] + const AboutImageWrapper = s.div` display: flex; @@ -53,9 +59,7 @@ export const About = () => { return ( <> About Us - - - + Imagine a campus where students line up to work for the biggest movers in climate innovation. diff --git a/src/components/EmblaCarousel.js b/src/components/EmblaCarousel.js new file mode 100644 index 0000000..d3638b6 --- /dev/null +++ b/src/components/EmblaCarousel.js @@ -0,0 +1,73 @@ +import React, { useCallback } from 'react' +import { DotButton, useDotButton } from './EmblaCarouselDotButton' +import { + PrevButton, + NextButton, + usePrevNextButtons +} from './EmblaCarouselArrowButtons' +import Autoplay from 'embla-carousel-autoplay' +import useEmblaCarousel from 'embla-carousel-react' + +const EmblaCarousel = (props) => { + const { slides, options } = props + const [emblaRef, emblaApi] = useEmblaCarousel(options, [Autoplay()]) + + const onNavButtonClick = useCallback((emblaApi) => { + const autoplay = emblaApi?.plugins()?.autoplay + if (!autoplay) return + + const resetOrStop = + autoplay.options.stopOnInteraction === false + ? autoplay.reset + : autoplay.stop + + resetOrStop() + }, []) + + const { selectedIndex, scrollSnaps, onDotButtonClick } = useDotButton( + emblaApi, + onNavButtonClick + ) + + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick + } = usePrevNextButtons(emblaApi, onNavButtonClick) + + return ( +
+
+
+ {slides.map((index) => ( +
+
{index}
+
+ ))} +
+
+ +
+
+ + +
+ +
+ {scrollSnaps.map((_, index) => ( + onDotButtonClick(index)} + className={'embla__dot'.concat( + index === selectedIndex ? ' embla__dot--selected' : '' + )} + /> + ))} +
+
+
+ ) +} + +export default EmblaCarousel diff --git a/src/components/EmblaCarouselArrowButtons.js b/src/components/EmblaCarouselArrowButtons.js new file mode 100644 index 0000000..424fab8 --- /dev/null +++ b/src/components/EmblaCarouselArrowButtons.js @@ -0,0 +1,78 @@ +import React, { useCallback, useEffect, useState } from 'react' + +export const usePrevNextButtons = (emblaApi, onButtonClick) => { + 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) => { + setPrevBtnDisabled(!emblaApi.canScrollPrev()) + setNextBtnDisabled(!emblaApi.canScrollNext()) + }, []) + + useEffect(() => { + if (!emblaApi) return + + onSelect(emblaApi) + emblaApi.on('reInit', onSelect) + emblaApi.on('select', onSelect) + }, [emblaApi, onSelect]) + + return { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick + } +} + +export const PrevButton = (props) => { + const { children, ...restProps } = props + + return ( + + ) +} + +export const NextButton = (props) => { + const { children, ...restProps } = props + + return ( + + ) +} diff --git a/src/components/EmblaCarouselDotButton.js b/src/components/EmblaCarouselDotButton.js new file mode 100644 index 0000000..2e722bf --- /dev/null +++ b/src/components/EmblaCarouselDotButton.js @@ -0,0 +1,49 @@ +import React, { useCallback, useEffect, useState } from 'react' + +export const useDotButton = (emblaApi, onButtonClick) => { + const [selectedIndex, setSelectedIndex] = useState(0) + const [scrollSnaps, setScrollSnaps] = useState([]) + + const onDotButtonClick = useCallback( + (index) => { + if (!emblaApi) return + emblaApi.scrollTo(index) + if (onButtonClick) onButtonClick(emblaApi) + }, + [emblaApi, onButtonClick] + ) + + const onInit = useCallback((emblaApi) => { + setScrollSnaps(emblaApi.scrollSnapList()) + }, []) + + const onSelect = useCallback((emblaApi) => { + 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 + } +} + +export const DotButton = (props) => { + const { children, ...restProps } = props + + return ( + + ) +} diff --git a/src/pages/index.js b/src/pages/index.js index d07d728..9026e30 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -22,7 +22,7 @@ export default function IndexPage() { - + diff --git a/src/styles/embla.css b/src/styles/embla.css new file mode 100644 index 0000000..79497bb --- /dev/null +++ b/src/styles/embla.css @@ -0,0 +1,144 @@ +.embla { + margin: 0; + --slide-height: 30vw; + --slide-spacing: 1rem; + --slide-size: 70vw; + --brand-primary: rgb(47, 112, 193); + --brand-secondary: rgb(116, 97, 195); + --brand-alternative: rgb(19, 120, 134); + --background-site: rgb(249, 249, 249); + --background-code: rgb(244, 244, 244); + --text-body: rgb(54, 49, 61); + --text-comment: rgb(99, 94, 105); + --text-high-contrast: rgb(49, 49, 49); + --text-medium-contrast: rgb(99, 94, 105); + --text-low-contrast: rgb(116, 109, 118); + --detail-high-contrast: rgb(192, 192, 192); + --detail-medium-contrast: rgb(234, 234, 234); + --detail-low-contrast: rgb(240, 240, 242); + --admonition-note: rgb(46, 109, 188); + --admonition-warning: rgb(255, 196, 9); + --admonition-danger: rgb(220, 38, 38); + --brand-primary-rgb-value: 47, 112, 193; + --brand-secondary-rgb-value: 116, 97, 195; + --brand-alternative-rgb-value: 19, 120, 134; + --background-site-rgb-value: 249, 249, 249; + --background-code-rgb-value: 244, 244, 244; + --text-body-rgb-value: 54, 49, 61; + --text-comment-rgb-value: 99, 94, 105; + --text-high-contrast-rgb-value: 49, 49, 49; + --text-medium-contrast-rgb-value: 99, 94, 105; + --text-low-contrast-rgb-value: 116, 109, 118; + --detail-high-contrast-rgb-value: 192, 192, 192; + --detail-medium-contrast-rgb-value: 234, 234, 234; + --detail-low-contrast-rgb-value: 240, 240, 242; + --admonition-note-rgb-value: 46, 109, 188; + --admonition-warning-rgb-value: 255, 196, 9; + --admonition-danger-rgb-value: 220, 38, 38; +} +.embla__viewport { + overflow: hidden; +} +.embla__container { + backface-visibility: hidden; + display: flex; + touch-action: pan-y; + margin-left: calc(var(--slide-spacing) * -1); +} +.embla__slide { + flex: 0 0 var(--slide-size); + min-width: 0; + padding-left: var(--slide-spacing); +} +.embla__slide__number { + box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast); + border-radius: 1.8rem; + font-size: 4rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + height: var(--slide-height); +} +.embla__controls { + display: grid; + grid-template-columns: auto 1fr; + justify-content: space-between; + gap: 1.2rem; + margin-top: 1.8rem; + padding-left: 40px; + padding-right: 40px; +} +.embla__buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.6rem; + align-items: center; +} +.embla__button { + -webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5); + -webkit-appearance: none; + appearance: none; + background-color: transparent; + touch-action: manipulation; + display: inline-flex; + text-decoration: none; + cursor: pointer; + border: 0; + padding: 0; + margin: 0; + box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast); + width: 3.6rem; + height: 3.6rem; + z-index: 1; + border-radius: 50%; + color: var(--text-body); + display: flex; + align-items: center; + justify-content: center; +} +.embla__button:disabled { + color: var(--detail-high-contrast); +} +.embla__button__svg { + width: 35%; + height: 35%; +} +.embla__dots { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + align-items: center; + margin-right: calc((2.6rem - 1.4rem) / 2 * -1); +} +.embla__dot { + -webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5); + -webkit-appearance: none; + appearance: none; + background-color: transparent; + touch-action: manipulation; + display: inline-flex; + text-decoration: none; + cursor: pointer; + border: 0; + padding: 0; + margin: 0; + width: 2.6rem; + height: 2.6rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} +.embla__dot:after { + box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast); + width: 1.4rem; + height: 1.4rem; + border-radius: 50%; + display: flex; + align-items: center; + content: ''; +} +.embla__dot--selected:after { + box-shadow: inset 0 0 0 0.2rem var(--text-body); +}