diff --git a/package.json b/package.json index 892c176..0e8781a 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,12 @@ "@types/react-dom": "18.2.22", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "embla-carousel": "^8.0.0", + "embla-carousel-react": "^8.0.0", "eslint": "8.57.0", "eslint-config-next": "14.1.4", "next": "14.1.4", + "next-themes": "^0.3.0", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^5.0.1", diff --git a/public/images/contentCardDefault.png b/public/images/contentCardDefault.png deleted file mode 100644 index 1ca2a9f..0000000 Binary files a/public/images/contentCardDefault.png and /dev/null differ diff --git a/public/images/content_card_gopportunities.webp b/public/images/content_card_gopportunities.webp new file mode 100644 index 0000000..dd21e45 Binary files /dev/null and b/public/images/content_card_gopportunities.webp differ diff --git a/public/images/content_card_guia_dev.webp b/public/images/content_card_guia_dev.webp new file mode 100644 index 0000000..dafd35b Binary files /dev/null and b/public/images/content_card_guia_dev.webp differ diff --git a/public/images/content_card_repowars.webp b/public/images/content_card_repowars.webp new file mode 100644 index 0000000..d0eb1ef Binary files /dev/null and b/public/images/content_card_repowars.webp differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 5f9721b..98fab1c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,20 @@ -import { Navbar, Button, NavbarMobile, SocialMedia } from "@/components" +import { ContentSection } from "@/components/sections/content/ContentSection" +import { ThemeSwitcherSkeleton } from "@/components/theme-switcher/ThemeSwitcher" +import dynamic from "next/dynamic" + +const ThemeSwitcher = dynamic( + () => import("@/components/theme-switcher/ThemeSwitcher"), + { + ssr: false, + loading: () => , + } +) export default function Home() { return ( - - - - - - Click me - - + + + ) } diff --git a/src/components/card/ContentCard.tsx b/src/components/card/ContentCard.tsx deleted file mode 100644 index 73315a7..0000000 --- a/src/components/card/ContentCard.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Image from 'next/image'; -import React from 'react'; - -export type ContentCardProps = { - imgSrc?: string; - title: string; - description: string; - tag?: string; -}; - -const ContentCard = ({ imgSrc = '/images/contentCardDefault.png', title, description, tag }: ContentCardProps) => { - return ( - - - - - - - - {title} - - {tag && ( - - {tag} - - )} - - {description} - - - ); -}; - -export default ContentCard; diff --git a/src/components/carousel/ArrowButtons.tsx b/src/components/carousel/ArrowButtons.tsx new file mode 100644 index 0000000..0aa868c --- /dev/null +++ b/src/components/carousel/ArrowButtons.tsx @@ -0,0 +1,90 @@ +import React, { + PropsWithChildren, + useCallback, + useEffect, + useState, +} from "react" +import type { EmblaCarouselType } from "embla-carousel" +import { BsArrowRight, BsArrowRightShort } from "react-icons/bs" +import { FaArrowLeft, FaArrowRight } from "react-icons/fa" + +type UsePrevNextButtonsType = { + prevBtnDisabled: boolean + nextBtnDisabled: boolean + onPrevButtonClick: () => void + onNextButtonClick: () => void +} + +export const usePrevNextButtons = ( + emblaApi: EmblaCarouselType | undefined +): UsePrevNextButtonsType => { + const [prevBtnDisabled, setPrevBtnDisabled] = useState(true) + const [nextBtnDisabled, setNextBtnDisabled] = useState(true) + + const onPrevButtonClick = useCallback(() => { + if (!emblaApi) return + emblaApi.scrollPrev() + }, [emblaApi]) + + const onNextButtonClick = useCallback(() => { + if (!emblaApi) return + emblaApi.scrollNext() + }, [emblaApi]) + + const onSelect = useCallback((emblaApi: EmblaCarouselType) => { + 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, + } +} + +type PropType = PropsWithChildren< + React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + > +> + +export const PrevButton: React.FC = (props) => { + const { children, ...restProps } = props + + return ( + + + {children} + + ) +} + +export const NextButton: React.FC = (props) => { + const { children, ...restProps } = props + + return ( + + + {children} + + ) +} diff --git a/src/components/carousel/Carousel.tsx b/src/components/carousel/Carousel.tsx new file mode 100644 index 0000000..811fcde --- /dev/null +++ b/src/components/carousel/Carousel.tsx @@ -0,0 +1,92 @@ +"use client" +import React, { useCallback, useEffect, useRef } from "react" +import type { + EmblaCarouselType, + EmblaEventType, + EmblaOptionsType, +} from "embla-carousel" +import useEmblaCarousel from "embla-carousel-react" +import { DotButton, useDotButton } from "./DotButton" +import { PrevButton, NextButton, usePrevNextButtons } from "./ArrowButtons" +import { cn } from "@/lib/utils" +import { useEmblaWithScaleTween } from "./hooks/useEmblaWithScaleTween" + +interface CarouselProps extends React.HTMLAttributes { + options?: EmblaOptionsType +} + +const Carousel: React.FC = ({ + children, + options, + className, + ...props +}) => { + const { + emblaRef, + scrollSnaps, + selectedIndex, + onDotButtonClick, + onNextButtonClick, + onPrevButtonClick, + prevBtnDisabled, + nextBtnDisabled, + } = useEmblaWithScaleTween(options) + + return ( + + + + {React.Children.map(children, (child) => ( + + {child} + + ))} + + + + + + + + {scrollSnaps.map((_, index) => ( + onDotButtonClick(index)} + className={cn( + "w-4 h-4 bg-on-primary-dark border border-[#353544] rounded-[0.25rem] transition-all", + { + "border-4 border-palette-blue-600 w-6 h-6": + index === selectedIndex, + } + )} + /> + ))} + + + + + + ) +} + +Carousel.displayName = "Carousel" + +export { Carousel } diff --git a/src/components/carousel/DotButton.tsx b/src/components/carousel/DotButton.tsx new file mode 100644 index 0000000..8cb0f7b --- /dev/null +++ b/src/components/carousel/DotButton.tsx @@ -0,0 +1,69 @@ +import React, { + PropsWithChildren, + useCallback, + useEffect, + useState, +} from "react" +import type { EmblaCarouselType } from "embla-carousel" + +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 + > +> + +export const DotButton: React.FC = (props) => { + const { children, ...restProps } = props + + return ( + + {children} + + ) +} diff --git a/src/components/carousel/hooks/useEmblaWithScaleTween.ts b/src/components/carousel/hooks/useEmblaWithScaleTween.ts new file mode 100644 index 0000000..c5a2dda --- /dev/null +++ b/src/components/carousel/hooks/useEmblaWithScaleTween.ts @@ -0,0 +1,107 @@ +import type { + EmblaCarouselType, + EmblaEventType, + EmblaOptionsType, +} from "embla-carousel" +import useEmblaCarousel from "embla-carousel-react" +import { useRef, useCallback, useEffect } from "react" +import { usePrevNextButtons } from "../ArrowButtons" +import { useDotButton } from "../DotButton" + +const TWEEN_FACTOR_BASE = 0.1 + +export function useEmblaWithScaleTween(options?: EmblaOptionsType) { + const [emblaRef, emblaApi] = useEmblaCarousel(options) + const tweenFactor = useRef(0) + const tweenNodes = useRef([]) + + const { selectedIndex, scrollSnaps, onDotButtonClick } = + useDotButton(emblaApi) + + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } = usePrevNextButtons(emblaApi) + + const setTweenNodes = useCallback((emblaApi: EmblaCarouselType): void => { + tweenNodes.current = emblaApi + .slideNodes() + .map((slideNode) => slideNode.children[0] as HTMLElement) + }, []) + + const setTweenFactor = useCallback((emblaApi: EmblaCarouselType) => { + tweenFactor.current = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length + }, []) + + const tweenScale = 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 scale = numberWithinRange(tweenValue, 0, 1).toString() + const tweenNode = tweenNodes.current[slideIndex] + tweenNode.style.transform = `scale(${scale})` + }) + }) + }, + [] + ) + + useEffect(() => { + if (!emblaApi) return + + setTweenNodes(emblaApi) + setTweenFactor(emblaApi) + tweenScale(emblaApi) + + emblaApi + .on("reInit", setTweenNodes) + .on("reInit", setTweenFactor) + .on("reInit", tweenScale) + .on("scroll", tweenScale) + }, [emblaApi, tweenScale]) + + return { + emblaRef, + scrollSnaps, + onDotButtonClick, + onPrevButtonClick, + onNextButtonClick, + selectedIndex, + prevBtnDisabled, + nextBtnDisabled, + } +} + +function numberWithinRange(number: number, min: number, max: number): number { + return Math.min(Math.max(number, min), max) +} diff --git a/src/components/card/ContentCard.stories.tsx b/src/components/content-card/ContentCard.stories.tsx similarity index 100% rename from src/components/card/ContentCard.stories.tsx rename to src/components/content-card/ContentCard.stories.tsx diff --git a/src/components/content-card/ContentCard.tsx b/src/components/content-card/ContentCard.tsx new file mode 100644 index 0000000..796636f --- /dev/null +++ b/src/components/content-card/ContentCard.tsx @@ -0,0 +1,62 @@ +import { cn } from "@/lib/utils" +import Image from "next/image" +import React from "react" +import { Heading } from "../heading/Heading" +import Link from "next/link" + +export interface ContentCardProps extends React.HTMLAttributes { + imgSrc: string + title: string + description: string + href: string + tag?: string +} + +const ContentCard = React.forwardRef( + ({ imgSrc, title, description, tag, href, className, ...props }, ref) => { + return ( + + + + + + + + + {title} + + + {tag && ( + + {tag} + + )} + + + {description} + + + + + ) + } +) + +ContentCard.displayName = "ContentCard" + +export { ContentCard } diff --git a/src/components/index.ts b/src/components/index.ts index c62173b..8b7d537 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,9 +1,7 @@ export { Button, buttonVariants } from "./button/Button" export { Navbar } from "./navbar/Navbar" export { SocialMedia } from "./social-media/SocialMedia" -export { Heading, headingVariants } from "./heading/Heading"; +export { Heading, headingVariants } from "./heading/Heading" export { MenuMobile as NavbarMobile } from "./navbar/mobile/MenuMobile" -export { Navbar } from "./navbar/Navbar" -export { SocialMedia } from "./social-media/SocialMedia" export { TestimonialCard } from "./testimonialCard/TestimonialCard" export { CommunityCard } from "./community-card/CommunityCard" diff --git a/src/components/sections/content/ContentSection.tsx b/src/components/sections/content/ContentSection.tsx new file mode 100644 index 0000000..6db1889 --- /dev/null +++ b/src/components/sections/content/ContentSection.tsx @@ -0,0 +1,70 @@ +import { ContentCard } from "@/components/content-card/ContentCard" +import { Carousel } from "@/components/carousel/Carousel" +import { Heading } from "@/components/heading/Heading" +import { cn } from "@/lib/utils" +import React from "react" + +interface ContentSectionProps extends React.HTMLAttributes {} + +const ContentSection = React.forwardRef( + ({ className, ...props }, ref) => { + return ( + + + + Conteúdos + + + Explore tudo o que nós temos criado até agora. + + + + + + + + + + + + + ) + } +) + +ContentSection.displayName = "ContentSection" + +export { ContentSection }
{description}
+ {description} +
+ Explore tudo o que nós temos criado até agora. +