From 00033193e0863c13b710745f3138169d3645f731 Mon Sep 17 00:00:00 2001 From: tylerapfledderer Date: Sun, 13 Oct 2024 20:09:16 -0400 Subject: [PATCH 01/17] feat: create ShadCN modal with temporary dialog --- .../ui/__stories__/Modal.stories.tsx | 33 ++++ src/components/ui/dialog-modal.tsx | 186 ++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 src/components/ui/__stories__/Modal.stories.tsx create mode 100644 src/components/ui/dialog-modal.tsx diff --git a/src/components/ui/__stories__/Modal.stories.tsx b/src/components/ui/__stories__/Modal.stories.tsx new file mode 100644 index 00000000000..679168e026b --- /dev/null +++ b/src/components/ui/__stories__/Modal.stories.tsx @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/react" +import { fn } from "@storybook/test" + +import ModalComponent from "../dialog-modal" + +const meta = { + title: "Molecules/Overlay Content/ShadCN Modal", + component: ModalComponent, + args: { + defaultOpen: true, + title: "Modal Title", + children: + "This is the base component to be used in the modal window. Please change the text to preview final content for ethereum.org", + actionButton: { + label: "Save", + onClick: fn(), + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Modal: Story = {} + +export const Full: Story = { + args: { + contentProps: { + size: "xl", + }, + }, +} diff --git a/src/components/ui/dialog-modal.tsx b/src/components/ui/dialog-modal.tsx new file mode 100644 index 00000000000..3747428dc19 --- /dev/null +++ b/src/components/ui/dialog-modal.tsx @@ -0,0 +1,186 @@ +import * as React from "react" +import { MdClose } from "react-icons/md" +import { tv, type VariantProps } from "tailwind-variants" +import * as DialogPrimitive from "@radix-ui/react-dialog" + +import { cn } from "@/lib/utils/cn" + +import { Button } from "./buttons/Button" +import { Center, Flex } from "./flex" + +import { useDisclosure } from "@/hooks/useDisclosure" + +const dialogVariant = tv({ + slots: { + content: + "data-[state=open]:animate-contentShow w-full fixed left-1/2 top-1/2 grid -translate-x-1/2 -translate-y-1/2 gap-4 rounded-md bg-white p-8 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none", + overlay: "data-[state=open]:animate-overlayShow fixed inset-0 bg-black/70", + header: "relative pe-12", + title: "text-2xl", + footer: "pt-8", + close: "text-md size-8", + }, + variants: { + size: { + md: { + content: "max-w-xl", + }, + xl: { + content: "max-w-[1004px]", + }, + }, + }, + defaultVariants: { + size: "md", + }, +}) + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +type DialogContentProps = React.ComponentPropsWithoutRef< + typeof DialogPrimitive.Content +> & + VariantProps + +const DialogContent = React.forwardRef< + React.ElementRef, + DialogContentProps +>(({ className, children, size, ...props }, ref) => ( + + + + {children} + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + children, + ...props +}: React.HTMLAttributes) => ( +
+ {children} +
+ + + Close + +
+
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export type ModalProps = Omit & { + children?: React.ReactNode + title?: React.ReactNode + actionButton?: { + label: string + onClick: () => void + } + contentProps?: DialogContentProps +} + +const Modal = ({ + children, + title, + actionButton, + contentProps, + defaultOpen, + ...restProps +}: ModalProps) => { + const { onClose, isOpen, setValue } = useDisclosure(defaultOpen) + + return ( + + + + {title} + + {children} + {actionButton && ( + + + + + + + )} + + + ) +} + +export default Modal + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} From 85ec9460c478b3eb87341d21d956fb49c0b45eee Mon Sep 17 00:00:00 2001 From: tylerapfledderer Date: Tue, 15 Oct 2024 23:26:51 -0400 Subject: [PATCH 02/17] chore(ui/Modal): rename "Full" story to "Xl" --- src/components/ui/__stories__/Modal.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/__stories__/Modal.stories.tsx b/src/components/ui/__stories__/Modal.stories.tsx index 679168e026b..ae11ca8f1c6 100644 --- a/src/components/ui/__stories__/Modal.stories.tsx +++ b/src/components/ui/__stories__/Modal.stories.tsx @@ -24,7 +24,7 @@ type Story = StoryObj export const Modal: Story = {} -export const Full: Story = { +export const Xl: Story = { args: { contentProps: { size: "xl", From 3c246eff58fdf242bd9ee1756bbe0afa56fc7ea1 Mon Sep 17 00:00:00 2001 From: tylerapfledderer Date: Wed, 16 Oct 2024 23:44:18 -0400 Subject: [PATCH 03/17] refactor(ui/dialog-modal): create provider for variant styles --- .../ui/__stories__/Modal.stories.tsx | 4 +- src/components/ui/dialog-modal.tsx | 133 ++++++++++++------ 2 files changed, 88 insertions(+), 49 deletions(-) diff --git a/src/components/ui/__stories__/Modal.stories.tsx b/src/components/ui/__stories__/Modal.stories.tsx index ae11ca8f1c6..6f044f8b5f5 100644 --- a/src/components/ui/__stories__/Modal.stories.tsx +++ b/src/components/ui/__stories__/Modal.stories.tsx @@ -26,8 +26,6 @@ export const Modal: Story = {} export const Xl: Story = { args: { - contentProps: { - size: "xl", - }, + size: "xl", }, } diff --git a/src/components/ui/dialog-modal.tsx b/src/components/ui/dialog-modal.tsx index 3747428dc19..c084e9a6018 100644 --- a/src/components/ui/dialog-modal.tsx +++ b/src/components/ui/dialog-modal.tsx @@ -29,13 +29,31 @@ const dialogVariant = tv({ content: "max-w-[1004px]", }, }, + isSimulator: { + true: {}, + }, }, defaultVariants: { size: "md", }, }) -const Dialog = DialogPrimitive.Root +type DialogVariants = VariantProps + +const DialogStylesContext = React.createContext(dialogVariant()) + +const useDialogStyles = () => React.useContext(DialogStylesContext) + +type DialogProps = DialogPrimitive.DialogProps & DialogVariants + +const Dialog = ({ size, isSimulator, ...props }: DialogProps) => { + const styles = dialogVariant({ size, isSimulator }) + return ( + + + + ) +} const DialogTrigger = DialogPrimitive.Trigger @@ -46,72 +64,84 @@ const DialogClose = DialogPrimitive.Close const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) +>(({ className, ...props }, ref) => { + const { overlay } = useDialogStyles() + return ( + + ) +}) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName type DialogContentProps = React.ComponentPropsWithoutRef< typeof DialogPrimitive.Content -> & - VariantProps +> const DialogContent = React.forwardRef< React.ElementRef, DialogContentProps ->(({ className, children, size, ...props }, ref) => ( - - - - {children} - - -)) +>(({ className, children, ...props }, ref) => { + const { content } = useDialogStyles() + return ( + + + + {children} + + + ) +}) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ className, children, ...props -}: React.HTMLAttributes) => ( -
- {children} -
- - - Close - -
-
-) +}: React.HTMLAttributes) => { + const { header, close } = useDialogStyles() + return ( +
+ {children} +
+ + + Close + +
+
+ ) +} DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ className, ...props -}: React.HTMLAttributes) => ( -
-) +}: React.HTMLAttributes) => { + const { footer } = useDialogStyles() + return
+} DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) +>(({ className, ...props }, ref) => { + const { title } = useDialogStyles() + return ( + + ) +}) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< @@ -126,9 +156,10 @@ const DialogDescription = React.forwardRef< )) DialogDescription.displayName = DialogPrimitive.Description.displayName -export type ModalProps = Omit & { +export type ModalProps = Omit & { children?: React.ReactNode title?: React.ReactNode + onClose?: () => void actionButton?: { label: string onClick: () => void @@ -142,9 +173,19 @@ const Modal = ({ actionButton, contentProps, defaultOpen, + onClose, ...restProps }: ModalProps) => { - const { onClose, isOpen, setValue } = useDisclosure(defaultOpen) + const { + onClose: onDisclosureClose, + isOpen, + setValue, + } = useDisclosure(defaultOpen) + + const handleClose = () => { + onClose?.() + onDisclosureClose() + } return ( @@ -156,7 +197,7 @@ const Modal = ({ {actionButton && ( - + + + diff --git a/src/pages/quizzes.tsx b/src/pages/quizzes.tsx index 408a7d335ce..5e690b30fb5 100644 --- a/src/pages/quizzes.tsx +++ b/src/pages/quizzes.tsx @@ -3,7 +3,7 @@ import { GetStaticProps, InferGetStaticPropsType, NextPage } from "next" import { useTranslation } from "next-i18next" import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { FaGithub } from "react-icons/fa" -import { Box, Flex, Icon, Stack, Text, useDisclosure } from "@chakra-ui/react" +import { Box, Flex, Icon, Stack, Text } from "@chakra-ui/react" import { BasePageProps, Lang, QuizKey, QuizStatus } from "@/lib/types" @@ -28,6 +28,7 @@ import { ethereumBasicsQuizzes, usingEthereumQuizzes } from "@/data/quizzes" import { INITIAL_QUIZ } from "@/lib/constants" +import { useDisclosure } from "@/hooks/useDisclosure" import HeroImage from "@/public/images/heroes/quizzes-hub-hero.png" const handleGHAdd = () => @@ -65,7 +66,7 @@ const QuizzesHubPage: NextPage< const [userStats, updateUserStats] = useLocalQuizData() const [quizStatus, setQuizStatus] = useState("neutral") const [currentQuiz, setCurrentQuiz] = useState(INITIAL_QUIZ) - const { onOpen, isOpen, onClose } = useDisclosure() + const { onOpen, isOpen, setValue } = useDisclosure() const commonQuizListProps = useMemo( () => ({ @@ -91,7 +92,7 @@ const QuizzesHubPage: NextPage< /> Date: Sun, 10 Nov 2024 23:46:19 -0500 Subject: [PATCH 05/17] refactor(Simulator): migrate to dialog-modal --- src/components/Simulator/SimulatorModal.tsx | 34 +++------------------ src/components/Simulator/index.tsx | 23 ++++++++------ src/components/ui/dialog-modal.tsx | 11 ++++--- 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/src/components/Simulator/SimulatorModal.tsx b/src/components/Simulator/SimulatorModal.tsx index 1d2d7dea07f..c38ef765efd 100644 --- a/src/components/Simulator/SimulatorModal.tsx +++ b/src/components/Simulator/SimulatorModal.tsx @@ -1,35 +1,9 @@ import React from "react" -import { - type ModalContentProps, - type ModalProps, - UseDisclosureReturn, -} from "@chakra-ui/react" -import Modal from "../Modal" +import Modal, { type ModalProps } from "../ui/dialog-modal" -type SimulatorModalProps = ModalContentProps & - Pick & { - isOpen: UseDisclosureReturn["isOpen"] - onClose: UseDisclosureReturn["onClose"] - children?: React.ReactNode - } +type SimulatorModalProps = Omit -export const SimulatorModal = ({ - isOpen, - onClose, - children, - ...restProps -}: SimulatorModalProps) => { - return ( - - {children} - - ) +export const SimulatorModal = (props: SimulatorModalProps) => { + return } diff --git a/src/components/Simulator/index.tsx b/src/components/Simulator/index.tsx index 64acaea2863..2d21463d214 100644 --- a/src/components/Simulator/index.tsx +++ b/src/components/Simulator/index.tsx @@ -4,6 +4,8 @@ import { Flex, type FlexProps, Grid } from "@chakra-ui/react" import { trackCustomEvent } from "@/lib/utils/matomo" +import type { ModalProps } from "../ui/dialog-modal" + import { PATH_ID_QUERY_PARAM, SIMULATOR_ID } from "./constants" import { Explanation } from "./Explanation" import type { @@ -41,14 +43,17 @@ export const Simulator = ({ children, data }: SimulatorProps) => { } // When simulator closed: log event, clear URL params and close modal - const onClose = (): void => { - trackCustomEvent({ - eventCategory: "simulator", - eventAction: `${pathId}_click`, - eventName: `close-from-step-${step + 1}`, - }) - // Clearing URL Params will reset pathId, and close modal - clearUrlParams() + const onClose: ModalProps["onOpenChange"] = (open) => { + console.log("🚀 ~ Simulator ~ open:", open) + if (!open) { + trackCustomEvent({ + eventCategory: "simulator", + eventAction: `${pathId}_click`, + eventName: `close-from-step-${step + 1}`, + }) + // Clearing URL Params will reset pathId, and close modal + clearUrlParams() + } } // Remove URL search param if invalid pathId @@ -181,7 +186,7 @@ export const Simulator = ({ children, data }: SimulatorProps) => { })} - +