From 63ea4ec09689d1504d00d8ece61b0069ea4c73a6 Mon Sep 17 00:00:00 2001 From: Eugene Boruhov Date: Mon, 4 Nov 2024 11:04:13 +0100 Subject: [PATCH] Make SimpleCarousel not so simple (#35) * Make SimpleCarousel not so simple * Fix TS error --- .../src/components/SectionContainer/index.tsx | 12 +- .../src/components/SectionContainer/types.ts | 1 + .../carousels/SimpleCarousel/index.tsx | 27 +++- .../carousels/SimpleCarousel/schema.ts | 53 ++++++- .../src/generated/extracted-schema.json | 86 +++++++++++ apps/sanity/src/generated/extracted-types.ts | 17 +++ .../src/lib/adapters/prepareImageProps.tsx | 11 +- .../src/components/SectionContainer/index.tsx | 5 +- .../src/components/SectionContainer/types.ts | 1 + .../carousels/SimpleCarousel/index.tsx | 25 +++- packages/tailwind-config/lib/plugin.ts | 10 +- .../carousels/SimpleCarousel/CarouselCard.tsx | 31 +++- .../carousels/SimpleCarousel/index.tsx | 141 +++++++++++++++++- .../carousels/SimpleCarousel/types.ts | 5 + .../sections/threeDElement/index.tsx | 1 + .../components/ui/GenericCarousel/index.tsx | 59 +++++++- .../ui/components/ui/GenericCarousel/types.ts | 9 ++ 17 files changed, 461 insertions(+), 33 deletions(-) diff --git a/apps/sanity/src/components/SectionContainer/index.tsx b/apps/sanity/src/components/SectionContainer/index.tsx index 728c4cd..cff29af 100644 --- a/apps/sanity/src/components/SectionContainer/index.tsx +++ b/apps/sanity/src/components/SectionContainer/index.tsx @@ -9,8 +9,15 @@ export default function SectionContainer({ className, sectionData, }: ISectionContainerProps) { - const { _key, theme, marginTop, marginBottom, paddingX, paddingY } = - sectionData; + const { + _key, + theme, + marginTop, + marginBottom, + paddingX, + paddingY, + noMaxWidth, + } = sectionData; const cleanMarginTop = stegaClean(marginTop); const cleanMarginBottom = stegaClean(marginBottom); @@ -31,6 +38,7 @@ export default function SectionContainer({ className={cn("mx-auto max-w-screen-xl px-4 py-8", { "px-0": paddingX === "none", "py-0": paddingY === "none", + "max-w-none": noMaxWidth, })} > {children} diff --git a/apps/sanity/src/components/SectionContainer/types.ts b/apps/sanity/src/components/SectionContainer/types.ts index 0d768e2..1ebd355 100644 --- a/apps/sanity/src/components/SectionContainer/types.ts +++ b/apps/sanity/src/components/SectionContainer/types.ts @@ -5,6 +5,7 @@ interface ISectionData { paddingY?: "none"; marginTop?: "none" | "base" | "lg"; marginBottom?: "none" | "base" | "lg"; + noMaxWidth?: boolean; } export interface ISectionContainerProps { diff --git a/apps/sanity/src/contentSections/carousels/SimpleCarousel/index.tsx b/apps/sanity/src/contentSections/carousels/SimpleCarousel/index.tsx index f0e4820..82c7cc9 100644 --- a/apps/sanity/src/contentSections/carousels/SimpleCarousel/index.tsx +++ b/apps/sanity/src/contentSections/carousels/SimpleCarousel/index.tsx @@ -1,8 +1,10 @@ +import { stegaClean } from "@sanity/client/stega"; import EmptyBlock from "@shared/ui/components/EmptyBlock"; import { SimpleCarousel as SimpleCarouselUI } from "@shared/ui"; import { prepareImageProps } from "@/lib/adapters/prepareImageProps"; +import { prepareRichTextProps } from "@/lib/adapters/prepareRichTextProps"; import SectionContainer from "@/components/SectionContainer"; import type { ISimpleCarouselProps } from "./types"; @@ -10,18 +12,37 @@ import type { ISimpleCarouselProps } from "./types"; export default function SimpleCarousel({ data }: ISimpleCarouselProps) { if (!data) return null; - const { slides } = data; + const { text, slides, fullWidth, params } = data; + const effect = stegaClean(data.effect); + const { loop, slidesPerView, spaceBetween } = params || {}; if (!slides || slides.length === 0) return ; const carouselSlides = slides.map((slide) => ({ image: prepareImageProps(slide.image), + text: prepareRichTextProps(slide.text), + effect, })); return ( - - + + ); } diff --git a/apps/sanity/src/contentSections/carousels/SimpleCarousel/schema.ts b/apps/sanity/src/contentSections/carousels/SimpleCarousel/schema.ts index c4eb199..426baa8 100644 --- a/apps/sanity/src/contentSections/carousels/SimpleCarousel/schema.ts +++ b/apps/sanity/src/contentSections/carousels/SimpleCarousel/schema.ts @@ -14,6 +14,10 @@ export const simpleCarouselCard = defineType({ title: "Simple Carousel Card", options: {}, fields: [ + defineField({ + name: "text", + type: "customRichText", + }), defineField({ name: "image", type: customImage.name, @@ -26,6 +30,7 @@ export const simpleCarouselCard = defineType({ }, prepare({ image }) { return { + title: "Slide", media: image, }; }, @@ -39,6 +44,11 @@ export default { type: "object", groups: commonGroups, fields: [ + defineField({ + group: CommonGroup.Content, + name: "text", + type: "customRichText", + }), defineField({ group: CommonGroup.Content, name: "slides", @@ -46,11 +56,52 @@ export default { of: [{ type: "simpleCarouselCard" }], validation: (Rule) => Rule.required(), }), + defineField({ + name: "effect", + type: "string", + group: CommonGroup.Style, + options: { + list: [ + { title: "Slide", value: "slide" }, + { title: "Coverflow", value: "coverflow" }, + { title: "Cube", value: "cube" }, + { title: "Fade", value: "fade" }, + { title: "Flip", value: "flip" }, + { title: "Cards", value: "cards" }, + ], + layout: "radio", + }, + initialValue: "slide", + }), + defineField({ + name: "fullWidth", + type: "boolean", + group: CommonGroup.Style, + }), + defineField({ + name: "params", + type: "object", + group: CommonGroup.Style, + fields: [ + defineField({ + name: "loop", + type: "boolean", + }), + defineField({ + name: "slidesPerView", + type: "number", + }), + defineField({ + name: "spaceBetween", + type: "number", + }), + ], + }), ...sectionMarginFields, ], preview: { select: { - slides: "preview.slides", + slides: "slides", }, prepare: ({ slides }: any) => ({ title: `Simple Carousel - ${slides.length} slides`, diff --git a/apps/sanity/src/generated/extracted-schema.json b/apps/sanity/src/generated/extracted-schema.json index 7b1bfef..4db0070 100644 --- a/apps/sanity/src/generated/extracted-schema.json +++ b/apps/sanity/src/generated/extracted-schema.json @@ -805,6 +805,14 @@ "value": "section.simpleCarousel" } }, + "text": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "customRichText" + }, + "optional": true + }, "slides": { "type": "objectAttribute", "value": { @@ -827,6 +835,76 @@ }, "optional": false }, + "effect": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "slide" + }, + { + "type": "string", + "value": "coverflow" + }, + { + "type": "string", + "value": "cube" + }, + { + "type": "string", + "value": "fade" + }, + { + "type": "string", + "value": "flip" + }, + { + "type": "string", + "value": "cards" + } + ] + }, + "optional": true + }, + "fullWidth": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "params": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "loop": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "slidesPerView": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "spaceBetween": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + }, + "optional": true + }, "marginTop": { "type": "objectAttribute", "value": { @@ -1601,6 +1679,14 @@ "value": "simpleCarouselCard" } }, + "text": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "customRichText" + }, + "optional": true + }, "image": { "type": "objectAttribute", "value": { diff --git a/apps/sanity/src/generated/extracted-types.ts b/apps/sanity/src/generated/extracted-types.ts index b195921..091421e 100644 --- a/apps/sanity/src/generated/extracted-types.ts +++ b/apps/sanity/src/generated/extracted-types.ts @@ -131,11 +131,19 @@ export type SectionWideSimpleCarousel = { export type SectionSimpleCarousel = { _type: "section.simpleCarousel"; + text?: CustomRichText; slides: Array< { _key: string; } & SimpleCarouselCard >; + effect?: "slide" | "coverflow" | "cube" | "fade" | "flip" | "cards"; + fullWidth?: boolean; + params?: { + loop?: boolean; + slidesPerView?: number; + spaceBetween?: number; + }; marginTop: "none" | "base" | "lg"; marginBottom: "none" | "base" | "lg"; }; @@ -234,6 +242,7 @@ export type WideSimpleCarouselCard = { export type SimpleCarouselCard = { _type: "simpleCarouselCard"; + text?: CustomRichText; image: CustomImage; }; @@ -884,11 +893,19 @@ export type PAGE_BY_SLUG_QUERYResult = { | { _key: string; _type: "section.simpleCarousel"; + text?: CustomRichText; slides: Array< { _key: string; } & SimpleCarouselCard >; + effect?: "cards" | "coverflow" | "cube" | "fade" | "flip" | "slide"; + fullWidth?: boolean; + params?: { + loop?: boolean; + slidesPerView?: number; + spaceBetween?: number; + }; marginTop: "base" | "lg" | "none"; marginBottom: "base" | "lg" | "none"; } diff --git a/apps/sanity/src/lib/adapters/prepareImageProps.tsx b/apps/sanity/src/lib/adapters/prepareImageProps.tsx index 6aca36a..5731196 100644 --- a/apps/sanity/src/lib/adapters/prepareImageProps.tsx +++ b/apps/sanity/src/lib/adapters/prepareImageProps.tsx @@ -15,7 +15,7 @@ export const urlForImage = (source: CustomImage["image"]) => { return undefined; } - return builder.image(source).auto("format").fit("max"); + return builder.image(source); }; export const prepareImageProps = (props?: CustomImage): IImageProps => { @@ -28,8 +28,13 @@ export const prepareImageProps = (props?: CustomImage): IImageProps => { fit: "cover", }; - const url = - urlForImage(props.image)?.height(props.height).fit("crop").url() || ""; + const url = props.image.asset?._ref.endsWith("svg") + ? urlForImage(props.image)?.url() || "" + : urlForImage(props.image) + ?.height(props.height) + .fit("max") + .auto("format") + .url() || ""; return { src: url, diff --git a/apps/storyblok/src/components/SectionContainer/index.tsx b/apps/storyblok/src/components/SectionContainer/index.tsx index a866a5b..ca42759 100644 --- a/apps/storyblok/src/components/SectionContainer/index.tsx +++ b/apps/storyblok/src/components/SectionContainer/index.tsx @@ -11,7 +11,8 @@ export default function SectionContainer({ blok, className, }: ISectionContainerProps) { - const { _uid, paddingX, paddingY, marginTop, marginBottom } = blok; + const { _uid, paddingX, paddingY, marginTop, marginBottom, noMaxWidth } = + blok; if (isDraftMode) { return ( @@ -31,6 +32,7 @@ export default function SectionContainer({ className={cn("mx-auto max-w-screen-xl px-4 py-8", { "px-0": paddingX === "none", "py-0": paddingY === "none", + "max-w-none": noMaxWidth, })} > {children} @@ -55,6 +57,7 @@ export default function SectionContainer({ className={cn("mx-auto max-w-screen-xl px-4 py-8", { "px-0": paddingX === "none", "py-0": paddingY === "none", + "max-w-none": noMaxWidth, })} > {children} diff --git a/apps/storyblok/src/components/SectionContainer/types.ts b/apps/storyblok/src/components/SectionContainer/types.ts index 4adb39a..a77ee52 100644 --- a/apps/storyblok/src/components/SectionContainer/types.ts +++ b/apps/storyblok/src/components/SectionContainer/types.ts @@ -5,6 +5,7 @@ export interface ISectionContainer extends SbBlokData { marginBottom?: "none" | "base" | "lg"; paddingX?: "none"; paddingY?: "none"; + noMaxWidth?: boolean; } export interface ISectionContainerProps { diff --git a/apps/storyblok/src/contentSections/carousels/SimpleCarousel/index.tsx b/apps/storyblok/src/contentSections/carousels/SimpleCarousel/index.tsx index 7f44ac1..467413b 100644 --- a/apps/storyblok/src/contentSections/carousels/SimpleCarousel/index.tsx +++ b/apps/storyblok/src/contentSections/carousels/SimpleCarousel/index.tsx @@ -3,23 +3,42 @@ import EmptyBlock from "@shared/ui/components/EmptyBlock"; import { SimpleCarousel as SimpleCarouselUI } from "@shared/ui"; import { prepareImageProps } from "@/lib/adapters/prepareImageProps"; +import { prepareRichTextProps } from "@/lib/adapters/prepareRichTextProps"; import SectionContainer from "@/components/SectionContainer"; import type { ISimpleCarouselProps } from "./types"; export default function SimpleCarousel({ blok }: ISimpleCarouselProps) { - const { slides } = blok; + const { text, slides, effect, fullWidth, params } = blok; + const { loop, slidesPerView, spaceBetween } = params?.[0] || {}; if (!slides || slides.length === 0) return ; const carouselSlides = slides.map((slide) => ({ image: prepareImageProps(slide.image[0]), + text: prepareRichTextProps(slide.text?.[0]), + effect, })); return ( - - + + ); } diff --git a/packages/tailwind-config/lib/plugin.ts b/packages/tailwind-config/lib/plugin.ts index 5d44f16..87a275b 100644 --- a/packages/tailwind-config/lib/plugin.ts +++ b/packages/tailwind-config/lib/plugin.ts @@ -1,9 +1,17 @@ import plugin from "tailwindcss/plugin"; +import { type PluginAPI } from "tailwindcss/types/config"; // todo: consider implementing themes feature using CSS variables export const customPlugin = plugin( // 1. Add CSS variable definitions to the base layer - function () {}, + function ({ addUtilities }: PluginAPI) { + addUtilities({ + ".mask-shadow-y": { + maskImage: + "linear-gradient(90deg, transparent, #fff 10%, #fff 90%, transparent)", + }, + }); + }, // 2. Extend the Tailwind theme with "themable" utilities { diff --git a/packages/ui/components/sections/carousels/SimpleCarousel/CarouselCard.tsx b/packages/ui/components/sections/carousels/SimpleCarousel/CarouselCard.tsx index 9b3ee14..375a2f7 100644 --- a/packages/ui/components/sections/carousels/SimpleCarousel/CarouselCard.tsx +++ b/packages/ui/components/sections/carousels/SimpleCarousel/CarouselCard.tsx @@ -1,21 +1,44 @@ +import { cn } from "../../../../utils"; import { Image } from "../../../ui/image"; import { ImageAspectRatio } from "../../../ui/image/types"; +import { RichText } from "../../../ui/richText"; import type { ICarouselCardProps } from "./types"; -export default function CarouselCard({ image }: ICarouselCardProps) { +export default function CarouselCard({ + image, + text, + isActive, + effect, +}: ICarouselCardProps) { const imageWrapperProps = { - className: "h-40", + className: "h-80 w-full", style: { aspectRatio: ImageAspectRatio[image.aspectRatio as ImageAspectRatio], }, }; return ( -
-

simple carousel card

+
+ {text && ( + + )}
); } diff --git a/packages/ui/components/sections/carousels/SimpleCarousel/index.tsx b/packages/ui/components/sections/carousels/SimpleCarousel/index.tsx index 9253175..44ea559 100644 --- a/packages/ui/components/sections/carousels/SimpleCarousel/index.tsx +++ b/packages/ui/components/sections/carousels/SimpleCarousel/index.tsx @@ -1,20 +1,145 @@ +"use client"; + +import { + EffectCards, + EffectCoverflow, + EffectCube, + EffectFade, + EffectFlip, + Navigation, +} from "swiper/modules"; + import { GenericCarousel } from "../../../ui/GenericCarousel"; import CarouselCard from "./CarouselCard"; import type { ISimpleCarouselProps } from "./types"; +import "swiper/css/bundle"; +import "swiper/css/effect-coverflow"; + +import React, { useEffect, useRef } from "react"; +import type { NavigationOptions } from "swiper/types"; + +import { cn } from "../../../../utils"; +import { type IGenericCarouselBaseProps } from "../../../ui/GenericCarousel/types"; + +const getEffectModule = (effect: IGenericCarouselBaseProps["effect"]) => { + switch (effect) { + case "fade": + return EffectFade; + case "cube": + return EffectCube; + case "flip": + return EffectFlip; + case "coverflow": + return EffectCoverflow; + case "cards": + return EffectCards; + default: + return undefined; + } +}; + +const ArrowButton = React.forwardRef( + ({ className, ...props }, ref) => ( + + ), +); + export function SimpleCarousel({ + text, slides, customModules, customModulesParams, + effect, + params, }: ISimpleCarouselProps) { + const effectModule = getEffectModule(effect); + + const prevButtonRef = useRef(null); + const nextButtonRef = useRef(null); + const [navigation, setNavigation] = React.useState({ + enabled: true, + prevEl: prevButtonRef.current, + nextEl: nextButtonRef.current, + }); + useEffect(() => { + if (prevButtonRef.current && nextButtonRef.current) { + setNavigation({ + enabled: true, + prevEl: prevButtonRef.current, + nextEl: nextButtonRef.current, + }); + } + }, [prevButtonRef, nextButtonRef]); + return ( - ({ - children: , - className: "!w-96", - }))} - customModules={customModules} - customModulesParams={customModulesParams} - /> +
+ + + ({ + children: (({ + isNext, + isActive, + }: { + isNext: boolean; + isActive: boolean; + }) => ( + + )) as unknown as React.ReactNode, + }))} + customModules={[ + Navigation, + ...(customModules || []), + ...(effectModule ? [effectModule] : []), + ]} + customModulesParams={{ + navigation, + ...(customModulesParams || {}), + }} + effect={effect} + params={params} + /> + + svg]:rotate-180", { + "right-[15%]": + effect && ["cube", "fade", "flip", "cards"].includes(effect), + })} + /> +
); } diff --git a/packages/ui/components/sections/carousels/SimpleCarousel/types.ts b/packages/ui/components/sections/carousels/SimpleCarousel/types.ts index f2d7f18..fc1a6ce 100644 --- a/packages/ui/components/sections/carousels/SimpleCarousel/types.ts +++ b/packages/ui/components/sections/carousels/SimpleCarousel/types.ts @@ -1,10 +1,15 @@ import type { IGenericCarouselBaseProps } from "../../../ui/GenericCarousel/types"; import type { IImageProps } from "../../../ui/image/types"; +import { type IRichTextProps } from "../../../ui/richText/types"; export interface ISimpleCarouselProps extends IGenericCarouselBaseProps { + text?: IRichTextProps; slides: ICarouselCardProps[]; } export interface ICarouselCardProps { image: IImageProps; + text?: IRichTextProps; + effect: IGenericCarouselBaseProps["effect"]; + isActive?: boolean; } diff --git a/packages/ui/components/sections/threeDElement/index.tsx b/packages/ui/components/sections/threeDElement/index.tsx index 693dcd0..1ec7994 100644 --- a/packages/ui/components/sections/threeDElement/index.tsx +++ b/packages/ui/components/sections/threeDElement/index.tsx @@ -13,6 +13,7 @@ const MODELS_MAP: Record = { export function ThreeDElement({ model }: IThreeDElementProps) { const ModelComponent = MODELS_MAP[model]; + return null; return (
diff --git a/packages/ui/components/ui/GenericCarousel/index.tsx b/packages/ui/components/ui/GenericCarousel/index.tsx index 2a95826..f5ac0e0 100644 --- a/packages/ui/components/ui/GenericCarousel/index.tsx +++ b/packages/ui/components/ui/GenericCarousel/index.tsx @@ -3,25 +3,70 @@ import { Swiper, SwiperSlide } from "swiper/react"; import { cn } from "../../../utils"; +import { RichText } from "../richText"; import type { IGenericCarouselProps } from "./types"; +const defaultEffectsConfig = { + coverflowEffect: { + rotate: 50, + stretch: 0, + depth: 100, + modifier: 1, + slideShadows: false, + }, + cubeEffect: { + shadow: true, + slideShadows: false, + shadowOffset: 20, + shadowScale: 0.94, + }, + fadeEffect: { + crossFade: false, + }, + cardsEffect: { + perSlideOffset: 8, + perSlideRotate: 2, + rotate: true, + slideShadows: false, + }, + flipEffect: { + limitRotation: true, + slideShadows: false, + }, +}; + export function GenericCarousel({ slides, + text, customModules, customModulesParams, + effect, + params, }: IGenericCarouselProps) { return ( -
+
+ {text && } + - {(slides || []).map((slide, i) => ( - - {slide.children} - - ))} + {(slides || []).map((slide, i) => { + return ( + + {slide.children} + + ); + })}
); diff --git a/packages/ui/components/ui/GenericCarousel/types.ts b/packages/ui/components/ui/GenericCarousel/types.ts index 6d958cd..88450bd 100644 --- a/packages/ui/components/ui/GenericCarousel/types.ts +++ b/packages/ui/components/ui/GenericCarousel/types.ts @@ -4,12 +4,20 @@ import type { SwiperModule, } from "swiper/types"; +import { type IRichTextProps } from "../richText/types"; + export interface IGenericCarouselBaseProps { customModules?: SwiperModule[]; customModulesParams?: { navigation?: NavigationOptions; autoplay?: AutoplayOptions; }; + effect?: "slide" | "fade" | "cube" | "flip" | "coverflow" | "cards"; + params?: { + loop?: boolean; + slidesPerView?: "auto" | number; + spaceBetween?: number; + }; } export interface IGenericCarouselProps extends IGenericCarouselBaseProps { @@ -17,4 +25,5 @@ export interface IGenericCarouselProps extends IGenericCarouselBaseProps { children: React.ReactNode; className?: string; }[]; + text?: IRichTextProps; }