feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)

This commit is contained in:
Kasper Fabricius Kristensen
2023-03-03 10:09:16 +01:00
committed by GitHub
parent d6b1ad1ccd
commit 40de54b010
928 changed files with 85441 additions and 384 deletions

View File

@@ -0,0 +1,114 @@
import clsx from "clsx"
import Button from "../../fundamentals/button"
import CrossIcon from "../../fundamentals/icons/cross-icon"
import { ReactFCWithChildren } from "../../../types/utils"
type FocusModalElementProps = {
className?: string
children?: React.ReactNode
}
type IFocusModal = ReactFCWithChildren<FocusModalElementProps> & {
Header: ReactFCWithChildren<FocusModalElementProps>
Main: ReactFCWithChildren<FocusModalElementProps>
BasicFocusModal: ReactFCWithChildren<BasicFocusModalProps>
}
type BasicFocusModalProps = {
handleClose: (e) => void
onSubmit: (e) => void
cancelText?: string
submitText?: string
children?: React.ReactNode
}
const FocusModal: IFocusModal = ({ className, children }) => (
<div
className={clsx(
"absolute inset-0 bg-grey-0 z-50 flex flex-col items-center",
className
)}
>
{children}
</div>
)
FocusModal.Header = ({ children, className }) => (
<div
className={clsx(
"w-full border-b py-4 border-b-grey-20 flex justify-center",
className
)}
>
{children}
</div>
)
FocusModal.Main = ({ children, className }) => (
<div className={clsx("w-full px-8 overflow-y-auto h-full", className)}>
{children}
</div>
)
FocusModal.BasicFocusModal = ({
handleClose,
onSubmit,
children,
cancelText = "Cancel",
submitText = "Save changes",
}) => {
return (
<FocusModal>
<BasicFocusModalHeader
handleClose={handleClose}
onSubmit={onSubmit}
cancelText={cancelText}
submitText={submitText}
/>
<FocusModal.Main>{children}</FocusModal.Main>
</FocusModal>
)
}
const BasicFocusModalHeader: React.FC<BasicFocusModalProps> = ({
handleClose,
onSubmit,
cancelText,
submitText,
}) => {
return (
<FocusModal.Header>
<div className="medium:w-8/12 w-full px-8 flex justify-between">
<Button
size="small"
variant="ghost"
onClick={handleClose}
className="border rounded-rounded w-8 h-8"
>
<CrossIcon size={20} />
</Button>
<div className="gap-x-small flex">
<Button
onClick={handleClose}
size="small"
variant="ghost"
className="border rounded-rounded"
>
{cancelText || "Cancel"}
</Button>
<Button
size="small"
variant="primary"
onClick={onSubmit}
className="rounded-rounded"
>
{submitText || "Save changes"}
</Button>
</div>
</div>
</FocusModal.Header>
)
}
export default FocusModal

View File

@@ -0,0 +1,161 @@
import * as Dialog from "@radix-ui/react-dialog"
import * as Portal from "@radix-ui/react-portal"
import clsx from "clsx"
import React from "react"
import { useWindowDimensions } from "../../../hooks/use-window-dimensions"
import CrossIcon from "../../fundamentals/icons/cross-icon"
type ModalState = {
portalRef: any
isLargeModal?: boolean
}
export const ModalContext = React.createContext<ModalState>({
portalRef: undefined,
isLargeModal: true,
})
export type ModalProps = {
isLargeModal?: boolean
handleClose: () => void
open?: boolean
children?: React.ReactNode
}
type ModalChildProps = {
className?: string
style?: React.CSSProperties
children?: React.ReactNode
}
type ModalHeaderProps = {
handleClose: () => void
children?: React.ReactNode
}
type ModalType = React.FC<ModalProps> & {
Body: React.FC<ModalChildProps>
Header: React.FC<ModalHeaderProps>
Footer: React.FC<ModalChildProps>
Content: React.FC<ModalChildProps>
}
const Overlay: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<Dialog.Overlay className="fixed bg-grey-90/40 z-50 grid top-0 left-0 right-0 bottom-0 place-items-center overflow-y-auto">
{children}
</Dialog.Overlay>
)
}
const Content: React.FC<React.PropsWithChildren> = ({ children }) => {
const { height } = useWindowDimensions()
const style = {
maxHeight: height - 64,
}
return (
<Dialog.Content
style={style}
className="bg-grey-0 min-w-modal rounded-rounded overflow-x-hidden"
>
{children}
</Dialog.Content>
)
}
const Modal: ModalType = ({
open = true,
handleClose,
isLargeModal = true,
children,
}) => {
const portalRef = React.useRef(null)
return (
<Dialog.Root open={open} onOpenChange={handleClose}>
<Portal.Portal ref={portalRef}>
<ModalContext.Provider value={{ portalRef, isLargeModal }}>
<Overlay>
<Content>{children}</Content>
</Overlay>
</ModalContext.Provider>
</Portal.Portal>
</Dialog.Root>
)
}
Modal.Body = ({ children, className, style }) => {
const { isLargeModal } = React.useContext(ModalContext)
return (
<div
style={style}
className={clsx("inter-base-regular h-full", className)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
)
}
Modal.Content = ({ children, className }) => {
const { isLargeModal } = React.useContext(ModalContext)
const { height } = useWindowDimensions()
const style = {
maxHeight: height - 90 - 141,
}
return (
<div
style={style}
className={clsx(
"px-7 pt-5 overflow-y-auto",
{
["w-largeModal pb-7"]: isLargeModal,
["pb-5"]: !isLargeModal,
},
className
)}
>
{children}
</div>
)
}
Modal.Header = ({ handleClose = undefined, children }) => {
return (
<div
className="pl-7 pt-3.5 pr-3.5 flex flex-col w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="pb-1 flex w-full justify-end">
{handleClose && (
<button onClick={handleClose} className="text-grey-50 cursor-pointer">
<CrossIcon size={20} />
</button>
)}
</div>
{children}
</div>
)
}
Modal.Footer = ({ children, className }) => {
const { isLargeModal } = React.useContext(ModalContext)
return (
<div
onClick={(e) => e.stopPropagation()}
className={clsx(
"px-7 bottom-0 pb-5 flex w-full",
{
"border-t border-grey-20 pt-4": isLargeModal,
},
className
)}
>
{children}
</div>
)
}
export default Modal

View File

@@ -0,0 +1,163 @@
import clsx from "clsx"
import React, { ReactNode, useContext, useReducer } from "react"
import Button from "../../fundamentals/button"
import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon"
import Modal, { ModalProps } from "../../molecules/modal"
enum LayeredModalActions {
PUSH,
POP,
RESET,
}
export type LayeredModalScreen = {
title: string
subtitle?: string
onBack: () => void
onConfirm?: () => void
view: ReactNode
}
export type ILayeredModalContext = {
screens: LayeredModalScreen[]
push: (screen: LayeredModalScreen) => void
pop: () => void
reset: () => void
}
const defaultContext: ILayeredModalContext = {
screens: [],
push: (screen) => {},
pop: () => {},
reset: () => {},
}
export const LayeredModalContext = React.createContext(defaultContext)
const reducer = (state, action) => {
switch (action.type) {
case LayeredModalActions.PUSH: {
return { ...state, screens: [...state.screens, action.payload] }
}
case LayeredModalActions.POP: {
return { ...state, screens: state.screens.slice(0, -1) }
}
case LayeredModalActions.RESET: {
return { ...state, screens: [] }
}
}
}
type LayeredModalProps = {
context: ILayeredModalContext
} & ModalProps
export const LayeredModalProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultContext)
return (
<LayeredModalContext.Provider
value={{
...state,
push: (screen: LayeredModalScreen) => {
dispatch({ type: LayeredModalActions.PUSH, payload: screen })
},
pop: () => {
dispatch({ type: LayeredModalActions.POP })
},
reset: () => {
dispatch({ type: LayeredModalActions.RESET })
},
}}
>
{children}
</LayeredModalContext.Provider>
)
}
const LayeredModal: React.FC<LayeredModalProps> = ({
context,
children,
handleClose,
open,
isLargeModal = true,
}) => {
const emptyScreensAndClose = () => {
context.reset()
handleClose()
}
const screen = context.screens[context.screens.length - 1]
return (
<Modal
open={open}
isLargeModal={isLargeModal}
handleClose={emptyScreensAndClose}
>
<Modal.Body
className={clsx(
"flex flex-col justify-between transition-transform duration-200",
{
"translate-x-0": typeof screen !== "undefined",
"translate-x-full": typeof screen === "undefined",
}
)}
>
{screen ? (
<>
<Modal.Header handleClose={emptyScreensAndClose}>
<div className="flex items-center">
<Button
variant="ghost"
size="small"
className="h-8 w-8 text-grey-50"
onClick={screen.onBack}
>
<ArrowLeftIcon size={20} />
</Button>
<div className="flex items-center gap-x-2xsmall">
<h2 className="inter-xlarge-semibold ml-5">{screen.title}</h2>
{screen.subtitle && (
<span className="inter-xlarge-regular text-grey-50">
({screen.subtitle})
</span>
)}
</div>
</div>
</Modal.Header>
{screen.view}
</>
) : (
<></>
)}
</Modal.Body>
<div
className={clsx("transition-transform duration-200", {
"-translate-x-full": typeof screen !== "undefined",
})}
>
<div
className={clsx("transition-display", {
"hidden opacity-0 delay-500": typeof screen !== "undefined",
})}
>
{children}
</div>
</div>
</Modal>
)
}
export const useLayeredModal = () => {
const context = useContext(LayeredModalContext)
if (context === null) {
throw new Error(
"useLayeredModal must be used within a LayeredModalProvider"
)
}
return context
}
export default LayeredModal

View File

@@ -0,0 +1,60 @@
import React, { PropsWithChildren } from "react"
import { AnimatePresence, motion } from "framer-motion"
const MODAL_WIDTH = 560
type SideModalProps = PropsWithChildren<{
close: () => void
isVisible: boolean
}>
/**
* Side modal displayed as right drawer on open.
*/
function SideModal(props: SideModalProps) {
const { isVisible, children, close } = props
return (
<AnimatePresence>
{isVisible && (
<>
<motion.div
onClick={close}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeInOut" }}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 99,
background: "rgba(0,0,0,.3)",
}}
></motion.div>
<motion.div
transition={{ ease: "easeInOut" }}
initial={{ right: -MODAL_WIDTH }}
style={{
position: "fixed",
height: "100%",
width: MODAL_WIDTH,
background: "white",
right: 0,
top: 0,
zIndex: 9999,
}}
className="rounded border overflow-hidden"
animate={{ right: 0 }}
exit={{ right: -MODAL_WIDTH }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
export default SideModal

View File

@@ -0,0 +1,227 @@
import clsx from "clsx"
import React, { ReactNode, useReducer } from "react"
import Button from "../../fundamentals/button"
import Modal, { ModalProps } from "../../molecules/modal"
import LayeredModal, { ILayeredModalContext } from "./layered-modal"
enum SteppedActions {
ENABLENEXTPAGE,
DISABLENEXTPAGE,
GOTONEXTPAGE,
GOTOPREVIOUSPAGE,
SETPAGE,
SUBMIT,
RESET,
}
type ISteppedContext = {
currentStep: number
nextStepEnabled: boolean
enableNextPage: () => void
disableNextPage: () => void
goToNextPage: () => void
goToPreviousPage: () => void
submit: () => void
reset: () => void
setPage: (page: number) => void
}
const defaultContext: ISteppedContext = {
currentStep: 0,
nextStepEnabled: true,
enableNextPage: () => {},
disableNextPage: () => {},
goToNextPage: () => {},
goToPreviousPage: () => {},
submit: () => {},
reset: () => {},
setPage: (page) => {},
}
export const SteppedContext = React.createContext(defaultContext)
const reducer = (state, action) => {
switch (action.type) {
case SteppedActions.ENABLENEXTPAGE: {
return { ...state, nextStepEnabled: true }
}
case SteppedActions.DISABLENEXTPAGE: {
return { ...state, nextStepEnabled: false }
}
case SteppedActions.GOTONEXTPAGE: {
return { ...state, currentStep: state.currentStep + 1 }
}
case SteppedActions.GOTOPREVIOUSPAGE: {
return { ...state, currentStep: Math.max(0, state.currentStep - 1) }
}
case SteppedActions.SETPAGE: {
return {
...state,
currentStep: action.payload > 0 ? action.payload : state.currentStep,
}
}
case SteppedActions.SUBMIT: {
return { ...state }
}
case SteppedActions.RESET: {
return { ...state, currentStep: 0, nextStepEnabled: true }
}
}
}
type SteppedProps = {
context: ISteppedContext
title: string
onSubmit: () => void
lastScreenIsSummary?: boolean
steps: ReactNode[]
layeredContext?: ILayeredModalContext
} & ModalProps
export const SteppedProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultContext)
return (
<SteppedContext.Provider
value={{
...state,
enableNextPage: () => {
dispatch({ type: SteppedActions.ENABLENEXTPAGE })
},
disableNextPage: () => {
dispatch({ type: SteppedActions.DISABLENEXTPAGE })
},
goToNextPage: () => {
dispatch({ type: SteppedActions.GOTONEXTPAGE })
},
goToPreviousPage: () => {
dispatch({ type: SteppedActions.GOTOPREVIOUSPAGE })
},
submit: () => {
dispatch({ type: SteppedActions.SUBMIT })
},
setPage: (page: number) => {
dispatch({ type: SteppedActions.SETPAGE, payload: page })
},
reset: () => {
dispatch({ type: SteppedActions.RESET })
},
}}
>
{children}
</SteppedContext.Provider>
)
}
const SteppedModal: React.FC<SteppedProps> = ({
context,
steps,
layeredContext,
title,
onSubmit,
lastScreenIsSummary = false,
handleClose,
isLargeModal = true,
}) => {
const resetAndClose = () => {
context.reset()
handleClose()
}
const resetAndSubmit = () => {
onSubmit()
}
return (
<ModalElement
layeredContext={layeredContext}
isLargeModal={isLargeModal}
handleClose={resetAndClose}
>
<Modal.Body
className={clsx(
"transition-transform flex flex-col justify-between duration-100 max-h-full"
)}
>
<Modal.Header handleClose={resetAndClose}>
<div className="flex flex-col">
<h2 className="inter-xlarge-semibold">{title}</h2>
{!lastScreenIsSummary ||
(lastScreenIsSummary &&
context.currentStep !== steps.length - 1 && (
<div className="flex items-center">
<span className="text-grey-50 inter-small-regular w-[70px] mr-4">{`Step ${
context.currentStep + 1
} of ${steps.length}`}</span>
{steps.map((_, i) => (
<span
key={i}
className={clsx(
"w-2 h-2 rounded-full mr-3",
{
"bg-grey-20": i > context.currentStep,
"bg-violet-60": context.currentStep >= i,
},
{
"outline-4 outline outline-violet-20":
context.currentStep === i,
}
)}
/>
))}
</div>
))}
</div>
</Modal.Header>
<Modal.Content>{steps[context.currentStep]}</Modal.Content>
</Modal.Body>
<Modal.Footer>
<div className="flex justify-end w-full gap-x-xsmall">
<Button
variant="ghost"
size="small"
disabled={context.currentStep === 0}
onClick={() => context.goToPreviousPage()}
className="w-[112px]"
>
Back
</Button>
<Button
variant="primary"
size="small"
disabled={!context.nextStepEnabled}
onClick={() =>
context.currentStep === steps.length - 1
? resetAndSubmit()
: context.goToNextPage()
}
className="w-[112px]"
>
{context.currentStep === steps.length - 1 ? "Submit" : "Next"}
</Button>
</div>
</Modal.Footer>
</ModalElement>
)
}
const ModalElement = ({
layeredContext,
handleClose,
isLargeModal = true,
children,
}) =>
layeredContext ? (
<LayeredModal
context={layeredContext}
handleClose={handleClose}
isLargeModal={isLargeModal}
>
{children}
</LayeredModal>
) : (
<Modal handleClose={handleClose} isLargeModal={isLargeModal}>
{children}
</Modal>
)
export default SteppedModal