Skip to content

Commit

Permalink
feat: add responsive dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
trinhdinhtai committed Jun 8, 2024
1 parent f774f5c commit 98960f6
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 10 deletions.
25 changes: 15 additions & 10 deletions components/auth/sign-in-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
"use client"

import { SiGithub, SiGoogle } from "react-icons/si"

import { useSignInModal } from "@/hooks/use-sign-in-modal"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogContent,
ResponsiveDialogHeader,
} from "@/components/ui/responsive-dialog"
import SocialSignInButton from "@/components/auth/social-sign-in-button"

export default function SignInModal() {
const { open, setOpen } = useSignInModal()

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<ResponsiveDialog open={open} onOpenChange={setOpen}>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<DialogTitle className="text-left text-2xl">Sign in</DialogTitle>
<DialogDescription className="text-left">
to continue to taitd.io.vn
</DialogDescription>
</DialogHeader>
</ResponsiveDialogHeader>

<SocialSignInButton />
</DialogContent>
</Dialog>
<ResponsiveDialogBody>
<SocialSignInButton />
</ResponsiveDialogBody>
</ResponsiveDialogContent>
</ResponsiveDialog>
)
}
118 changes: 118 additions & 0 deletions components/ui/drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client"

import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"

import { cn } from "@/lib/utils"

const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"

const DrawerTrigger = DrawerPrimitive.Trigger

const DrawerPortal = DrawerPrimitive.Portal

const DrawerClose = DrawerPrimitive.Close

const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName

const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"

const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"

const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"

const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName

const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName

export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
136 changes: 136 additions & 0 deletions components/ui/responsive-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as React from "react"

import { cn } from "@/lib/utils"
import { useMediaQuery } from "@/hooks/use-media-query"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"

type ResponsiveDialogContextType = {
isDesktop: boolean
}

const ResponsiveDialogContext = React.createContext<
ResponsiveDialogContextType | undefined
>(undefined)

const useResponsiveDialog = () => {
const context = React.useContext(ResponsiveDialogContext)

if (!context) {
throw new Error(
"useResponsiveDialog must be used within a ResponsiveDialog"
)
}

return context
}

type ResponsiveDialogProps = {
children?: React.ReactNode
onOpenChange?: (isOpen: boolean) => void
open?: boolean
}

const ResponsiveDialog = (props: ResponsiveDialogProps) => {
const isDesktop = useMediaQuery("(min-width: 768px)")
const Comp = isDesktop ? Dialog : Drawer

return (
<ResponsiveDialogContext.Provider value={{ isDesktop }}>
<Comp {...props} />
</ResponsiveDialogContext.Provider>
)
}

type BaseProps = {
children?: React.ReactNode
className?: React.ComponentProps<"div">["className"]
asChild?: boolean
}

const ResponsiveDialogTrigger = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogTrigger : DrawerTrigger

return <Comp {...props} />
}

const ResponsiveDialogContent = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogContent : DrawerContent

return <Comp {...props} />
}

const ResponsiveDialogBody = ({ className, ...props }: BaseProps) => {
const { isDesktop } = useResponsiveDialog()

return (
<div className={cn({ "px-4 py-2": !isDesktop }, className)} {...props} />
)
}

const ResponsiveDialogHeader = ({ className, ...props }: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogHeader : DrawerHeader

return (
<Comp className={cn({ "text-left": !isDesktop }, className)} {...props} />
)
}

const ResponsiveDialogTitle = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogTitle : DrawerTitle

return <Comp {...props} />
}

const ResponsiveDialogDescription = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogDescription : DrawerDescription

return <Comp {...props} />
}

const ResponsiveDialogFooter = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()
const Comp = isDesktop ? DialogFooter : DrawerFooter

return <Comp {...props} />
}

const ResponsiveDialogClose = (props: BaseProps) => {
const { isDesktop } = useResponsiveDialog()

return !isDesktop && <DrawerClose {...props} />
}

export {
ResponsiveDialog,
ResponsiveDialogTrigger,
ResponsiveDialogContent,
ResponsiveDialogBody,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogDescription,
ResponsiveDialogFooter,
ResponsiveDialogClose,
}
19 changes: 19 additions & 0 deletions hooks/use-media-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from "react"

export function useMediaQuery(query: string) {
const [value, setValue] = useState(false)

useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}

const result = matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)

return () => result.removeEventListener("change", onChange)
}, [query])

return value
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"tailwindcss-animate": "^1.0.7",
"typescript": "5.4.5",
"unist-util-visit": "^5.0.0",
"vaul": "^0.9.1",
"zod": "^3.21.4",
"zustand": "^4.5.2"
},
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 98960f6

Please sign in to comment.