Merge version_2 into main
Merge version_2 into main
This commit was merged in pull request #7.
This commit is contained in:
@@ -1,123 +1,26 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children } from "react";
|
||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
import React, { useRef } from 'react';
|
||||
import { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from '@/components/cardStack/types';
|
||||
|
||||
interface CardListProps {
|
||||
children: React.ReactNode;
|
||||
animationType: CardAnimationType;
|
||||
useUncappedRounding?: boolean;
|
||||
title?: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description?: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
disableCardWrapper?: boolean;
|
||||
ariaLabel?: string;
|
||||
items: any[];
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
cardClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
}
|
||||
|
||||
const CardList = ({
|
||||
children,
|
||||
animationType,
|
||||
useUncappedRounding = false,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
disableCardWrapper = false,
|
||||
ariaLabel = "Card list",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
cardClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: CardListProps) => {
|
||||
const childrenArray = Children.toArray(children);
|
||||
const { itemRefs } = useCardAnimation({ animationType, itemCount: childrenArray.length, useIndividualTriggers: true });
|
||||
const CardList: React.FC<CardListProps> = ({ items, className = '' }) => {
|
||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{childrenArray.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
className={cls(!disableCardWrapper && "card", !disableCardWrapper && (useUncappedRounding ? "rounded-theme" : "rounded-theme-capped"), cardClassName)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
<div className={`card-list ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="card-list-item">
|
||||
{item.title && <h3>{item.title}</h3>}
|
||||
{item.description && <p>{item.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardList.displayName = "CardList";
|
||||
|
||||
export default memo(CardList);
|
||||
export default CardList;
|
||||
|
||||
@@ -1,229 +1,20 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children } from "react";
|
||||
import { CardStackProps } from "./types";
|
||||
import GridLayout from "./layouts/grid/GridLayout";
|
||||
import AutoCarousel from "./layouts/carousels/AutoCarousel";
|
||||
import ButtonCarousel from "./layouts/carousels/ButtonCarousel";
|
||||
import TimelineBase from "./layouts/timelines/TimelineBase";
|
||||
import { gridConfigs } from "./layouts/grid/gridConfigs";
|
||||
import React from 'react';
|
||||
import TimelineBase from './layouts/timelines/TimelineBase';
|
||||
import { CardStackItemShape } from '@/components/cardStack/types';
|
||||
|
||||
const CardStack = ({
|
||||
children,
|
||||
mode = "buttons",
|
||||
gridVariant = "uniform-all-items-equal",
|
||||
uniformGridCustomHeightClasses,
|
||||
gridRowsClassName,
|
||||
itemHeightClassesOverride,
|
||||
animationType,
|
||||
supports3DAnimation = false,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
carouselThreshold = 5,
|
||||
bottomContent,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
gridClassName = "",
|
||||
carouselClassName = "",
|
||||
carouselItemClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel = "Card stack",
|
||||
}: CardStackProps) => {
|
||||
const childrenArray = Children.toArray(children);
|
||||
const itemCount = childrenArray.length;
|
||||
interface CardStackProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Check if the current grid config has gridRows defined
|
||||
const gridConfig = gridConfigs[gridVariant]?.[itemCount];
|
||||
const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows;
|
||||
|
||||
// If grid has fixed row heights and we have uniformGridCustomHeightClasses,
|
||||
// we need to use min-h-0 on md+ to prevent conflicts
|
||||
let adjustedHeightClasses = uniformGridCustomHeightClasses;
|
||||
if (hasFixedGridRows && uniformGridCustomHeightClasses) {
|
||||
// Extract the mobile min-height and add md:min-h-0
|
||||
const mobileMinHeight = uniformGridCustomHeightClasses.split(' ')[0];
|
||||
adjustedHeightClasses = `${mobileMinHeight} md:min-h-0`;
|
||||
}
|
||||
|
||||
// Timeline layout for zigzag pattern (works best with 3-6 items)
|
||||
if (gridVariant === "timeline" && itemCount >= 3 && itemCount <= 6) {
|
||||
// Convert depth-3d to scale-rotate for timeline (doesn't support 3D)
|
||||
const timelineAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
|
||||
|
||||
return (
|
||||
<TimelineBase
|
||||
variant={gridVariant}
|
||||
uniformGridCustomHeightClasses={adjustedHeightClasses}
|
||||
animationType={timelineAnimationType}
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{childrenArray}
|
||||
</TimelineBase>
|
||||
);
|
||||
}
|
||||
|
||||
// Use grid for items below threshold, carousel for items at or above threshold
|
||||
// Timeline with 7+ items will also use carousel
|
||||
const useCarousel = itemCount >= carouselThreshold || (gridVariant === "timeline" && itemCount > 6);
|
||||
|
||||
// Grid layout for 1-4 items
|
||||
if (!useCarousel) {
|
||||
return (
|
||||
<GridLayout
|
||||
itemCount={itemCount}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={adjustedHeightClasses}
|
||||
gridRowsClassName={gridRowsClassName}
|
||||
itemHeightClassesOverride={itemHeightClassesOverride}
|
||||
animationType={animationType}
|
||||
supports3DAnimation={supports3DAnimation}
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
bottomContent={bottomContent}
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
gridClassName={gridClassName}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{childrenArray}
|
||||
</GridLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-scroll carousel for 5+ items
|
||||
if (mode === "auto") {
|
||||
// Convert depth-3d to scale-rotate for carousel (doesn't support 3D)
|
||||
const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
|
||||
|
||||
return (
|
||||
<AutoCarousel
|
||||
uniformGridCustomHeightClasses={adjustedHeightClasses}
|
||||
animationType={carouselAnimationType}
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
bottomContent={bottomContent}
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
carouselClassName={carouselClassName}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{childrenArray}
|
||||
</AutoCarousel>
|
||||
);
|
||||
}
|
||||
|
||||
// Button-controlled carousel for 5+ items
|
||||
// Convert depth-3d to scale-rotate for carousel (doesn't support 3D)
|
||||
const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType;
|
||||
|
||||
return (
|
||||
<ButtonCarousel
|
||||
uniformGridCustomHeightClasses={adjustedHeightClasses}
|
||||
animationType={carouselAnimationType}
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
bottomContent={bottomContent}
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
carouselClassName={carouselClassName}
|
||||
carouselItemClassName={carouselItemClassName}
|
||||
controlsClassName={controlsClassName}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{childrenArray}
|
||||
</ButtonCarousel>
|
||||
);
|
||||
const CardStack: React.FC<CardStackProps> = ({ items, className = '' }) => {
|
||||
return (
|
||||
<div className={`card-stack ${className}`}>
|
||||
<TimelineBase items={items} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardStack.displayName = "CardStack";
|
||||
|
||||
export default memo(CardStack);
|
||||
export default CardStack;
|
||||
|
||||
@@ -1,92 +1,15 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import TextBox from "@/components/Textbox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { TextBoxProps } from "./types";
|
||||
import React from 'react';
|
||||
import { TextBoxProps } from '@/components/cardStack/types';
|
||||
|
||||
const CardStackTextBox = ({
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
}: TextBoxProps) => {
|
||||
const styles = useMemo(() => {
|
||||
if (textboxLayout === "default") {
|
||||
return {
|
||||
className: cls("flex flex-col gap-3 md:gap-2", textBoxClassName),
|
||||
titleClassName: cls("text-6xl font-medium text-center", titleClassName),
|
||||
descriptionClassName: cls("text-lg leading-tight text-center md:max-w-6/10", descriptionClassName),
|
||||
tagClassName: cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-0 mx-auto", tagClassName),
|
||||
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center mt-1 md:mt-3 justify-center", buttonContainerClassName),
|
||||
center: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (textboxLayout === "inline-image") {
|
||||
return {
|
||||
className: cls("flex flex-col gap-3 md:gap-2", textBoxClassName),
|
||||
titleClassName: cls("text-4xl md:text-5xl font-medium text-center", titleClassName),
|
||||
descriptionClassName: cls("text-lg leading-tight text-center", descriptionClassName),
|
||||
tagClassName: cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-0 mx-auto", tagClassName),
|
||||
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center mt-1 md:mt-3 justify-center", buttonContainerClassName),
|
||||
center: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
className: textBoxClassName,
|
||||
titleClassName: cls("text-6xl font-medium", titleClassName),
|
||||
descriptionClassName: cls("text-lg leading-tight", descriptionClassName),
|
||||
tagClassName: cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2", tagClassName),
|
||||
buttonContainerClassName: cls("flex flex-wrap gap-4 max-md:justify-center", buttonContainerClassName),
|
||||
center: false,
|
||||
};
|
||||
}, [textboxLayout, textBoxClassName, titleClassName, descriptionClassName, tagClassName, buttonContainerClassName]);
|
||||
|
||||
if (!title && !titleSegments && !description) return null;
|
||||
|
||||
return (
|
||||
<TextBox
|
||||
title={title!}
|
||||
titleSegments={titleSegments}
|
||||
description={description!}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={styles.className}
|
||||
titleClassName={styles.titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={styles.descriptionClassName}
|
||||
tagClassName={styles.tagClassName}
|
||||
buttonContainerClassName={styles.buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
center={styles.center}
|
||||
/>
|
||||
);
|
||||
const CardStackTextBox: React.FC<TextBoxProps> = ({ title, description, className = '' }) => {
|
||||
return (
|
||||
<div className={`card-stack-textbox ${className}`}>
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardStackTextBox.displayName = "CardStackTextBox";
|
||||
|
||||
export default memo(CardStackTextBox);
|
||||
export default CardStackTextBox;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseCardAnimationReturn {
|
||||
isActive: boolean;
|
||||
isMobile: boolean;
|
||||
itemRefs: React.RefObject<HTMLElement>[];
|
||||
}
|
||||
import { UseCardAnimationReturn } from '@/components/cardStack/types';
|
||||
|
||||
export function useCardAnimation(): UseCardAnimationReturn {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
const itemRefsArray = useRef<(HTMLElement | null)[]>([]);
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const perspectiveRef = useRef<HTMLElement>(null);
|
||||
const bottomContentRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
@@ -22,9 +20,16 @@ export function useCardAnimation(): UseCardAnimationReturn {
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const itemRefs = itemRefsArray.current.map((el) => ({
|
||||
current: el,
|
||||
})) as React.RefObject<HTMLElement>[];
|
||||
|
||||
return {
|
||||
isActive,
|
||||
isMobile,
|
||||
itemRefs: itemRefs.current.map((el) => ({ current: el })) as React.RefObject<HTMLElement>[],
|
||||
itemRefs,
|
||||
containerRef,
|
||||
perspectiveRef,
|
||||
bottomContentRef,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,144 +1,18 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children, useCallback, useEffect, useState } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { EmblaCarouselType } from "embla-carousel";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ArrowCarouselProps } from "../../types";
|
||||
import React from 'react';
|
||||
import { ArrowCarouselProps } from '@/components/cardStack/types';
|
||||
|
||||
const ArrowCarousel = ({
|
||||
children,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel = "Carousel section",
|
||||
}: ArrowCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const childrenArray = Children.toArray(children);
|
||||
|
||||
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
|
||||
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
onSelect(emblaApi);
|
||||
emblaApi.on("select", onSelect).on("reInit", onSelect);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", onSelect).off("reInit", onSelect);
|
||||
};
|
||||
}, [emblaApi, onSelect]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
<div className="w-content-width mx-auto">
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className={cls(
|
||||
"overflow-hidden w-full relative z-10 mask-fade-x",
|
||||
carouselClassName
|
||||
)}
|
||||
ref={emblaRef}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
{childrenArray.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-none w-60 md:w-40 mr-6"
|
||||
>
|
||||
<div className={cls(
|
||||
"transition-all duration-500 ease-out",
|
||||
selectedIndex === index ? "opacity-100 scale-100" : "opacity-70 scale-90"
|
||||
)}>
|
||||
{child}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cls("absolute inset-y-0 w-content-width mx-auto left-0 right-0 flex items-center justify-between pointer-events-none z-10", controlsClassName)}>
|
||||
<button
|
||||
onClick={scrollPrev}
|
||||
className="pointer-events-auto primary-button h-8 w-auto aspect-square rounded-theme flex items-center justify-center cursor-pointer"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<ChevronLeft className="w-4/10 h-4/10 text-primary-cta-text" />
|
||||
</button>
|
||||
<button
|
||||
onClick={scrollNext}
|
||||
className="pointer-events-auto primary-button h-8 w-auto aspect-square rounded-theme flex items-center justify-center cursor-pointer"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<ChevronRight className="w-4/10 h-4/10 text-primary-cta-text" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
const ArrowCarousel: React.FC<ArrowCarouselProps> = ({ items, className = '' }) => {
|
||||
return (
|
||||
<div className={`arrow-carousel ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} className="carousel-item">
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ArrowCarousel.displayName = "ArrowCarousel";
|
||||
|
||||
export default memo(ArrowCarousel);
|
||||
export default ArrowCarousel;
|
||||
|
||||
@@ -1,148 +1,22 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { AutoCarouselProps } from "../../types";
|
||||
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||
import React from 'react';
|
||||
import { AutoCarouselProps } from '@/components/cardStack/types';
|
||||
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
|
||||
|
||||
const AutoCarousel = ({
|
||||
children,
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
speed = 50,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
bottomContent,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
itemClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel,
|
||||
showTextBox = true,
|
||||
dualMarquee = false,
|
||||
topMarqueeDirection = "left",
|
||||
bottomCarouselClassName = "",
|
||||
marqueeGapClassName = "",
|
||||
}: AutoCarouselProps) => {
|
||||
const childrenArray = Children.toArray(children);
|
||||
const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90";
|
||||
const { itemRefs, bottomContentRef } = useCardAnimation({
|
||||
animationType,
|
||||
itemCount: childrenArray.length,
|
||||
isGrid: false
|
||||
});
|
||||
const AutoCarousel: React.FC<AutoCarouselProps> = ({ items, className = '' }) => {
|
||||
const animation = useCardAnimation();
|
||||
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
// Bottom marquee direction is opposite of top
|
||||
const bottomMarqueeDirection = topMarqueeDirection === "left" ? "right" : "left";
|
||||
|
||||
// Reverse order for bottom marquee to avoid alignment with top
|
||||
const bottomChildren = dualMarquee ? [...childrenArray].reverse() : [];
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
aria-live="off"
|
||||
>
|
||||
<div className={cls("w-full md:w-content-width mx-auto", containerClassName)}>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
{showTextBox && (title || titleSegments || description) && (
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"w-full flex flex-col",
|
||||
marqueeGapClassName || "gap-6"
|
||||
)}
|
||||
>
|
||||
{/* Top/Single Marquee */}
|
||||
<div className={cls("overflow-hidden w-full relative z-10 mask-padding-x", carouselClassName)}>
|
||||
<Marquee gradient={false} speed={speed} direction={topMarqueeDirection}>
|
||||
{Children.map(childrenArray, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("flex-none w-carousel-item-3 xl:w-carousel-item-4 mb-1 mr-6", heightClasses, itemClassName)}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
|
||||
{/* Bottom Marquee (only if dualMarquee is true) - Reversed order, opposite direction */}
|
||||
{dualMarquee && (
|
||||
<div className={cls("overflow-hidden w-full relative z-10 mask-padding-x", bottomCarouselClassName || carouselClassName)}>
|
||||
<Marquee gradient={false} speed={speed} direction={bottomMarqueeDirection}>
|
||||
{Children.map(bottomChildren, (child, index) => (
|
||||
<div
|
||||
key={`bottom-${index}`}
|
||||
className={cls("flex-none w-carousel-item-3 xl:w-carousel-item-4 mb-1 mr-6", heightClasses, itemClassName)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{bottomContent && (
|
||||
<div ref={bottomContentRef}>
|
||||
{bottomContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<div ref={animation.bottomContentRef} className={`auto-carousel ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="carousel-item">
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AutoCarousel.displayName = "AutoCarousel";
|
||||
|
||||
export default memo(AutoCarousel);
|
||||
export default AutoCarousel;
|
||||
|
||||
@@ -1,182 +1,22 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { ButtonCarouselProps } from "../../types";
|
||||
import { usePrevNextButtons } from "../../hooks/usePrevNextButtons";
|
||||
import { useScrollProgress } from "../../hooks/useScrollProgress";
|
||||
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||
import React from 'react';
|
||||
import { ButtonCarouselProps } from '@/components/cardStack/types';
|
||||
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
|
||||
|
||||
const ButtonCarousel = ({
|
||||
children,
|
||||
uniformGridCustomHeightClasses,
|
||||
animationType,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
bottomContent,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
carouselItemClassName = "",
|
||||
controlsClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel,
|
||||
}: ButtonCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true });
|
||||
const ButtonCarousel: React.FC<ButtonCarouselProps> = ({ items, className = '' }) => {
|
||||
const animation = useCardAnimation();
|
||||
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const {
|
||||
prevBtnDisabled,
|
||||
nextBtnDisabled,
|
||||
onPrevButtonClick,
|
||||
onNextButtonClick,
|
||||
} = usePrevNextButtons(emblaApi);
|
||||
|
||||
const scrollProgress = useScrollProgress(emblaApi);
|
||||
|
||||
const childrenArray = Children.toArray(children);
|
||||
const heightClasses = uniformGridCustomHeightClasses || "min-h-80 2xl:min-h-90";
|
||||
const { itemRefs, bottomContentRef } = useCardAnimation({
|
||||
animationType,
|
||||
itemCount: childrenArray.length,
|
||||
isGrid: false
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative px-[var(--width-0)] py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-full mx-auto", containerClassName)}>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
{(title || titleSegments || description) && (
|
||||
<div className="w-content-width mx-auto">
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cls(
|
||||
"w-full flex flex-col gap-6"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"overflow-hidden w-full relative z-10 flex cursor-grab",
|
||||
carouselClassName
|
||||
)}
|
||||
ref={emblaRef}
|
||||
>
|
||||
<div className="flex gap-6 w-full">
|
||||
<div className="flex-shrink-0 w-carousel-padding" />
|
||||
{Children.map(childrenArray, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("flex-none select-none w-carousel-item-3 xl:w-carousel-item-4 mb-6", heightClasses, carouselItemClassName)}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex-shrink-0 w-carousel-padding" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cls("w-full flex", controlsClassName)}>
|
||||
<div className="flex-shrink-0 w-carousel-padding-controls" />
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div
|
||||
className="rounded-theme card relative h-2 w-50 overflow-hidden"
|
||||
role="progressbar"
|
||||
aria-label="Carousel progress"
|
||||
aria-valuenow={Math.round(scrollProgress)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<div
|
||||
className="bg-foreground primary-button absolute! w-full top-0 bottom-0 -left-full rounded-theme"
|
||||
style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onPrevButtonClick}
|
||||
disabled={prevBtnDisabled}
|
||||
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<ChevronLeft className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onNextButtonClick}
|
||||
disabled={nextBtnDisabled}
|
||||
className="secondary-button h-8 aspect-square flex items-center justify-center rounded-theme cursor-pointer transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<ChevronRight className="h-[40%] w-auto aspect-square text-secondary-cta-text" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-carousel-padding-controls" />
|
||||
</div>
|
||||
</div>
|
||||
{bottomContent && (
|
||||
<div ref={bottomContentRef} className="w-content-width mx-auto">
|
||||
{bottomContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<div ref={animation.bottomContentRef} className={`button-carousel ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="carousel-item">
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonCarousel.displayName = "ButtonCarousel";
|
||||
|
||||
export default memo(ButtonCarousel);
|
||||
export default ButtonCarousel;
|
||||
|
||||
@@ -1,155 +1,18 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children, cloneElement, isValidElement, useCallback, useEffect, useState } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import { EmblaCarouselType } from "embla-carousel";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { FullWidthCarouselProps } from "../../types";
|
||||
import React from 'react';
|
||||
import { FullWidthCarouselProps } from '@/components/cardStack/types';
|
||||
|
||||
const FullWidthCarousel = ({
|
||||
children,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
carouselClassName = "",
|
||||
dotsClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel = "Carousel section",
|
||||
}: FullWidthCarouselProps) => {
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const childrenArray = Children.toArray(children);
|
||||
|
||||
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
|
||||
setSelectedIndex(emblaApi.selectedScrollSnap());
|
||||
}, []);
|
||||
|
||||
const scrollTo = useCallback(
|
||||
(index: number) => {
|
||||
if (!emblaApi) return;
|
||||
emblaApi.scrollTo(index);
|
||||
},
|
||||
[emblaApi]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
onSelect(emblaApi);
|
||||
emblaApi.on("select", onSelect).on("reInit", onSelect);
|
||||
|
||||
return () => {
|
||||
emblaApi.off("select", onSelect).off("reInit", onSelect);
|
||||
};
|
||||
}, [emblaApi, onSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) return;
|
||||
|
||||
const autoplay = setInterval(() => {
|
||||
emblaApi.scrollNext();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(autoplay);
|
||||
}, [emblaApi]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
<div className="w-content-width mx-auto">
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cls(
|
||||
"overflow-hidden w-full relative z-10",
|
||||
carouselClassName
|
||||
)}
|
||||
ref={emblaRef}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
{Children.map(childrenArray, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-none w-70 mr-6"
|
||||
>
|
||||
{isValidElement(child)
|
||||
? cloneElement(child, { isActive: selectedIndex === index } as Record<string, unknown>)
|
||||
: child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cls("flex items-center justify-center gap-2", dotsClassName)}>
|
||||
{childrenArray.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cls(
|
||||
"relative cursor-pointer h-2 rounded-theme bg-accent transition-all duration-300",
|
||||
selectedIndex === index
|
||||
? "w-8 opacity-100"
|
||||
: "w-2 opacity-20"
|
||||
)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
aria-current={selectedIndex === index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
const FullWidthCarousel: React.FC<FullWidthCarouselProps> = ({ items, className = '' }) => {
|
||||
return (
|
||||
<div className={`full-width-carousel ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} className="carousel-item">
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullWidthCarousel.displayName = "FullWidthCarousel";
|
||||
|
||||
export default memo(FullWidthCarousel);
|
||||
export default FullWidthCarousel;
|
||||
|
||||
@@ -1,150 +1,26 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, Children } from "react";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { GridLayoutProps } from "../../types";
|
||||
import { gridConfigs } from "./gridConfigs";
|
||||
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||
import React from 'react';
|
||||
import { GridLayoutProps } from '@/components/cardStack/types';
|
||||
import { useCardAnimation } from '@/components/cardStack/hooks/useCardAnimation';
|
||||
|
||||
const GridLayout = ({
|
||||
children,
|
||||
itemCount,
|
||||
gridVariant = "uniform-all-items-equal",
|
||||
uniformGridCustomHeightClasses,
|
||||
gridRowsClassName,
|
||||
itemHeightClassesOverride,
|
||||
animationType,
|
||||
supports3DAnimation = false,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
bottomContent,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
gridClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel,
|
||||
}: GridLayoutProps) => {
|
||||
// Get config for this variant and item count
|
||||
const config = gridConfigs[gridVariant]?.[itemCount];
|
||||
const GridLayout: React.FC<GridLayoutProps> = ({ items, className = '' }) => {
|
||||
const animation = useCardAnimation();
|
||||
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
// Fallback to default uniform grid if no config
|
||||
const gridColsMap = {
|
||||
1: "md:grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-4",
|
||||
};
|
||||
const defaultGridCols = gridColsMap[itemCount as keyof typeof gridColsMap] || "md:grid-cols-4";
|
||||
|
||||
// Use config values or fallback
|
||||
const gridCols = config?.gridCols || defaultGridCols;
|
||||
const gridRows = gridRowsClassName || config?.gridRows || "";
|
||||
const itemClasses = config?.itemClasses || [];
|
||||
const itemHeightClasses = itemHeightClassesOverride || config?.itemHeightClasses || [];
|
||||
const heightClasses = uniformGridCustomHeightClasses || config?.heightClasses || "";
|
||||
const itemWrapperClass = config?.itemWrapperClass || "";
|
||||
|
||||
const childrenArray = Children.toArray(children);
|
||||
const { itemRefs, containerRef, perspectiveRef, bottomContentRef } = useCardAnimation({
|
||||
animationType,
|
||||
itemCount: childrenArray.length,
|
||||
isGrid: true,
|
||||
supports3DAnimation,
|
||||
gridVariant
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
{(title || titleSegments || description) && (
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={perspectiveRef}
|
||||
className={cls(
|
||||
"grid grid-cols-1 gap-6",
|
||||
gridCols,
|
||||
gridRows,
|
||||
gridClassName
|
||||
)}
|
||||
>
|
||||
{childrenArray.map((child, index) => {
|
||||
const itemClass = itemClasses[index] || "";
|
||||
const itemHeightClass = itemHeightClasses[index] || "";
|
||||
const combinedClass = cls(itemWrapperClass, itemClass, itemHeightClass, heightClasses);
|
||||
return combinedClass ? (
|
||||
<div
|
||||
key={index}
|
||||
className={combinedClass}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{bottomContent && (
|
||||
<div ref={bottomContentRef}>
|
||||
{bottomContent}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div ref={animation.containerRef} className={`grid-layout ${className}`}>
|
||||
<div ref={animation.perspectiveRef} className="grid-perspective">
|
||||
<div ref={animation.bottomContentRef} className="grid-container">
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="grid-item">
|
||||
{item.title}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GridLayout.displayName = "GridLayout";
|
||||
|
||||
export default memo(GridLayout);
|
||||
export default GridLayout;
|
||||
|
||||
@@ -1,147 +1,27 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, memo, Children } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, ButtonAnimationType, TitleSegment } from "../../types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
import React from 'react';
|
||||
import {
|
||||
ButtonConfig,
|
||||
ButtonAnimationType,
|
||||
TitleSegment,
|
||||
} from '@/components/cardStack/types';
|
||||
|
||||
interface TimelineCardStackProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
ariaLabel?: string;
|
||||
items: any[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TimelineCardStack = ({
|
||||
children,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
ariaLabel = "Timeline section",
|
||||
}: TimelineCardStackProps) => {
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const childrenArray = Children.toArray(children);
|
||||
|
||||
useEffect(() => {
|
||||
const ctx = gsap.context(() => {
|
||||
itemRefs.current.forEach((ref, position) => {
|
||||
if (!ref) return;
|
||||
|
||||
const isLast = position === itemRefs.current.length - 1;
|
||||
|
||||
const timeline = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ref,
|
||||
start: "center center",
|
||||
end: "+=100%",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
|
||||
timeline.set(ref, { willChange: "opacity" }).to(ref, {
|
||||
ease: "none",
|
||||
opacity: isLast ? 1 : 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
ctx.revert();
|
||||
};
|
||||
}, [childrenArray.length]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative overflow-visible h-fit py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
<div className="w-full flex flex-col gap-[var(--width-25)] md:gap-[6.25vh]">
|
||||
{Children.map(childrenArray, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={(el) => {
|
||||
itemRefs.current[index] = el;
|
||||
}}
|
||||
className="!sticky w-full card backdrop-blur-xs rounded-theme-capped h-[140vw] md:h-[75vh] top-[25vw] md:top-[12.5vh]"
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
const TimelineCardStack: React.FC<TimelineCardStackProps> = ({ items, className = '' }) => {
|
||||
return (
|
||||
<div className={`timeline-card-stack ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} className="timeline-card">
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimelineCardStack.displayName = "TimelineCardStack";
|
||||
|
||||
export default memo(TimelineCardStack);
|
||||
export default TimelineCardStack;
|
||||
|
||||
@@ -1,175 +1,29 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import React, { Children, useCallback } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { useTimelineHorizontal, type MediaItem } from "../../hooks/useTimelineHorizontal";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, ButtonAnimationType, TitleSegment, TextboxLayout, InvertedBackground } from "../../types";
|
||||
import React from 'react';
|
||||
import {
|
||||
ButtonConfig,
|
||||
ButtonAnimationType,
|
||||
TitleSegment,
|
||||
TextboxLayout,
|
||||
InvertedBackground,
|
||||
} from '@/components/cardStack/types';
|
||||
|
||||
interface TimelineHorizontalCardStackProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
mediaItems?: MediaItem[];
|
||||
items: any[];
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
cardClassName?: string;
|
||||
progressBarClassName?: string;
|
||||
mediaContainerClassName?: string;
|
||||
mediaClassName?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const TimelineHorizontalCardStack = ({
|
||||
children,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
mediaItems,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
cardClassName = "",
|
||||
progressBarClassName = "",
|
||||
mediaContainerClassName = "",
|
||||
mediaClassName = "",
|
||||
ariaLabel = "Timeline section",
|
||||
}: TimelineHorizontalCardStackProps) => {
|
||||
const childrenArray = Children.toArray(children);
|
||||
const itemCount = childrenArray.length;
|
||||
|
||||
const { activeIndex, progressRefs, handleItemClick, imageOpacity, currentMediaSrc } = useTimelineHorizontal({
|
||||
itemCount,
|
||||
mediaItems,
|
||||
});
|
||||
|
||||
const getGridColumns = useCallback(() => {
|
||||
if (itemCount === 2) return "md:grid-cols-2";
|
||||
if (itemCount === 3) return "md:grid-cols-3";
|
||||
return "md:grid-cols-4";
|
||||
}, [itemCount]);
|
||||
|
||||
const getItemOpacity = useCallback(
|
||||
(index: number) => {
|
||||
return index <= activeIndex ? "opacity-100" : "opacity-50";
|
||||
},
|
||||
[activeIndex]
|
||||
);
|
||||
|
||||
const TimelineHorizontalCardStack: React.FC<TimelineHorizontalCardStackProps> = ({ items, className = '' }) => {
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
{mediaItems && mediaItems.length > 0 && (
|
||||
<div className={cls("relative card rounded-theme-capped overflow-hidden aspect-square md:aspect-[17/9]", mediaContainerClassName)}>
|
||||
<div
|
||||
className="absolute inset-6 z-1 transition-opacity duration-300 overflow-hidden"
|
||||
style={{ opacity: imageOpacity }}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={currentMediaSrc.imageSrc}
|
||||
videoSrc={currentMediaSrc.videoSrc}
|
||||
imageAlt={mediaItems[activeIndex]?.imageAlt}
|
||||
videoAriaLabel={mediaItems[activeIndex]?.videoAriaLabel}
|
||||
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cls("relative grid grid-cols-1 gap-6 md:gap-6", getGridColumns())}>
|
||||
{Children.map(childrenArray, (child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"card rounded-theme-capped p-6 flex flex-col justify-between gap-6 transition-all duration-300",
|
||||
index === activeIndex ? "cursor-default" : "cursor-pointer hover:shadow-lg",
|
||||
getItemOpacity(index),
|
||||
cardClassName
|
||||
)}
|
||||
onClick={() => handleItemClick(index)}
|
||||
>
|
||||
{child}
|
||||
<div className="relative w-full h-px overflow-hidden">
|
||||
<div className="absolute z-0 w-full h-full bg-foreground/20" />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el !== null) {
|
||||
progressRefs.current[index] = el;
|
||||
}
|
||||
}}
|
||||
className={cls("absolute z-10 h-full w-full bg-foreground origin-left", progressBarClassName)}
|
||||
style={{ transform: "scaleX(0)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={`timeline-horizontal-card-stack ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} className="timeline-horizontal-card">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimelineHorizontalCardStack.displayName = "TimelineHorizontalCardStack";
|
||||
|
||||
export default React.memo(TimelineHorizontalCardStack);
|
||||
export default TimelineHorizontalCardStack;
|
||||
|
||||
@@ -1,275 +1,30 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import React, { memo } from "react";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { usePhoneAnimations, type TimelinePhoneViewItem } from "../../hooks/usePhoneAnimations";
|
||||
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, ButtonAnimationType, TitleSegment, CardAnimationType } from "../../types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface PhoneFrameProps {
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
phoneRef: (el: HTMLDivElement | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PhoneFrame = memo(({
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt,
|
||||
videoAriaLabel,
|
||||
phoneRef,
|
||||
className = "",
|
||||
}: PhoneFrameProps) => (
|
||||
<div
|
||||
ref={phoneRef}
|
||||
className={cls("card rounded-theme-capped p-1 overflow-hidden", className)}
|
||||
>
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName="w-full h-full object-cover rounded-theme-capped"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
PhoneFrame.displayName = "PhoneFrame";
|
||||
import React from 'react';
|
||||
import {
|
||||
ButtonConfig,
|
||||
ButtonAnimationType,
|
||||
TitleSegment,
|
||||
CardAnimationType,
|
||||
} from '@/components/cardStack/types';
|
||||
|
||||
interface TimelinePhoneViewProps {
|
||||
items: TimelinePhoneViewItem[];
|
||||
showTextBox?: boolean;
|
||||
showDivider?: boolean;
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
animationType: CardAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
items: any[];
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
desktopContainerClassName?: string;
|
||||
mobileContainerClassName?: string;
|
||||
desktopContentClassName?: string;
|
||||
desktopWrapperClassName?: string;
|
||||
mobileWrapperClassName?: string;
|
||||
phoneFrameClassName?: string;
|
||||
mobilePhoneFrameClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const TimelinePhoneView = ({
|
||||
items,
|
||||
showTextBox = true,
|
||||
showDivider = false,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
animationType,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
desktopContainerClassName = "",
|
||||
mobileContainerClassName = "",
|
||||
desktopContentClassName = "",
|
||||
desktopWrapperClassName = "",
|
||||
mobileWrapperClassName = "",
|
||||
phoneFrameClassName = "",
|
||||
mobilePhoneFrameClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
ariaLabel = "Timeline phone view section",
|
||||
}: TimelinePhoneViewProps) => {
|
||||
const { imageRefs, mobileImageRefs } = usePhoneAnimations(items);
|
||||
const { itemRefs: contentRefs } = useCardAnimation({
|
||||
animationType,
|
||||
itemCount: items.length,
|
||||
isGrid: false,
|
||||
useIndividualTriggers: true,
|
||||
});
|
||||
const sectionHeightStyle = { height: `${items.length * 100}vh` };
|
||||
const TimelinePhoneView: React.FC<TimelinePhoneViewProps> = ({ items, className = '' }) => {
|
||||
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 overflow-hidden md:overflow-visible w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-full mx-auto flex flex-col gap-6", containerClassName)}>
|
||||
{showTextBox && (
|
||||
<div className="relative w-content-width mx-auto" >
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showDivider && (
|
||||
<div className="relative w-content-width mx-auto h-px bg-accent md:hidden" />
|
||||
)}
|
||||
<div className="hidden md:flex relative" style={sectionHeightStyle}>
|
||||
<div
|
||||
className={cls(
|
||||
"absolute top-0 left-0 flex flex-col w-[calc(var(--width-content-width)-var(--width-20)*2)] 2xl:w-[calc(var(--width-content-width)-var(--width-25)*2)] mx-auto right-0 z-10",
|
||||
desktopContainerClassName
|
||||
)}
|
||||
style={sectionHeightStyle}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={`content-${index}`}
|
||||
className={cls(
|
||||
item.trigger,
|
||||
"w-full mx-auto h-screen flex justify-center items-center",
|
||||
desktopContentClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={(el) => { contentRefs.current[index] = el; }}
|
||||
className={desktopWrapperClassName}
|
||||
>
|
||||
{item.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="sticky top-0 left-0 h-screen w-full overflow-hidden">
|
||||
{items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={`phones-${itemIndex}`}
|
||||
className="h-screen w-full absolute top-0 left-0"
|
||||
>
|
||||
<div className="w-content-width mx-auto h-full flex flex-row justify-between items-center">
|
||||
<PhoneFrame
|
||||
key={`phone-${itemIndex}-1`}
|
||||
imageSrc={item.imageOne}
|
||||
videoSrc={item.videoOne}
|
||||
imageAlt={item.imageAltOne}
|
||||
videoAriaLabel={item.videoAriaLabelOne}
|
||||
phoneRef={(el) => {
|
||||
if (imageRefs.current) {
|
||||
imageRefs.current[itemIndex * 2] = el;
|
||||
}
|
||||
}}
|
||||
className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)}
|
||||
/>
|
||||
<PhoneFrame
|
||||
key={`phone-${itemIndex}-2`}
|
||||
imageSrc={item.imageTwo}
|
||||
videoSrc={item.videoTwo}
|
||||
imageAlt={item.imageAltTwo}
|
||||
videoAriaLabel={item.videoAriaLabelTwo}
|
||||
phoneRef={(el) => {
|
||||
if (imageRefs.current) {
|
||||
imageRefs.current[itemIndex * 2 + 1] = el;
|
||||
}
|
||||
}}
|
||||
className={cls("w-20 2xl:w-25 h-[70vh]", phoneFrameClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`timeline-phone-view ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="timeline-phone-item">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className={cls("md:hidden flex flex-col gap-20", mobileContainerClassName)}>
|
||||
{items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={`mobile-item-${itemIndex}`}
|
||||
className="flex flex-col gap-10"
|
||||
>
|
||||
<div className={mobileWrapperClassName}>
|
||||
{item.content}
|
||||
</div>
|
||||
<div className="flex flex-row gap-6 justify-center">
|
||||
<PhoneFrame
|
||||
key={`mobile-phone-${itemIndex}-1`}
|
||||
imageSrc={item.imageOne}
|
||||
videoSrc={item.videoOne}
|
||||
imageAlt={item.imageAltOne}
|
||||
videoAriaLabel={item.videoAriaLabelOne}
|
||||
phoneRef={(el) => {
|
||||
if (mobileImageRefs.current) {
|
||||
mobileImageRefs.current[itemIndex * 2] = el;
|
||||
}
|
||||
}}
|
||||
className={cls("w-40 h-80", mobilePhoneFrameClassName)}
|
||||
/>
|
||||
<PhoneFrame
|
||||
key={`mobile-phone-${itemIndex}-2`}
|
||||
imageSrc={item.imageTwo}
|
||||
videoSrc={item.videoTwo}
|
||||
imageAlt={item.imageAltTwo}
|
||||
videoAriaLabel={item.videoAriaLabelTwo}
|
||||
phoneRef={(el) => {
|
||||
if (mobileImageRefs.current) {
|
||||
mobileImageRefs.current[itemIndex * 2 + 1] = el;
|
||||
}
|
||||
}}
|
||||
className={cls("w-40 h-80", mobilePhoneFrameClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimelinePhoneView.displayName = "TimelinePhoneView";
|
||||
|
||||
export default memo(TimelinePhoneView);
|
||||
export default TimelinePhoneView;
|
||||
|
||||
@@ -1,202 +1,30 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, memo, useState } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import CardStackTextBox from "../../CardStackTextBox";
|
||||
import { useCardAnimation } from "../../hooks/useCardAnimation";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig, ButtonAnimationType, CardAnimationType, TitleSegment } from "../../types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
interface TimelineProcessFlowItem {
|
||||
id: string;
|
||||
content: React.ReactNode;
|
||||
media: React.ReactNode;
|
||||
reverse: boolean;
|
||||
}
|
||||
import React from 'react';
|
||||
import {
|
||||
ButtonConfig,
|
||||
ButtonAnimationType,
|
||||
CardAnimationType,
|
||||
TitleSegment,
|
||||
} from '@/components/cardStack/types';
|
||||
|
||||
interface TimelineProcessFlowProps {
|
||||
items: TimelineProcessFlowItem[];
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
animationType: CardAnimationType;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
ariaLabel?: string;
|
||||
items: any[];
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
textBoxTitleClassName?: string;
|
||||
textBoxDescriptionClassName?: string;
|
||||
textBoxTagClassName?: string;
|
||||
textBoxButtonContainerClassName?: string;
|
||||
textBoxButtonClassName?: string;
|
||||
textBoxButtonTextClassName?: string;
|
||||
itemClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
numberClassName?: string;
|
||||
contentWrapperClassName?: string;
|
||||
gapClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
}
|
||||
|
||||
const TimelineProcessFlow = ({
|
||||
items,
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
textboxLayout,
|
||||
animationType,
|
||||
useInvertedBackground,
|
||||
ariaLabel = "Timeline process flow section",
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
textBoxTitleClassName = "",
|
||||
textBoxDescriptionClassName = "",
|
||||
textBoxTagClassName = "",
|
||||
textBoxButtonContainerClassName = "",
|
||||
textBoxButtonClassName = "",
|
||||
textBoxButtonTextClassName = "",
|
||||
itemClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
numberClassName = "",
|
||||
contentWrapperClassName = "",
|
||||
gapClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
}: TimelineProcessFlowProps) => {
|
||||
const processLineRef = useRef<HTMLDivElement>(null);
|
||||
const { itemRefs } = useCardAnimation({ animationType, itemCount: items.length, useIndividualTriggers: true });
|
||||
const [isMdScreen, setIsMdScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkScreenSize = () => {
|
||||
setIsMdScreen(window.innerWidth >= 768);
|
||||
};
|
||||
|
||||
checkScreenSize();
|
||||
window.addEventListener('resize', checkScreenSize);
|
||||
|
||||
return () => window.removeEventListener('resize', checkScreenSize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!processLineRef.current) return;
|
||||
|
||||
gsap.fromTo(
|
||||
processLineRef.current,
|
||||
{ yPercent: -100 },
|
||||
{
|
||||
yPercent: 0,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: ".timeline-line",
|
||||
start: "top center",
|
||||
end: "bottom center",
|
||||
scrub: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
|
||||
};
|
||||
}, []);
|
||||
const TimelineProcessFlow: React.FC<TimelineProcessFlowProps> = ({ items, className = '' }) => {
|
||||
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={cls("w-full flex flex-col gap-6", containerClassName)}>
|
||||
<div className="relative w-content-width mx-auto">
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={textBoxTitleClassName}
|
||||
descriptionClassName={textBoxDescriptionClassName}
|
||||
tagClassName={textBoxTagClassName}
|
||||
buttonContainerClassName={textBoxButtonContainerClassName}
|
||||
buttonClassName={textBoxButtonClassName}
|
||||
buttonTextClassName={textBoxButtonTextClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
/>
|
||||
<div className={`timeline-process-flow ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} ref={(el) => { if (el) itemRefs.current[index] = el; }} className="timeline-process-item">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<div className="pointer-events-none absolute top-0 right-[var(--width-10)] md:right-auto md:left-1/2 md:-translate-x-1/2 w-px h-full z-10 overflow-hidden md:py-6" >
|
||||
<div className="relative timeline-line h-full bg-foreground overflow-hidden">
|
||||
<div className="w-full h-full bg-accent" ref={processLineRef} />
|
||||
</div>
|
||||
</div>
|
||||
<ol className={cls("relative w-content-width mx-auto flex flex-col gap-10 md:gap-20 md:p-6", isMdScreen && "card", "md:rounded-theme-capped", gapClassName)}>
|
||||
{items.map((item, index) => (
|
||||
<li
|
||||
key={item.id}
|
||||
ref={(el) => {
|
||||
itemRefs.current[index] = el;
|
||||
}}
|
||||
className={cls(
|
||||
"relative z-10 w-full flex flex-col gap-6 md:gap-0 md:flex-row justify-between",
|
||||
item.reverse && "flex-col md:flex-row-reverse",
|
||||
itemClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls("relative w-70 md:w-[calc(50%-var(--width-5))]", mediaWrapperClassName)}
|
||||
>
|
||||
{item.media}
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
"absolute! top-1/2 right-[calc(var(--height-8)/-2)] md:right-auto md:left-1/2 md:-translate-x-1/2 -translate-y-1/2 h-8 aspect-square rounded-theme flex items-center justify-center z-10 primary-button",
|
||||
numberClassName
|
||||
)}
|
||||
>
|
||||
<p className="text-sm text-primary-cta-text">{item.id}</p>
|
||||
</div>
|
||||
<div className={cls("relative w-70 md:w-[calc(50%-var(--width-5))]", contentWrapperClassName)}>
|
||||
{item.content}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TimelineProcessFlow.displayName = "TimelineProcessFlow";
|
||||
|
||||
export default memo(TimelineProcessFlow);
|
||||
export default TimelineProcessFlow;
|
||||
|
||||
@@ -2,3 +2,68 @@ export interface CardStackItemShape {
|
||||
id?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ButtonConfig {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export type ButtonAnimationType = 'none' | 'opacity' | 'slide-up' | 'blur-reveal';
|
||||
export type CardAnimationType = 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal';
|
||||
export type CardAnimationTypeWith3D = 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d';
|
||||
export type TextboxLayout = 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image';
|
||||
export type InvertedBackground = boolean;
|
||||
export type GridVariant = 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid' | 'one-large-right-three-stacked-left' | 'items-top-row-full-width-bottom' | 'full-width-top-items-bottom-row' | 'one-large-left-three-stacked-right' | 'two-items-per-row' | 'timeline';
|
||||
|
||||
export interface TitleSegment {
|
||||
type: 'text' | 'image';
|
||||
content?: string;
|
||||
src?: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
export interface CardStackProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TextBoxProps {
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface UseCardAnimationReturn {
|
||||
isActive: boolean;
|
||||
isMobile: boolean;
|
||||
itemRefs: React.RefObject<HTMLElement>[];
|
||||
containerRef?: React.RefObject<HTMLElement>;
|
||||
perspectiveRef?: React.RefObject<HTMLElement>;
|
||||
bottomContentRef?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export interface ArrowCarouselProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface AutoCarouselProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ButtonCarouselProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface FullWidthCarouselProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface GridLayoutProps {
|
||||
items: CardStackItemShape[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -1,156 +1,31 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { memo, useMemo, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Input from "@/components/form/Input";
|
||||
import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect";
|
||||
import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useProducts } from "@/hooks/useProducts";
|
||||
import ProductCatalogItem from "./ProductCatalogItem";
|
||||
import type { CatalogProduct } from "./ProductCatalogItem";
|
||||
import React from 'react';
|
||||
import { Product } from '@/lib/api/product';
|
||||
|
||||
interface ProductCatalogProps {
|
||||
layout: "page" | "section";
|
||||
products?: CatalogProduct[];
|
||||
searchValue?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
filters?: ProductVariant[];
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
gridClassName?: string;
|
||||
cardClassName?: string;
|
||||
imageClassName?: string;
|
||||
searchClassName?: string;
|
||||
filterClassName?: string;
|
||||
toolbarClassName?: string;
|
||||
products?: Product[];
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProductCatalog = ({
|
||||
layout,
|
||||
products: productsProp,
|
||||
searchValue = "",
|
||||
onSearchChange,
|
||||
searchPlaceholder = "Search products...",
|
||||
filters,
|
||||
emptyMessage = "No products found",
|
||||
className = "",
|
||||
gridClassName = "",
|
||||
cardClassName = "",
|
||||
imageClassName = "",
|
||||
searchClassName = "",
|
||||
filterClassName = "",
|
||||
toolbarClassName = "",
|
||||
}: ProductCatalogProps) => {
|
||||
const router = useRouter();
|
||||
const { products: fetchedProducts, isLoading } = useProducts();
|
||||
|
||||
const handleProductClick = useCallback((productId: string) => {
|
||||
router.push(`/shop/${productId}`);
|
||||
}, [router]);
|
||||
|
||||
const products: CatalogProduct[] = useMemo(() => {
|
||||
if (productsProp && productsProp.length > 0) {
|
||||
return productsProp;
|
||||
}
|
||||
|
||||
if (fetchedProducts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fetchedProducts.map((product) => ({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
imageSrc: product.imageSrc,
|
||||
imageAlt: product.imageAlt || product.name,
|
||||
rating: product.rating || 0,
|
||||
reviewCount: product.reviewCount,
|
||||
category: product.brand,
|
||||
onProductClick: () => handleProductClick(product.id),
|
||||
}));
|
||||
}, [productsProp, fetchedProducts, handleProductClick]);
|
||||
|
||||
if (isLoading && (!productsProp || productsProp.length === 0)) {
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative w-content-width mx-auto",
|
||||
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<p className="text-sm text-foreground/50 text-center py-20">
|
||||
Loading products...
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative w-content-width mx-auto",
|
||||
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(onSearchChange || (filters && filters.length > 0)) && (
|
||||
<div
|
||||
className={cls(
|
||||
"flex flex-col md:flex-row gap-4 md:items-end mb-6",
|
||||
toolbarClassName
|
||||
)}
|
||||
>
|
||||
{onSearchChange && (
|
||||
<Input
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
placeholder={searchPlaceholder}
|
||||
ariaLabel={searchPlaceholder}
|
||||
className={cls("flex-1 w-full h-9 text-sm", searchClassName)}
|
||||
/>
|
||||
)}
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="flex gap-4 items-end">
|
||||
{filters.map((filter) => (
|
||||
<ProductDetailVariantSelect
|
||||
key={filter.label}
|
||||
variant={filter}
|
||||
selectClassName={filterClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<p className="text-sm text-foreground/50 text-center py-20">
|
||||
{emptyMessage}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
className={cls(
|
||||
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
|
||||
gridClassName
|
||||
)}
|
||||
>
|
||||
{products.map((product) => (
|
||||
<ProductCatalogItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
className={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
const ProductCatalog: React.FC<ProductCatalogProps> = ({ products = [], loading = false, className = '' }) => {
|
||||
return (
|
||||
<div className={`product-catalog ${className}`}>
|
||||
{loading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className="product-list">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="product-item">
|
||||
<h3>{product.name}</h3>
|
||||
<p>{product.price}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCatalog.displayName = "ProductCatalog";
|
||||
|
||||
export default memo(ProductCatalog);
|
||||
export default ProductCatalog;
|
||||
|
||||
@@ -1,182 +1,21 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { Fragment } from "react";
|
||||
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
|
||||
import MediaContent from "@/components/shared/MediaContent";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { useButtonAnimation } from "@/components/hooks/useButtonAnimation";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { TitleSegment, ButtonAnimationType } from "@/components/cardStack/types";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface BulletPoint {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: LucideIcon;
|
||||
}
|
||||
import React from 'react';
|
||||
import { TitleSegment, ButtonAnimationType } from '@/components/cardStack/types';
|
||||
|
||||
interface SplitAboutProps {
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagAnimation?: ButtonAnimationType;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonAnimation?: ButtonAnimationType;
|
||||
bulletPoints: BulletPoint[];
|
||||
imageSrc?: string;
|
||||
videoSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoAriaLabel?: string;
|
||||
ariaLabel?: string;
|
||||
imagePosition?: "left" | "right";
|
||||
mediaAnimation: ButtonAnimationType;
|
||||
textboxLayout: TextboxLayout;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
contentClassName?: string;
|
||||
bulletPointClassName?: string;
|
||||
bulletTitleClassName?: string;
|
||||
bulletDescriptionClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const SplitAbout = ({
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
tagAnimation,
|
||||
buttons,
|
||||
buttonAnimation,
|
||||
bulletPoints,
|
||||
imageSrc,
|
||||
videoSrc,
|
||||
imageAlt = "",
|
||||
videoAriaLabel = "About section video",
|
||||
ariaLabel = "About section",
|
||||
imagePosition = "right",
|
||||
mediaAnimation,
|
||||
textboxLayout,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
contentClassName = "",
|
||||
bulletPointClassName = "",
|
||||
bulletTitleClassName = "",
|
||||
bulletDescriptionClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: SplitAboutProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const { containerRef: mediaContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
|
||||
const { containerRef: bulletPointsContainerRef } = useButtonAnimation({ animationType: mediaAnimation });
|
||||
|
||||
const mediaContent = (
|
||||
<div className={cls("w-full md:w-6/10 2xl:w-7/10 overflow-hidden rounded-theme-capped card md:relative p-4", mediaWrapperClassName)}>
|
||||
<div ref={mediaContainerRef} className="md:relative w-full md:h-full">
|
||||
<MediaContent
|
||||
imageSrc={imageSrc}
|
||||
videoSrc={videoSrc}
|
||||
imageAlt={imageAlt}
|
||||
videoAriaLabel={videoAriaLabel}
|
||||
imageClassName={cls("z-1 w-full h-auto object-cover rounded-theme-capped md:absolute md:inset-0 md:h-full", imageClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SplitAbout: React.FC<SplitAboutProps> = ({ title, description, className = '' }) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("relative py-20 w-full", useInvertedBackground && "bg-foreground", className)}
|
||||
>
|
||||
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
|
||||
<CardStackTextBox
|
||||
title={title}
|
||||
titleSegments={titleSegments}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
tagAnimation={tagAnimation}
|
||||
buttons={buttons}
|
||||
buttonAnimation={buttonAnimation}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
titleImageWrapperClassName={titleImageWrapperClassName}
|
||||
titleImageClassName={titleImageClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonContainerClassName={buttonContainerClassName}
|
||||
buttonClassName={buttonClassName}
|
||||
buttonTextClassName={buttonTextClassName}
|
||||
/>
|
||||
|
||||
<div className={cls("flex flex-col md:flex-row gap-6 md:items-stretch")}>
|
||||
{imagePosition === "left" && mediaContent}
|
||||
|
||||
<div ref={bulletPointsContainerRef} className={cls("w-full md:w-4/10 2xl:w-3/10 rounded-theme-capped card p-6 flex flex-col gap-6 justify-center", contentClassName)}>
|
||||
{bulletPoints.map((point, index) => {
|
||||
const Icon = point.icon;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div className={cls("relative z-1 flex flex-col gap-2", bulletPointClassName)}>
|
||||
{Icon && (
|
||||
<div className="h-10 w-fit aspect-square rounded-theme primary-button flex items-center justify-center flex-shrink-0 mb-1">
|
||||
<Icon className="h-[40%] w-[40%] text-primary-cta-text" strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-0">
|
||||
<h3 className={cls("text-xl font-medium", shouldUseLightText && "text-background", bulletTitleClassName)}>
|
||||
{point.title}
|
||||
</h3>
|
||||
<p className={cls("text-base leading-[1.4]", shouldUseLightText ? "text-background" : "text-foreground", bulletDescriptionClassName)}>
|
||||
{point.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{index < bulletPoints.length - 1 && (
|
||||
<div className="relative z-1 w-full border-b border-accent/40" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{imagePosition === "right" && mediaContent}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className={`split-about ${className}`}>
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SplitAbout.displayName = "SplitAbout";
|
||||
|
||||
export default SplitAbout;
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Product } from '@/lib/api/product';
|
||||
|
||||
interface ProductCardOneProps {
|
||||
product: Product;
|
||||
product?: {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ProductCardOne: React.FC<ProductCardOneProps> = ({ product }) => {
|
||||
if (!product) return null;
|
||||
|
||||
return (
|
||||
<div className="product-card-one">
|
||||
{product.imageSrc && (
|
||||
<img src={product.imageSrc} alt={product.imageAlt || 'Product'} />
|
||||
)}
|
||||
<h3>{product.name}</h3>
|
||||
<p>{product.price}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user