diff --git a/app/layout.tsx b/app/layout.tsx index 9345126..d025e2e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -21,7 +21,7 @@ const bodyCn = clsx( 'bg-fixed', ); -const mainCn = clsx('flex', 'flex-col', 'items-center', 'justify-center', 'p-24'); +const mainCn = clsx('flex', 'flex-col', 'items-center', 'justify-center', 'p-24', 'grow'); /** * @param {{children}} props Props. diff --git a/app/page.tsx b/app/page.tsx index 8d05475..8411a35 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,47 @@ -import Title from '@/components/Title/Title'; +import clsx from 'clsx'; +import Link from 'next/link'; +import AppearInViewport from '@/components/AppearInViewport/AppearInViewport'; +import MainPageVideoPlayer from '@/components/MainPageVideoPlayer/MainPageVideoPlayer'; + +const containerCn = clsx('relative', 'z-10', 'flex', 'flex-col', 'items-center', 'grow'); +const titleCn = clsx('font-light', 'text-8xl'); +const subtitleCn = clsx('text-1.5xl', 'font-light', 'tracking-widest', 'text-center'); +const titleContainerCn = clsx('font-sans', 'select-none'); +const exploreCn = clsx('text-center', 'text-2xl', 'mt-auto'); +const exploreLinkCn = clsx('tracking-wider', 'animated-link'); + +const exploreLinkVariants = { + visible: {opacity: 1, y: 0, scale: 1}, + hidden: {opacity: 0, y: '2rem', scale: 0.9}, +}; /** * @returns React component. */ export default function Home() { return ( -
- - </div> + <> + <div className={containerCn}> + <AppearInViewport className={titleContainerCn}> + <h1 className={titleCn}> + SALSA<span className="font-extrabold">VIVA</span> + </h1> + <h6 className={subtitleCn}>SOCIAL DANCE SCHOOL</h6> + </AppearInViewport> + <AppearInViewport + transition={{delay: 1.2}} + className={exploreCn} + variants={exploreLinkVariants} + > + <Link + href="/about" + className={exploreLinkCn} + > + EXPLORE + </Link> + </AppearInViewport> + </div> + <MainPageVideoPlayer /> + </> ); } diff --git a/app/template.tsx b/app/template.tsx index cdaffd9..7116f61 100644 --- a/app/template.tsx +++ b/app/template.tsx @@ -26,6 +26,7 @@ export default function Template({children}: {children: React.ReactNode}) { <SocialIcons /> <AnimatePresence mode="wait"> <m.div + className="h-full grow flex" initial={{x: 300, opacity: 0}} animate={{x: 0, opacity: 1}} exit={{x: 300, opacity: 0}} diff --git a/components/AppearInViewport/AppearInViewport.tsx b/components/AppearInViewport/AppearInViewport.tsx index 4a4e439..7b9b5b9 100644 --- a/components/AppearInViewport/AppearInViewport.tsx +++ b/components/AppearInViewport/AppearInViewport.tsx @@ -6,7 +6,7 @@ import {ReactNode, forwardRef} from 'react'; // eslint-disable-next-line jsdoc/require-jsdoc type SafeHTMLMotionProps = Omit< HTMLMotionProps<'div'>, - 'variants' | 'initial' | 'whileInView' | 'viewport' | 'ref' + 'initial' | 'whileInView' | 'viewport' | 'ref' >; /** @@ -16,7 +16,7 @@ type AppearOnScreenProps = { children: ReactNode; } & SafeHTMLMotionProps; -const variants: Variants = { +const defaultVariants: Variants = { visible: {opacity: 1}, hidden: {opacity: 0}, }; @@ -28,7 +28,7 @@ const defaultTransition = {duration: 0.5, delay: 0.3}; * @returns React component. */ const AppearInViewport = forwardRef<HTMLDivElement, AppearOnScreenProps>(function AppearInViewport( - {children, transition, ...rest}, + {children, variants, transition, ...rest}, ref, ) { return ( @@ -37,7 +37,7 @@ const AppearInViewport = forwardRef<HTMLDivElement, AppearOnScreenProps>(functio whileInView="visible" viewport={{once: true}} transition={{...defaultTransition, ...transition}} - variants={variants} + variants={variants ?? defaultVariants} ref={ref} {...rest} > diff --git a/components/Loader/Loader.tsx b/components/Loader/Loader.tsx index 0213ee7..06affac 100644 --- a/components/Loader/Loader.tsx +++ b/components/Loader/Loader.tsx @@ -25,7 +25,7 @@ type LoaderProps = { */ export default function Loader({size = 'md', grow = false}: LoaderProps) { const containerClassName = clsx( - 'flex justify-content-center align-items-center p-4', + 'absolute flex justify-content-center align-items-center p-4', grow && 'min-h-screen w-full', ); diff --git a/components/MainPageVideoPlayer/MainPageVideoPlayer.tsx b/components/MainPageVideoPlayer/MainPageVideoPlayer.tsx new file mode 100644 index 0000000..2ac09f5 --- /dev/null +++ b/components/MainPageVideoPlayer/MainPageVideoPlayer.tsx @@ -0,0 +1,35 @@ +'use client'; + +import clsx from 'clsx'; +import {AnimatePresence} from 'framer-motion'; +import {useState} from 'react'; +import bgVideoConfig from './bgVideosConfig'; +import VideoBackground from './VideoBackground'; + +const containerCn = clsx('fixed', 'top-0', 'left-0', 'w-full', 'h-full'); + +/** + * @returns React component. + */ +export default function MainPageVideoPlayer() { + const [vidIndex, setVidIndex] = useState(0); + // eslint-disable-next-line jsdoc/require-jsdoc + const incrementVidIndex = () => + vidIndex < bgVideoConfig.length - 1 ? setVidIndex(vidIndex + 1) : setVidIndex(0); + const {src, type} = bgVideoConfig[vidIndex]; + + return ( + <div className={containerCn}> + <AnimatePresence mode="wait"> + { + <VideoBackground + key={src} + src={src} + type={type} + onBeforeEnded={incrementVidIndex} + /> + } + </AnimatePresence> + </div> + ); +} diff --git a/components/MainPageVideoPlayer/VideoBackground.tsx b/components/MainPageVideoPlayer/VideoBackground.tsx new file mode 100644 index 0000000..ab67123 --- /dev/null +++ b/components/MainPageVideoPlayer/VideoBackground.tsx @@ -0,0 +1,52 @@ +import clsx from 'clsx'; +import {m} from 'framer-motion'; +import {usePageVisibility} from 'react-page-visibility'; +import {useEffect} from 'react'; +import useVideoBeforeEnd from './useVideoBeforeEnd'; + +/** + * Props. + */ +type VideoBackgroundProps = { + src: string; + type: string; + onBeforeEnded: () => void; +}; + +const videoCn = clsx('w-full', 'h-full', 'object-cover'); + +/** + * @param {VideoBackgroundProps} props Props. + * @returns React component. + */ +export default function VideoBackground({src, type, onBeforeEnded}: VideoBackgroundProps) { + const [ref] = useVideoBeforeEnd(onBeforeEnded, 1.5); + const isVisible = usePageVisibility(); + + useEffect(() => { + if (isVisible) { + ref.current?.play(); + } else { + ref.current?.pause(); + } + }, [isVisible, ref]); + + return ( + <m.video + muted + autoPlay + className={videoCn} + initial={{opacity: 0, scale: 1.5}} + animate={{opacity: 1, scale: 1}} + exit={{opacity: 0, scale: 1.3}} + transition={{duration: 0.75, type: 'easeOut'}} + ref={ref} + > + <source + src={src} + type={type} + /> + <track kind="captions" /> + </m.video> + ); +} diff --git a/components/MainPageVideoPlayer/bgVideosConfig.ts b/components/MainPageVideoPlayer/bgVideosConfig.ts new file mode 100644 index 0000000..49fc30e --- /dev/null +++ b/components/MainPageVideoPlayer/bgVideosConfig.ts @@ -0,0 +1,12 @@ +/** + * Config for the background videos. + */ +const bgVideoConfig = [ + {src: '/videos/main.0.mp4', type: 'video/mp4'}, + {src: '/videos/main.1.mp4', type: 'video/mp4'}, + {src: '/videos/main.2.mp4', type: 'video/mp4'}, + {src: '/videos/main.3.mp4', type: 'video/mp4'}, + {src: '/videos/main.4.mp4', type: 'video/mp4'}, +] as const; + +export default bgVideoConfig; diff --git a/components/MainPageVideoPlayer/useVideoBeforeEnd.ts b/components/MainPageVideoPlayer/useVideoBeforeEnd.ts new file mode 100644 index 0000000..e765334 --- /dev/null +++ b/components/MainPageVideoPlayer/useVideoBeforeEnd.ts @@ -0,0 +1,30 @@ +import {useEffect, useRef} from 'react'; + +/** + * Run callback when video is about to end. + * @param onBeforeEnd Callback to run when video is about to end. + * @param timeBeforeEnd Time before end in seconds. + * @returns Ref to the video element. + */ +export default function useVideoBeforeEnd(onBeforeEnd: () => void, timeBeforeEnd: number) { + const ref = useRef<HTMLVideoElement>(null); + + useEffect(() => { + const video = ref.current!; + // eslint-disable-next-line jsdoc/require-jsdoc + const handleTimeUpdate = () => { + if (video.currentTime > video.duration - timeBeforeEnd) { + onBeforeEnd(); + } + }; + + if (video) { + video.addEventListener('timeupdate', handleTimeUpdate); + return () => { + video.removeEventListener('timeupdate', handleTimeUpdate); + }; + } + }, [ref, onBeforeEnd, timeBeforeEnd]); + + return [ref]; +} diff --git a/components/Menu/MenuDynamicBg.tsx b/components/Menu/MenuDynamicBg.tsx index fae84f6..f3294b6 100644 --- a/components/Menu/MenuDynamicBg.tsx +++ b/components/Menu/MenuDynamicBg.tsx @@ -6,7 +6,7 @@ import useNetworkSpeed from '@/lib/shared/useNetworkSpeed'; import {MenuContext} from './MenuContext'; // eslint-disable-next-line jsdoc/require-jsdoc -const dynamicBgCn = clsx('absolute', 'top-0', 'left-0', 'w-full', 'h-full', 'z-20'); +const dynamicBgCn = clsx('absolute', 'top-0', 'left-0', 'w-full', 'h-full', 'z-30'); /** * @returns React component. diff --git a/components/Menu/MenuItem.tsx b/components/Menu/MenuItem.tsx index 722714d..b3d32d7 100644 --- a/components/Menu/MenuItem.tsx +++ b/components/Menu/MenuItem.tsx @@ -32,7 +32,7 @@ const variants = { }, }; -const itemCn = clsx('text-5xl', 'm-4', 'text-center', 'z-30', 'relative'); +const itemCn = clsx('text-5xl', 'm-4', 'text-center', 'z-40', 'relative'); /** * @param {MenuItemProps} props Props. diff --git a/components/Menu/MenuList.tsx b/components/Menu/MenuList.tsx index dabfcd4..771baae 100644 --- a/components/Menu/MenuList.tsx +++ b/components/Menu/MenuList.tsx @@ -44,7 +44,7 @@ const navCn = clsx( 'left-0', 'w-full', 'h-full', - 'z-10', + 'z-20', 'bg-gradient-to-bl from-accent0 to-alternate', 'flex', 'flex-col', diff --git a/components/Menu/MenuToggle.tsx b/components/Menu/MenuToggle.tsx index 1b933d4..ca28c3d 100644 --- a/components/Menu/MenuToggle.tsx +++ b/components/Menu/MenuToggle.tsx @@ -14,7 +14,7 @@ type MenuToggleProps = { className?: string; }; -const containerCn = clsx('absolute', 'z-30'); +const containerCn = clsx('absolute', 'z-40'); const btnCn = clsx('rounded-full'); /** diff --git a/components/ScrollToTop/ScrollToTop.tsx b/components/ScrollToTop/ScrollToTop.tsx index edaa590..f88937b 100644 --- a/components/ScrollToTop/ScrollToTop.tsx +++ b/components/ScrollToTop/ScrollToTop.tsx @@ -4,6 +4,7 @@ import {CSSProperties, useCallback, useState} from 'react'; import {m} from 'framer-motion'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCircleArrowUp} from '@fortawesome/free-solid-svg-icons'; +import clsx from 'clsx'; import useScroll, {ScrollHandler} from '@/lib/shared/useScroll'; import isPrefersReducedMotion from '@/utils/isPrefersReducedMotion'; @@ -20,6 +21,8 @@ const variants = { hidden: {opacity: 0, y: '2rem'}, }; +const btnCn = clsx('fixed'); + /** * @param {ScrollToTopButtonProps} props Props. * @returns React component. @@ -38,7 +41,7 @@ export default function ScrollToTopButton({offset = 1000, ...position}: ScrollTo return ( <m.button - className="fixed" + className={btnCn} initial="hidden" animate={visible ? 'visible' : 'hidden'} variants={variants} diff --git a/components/SocialIcons/SocialIcons.tsx b/components/SocialIcons/SocialIcons.tsx index 72cddc8..79457ff 100644 --- a/components/SocialIcons/SocialIcons.tsx +++ b/components/SocialIcons/SocialIcons.tsx @@ -19,6 +19,7 @@ const iconsCn = clsx( 'left-4', '-translate-y-2/4', 'top-2/4', + 'z-10', ); const iconCn = clsx('hover:scale-105 hover:-translate-y-0.5 transition-transform'); diff --git a/components/Title/Title.tsx b/components/Title/Title.tsx deleted file mode 100644 index 1ec287c..0000000 --- a/components/Title/Title.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {memo} from 'react'; -import clsx from 'clsx'; -import AppearInViewport from '../AppearInViewport/AppearInViewport'; - -const titleCn = clsx('font-light', 'text-9xl', 'text-transparent', 'bg-clip-text'); -const subtitleCn = clsx('text-2xl', 'font-light', 'tracking-widest', 'text-center'); -const titleContainerCn = clsx( - 'font-sans', - 'select-none', - 'bg-gradient-to-r', - 'from-accent3', - 'to-accent0', - 'bg-clip-text', -); - -/** - * @returns React component. - */ -function Title() { - return ( - <AppearInViewport className={titleContainerCn}> - <h1 className={titleCn}> - SALSA<span className="font-extrabold">VIVA</span> - </h1> - <h6 className={subtitleCn}>SOCIAL DANCE SCHOOL</h6> - </AppearInViewport> - ); -} - -export default memo(Title); diff --git a/public/videos/main.0.mp4 b/public/videos/main.0.mp4 new file mode 100644 index 0000000..31b8be4 Binary files /dev/null and b/public/videos/main.0.mp4 differ diff --git a/public/videos/main.1.mp4 b/public/videos/main.1.mp4 new file mode 100644 index 0000000..5e991ed Binary files /dev/null and b/public/videos/main.1.mp4 differ diff --git a/public/videos/main.2.mp4 b/public/videos/main.2.mp4 new file mode 100644 index 0000000..401c334 Binary files /dev/null and b/public/videos/main.2.mp4 differ diff --git a/public/videos/main.3.mp4 b/public/videos/main.3.mp4 new file mode 100644 index 0000000..1e44b25 Binary files /dev/null and b/public/videos/main.3.mp4 differ diff --git a/public/videos/main.4.mp4 b/public/videos/main.4.mp4 new file mode 100644 index 0000000..cdf741f Binary files /dev/null and b/public/videos/main.4.mp4 differ