Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fe): confirm modal when leaving settings page #2261

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import BaseModal from '@/components/BaseModal'
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogFooter
} from '@/components/shadcn/alert-dialog'

interface ModalProps {
open: boolean
handleOpen: () => void
handleClose: () => void
confirmAction: () => void
title?: string
description?: string
}

/**
*
* ConfirmModal component renders a modal dialog with confirm and cancel actions.
*
* @param open - Determines if the modal is open.
* @param handleClose - Function to close the modal.
* @param confirmAction - Function to execute when the user confirms.
* @param title - Title of the modal.
* @param description - Description of the modal.
*
* @remarks
* * AlertDialogFooter section (Button section) is separated using ConfirmModal component for reusability.
*/
export default function ConfirmModal({
open,
handleClose,
confirmAction,
title = '',
description = ''
}: ModalProps) {
return (
<BaseModal
handleClose={handleClose}
title={title}
open={open}
description={description}
modalBgDarkMode={false}
>
<AlertDialogFooter>
<AlertDialogAction
className="border-none bg-slate-100 text-[#3333334D] hover:bg-slate-200"
onClick={confirmAction}
>
Leave
</AlertDialogAction>
<AlertDialogCancel
className="bg-primary hover:bg-primary-strong border-none text-white"
onClick={handleClose}
>
Stay
</AlertDialogCancel>
</AlertDialogFooter>
</BaseModal>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useRouter } from 'next/navigation'
import type { MutableRefObject } from 'react'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'

// const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
Expand All @@ -22,6 +22,9 @@
updateNow: boolean
) => {
const router = useRouter()
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
const [confirmAction, setConfirmAction] = useState<() => void>(() => () => {})

useEffect(() => {
const originalPush = router.push
const newPush = (
Expand All @@ -37,12 +40,11 @@
return
}
if (!bypassConfirmation.current) {
const isConfirmed = window.confirm(
'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?'
)
if (isConfirmed) {
setIsConfirmModalOpen(true)
setConfirmAction(() => () => {
setIsConfirmModalOpen(false)
originalPush(href as Route, options)
}
})
return
}
originalPush(href as Route, options)
Expand All @@ -51,5 +53,7 @@
return () => {
router.push = originalPush
}
}, [router, bypassConfirmation.current])

Check warning on line 56 in apps/frontend/app/(client)/(main)/settings/_components/ConfirmNavigation.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has missing dependencies: 'bypassConfirmation' and 'updateNow'. Either include them or remove the dependency array. Mutable values like 'bypassConfirmation.current' aren't valid dependencies because mutating them doesn't re-render the component

return { isConfirmModalOpen, setIsConfirmModalOpen, confirmAction }
}
14 changes: 12 additions & 2 deletions apps/frontend/app/(client)/(main)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import ConfirmModal from './_components/ConfirmModal'
import { useConfirmNavigation } from './_components/ConfirmNavigation'
import CurrentPwSection from './_components/CurrentPwSection'
import IdSection from './_components/IdSection'
Expand Down Expand Up @@ -91,8 +92,6 @@
fetchDefaultProfile()
}, [])

useConfirmNavigation(bypassConfirmation, !!updateNow)

const {
register,
handleSubmit,
Expand All @@ -112,6 +111,8 @@
}
})

const { isConfirmModalOpen, setIsConfirmModalOpen, confirmAction } =
useConfirmNavigation(bypassConfirmation, !!updateNow)
const [isCheckButtonClicked, setIsCheckButtonClicked] =
useState<boolean>(false)
const [isPasswordCorrect, setIsPasswordCorrect] = useState<boolean>(false)
Expand Down Expand Up @@ -152,7 +153,7 @@
setValue('newPassword', newPassword)
setValue('confirmPassword', confirmPassword)
}
}, [isPasswordsMatch, newPassword, confirmPassword])

Check warning on line 156 in apps/frontend/app/(client)/(main)/settings/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook useEffect has a missing dependency: 'setValue'. Either include it or remove the dependency array

const onSubmit = async (data: SettingsFormat) => {
try {
Expand Down Expand Up @@ -324,6 +325,15 @@
onSubmitClick={onSubmitClick}
/>
</form>

<ConfirmModal
title="Are you sure you want to leave?"
description={`Your changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?`}
open={isConfirmModalOpen}
handleOpen={() => setIsConfirmModalOpen(true)}
handleClose={() => setIsConfirmModalOpen(false)}
confirmAction={confirmAction}
/>
</div>
)
}
68 changes: 68 additions & 0 deletions apps/frontend/components/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Loader2 } from 'lucide-react'
import React, { Fragment, type ReactNode } from 'react'
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogTitle
} from './shadcn/alert-dialog'

interface BaseModalProps {
open: boolean
handleClose: () => void
children?: ReactNode
loading?: boolean
loadingMessage?: string
title?: string
description?: string
modalBgDarkMode?: boolean
}

/**
*
* @remarks
* * Use BaseModal Component by creating a new component(which includes 'AlertDialogFooter') that extends BaseModal
* * AlertDialogFooter section (Button section) is separated using ConfirmModal component for reusability.
*/
export default function BaseModal({
open,
handleClose,
children,
loading = false,
loadingMessage = '',
title = '',
description = '',
modalBgDarkMode = false
}: BaseModalProps) {
const formattedDescription =
description.split('\n').map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
)) ?? ''

return (
<AlertDialog open={open} onOpenChange={handleClose}>
<AlertDialogOverlay darkMode={modalBgDarkMode} />
<AlertDialogContent className="max-w-[428px]">
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription className="min-w-72">
{loading ? (
<div className="flex flex-col items-center justify-center">
<Loader2 size={32} className="animate-spin" />
<span className="mt-2 text-sm">{loadingMessage}</span>
</div>
) : (
formattedDescription
)}
</AlertDialogDescription>
</AlertDialogHeader>
{children}
</AlertDialogContent>
</AlertDialog>
)
}
11 changes: 8 additions & 3 deletions apps/frontend/components/shadcn/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ const AlertDialogPortal = AlertDialogPrimitive.Portal

const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> & {
darkMode?: boolean
}
>(({ className, darkMode = false, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50',
darkMode
? 'bg-black/80 backdrop-blur-sm'
: 'bg-gray-300/20 backdrop-blur-sm',
className
)}
{...props}
Expand Down
Loading