Skip to content

Commit

Permalink
feat(fe): confirm modal when leaving settings page (#2261)
Browse files Browse the repository at this point in the history
* feat(fe): confirm modal when leaving settings page

* feat(fe): add dark and bright mode

* chore(fe): add comments for basemodal

* fix(fe): decide darkmode at ConfirmModal

* chore(fe): add explanation for BaseModal component

* chore(fe): add explanation for Basemodal component_2

* chore(fe): change comment for BaseModal

* fix(fe): change description props type to string from ReactNode

* chore(fe): import types

* chore(fe): change comment for basemodal

* chore(fe): change comment for confirmmodal

* fix(fe): move confirmNavigation to utils file

* chore(fe): change darkmode props name for BaseModal

* chore(fe): change frament tag to p tag

* chore(fe): remove duplicate backdrop blur by setting it as a default class

* fix(fe): change dependency for ConfirmNavigation

* fix(fe): change dependency for ConfirmNavigation
  • Loading branch information
jihorobert authored Jan 2, 2025
1 parent a82e9f0 commit b945f8e
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 13 deletions.
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}
darkMode={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 { Route } from 'next'
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 @@ export const useConfirmNavigation = (
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 @@ export const useConfirmNavigation = (
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 @@ export const useConfirmNavigation = (
return () => {
router.push = originalPush
}
}, [router, bypassConfirmation.current])
}, [router, bypassConfirmation])

return { isConfirmModalOpen, setIsConfirmModalOpen, confirmAction }
}
16 changes: 13 additions & 3 deletions apps/frontend/app/(client)/(main)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
import { useConfirmNavigation } from './_components/ConfirmNavigation'
import ConfirmModal from './_components/ConfirmModal'
import CurrentPwSection from './_components/CurrentPwSection'
import IdSection from './_components/IdSection'
import LogoSection from './_components/LogoSection'
Expand All @@ -20,6 +20,7 @@ import ReEnterNewPwSection from './_components/ReEnterNewPwSection'
import SaveButton from './_components/SaveButton'
import StudentIdSection from './_components/StudentIdSection'
import TopicSection from './_components/TopicSection'
import { useConfirmNavigation } from './_libs/utils'

interface getProfile {
username: string // ID
Expand Down Expand Up @@ -91,8 +92,6 @@ export default function Page() {
fetchDefaultProfile()
}, [])

useConfirmNavigation(bypassConfirmation, !!updateNow)

const {
register,
handleSubmit,
Expand All @@ -112,6 +111,8 @@ export default function Page() {
}
})

const { isConfirmModalOpen, setIsConfirmModalOpen, confirmAction } =
useConfirmNavigation(bypassConfirmation, !!updateNow)
const [isCheckButtonClicked, setIsCheckButtonClicked] =
useState<boolean>(false)
const [isPasswordCorrect, setIsPasswordCorrect] = useState<boolean>(false)
Expand Down Expand Up @@ -324,6 +325,15 @@ export default function Page() {
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>
)
}
64 changes: 64 additions & 0 deletions apps/frontend/components/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Loader2 } from 'lucide-react'
import React, { 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
darkMode?: 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 = '',
darkMode = false
}: BaseModalProps) {
const formattedDescription =
description.split('\n').map((line, index) => <p key={index}>{line}</p>) ??
''

return (
<AlertDialog open={open} onOpenChange={handleClose}>
<AlertDialogOverlay darkMode={darkMode} />
<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>
)
}
9 changes: 6 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,14 @@ 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 backdrop-blur-sm',
darkMode ? 'bg-black/80' : 'bg-gray-300/20',
className
)}
{...props}
Expand Down

0 comments on commit b945f8e

Please sign in to comment.