-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add LP img assets and Carousel comp
- Loading branch information
1 parent
87d6a27
commit 56b01a6
Showing
11 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { ImageCarousel } from "./ImageCarousel"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
const meta = { | ||
title: "Components/ImageCarousel", | ||
component: ImageCarousel, | ||
decorators: [ | ||
(Story) => ( | ||
<div style={{ height: "100%", width: "100%", display: "grid", placeItems: "center" }}> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
} satisfies Meta<typeof ImageCarousel>; | ||
|
||
export default meta; | ||
|
||
/////////////////////////////////////////////////////////// | ||
// STORIES | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const BasicDemo = { | ||
args: { | ||
images: [ | ||
"https://picsum.photos/seed/yvyiJ/640/480", | ||
"https://picsum.photos/seed/13nxj7X7/640/480", | ||
"https://picsum.photos/seed/BLA60wJBa/640/480", | ||
"https://picsum.photos/seed/NmFcRNn/640/480", | ||
"https://picsum.photos/seed/YpRUOrcLl/640/480", | ||
].map((imgURL, index) => ({ | ||
label: `Foo Random Image ${index + 1}`, | ||
src: imgURL, | ||
})), | ||
showImageLabels: true, | ||
style: { | ||
height: "30rem", | ||
width: "40rem", | ||
}, | ||
}, | ||
} satisfies Story; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
import { useState } from "react"; | ||
import SwipeableViews, { type SwipeableViewsProps } from "react-swipeable-views-react-18-fix"; | ||
import { styled } from "@mui/material/styles"; | ||
import IconButton from "@mui/material/IconButton"; | ||
import MobileStepper, { | ||
mobileStepperClasses, | ||
type MobileStepperProps, | ||
} from "@mui/material/MobileStepper"; | ||
import Paper, { type PaperProps } from "@mui/material/Paper"; | ||
import Text from "@mui/material/Typography"; | ||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; | ||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; | ||
import { imageCarouselClassNames } from "./classNames"; | ||
import type { Simplify, SetRequired } from "type-fest"; | ||
|
||
/** | ||
* This component displays images in a carousel that allows the user to navigate | ||
* between multiple images using left/right swipes and/or the back/next buttons. | ||
* | ||
* This component is based on the Mui `MobileStepper` demo: | ||
* https://mui.com/material-ui/react-stepper/#text-with-carousel-effect | ||
* | ||
* `react-swipeable-views` docs: https://react-swipeable-views.com/api/api/ | ||
*/ | ||
export const ImageCarousel = ({ | ||
images, | ||
initialImageIndex = 0, | ||
showImageLabels = false, | ||
SwipeableViewsProps = {}, | ||
MobileStepperProps = {}, | ||
...paperProps | ||
}: ImageCarouselProps) => { | ||
// If an image exists at the given index, use it. Otherwise, use zero. | ||
const [activeImgIndex, setActiveImgIndex] = useState( | ||
images?.[initialImageIndex] ? initialImageIndex : 0 | ||
); | ||
|
||
const numImages = images.length; | ||
|
||
const handleBack = () => { | ||
setActiveImgIndex((prevActiveImgIndex) => { | ||
const maybeNextIndex = prevActiveImgIndex - 1; | ||
return maybeNextIndex < 0 | ||
? numImages - 1 // Wrap around to the last image | ||
: maybeNextIndex; | ||
}); | ||
}; | ||
|
||
const handleNext = () => { | ||
setActiveImgIndex((prevActiveImgIndex) => { | ||
const maybeNextIndex = prevActiveImgIndex + 1; | ||
return maybeNextIndex > numImages - 1 | ||
? 0 // Wrap around to the first image | ||
: maybeNextIndex; | ||
}); | ||
}; | ||
|
||
const handleChangeIndex = (step: number) => setActiveImgIndex(step); | ||
|
||
// Destructure any provided styles from the `SwipeableViewsProps` object for merging: | ||
const { | ||
style: swipeableViewsStyles, | ||
containerStyle: swipeableViewsContainerStyles, | ||
slideStyle: swipeableViewsSlideStyles, | ||
...swipeableViewsProps | ||
} = SwipeableViewsProps; | ||
|
||
return ( | ||
<StyledPaper className={imageCarouselClassNames.root} {...paperProps}> | ||
{showImageLabels && ( | ||
<Paper elevation={0} className={imageCarouselClassNames.headerRoot}> | ||
<Text className={imageCarouselClassNames.headerText}> | ||
{images[activeImgIndex]?.label ?? "?"} | ||
</Text> | ||
</Paper> | ||
)} | ||
|
||
<SwipeableViews | ||
index={activeImgIndex} | ||
onChangeIndex={handleChangeIndex} | ||
enableMouseEvents | ||
style={{ | ||
// THE ROOT SwipeableViews <div> (there's only 1) | ||
flexGrow: 1, | ||
flexShrink: 1, | ||
...swipeableViewsStyles, | ||
}} | ||
containerStyle={{ | ||
// THE CHILD OF THE ROOT SwipeableViews <div> (there's only 1) | ||
height: "100%", | ||
...swipeableViewsContainerStyles, | ||
}} | ||
slideStyle={{ | ||
// THE DIV THAT WRAPS EACH <img> | ||
display: "flex", | ||
justifyContent: "center", | ||
alignItems: "center", | ||
...swipeableViewsSlideStyles, | ||
}} | ||
{...swipeableViewsProps} | ||
> | ||
{images.map(({ src, label, alt, ...imgProps }) => ( | ||
<img | ||
key={label} | ||
src={src} | ||
alt={alt || label} | ||
className={imageCarouselClassNames.image} | ||
{...imgProps} | ||
/> | ||
))} | ||
</SwipeableViews> | ||
<MobileStepper | ||
steps={numImages} | ||
activeStep={activeImgIndex} | ||
variant={numImages > 5 ? "text" : "dots"} | ||
position="static" | ||
backButton={ | ||
<IconButton onClick={handleBack}> | ||
<KeyboardArrowLeft /> | ||
</IconButton> | ||
} | ||
nextButton={ | ||
<IconButton onClick={handleNext}> | ||
<KeyboardArrowRight /> | ||
</IconButton> | ||
} | ||
{...MobileStepperProps} | ||
/> | ||
</StyledPaper> | ||
); | ||
}; | ||
|
||
const StyledPaper = styled(Paper)(({ style = {} }) => { | ||
const borderRadius = style?.borderRadius || "inherit"; | ||
|
||
return { | ||
// THE ROOT PAPER | ||
display: "flex", | ||
flexDirection: "column", | ||
justifyContent: "center", | ||
// some default dimensions to ensure the carousel never extends beyond the viewport: | ||
maxHeight: style?.maxHeight || "95vh", | ||
maxWidth: style?.maxWidth || "95vw", | ||
height: style?.height || "100%", | ||
width: style?.width || "100%", | ||
|
||
borderTopLeftRadius: borderRadius, | ||
borderTopRightRadius: borderRadius, | ||
borderBottomLeftRadius: borderRadius, | ||
borderBottomRightRadius: borderRadius, | ||
|
||
// HEADER | ||
[`& > .${imageCarouselClassNames.headerRoot}`]: { | ||
display: "flex", | ||
alignItems: "center", | ||
padding: "1rem", | ||
borderBottom: "1px solid black", | ||
backgroundColor: "inherit", | ||
borderTopLeftRadius: "inherit", | ||
borderTopRightRadius: "inherit", | ||
borderBottomLeftRadius: "0 !important", | ||
borderBottomRightRadius: "0 !important", | ||
}, | ||
|
||
// IMAGES | ||
[`& .${imageCarouselClassNames.image}`]: { | ||
maxHeight: "98%", | ||
maxWidth: "98%", | ||
objectFit: "contain", | ||
imageRendering: "crisp-edges", | ||
}, | ||
|
||
// FOOTER/MOBILE STEPPER CONTROLS | ||
[`& > .${mobileStepperClasses.root}`]: { | ||
backgroundColor: "inherit", | ||
borderTop: "1px solid black", | ||
borderTopLeftRadius: "0 !important", | ||
borderTopRightRadius: "0 !important", | ||
borderBottomLeftRadius: "inherit", | ||
borderBottomRightRadius: "inherit", | ||
}, | ||
}; | ||
}); | ||
|
||
/** | ||
* A `label` for an image and its `src` URL. If `alt` is not provided, the `label` is used. | ||
*/ | ||
export type CarouselImageConfig = { label: string } & SetRequired< | ||
React.ComponentPropsWithoutRef<"img">, | ||
"src" | ||
>; | ||
|
||
export type ImageCarouselProps = { | ||
/** An array of {@link CarouselImageConfig|image config} objects. */ | ||
images: Array<CarouselImageConfig>; | ||
/** | ||
* The index of the image to display first. If an {@link CarouselImageConfig|image config} | ||
* does not exist at the given index, the zero-index image is used as a fallback. | ||
*/ | ||
initialImageIndex?: number; | ||
showImageLabels?: boolean; | ||
SwipeableViewsProps?: Simplify< | ||
Pick< | ||
SwipeableViewsProps, | ||
| "animateHeight" | ||
| "animateTransitions" | ||
| "containerStyle" | ||
| "disabled" | ||
| "hysteresis" | ||
| "onSwitching" | ||
| "onTransitionEnd" | ||
| "resistance" | ||
| "slideClassName" | ||
| "slideStyle" | ||
| "springConfig" | ||
| "style" | ||
| "threshold" | ||
> | ||
>; | ||
MobileStepperProps?: Simplify< | ||
Omit<MobileStepperProps, "steps" | "activeStep" | "backButton" | "nextButton" | "children"> | ||
>; | ||
} & Omit<PaperProps, "children">; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* Class names for `ImageCarousel` components (src/components/ImageCarousel/). | ||
*/ | ||
export const imageCarouselClassNames = { | ||
root: "image-carousel__root", | ||
headerRoot: "image-carousel__header-root", | ||
headerText: "image-carousel__header-text", | ||
image: "image-carousel__image", | ||
} as const; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./ImageCarousel"; | ||
export * from "./classNames"; |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { ProductImage, PRODUCT_IMAGES } from "./ProductImage"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
const meta = { | ||
title: "Pages/LandingPage/ProductImage", | ||
component: ProductImage, | ||
decorators: [ | ||
(Story) => ( | ||
<div style={{ height: "100%", width: "100%", display: "grid", placeItems: "center" }}> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
args: { | ||
ImageCarouselProps: { | ||
showImageLabels: true, | ||
}, | ||
style: { maxHeight: "100%", maxWidth: "100%" }, | ||
}, | ||
} satisfies Meta<typeof ProductImage>; | ||
|
||
export default meta; | ||
|
||
/////////////////////////////////////////////////////////// | ||
// STORIES | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const DashboardDesktopView = { | ||
args: { ...PRODUCT_IMAGES[0] }, | ||
} satisfies Story; | ||
|
||
export const CreateInvoiceMobileView = { | ||
args: { ...PRODUCT_IMAGES[1] }, | ||
} satisfies Story; | ||
|
||
export const DataGridDemo = { | ||
args: { ...PRODUCT_IMAGES[2] }, | ||
} satisfies Story; | ||
|
||
export const ListViewMobileDemo = { | ||
args: { ...PRODUCT_IMAGES[3] }, | ||
} satisfies Story; |
Oops, something went wrong.