feat: add confirmation modal
This commit is contained in:
parent
381c3d6df4
commit
b146e4c3a8
128
app/components/Modal.tsx
Normal file
128
app/components/Modal.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import { Fragment, type SetStateAction, useState } from 'react'
|
||||
|
||||
import Button from './Button'
|
||||
|
||||
type HookParameters = {
|
||||
title: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
variant?: 'danger' | 'confirm';
|
||||
|
||||
// Optional because the button submits
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
type Properties = {
|
||||
readonly isOpen: boolean;
|
||||
readonly setIsOpen: (value: SetStateAction<boolean>) => void;
|
||||
readonly parameters: HookParameters;
|
||||
}
|
||||
|
||||
export default function useModal(properties: HookParameters) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
return {
|
||||
Modal: (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
parameters={properties}
|
||||
/>
|
||||
),
|
||||
|
||||
open: () => {
|
||||
setIsOpen(true)
|
||||
},
|
||||
|
||||
close: () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Modal({ parameters, isOpen, setIsOpen }: Properties) {
|
||||
return (
|
||||
<Transition
|
||||
show={isOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as='div'
|
||||
className='relative z-50'
|
||||
onClose={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<Transition.Child
|
||||
enter='ease-out duration-100'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-75'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
as={Fragment}
|
||||
>
|
||||
<div className='fixed inset-0 bg-black/30' aria-hidden='true'/>
|
||||
</Transition.Child>
|
||||
<div className='fixed inset-0 flex w-screen items-center justify-center'>
|
||||
<Transition.Child
|
||||
enter='transition ease-out duration-100'
|
||||
enterFrom='transform opacity-0 scale-95'
|
||||
enterTo='transform opacity-100 scale-100'
|
||||
leave='transition ease-in duration-75'
|
||||
leaveFrom='transform opacity-100 scale-100'
|
||||
leaveTo='transform opacity-0 scale-95'
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog.Panel className={clsx(
|
||||
'rounded-lg p-4 w-full max-w-md',
|
||||
'bg-white dark:bg-black relative',
|
||||
'border border-gray-200 dark:border-zinc-800'
|
||||
)}
|
||||
>
|
||||
<XMarkIcon
|
||||
className={clsx(
|
||||
'absolute top-3 right-3 rounded-lg p-1.5',
|
||||
'w-8 h-8 text-gray-500 dark:text-gray-400',
|
||||
'hover:bg-gray-100 dark:hover:bg-zinc-800'
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
<Dialog.Title className='text-xl font-bold'>
|
||||
{parameters.title}
|
||||
</Dialog.Title>
|
||||
{parameters.description ? (
|
||||
<Dialog.Description className='text-gray-500 dark:text-gray-400 mt-1'>
|
||||
{parameters.description}
|
||||
</Dialog.Description>
|
||||
) : undefined}
|
||||
<Button
|
||||
variant='emphasized'
|
||||
type='submit'
|
||||
className={clsx(
|
||||
'w-full mt-12',
|
||||
parameters.variant === 'danger'
|
||||
? 'bg-red-800 dark:bg-red-500 focus:ring-red-500 dark:focus:ring-red-500'
|
||||
: ''
|
||||
)}
|
||||
onClick={async () => {
|
||||
if (parameters.onConfirm) {
|
||||
await parameters.onConfirm()
|
||||
}
|
||||
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{parameters.buttonText ?? 'Confirm'}
|
||||
</Button>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useFetcher } from '@remix-run/react'
|
||||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Button from '~/components/Button'
|
||||
import useModal from '~/components/Modal'
|
||||
import Spinner from '~/components/Spinner'
|
||||
|
||||
type Properties = {
|
||||
@ -13,8 +11,22 @@ type Properties = {
|
||||
}
|
||||
|
||||
export default function Modal({ isEnabled, disabled }: Properties) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const fetcher = useFetcher()
|
||||
const { Modal, open } = useModal({
|
||||
title: `${isEnabled ? 'Disable' : 'Enable'} Magic DNS`,
|
||||
variant: isEnabled ? 'danger' : 'confirm',
|
||||
buttonText: `${isEnabled ? 'Disable' : 'Enable'} Magic DNS`,
|
||||
description: 'Devices will no longer be accessible via your tailnet domain. The search domain will also be disabled.',
|
||||
onConfirm: () => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dns_config.magic_dns': !isEnabled
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -23,7 +35,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
|
||||
className='w-fit text-sm'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
open()
|
||||
}}
|
||||
>
|
||||
{fetcher.state === 'idle' ? undefined : (
|
||||
@ -31,46 +43,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
|
||||
)}
|
||||
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
||||
</Button>
|
||||
<Dialog
|
||||
className='relative z-50'
|
||||
open={isOpen} onClose={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='fixed inset-0 bg-black/30' aria-hidden='true'/>
|
||||
<div className='fixed inset-0 flex w-screen items-center justify-center'>
|
||||
<Dialog.Panel className='bg-white rounded-lg p-4 w-full max-w-md'>
|
||||
<Dialog.Title className='text-lg font-bold'>
|
||||
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className='text-gray-500 dark:text-gray-400'>
|
||||
Devices will no longer be accessible via your tailnet domain.
|
||||
The search domain will also be disabled.
|
||||
</Dialog.Description>
|
||||
<Button
|
||||
variant='emphasized'
|
||||
type='submit'
|
||||
className={clsx(
|
||||
'w-full mt-12',
|
||||
isEnabled ? 'bg-red-800 dark:bg-red-500' : ''
|
||||
)}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dns_config.magic_dns': !isEnabled
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{isEnabled ? 'Disable' : 'Enable'}
|
||||
</Button>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
{Modal}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user