(there's only 1)
+ height: "100%",
+ ...swipeableViewsContainerStyles,
+ }}
+ slideStyle={{
+ // THE DIV THAT WRAPS EACH
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ ...swipeableViewsSlideStyles,
+ }}
+ {...swipeableViewsProps}
+ >
+ {images.map(({ src, label, alt, ...imgProps }) => (
+
+ ))}
+
+
5 ? "text" : "dots"}
+ position="static"
+ backButton={
+
+
+
+ }
+ nextButton={
+
+
+
+ }
+ {...MobileStepperProps}
+ />
+
+ );
+};
+
+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;
+ /**
+ * 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
+ >;
+} & Omit;
diff --git a/src/components/ImageCarousel/classNames.ts b/src/components/ImageCarousel/classNames.ts
new file mode 100644
index 00000000..ad7c2175
--- /dev/null
+++ b/src/components/ImageCarousel/classNames.ts
@@ -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;
diff --git a/src/components/ImageCarousel/index.ts b/src/components/ImageCarousel/index.ts
new file mode 100644
index 00000000..f74b159d
--- /dev/null
+++ b/src/components/ImageCarousel/index.ts
@@ -0,0 +1,2 @@
+export * from "./ImageCarousel";
+export * from "./classNames";
diff --git a/src/images/demo_desktop_dashboard.webp b/src/images/demo_desktop_dashboard.webp
new file mode 100644
index 00000000..1b03961a
Binary files /dev/null and b/src/images/demo_desktop_dashboard.webp differ
diff --git a/src/images/demo_desktop_workorders_datagrid.webp b/src/images/demo_desktop_workorders_datagrid.webp
new file mode 100644
index 00000000..c115759c
Binary files /dev/null and b/src/images/demo_desktop_workorders_datagrid.webp differ
diff --git a/src/images/demo_mobile_create_invoice.webp b/src/images/demo_mobile_create_invoice.webp
new file mode 100644
index 00000000..cb5967a1
Binary files /dev/null and b/src/images/demo_mobile_create_invoice.webp differ
diff --git a/src/images/demo_mobile_workorders_list.webp b/src/images/demo_mobile_workorders_list.webp
new file mode 100644
index 00000000..fca88aee
Binary files /dev/null and b/src/images/demo_mobile_workorders_list.webp differ
diff --git a/src/images/landing_page_bg.webp b/src/images/landing_page_bg.webp
new file mode 100644
index 00000000..b554a82e
Binary files /dev/null and b/src/images/landing_page_bg.webp differ
diff --git a/src/pages/LandingPage/ProductImage.stories.tsx b/src/pages/LandingPage/ProductImage.stories.tsx
new file mode 100644
index 00000000..bff88d8a
--- /dev/null
+++ b/src/pages/LandingPage/ProductImage.stories.tsx
@@ -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) => (
+
+
+
+ ),
+ ],
+ args: {
+ ImageCarouselProps: {
+ showImageLabels: true,
+ },
+ style: { maxHeight: "100%", maxWidth: "100%" },
+ },
+} satisfies Meta;
+
+export default meta;
+
+///////////////////////////////////////////////////////////
+// STORIES
+
+type Story = StoryObj;
+
+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;
diff --git a/src/pages/LandingPage/ProductImage.tsx b/src/pages/LandingPage/ProductImage.tsx
new file mode 100644
index 00000000..ee9d21cc
--- /dev/null
+++ b/src/pages/LandingPage/ProductImage.tsx
@@ -0,0 +1,82 @@
+import { useState } from "react";
+import { styled } from "@mui/material/styles";
+import Box, { type BoxProps } from "@mui/material/Box";
+import Dialog, { dialogClasses } from "@mui/material/Dialog";
+import { globalClassNames } from "@/app/GlobalStyles/classNames";
+import {
+ ImageCarousel,
+ type ImageCarouselProps,
+ type CarouselImageConfig,
+} from "@/components/ImageCarousel";
+import { NoMaxWidthTooltip } from "@/components/Tooltips";
+import demoDesktopDashboardImageSrc from "@/images/demo_desktop_dashboard.webp";
+import demoDesktopDataGridImageSrc from "@/images/demo_desktop_workorders_datagrid.webp";
+import demoMobileCreateInvoiceImageSrc from "@/images/demo_mobile_create_invoice.webp";
+import demoMobileListViewImageSrc from "@/images/demo_mobile_workorders_list.webp";
+import type { OverrideProperties } from "type-fest";
+
+export const ProductImage = ({
+ label,
+ src,
+ ImageCarouselProps = {},
+ ...boxProps
+}: ProductImageProps) => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const handleOpen = () => setIsModalOpen(true);
+ const handleClose = () => setIsModalOpen(false);
+
+ return (
+ <>
+
+
+
+ {isModalOpen && (
+
+ image.label === label)}
+ showImageLabels
+ {...ImageCarouselProps}
+ />
+
+ )}
+ >
+ );
+};
+
+export const PRODUCT_IMAGES = [
+ { label: "Fixit Dashboard demo", src: demoDesktopDashboardImageSrc },
+ { label: "Fixit Create-Invoice mobile demo", src: demoMobileCreateInvoiceImageSrc },
+ { label: "Fixit Data-grid demo", src: demoDesktopDataGridImageSrc },
+ { label: "Fixit List-view mobile demo", src: demoMobileListViewImageSrc },
+] as const satisfies Array;
+
+const StyledDialog = styled(Dialog)({
+ // Starting from div.MuiDialog-root
+ [`& > .${dialogClasses.container}`]: {
+ //
+ [`& > .${dialogClasses.paper}`]: {
+ width: "80vw",
+ maxWidth: "80vw",
+ height: "80vh",
+ maxHeight: "80vh",
+ overflow: "hidden",
+ borderRadius: "0.5rem",
+ },
+ },
+});
+
+export type ProductImageProps = OverrideProperties<
+ CarouselImageConfig,
+ {
+ label: (typeof PRODUCT_IMAGES)[number]["label"];
+ }
+> & {
+ ImageCarouselProps?: Omit;
+} & Omit;