Wrap dialogs with ControlledDialog for consistent modal state management

This commit is contained in:
Mike Cao committed 2026-07-30 22:57:49 -07:00
1 parent 6ac2b9377c
commit 0941ea06f9
16 files changed
+396 -329

No files matched your search

@@ -1,6 +1,7 @@
import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from '@umami/react-zen';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { DateDistance } from '@/components/common/DateDistance';
import Link from '@/components/common/Link';
import { SortableLabel } from '@/components/common/SortableLabel';
@@ -93,11 +94,13 @@ export function AdminTeamsTable({
</DataColumn>
)}
</DataTable>
<Modal isOpen={!!deleteTeam}>
<Dialog style={{ width: 400 }}>
<TeamDeleteForm teamId={deleteTeam} onClose={() => setDeleteTeam(null)} />
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={!!deleteTeam}>
<Dialog style={{ width: 400 }}>
<TeamDeleteForm teamId={deleteTeam} onClose={() => setDeleteTeam(null)} />
</Dialog>
</Modal>
</ControlledDialog>
</>
);
}
+12 -9
View File
@@ -1,6 +1,7 @@
import { DataColumn, DataTable, Icon, MenuItem, Modal, Row, Text } from '@umami/react-zen';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { DateDistance } from '@/components/common/DateDistance';
import Link from '@/components/common/Link';
import { SortableLabel } from '@/components/common/SortableLabel';
@@ -84,15 +85,17 @@ export function UsersTable({
</DataColumn>
)}
</DataTable>
<Modal isOpen={!!deleteUser}>
<UserDeleteForm
userId={deleteUser?.id}
username={deleteUser?.username}
onClose={() => {
setDeleteUser(null);
}}
/>
</Modal>
<ControlledDialog>
<Modal isOpen={!!deleteUser}>
<UserDeleteForm
userId={deleteUser?.id}
username={deleteUser?.username}
onClose={() => {
setDeleteUser(null);
}}
/>
</Modal>
</ControlledDialog>
</>
);
}
@@ -13,6 +13,7 @@ import {
} from '@umami/react-zen';
import { useState } from 'react';
import { Badge } from '@/components/common/Badge';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
import {
useDeleteQuery,
@@ -112,19 +113,21 @@ export function UserTwoFactorSettings({ userId }: { userId: string }) {
)}
</Column>
<Modal isOpen={showReset} onOpenChange={open => !open && setShowReset(false)}>
<Dialog title={t(labels.twoFactorReset)} style={{ width: 400 }}>
<TypeConfirmationForm
confirmationValue={CONFIRM_VALUE}
onConfirm={handleReset}
onClose={() => setShowReset(false)}
isLoading={isResetting}
error={resetError}
buttonLabel={t(labels.reset)}
buttonVariant="danger"
/>
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={showReset} onOpenChange={open => !open && setShowReset(false)}>
<Dialog title={t(labels.twoFactorReset)} style={{ width: 400 }}>
<TypeConfirmationForm
confirmationValue={CONFIRM_VALUE}
onConfirm={handleReset}
onClose={() => setShowReset(false)}
isLoading={isResetting}
error={resetError}
buttonLabel={t(labels.reset)}
buttonVariant="danger"
/>
</Dialog>
</Modal>
</ControlledDialog>
</>
);
}
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { WebsiteDeleteForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { DateDistance } from '@/components/common/DateDistance';
import Link from '@/components/common/Link';
import { SortableLabel } from '@/components/common/SortableLabel';
@@ -92,11 +93,13 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
}}
</DataColumn>
</DataTable>
<Modal isOpen={!!deleteWebsite}>
<Dialog style={{ width: 400 }}>
<WebsiteDeleteForm websiteId={deleteWebsite} onClose={() => setDeleteWebsite(null)} />
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={!!deleteWebsite}>
<Dialog style={{ width: 400 }}>
<WebsiteDeleteForm websiteId={deleteWebsite} onClose={() => setDeleteWebsite(null)} />
</Dialog>
</Modal>
</ControlledDialog>
</>
);
}
@@ -10,6 +10,7 @@ import {
TooltipTrigger,
} from '@umami/react-zen';
import { useMemo, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { Panel } from '@/components/common/Panel';
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
import { Pencil, Plus, X } from '@/components/icons';
@@ -121,29 +122,31 @@ export function BoardEditColumn({
</Box>
</Column>
)}
<Modal isOpen={showSelect} onOpenChange={setShowSelect}>
<Dialog
title={t(labels.selectComponent)}
style={{
width: '1200px',
maxWidth: 'calc(100vw - 40px)',
maxHeight: 'calc(100dvh - 40px)',
padding: '32px',
}}
>
{() => (
<BoardComponentSelect
teamId={teamId}
boardType={boardType}
boardEntityType={boardEntityType}
boardEntityId={boardEntityId}
initialConfig={component}
onSelect={handleSelect}
onClose={() => setShowSelect(false)}
/>
)}
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={showSelect} onOpenChange={setShowSelect}>
<Dialog
title={t(labels.selectComponent)}
style={{
width: '1200px',
maxWidth: 'calc(100vw - 40px)',
maxHeight: 'calc(100dvh - 40px)',
padding: '32px',
}}
>
{() => (
<BoardComponentSelect
teamId={teamId}
boardType={boardType}
boardEntityType={boardEntityType}
boardEntityId={boardEntityId}
initialConfig={component}
onSelect={handleSelect}
onClose={() => setShowSelect(false)}
/>
)}
</Dialog>
</Modal>
</ControlledDialog>
</Panel>
);
}
@@ -1,5 +1,6 @@
import { Dialog, Modal } from '@umami/react-zen';
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useMobile, useNavigation } from '@/components/hooks';
export function ExpandedViewModal({
@@ -28,25 +29,27 @@ export function ExpandedViewModal({
};
return (
<Modal isOpen={!!view} onOpenChange={handleOpenChange}>
<Dialog
style={{
maxWidth: 1320,
width: '100vw',
height: isMobile ? '100dvh' : 'calc(100dvh - 40px)',
overflow: 'hidden',
}}
>
{({ close }) => {
return (
<WebsiteExpandedView
websiteId={websiteId}
excludedIds={excludedIds}
onClose={() => handleClose(close)}
/>
);
}}
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={!!view} onOpenChange={handleOpenChange}>
<Dialog
style={{
maxWidth: 1320,
width: '100vw',
height: isMobile ? '100dvh' : 'calc(100dvh - 40px)',
overflow: 'hidden',
}}
>
{({ close }) => {
return (
<WebsiteExpandedView
websiteId={websiteId}
excludedIds={excludedIds}
onClose={() => handleClose(close)}
/>
);
}}
</Dialog>
</Modal>
</ControlledDialog>
);
}
@@ -1,6 +1,7 @@
'use client';
import { Column, Dialog, Sheet, type SheetProps } from '@umami/react-zen';
import { ReplayPlayback } from '@/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayback';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useNavigation } from '@/components/hooks';
import { buildPath } from '@/lib/url';
import styles from './ReplayModal.module.css';
@@ -34,25 +35,27 @@ export function ReplayModal({ websiteId, replayId, className, ...props }: Replay
};
return (
<Sheet
{...props}
side="bottom"
size="calc(100dvh - 80px)"
className={modalClassName}
isOpen={!!activeReplayId}
onOpenChange={handleOpenChange}
>
<Column height="100%">
<Dialog className={styles.sheet}>
{({ close }) => (
<Column padding="6">
{activeReplayId && (
<ReplayPlayback websiteId={websiteId} replayId={activeReplayId} onClose={close} />
)}
</Column>
)}
</Dialog>
</Column>
</Sheet>
<ControlledDialog>
<Sheet
{...props}
side="bottom"
size="calc(100dvh - 80px)"
className={modalClassName}
isOpen={!!activeReplayId}
onOpenChange={handleOpenChange}
>
<Column height="100%">
<Dialog className={styles.sheet}>
{({ close }) => (
<Column padding="6">
{activeReplayId && (
<ReplayPlayback websiteId={websiteId} replayId={activeReplayId} onClose={close} />
)}
</Column>
)}
</Dialog>
</Column>
</Sheet>
</ControlledDialog>
);
}
@@ -1,6 +1,7 @@
'use client';
import { Column, Dialog, Sheet, type SheetProps } from '@umami/react-zen';
import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useNavigation } from '@/components/hooks';
import styles from './SessionModal.module.css';
@@ -23,28 +24,30 @@ export function SessionModal({ websiteId, className, ...props }: SessionModalPro
};
return (
<Sheet
side="bottom"
size="calc(100dvh - 80px)"
className={[styles.modal, className].filter(Boolean).join(' ')}
isOpen={!!session}
onOpenChange={handleOpenChange}
{...props}
>
<Column height="100%">
<Dialog className="rounded-lg">
{({ close }) => (
<Column padding="10">
<SessionProfile
websiteId={websiteId}
sessionId={session}
showReplays={!isSharePage}
onClose={() => close()}
/>
</Column>
)}
</Dialog>
</Column>
</Sheet>
<ControlledDialog>
<Sheet
side="bottom"
size="calc(100dvh - 80px)"
className={[styles.modal, className].filter(Boolean).join(' ')}
isOpen={!!session}
onOpenChange={handleOpenChange}
{...props}
>
<Column height="100%">
<Dialog className="rounded-lg">
{({ close }) => (
<Column padding="10">
<SessionProfile
websiteId={websiteId}
sessionId={session}
showReplays={!isSharePage}
onClose={() => close()}
/>
</Column>
)}
</Dialog>
</Column>
</Sheet>
</ControlledDialog>
);
}
@@ -1,6 +1,7 @@
'use client';
import { Column, Dialog, Sheet } from '@umami/react-zen';
import { useRouter, useSearchParams } from 'next/navigation';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { buildPath } from '@/lib/url';
import styles from './SessionModal.module.css';
import { SessionProfile } from './SessionProfile';
@@ -29,20 +30,22 @@ export function SessionProfileModal({
};
return (
<Sheet
side="bottom"
size="calc(100dvh - 80px)"
className={styles.modal}
isOpen
onOpenChange={handleOpenChange}
>
<Column height="100%">
<Dialog className="rounded-lg">
<Column padding="10">
<SessionProfile websiteId={websiteId} sessionId={sessionId} onClose={closeModal} />
</Column>
</Dialog>
</Column>
</Sheet>
<ControlledDialog>
<Sheet
side="bottom"
size="calc(100dvh - 80px)"
className={styles.modal}
isOpen
onOpenChange={handleOpenChange}
>
<Column height="100%">
<Dialog className="rounded-lg">
<Column padding="10">
<SessionProfile websiteId={websiteId} sessionId={sessionId} onClose={closeModal} />
</Column>
</Dialog>
</Column>
</Sheet>
</ControlledDialog>
);
}
@@ -0,0 +1,12 @@
'use client';
import { DialogTrigger } from '@umami/react-zen';
import type { ReactElement } from 'react';
export function ControlledDialog({ children }: { children: ReactElement }) {
return (
<DialogTrigger>
<span hidden />
{children}
</DialogTrigger>
);
}
+15 -12
View File
@@ -1,5 +1,6 @@
import { Dialog, ListItem, ListSeparator, Modal, Select, type SelectProps } from '@umami/react-zen';
import { Fragment, type Key, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { DateDisplay } from '@/components/common/DateDisplay';
import { useMessages, useMobile } from '@/components/hooks';
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
@@ -128,18 +129,20 @@ export function DateFilter({
})}
</Select>
{showPicker && (
<Modal isOpen={true}>
<Dialog>
<DatePickerForm
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
maxDate={getMaxSelectableDate()}
onChange={handlePickerChange}
onClose={() => setShowPicker(false)}
/>
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={true}>
<Dialog>
<DatePickerForm
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
maxDate={getMaxSelectableDate()}
onChange={handlePickerChange}
onClose={() => setShowPicker(false)}
/>
</Dialog>
</Modal>
</ControlledDialog>
)}
</>
);
+10 -7
View File
@@ -7,6 +7,7 @@ import {
Modal,
} from '@umami/react-zen';
import type { CSSProperties, ReactNode } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { IconLabel } from '@/components/common/IconLabel';
import { useMobile } from '@/components/hooks';
@@ -64,13 +65,15 @@ export function DialogButton({
if (isOpen !== undefined) {
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
placement={isMobile ? 'fullscreen' : 'center'}
>
{dialog}
</Modal>
<ControlledDialog>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
placement={isMobile ? 'fullscreen' : 'center'}
>
{dialog}
</Modal>
</ControlledDialog>
);
}
+13 -10
View File
@@ -11,6 +11,7 @@ import {
Text,
} from '@umami/react-zen';
import { type ReactNode, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useMessages } from '@/components/hooks';
import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery';
import { Edit, MoreHorizontal, Trash } from '@/components/icons';
@@ -103,16 +104,18 @@ export function ReportEditButton({
>
{children}
</DialogButton>
<Modal isOpen={showDelete} onOpenChange={open => !open && handleClose()}>
<AlertDialog
title={t(labels.delete)}
onConfirm={handleDelete}
onCancel={handleClose}
isDanger
>
<Row gap="1">{t(messages.confirmDelete, { target: name })}</Row>
</AlertDialog>
</Modal>
<ControlledDialog>
<Modal isOpen={showDelete} onOpenChange={open => !open && handleClose()}>
<AlertDialog
title={t(labels.delete)}
onConfirm={handleDelete}
onCancel={handleClose}
isDanger
>
<Row gap="1">{t(messages.confirmDelete, { target: name })}</Row>
</AlertDialog>
</Modal>
</ControlledDialog>
</>
);
}
+51 -38
View File
@@ -1,7 +1,18 @@
'use client';
import { Button, Column, Dialog, Modal, Row, Text } from '@umami/react-zen';
import { Form, FormButtons, FormField, FormSubmitButton, PasswordField } from '@umami/react-zen';
import {
Button,
Column,
Dialog,
Form,
FormButtons,
FormField,
FormSubmitButton,
Modal,
PasswordField,
Text,
} from '@umami/react-zen';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { OtpInput } from '@/components/common/OtpInput';
import { useMessages, useUpdateQuery } from '@/components/hooks';
@@ -24,41 +35,43 @@ export function TwoFactorDisableModal({ onClose, onSuccess }: TwoFactorDisableMo
};
return (
<Modal isOpen={true} onOpenChange={onClose}>
<Dialog title={t(labels.twoFactorDisableTitle)} style={{ width: 420 }}>
{({ close }) => (
<Column gap="4">
<Text>{t(messages.twoFactorDisableDescription)}</Text>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
<FormField label={t(labels.password)} name="password">
<PasswordField autoComplete="current-password" onChange={resetError} />
</FormField>
<Column gap="1">
<Text size="sm">{t(labels.twoFactorEnterCode)}</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
resetError();
}}
disabled={isPending}
/>
</Column>
<FormButtons>
<Button variant="outline" onPress={close} isDisabled={isPending}>
{t(labels.cancel)}
</Button>
<FormSubmitButton
variant="danger"
isDisabled={isPending || otpValue.length !== 6 || !!error}
>
{t(labels.twoFactorDisable)}
</FormSubmitButton>
</FormButtons>
</Form>
</Column>
)}
</Dialog>
</Modal>
<ControlledDialog>
<Modal isOpen={true} onOpenChange={onClose}>
<Dialog title={t(labels.twoFactorDisableTitle)} style={{ width: 420 }}>
{({ close }) => (
<Column gap="4">
<Text>{t(messages.twoFactorDisableDescription)}</Text>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
<FormField label={t(labels.password)} name="password">
<PasswordField autoComplete="current-password" onChange={resetError} />
</FormField>
<Column gap="1">
<Text size="sm">{t(labels.twoFactorEnterCode)}</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
resetError();
}}
disabled={isPending}
/>
</Column>
<FormButtons>
<Button variant="outline" onPress={close} isDisabled={isPending}>
{t(labels.cancel)}
</Button>
<FormSubmitButton
variant="danger"
isDisabled={isPending || otpValue.length !== 6 || !!error}
>
{t(labels.twoFactorDisable)}
</FormSubmitButton>
</FormButtons>
</Form>
</Column>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}
+92 -89
View File
@@ -19,6 +19,7 @@ import {
} from '@umami/react-zen';
import { LucideCopy } from 'lucide-react';
import { type ReactNode, useEffect, useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { OtpInput } from '@/components/common/OtpInput';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import styles from './TwoFactorSetupModal.module.css';
@@ -123,96 +124,98 @@ export function TwoFactorSetupModal({ required, onClose }: TwoFactorSetupModalPr
const preventDismiss = required ? () => {} : undefined;
return (
<Modal isOpen={true} onOpenChange={preventDismiss}>
<Dialog title={t(labels.twoFactorSetupTitle)} className={styles.twoFactorSetupModal}>
{({ close }) => (
<Column gap="9">
<Text>
{required && t(messages.twoFactorSetupRequiredDescription) + ' '}
{t(messages.twoFactorSetupDescription)}
</Text>
<ControlledDialog>
<Modal isOpen={true} onOpenChange={preventDismiss}>
<Dialog title={t(labels.twoFactorSetupTitle)} className={styles.twoFactorSetupModal}>
{() => (
<Column gap="9">
<Text>
{required && `${t(messages.twoFactorSetupRequiredDescription)} `}
{t(messages.twoFactorSetupDescription)}
</Text>
{/* Step 1 - Scan QR Code */}
<Step
tag={t(labels.twoFactorStep1)}
title={t(labels.twoFactorScanQr)}
details={t(messages.twoFactorStep1Description)}
>
<Box padding="2" shadow="lg" borderRadius="lg" border width="fit">
<Row alignItems="center" gap="2">
{qrCodeDataUrl && (
<Image
src={qrCodeDataUrl}
alt="QR code"
className={styles.qrCodeImage}
borderRadius="lg"
/>
)}
<Column>
<Text weight="bold">{t(labels.twoFactorCantScan)}</Text>
<Column gap="2">
<Text size="sm">{t(labels.twoFactorManualEntry)}</Text>
<Code>{manualKey}</Code>
<div>
<Button variant="outline" onPress={handleCopy}>
<Icon size="sm">
<LucideCopy />
</Icon>
{copied ? t(labels.twoFactorCodeCopied) : t(labels.twoFactorCopyCode)}
</Button>
</div>
</Column>
</Column>
</Row>
</Box>
</Step>
{/* Step 2 - Enter Verification Code */}
<Step
tag={t(labels.twoFactorStep2)}
title={t(labels.twoFactorGetCode)}
details={t(messages.twoFactorStep2Description)}
>
<Column gap="3.5">
<Text size="sm" weight="bold">
{t(labels.twoFactorEnterCode)}
</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
if (error) setError(null);
}}
onComplete={handleConfirm}
disabled={isConfirming}
/>
</Column>
</Step>
{error && (
<Alert variant="danger">
<AlertTitle>{t(messages.error)}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Row gap="2" justifyContent="flex-end">
{!required && (
<Button variant="outline" onPress={handleCancel} isDisabled={isConfirming}>
{t(labels.cancel)}
</Button>
)}
<Button
variant="primary"
onPress={() => handleConfirm()}
isDisabled={otpValue.length !== 6 || isConfirming || !!error}
{/* Step 1 - Scan QR Code */}
<Step
tag={t(labels.twoFactorStep1)}
title={t(labels.twoFactorScanQr)}
details={t(messages.twoFactorStep1Description)}
>
{t(labels.confirm)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
<Box padding="2" shadow="lg" borderRadius="lg" border width="fit">
<Row alignItems="center" gap="2">
{qrCodeDataUrl && (
<Image
src={qrCodeDataUrl}
alt="QR code"
className={styles.qrCodeImage}
borderRadius="lg"
/>
)}
<Column>
<Text weight="bold">{t(labels.twoFactorCantScan)}</Text>
<Column gap="2">
<Text size="sm">{t(labels.twoFactorManualEntry)}</Text>
<Code>{manualKey}</Code>
<div>
<Button variant="outline" onPress={handleCopy}>
<Icon size="sm">
<LucideCopy />
</Icon>
{copied ? t(labels.twoFactorCodeCopied) : t(labels.twoFactorCopyCode)}
</Button>
</div>
</Column>
</Column>
</Row>
</Box>
</Step>
{/* Step 2 - Enter Verification Code */}
<Step
tag={t(labels.twoFactorStep2)}
title={t(labels.twoFactorGetCode)}
details={t(messages.twoFactorStep2Description)}
>
<Column gap="3.5">
<Text size="sm" weight="bold">
{t(labels.twoFactorEnterCode)}
</Text>
<OtpInput
value={otpValue}
onChange={val => {
setOtpValue(val);
if (error) setError(null);
}}
onComplete={handleConfirm}
disabled={isConfirming}
/>
</Column>
</Step>
{error && (
<Alert variant="danger">
<AlertTitle>{t(messages.error)}</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Row gap="2" justifyContent="flex-end">
{!required && (
<Button variant="outline" onPress={handleCancel} isDisabled={isConfirming}>
{t(labels.cancel)}
</Button>
)}
<Button
variant="primary"
onPress={() => handleConfirm()}
isDisabled={otpValue.length !== 6 || isConfirming || !!error}
>
{t(labels.confirm)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}
+43 -40
View File
@@ -1,6 +1,7 @@
'use client';
import { Button, Column, Dialog, Modal, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
import { ControlledDialog } from '@/components/common/ControlledDialog';
import { useMessages } from '@/components/hooks';
import styles from './TwoFactorSuccessModal.module.css';
@@ -30,51 +31,53 @@ export function TwoFactorSuccessModal({ backupCodes, onClose }: TwoFactorSuccess
};
return (
<Modal isOpen={true}>
<Dialog title={t(labels.twoFactorSuccessTitle)} className={styles.dialog}>
{() => (
<Column gap="5">
<Text>{t(messages.twoFactorEnabledDescription)}</Text>
<ControlledDialog>
<Modal isOpen={true}>
<Dialog title={t(labels.twoFactorSuccessTitle)} className={styles.dialog}>
{() => (
<Column gap="5">
<Text>{t(messages.twoFactorEnabledDescription)}</Text>
<Column gap="2">
<Text weight="bold">{t(labels.twoFactorSaveBackupCodes)}</Text>
<Text>{t(messages.twoFactorBackupDescription)}</Text>
<Column gap="2">
<Text weight="bold">{t(labels.twoFactorSaveBackupCodes)}</Text>
<Text>{t(messages.twoFactorBackupDescription)}</Text>
<div className={styles.codesGrid}>
{backupCodes.map((code, i) => (
<code key={i} className={styles.code}>
{code}
</code>
))}
</div>
<div className={styles.codesGrid}>
{backupCodes.map((code, i) => (
<code key={i} className={styles.code}>
{code}
</code>
))}
</div>
<Row>
<Button variant="outline" onPress={handleDownload}>
{t(labels.twoFactorDownloadCodes)}
<Row>
<Button variant="outline" onPress={handleDownload}>
{t(labels.twoFactorDownloadCodes)}
</Button>
</Row>
</Column>
<Row gap="2" alignItems="center">
<input
type="checkbox"
id="saved-codes"
checked={saved}
onChange={e => setSaved(e.target.checked)}
/>
<label htmlFor="saved-codes">
<Text>{t(labels.twoFactorSavedCodes)}</Text>
</label>
</Row>
<Row justifyContent="flex-end">
<Button variant="primary" onPress={onClose} isDisabled={!saved}>
{t(labels.close)}
</Button>
</Row>
</Column>
<Row gap="2" alignItems="center">
<input
type="checkbox"
id="saved-codes"
checked={saved}
onChange={e => setSaved(e.target.checked)}
/>
<label htmlFor="saved-codes">
<Text>{t(labels.twoFactorSavedCodes)}</Text>
</label>
</Row>
<Row justifyContent="flex-end">
<Button variant="primary" onPress={onClose} isDisabled={!saved}>
{t(labels.close)}
</Button>
</Row>
</Column>
)}
</Dialog>
</Modal>
)}
</Dialog>
</Modal>
</ControlledDialog>
);
}