diff --git a/components/Carousel.tsx b/components/Carousel.tsx new file mode 100644 index 0000000..c9f7b18 --- /dev/null +++ b/components/Carousel.tsx @@ -0,0 +1,121 @@ +import styles from "@/styles/Carousel.module.css"; +import React, { useCallback, useEffect, useRef } from "react"; +import Link from "next/link"; +import { + EmblaCarouselType, + EmblaEventType, + EmblaOptionsType, +} from "embla-carousel"; +import useEmblaCarousel from "embla-carousel-react"; +import { NextButton, PrevButton, usePrevNextButtons } from "./CarouselArrows"; + +const TWEEN_FACTOR_BASE = 0.84; + +const numberWithinRange = (number: number, min: number, max: number): number => + Math.min(Math.max(number, min), max); + +type PropType = { + slides: number[]; + options?: EmblaOptionsType; +}; + +const Carousel: React.FC = (props) => { + const { slides, options } = props; + const [emblaRef, emblaApi] = useEmblaCarousel(options); + const tweenFactor = useRef(0); + + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } = usePrevNextButtons(emblaApi); + + const setTweenFactor = useCallback((emblaApi: EmblaCarouselType) => { + tweenFactor.current = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length; + }, []); + + const tweenOpacity = 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 - Math.abs(diffToTarget * tweenFactor.current); + const opacity = numberWithinRange(tweenValue, 0, 1).toString(); + emblaApi.slideNodes()[slideIndex].style.opacity = opacity; + }); + }); + }, + [], + ); + + useEffect(() => { + if (!emblaApi) return; + + setTweenFactor(emblaApi); + tweenOpacity(emblaApi); + emblaApi + .on("reInit", setTweenFactor) + .on("reInit", tweenOpacity) + .on("scroll", tweenOpacity) + .on("slideFocus", tweenOpacity); + }, [emblaApi, tweenOpacity]); + + return ( +
+
+
+ {slides.map((index) => ( +
+ Your alt text +
+ ))} +
+
+ +
+
+ + +
+
+ + Katso Lisää + +
+
+
+ ); +}; + +export default Carousel; diff --git a/components/CarouselArrows.tsx b/components/CarouselArrows.tsx new file mode 100644 index 0000000..09e143f --- /dev/null +++ b/components/CarouselArrows.tsx @@ -0,0 +1,95 @@ +import React, { + ComponentPropsWithRef, + useCallback, + useEffect, + useState, +} from "react"; +import { EmblaCarouselType } from "embla-carousel"; + +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/components/Navbar.tsx b/components/Navbar.tsx index c717ad3..70aa401 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -10,7 +10,7 @@ import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import Image from "next/image"; import Link from "next/link"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import close from "../public/close.svg"; import menu from "../public/menu.svg"; import sato_logo_nav from "../public/sato_logo_nav.png"; @@ -22,6 +22,25 @@ const Navbar = () => { right: false, }); + useEffect(() => { + let prevScrollpos = window.scrollY; + const handleScroll = () => { + const currentScrollpos = window.scrollY; + const navWrapper = document.getElementById("navContainer"); + if (navWrapper) { + if (prevScrollpos > currentScrollpos) { + navWrapper.style.top = "0"; + } else { + navWrapper.style.top = "-10rem"; + } + } + prevScrollpos = currentScrollpos; + }; + window.addEventListener("scroll", handleScroll); + + return () => window.removeEventListener("scroll", handleScroll); + }, []); + const toggleDrawer = (anchor: Anchor, open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { @@ -72,29 +91,31 @@ const Navbar = () => { ); return ( - + ); }; diff --git a/package-lock.json b/package-lock.json index 0a67305..1e4ff94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.5", "@mui/material": "^5.15.20", "dotenv": "^16.4.5", + "embla-carousel-react": "^8.1.6", "next": "14.2.4", "react": "^18", "react-dom": "^18" @@ -3261,6 +3262,34 @@ "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==", "dev": true }, + "node_modules/embla-carousel": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.1.6.tgz", + "integrity": "sha512-9n7FVsbPAs1KD+JmO84DnEDOZMXPBQbLujjMQqvsBRN2CDWwgZ9hRSNapztdPnyJfzOIxowGmj0BUQ8ACYAPkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.1.6.tgz", + "integrity": "sha512-DHxwFzF63yVrU95Eo58E9Xr5b6Y9ul6TTsqb/rtwMi+jXudAmIqN1i9iBxQ73i8jKuUVxll/ziNYMmnWvrdQJQ==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.1.6", + "embla-carousel-reactive-utils": "8.1.6" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.1.6.tgz", + "integrity": "sha512-Wg+J2YoqLqkaqsXi7fTJaLmXm6BpgDRJ0EfTdvQ4KE/ip5OsUuKGpJsEQDTt4waGXSDyZhIBlfoQtgGJeyYQ1Q==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.1.6" + } + }, "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 b75a0f6..2f41d6e 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "@emotion/styled": "^11.11.5", "@mui/material": "^5.15.20", "dotenv": "^16.4.5", + "embla-carousel-react": "^8.1.6", "next": "14.2.4", "react": "^18", - "react-dom": "^18" + "react-dom": "^18" }, "devDependencies": { "@testing-library/react": "^16.0.0", diff --git a/pages/index.tsx b/pages/index.tsx index 4120b6b..99b4850 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,6 +15,13 @@ import Link from "next/link"; import aino from "../public/aino.png"; import arrowBlue from "../public/arrow_forward_blue.svg"; import arrowWhite from "../public/arrow_forward_white.svg"; +import Carousel from "@/components/Carousel"; +import cAside from "../public/contact-aside.png"; +import { EmblaOptionsType } from "embla-carousel"; + +const OPTIONS: EmblaOptionsType = { loop: true }; +const SLIDE_COUNT = 10; +const SLIDES = Array.from(Array(SLIDE_COUNT).keys()); export default function Home() { return ( @@ -123,7 +130,102 @@ export default function Home() { {/* News */} -
+
+

Uutisia

+ + + + + + Title + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + Itaque, numquam magnam, eum nihil adipisci tenetur quasi vel + minima nemo ratione molestiae in ab laborum perferendis + beatae impedit dolorem iusto sunt. + + + + + + + + + + + + Title + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + Itaque, numquam magnam, eum nihil adipisci tenetur quasi vel + minima nemo ratione molestiae in ab laborum perferendis + beatae impedit dolorem iusto sunt. + + + + + + + + +
+ {/* Calendar */} +
+ +

Kalenteri

+
+
+ {/* Carousel */} +
+

Osakuntalehti Karhunkierros

+ + + + +
+ {/* Contact */} +
+
+
+

Postia hallitikselle

+

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. + Asperiores facere at minus officiis nesciunt? Quos labore + dolorem et mollitia quia. Recusandae dolores modi quaerat + magnam! Autem distinctio ipsa a alias. +

+ +

Häirintälomake

+

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. + Asperiores facere at minus officiis nesciunt? Quos labore + dolorem et mollitia quia. Recusandae dolores modi quaerat + magnam! Autem distinctio ipsa a alias. +

+ +
+ various messaging icons +
+
+ + {/* Footer */} +
); diff --git a/public/contact-aside.png b/public/contact-aside.png new file mode 100644 index 0000000..cc90689 Binary files /dev/null and b/public/contact-aside.png differ diff --git a/styles/Home.module.css b/styles/Home.module.css index 4856dc1..e08a1fe 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -101,3 +101,99 @@ background-color: var(--blue100); color: var(--blue300); } + +.news { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 70%; + margin: 5rem 0 5rem 0; +} + +.cardContainer { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 1rem; +} + +.newsCard { + width: 40%; +} + +.newCardContent { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; +} + +.cardTitle { + font-weight: 700; +} + +.cardDescription { + font-size: 0.8rem; +} + +/* Calendar */ +.calendarSection { + background-color: var(--orange100); + width: 100%; + height: 50rem; + display: flex; + justify-content: center; + align-items: center; + padding: 5rem 0 5rem 0; +} + +.sectionContainer { + width: 70%; + height: 100%; +} + +/* Carousel */ +.karhunkierros { + width: 70%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + margin: 5rem 0 5rem 0; +} + +.carouselContainer { + margin: 1rem 0 1rem 0; +} + +/* Contact */ +.contact { + background-color: var(--blue100); + width: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 5rem 0 5rem 0; +} + +.contactSectionContainer { + width: 70%; + height: 100%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; +} + +.contactInfo { + width: 40%; +} + +/* footer */ +.footer { + width: 100%; + height: 20rem; + background-color: var(--blue300); +} diff --git a/styles/Navbar.module.css b/styles/Navbar.module.css index 9f7e29f..0309602 100644 --- a/styles/Navbar.module.css +++ b/styles/Navbar.module.css @@ -1,9 +1,19 @@ +.navContainer { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + position: fixed; + background-color: var(--blue100); + z-index: 999; + transition: 0.25s ease; +} + .navbar { display: flex; flex-direction: row; width: 80%; justify-content: space-between; align-items: center; - position: fixed; top: 0; } diff --git a/styles/globals.css b/styles/globals.css index 7fa4fc3..8583fb4 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -23,3 +23,82 @@ body { font-family: "Raleway", sans-serif; max-width: 2150px; } + +/* EMBLA CAROUSEL*/ +.embla { + width: 100%; + margin: auto; + --slide-height: 19rem; + --slide-spacing: 1rem; + --slide-size: 30%; +} +.embla__viewport { + overflow: hidden; +} +.embla__container { + backface-visibility: hidden; + display: flex; + touch-action: pan-y pinch-zoom; + 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__img { + border-radius: 1.8rem; + display: block; + height: var(--slide-height); + width: 100%; + object-fit: cover; +} +.embla__controls { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + grid-template-columns: auto 1fr; + justify-content: space-between; + margin-top: 1.8rem; +} +.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__link, +.embla__link:visited { + color: var(--black); + font-weight: 700; +} diff --git a/temp_json/carousel.json b/temp_json/carousel.json new file mode 100644 index 0000000..587fdaa --- /dev/null +++ b/temp_json/carousel.json @@ -0,0 +1,40 @@ +{ + "images": [ + { + "url": "https://images.unsplash.com/photo-1709884735017-114f4a31f944?q=80&w=2129&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "boat" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1669357657874-34944fa0be68?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "plant" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1684445034763-013f0525c40c?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "chair" + }, + { + "url": "https://images.unsplash.com/photo-1709884735017-114f4a31f944?q=80&w=2129&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "boat" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1669357657874-34944fa0be68?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "plant" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1684445034763-013f0525c40c?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "chair" + }, + { + "url": "https://images.unsplash.com/photo-1709884735017-114f4a31f944?q=80&w=2129&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "boat" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1669357657874-34944fa0be68?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "plant" + }, + { + "url": "https://plus.unsplash.com/premium_photo-1684445034763-013f0525c40c?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "alt_text": "chair" + } + ] +}