Update CharacterModal
* Adapts styles for CSS modules * Adds an alert if the user tries to close a dialog with changes without saving * Uses constants instead of functions for rendering helpers * Fixes validation
This commit is contained in:
parent
2cd6513aa4
commit
0b21bf768a
2 changed files with 233 additions and 202 deletions
|
|
@ -1,78 +1,32 @@
|
||||||
.Character.DialogContent {
|
.mods {
|
||||||
gap: $unit;
|
display: flex;
|
||||||
min-width: 480px;
|
flex-direction: column;
|
||||||
|
gap: $unit-4x;
|
||||||
|
padding: 0 $unit-4x $unit-2x;
|
||||||
|
|
||||||
@include breakpoint(phone) {
|
section {
|
||||||
min-width: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DialogHeader {
|
|
||||||
transition: 0.18s padding-top ease-in-out;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
&.Scrolled {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
|
||||||
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.34);
|
|
||||||
padding-top: $unit-2x;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
transition: 0.2s width ease-in-out;
|
|
||||||
width: $unit-6x !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DialogTitle {
|
|
||||||
font-size: $font-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SubTitle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mods {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit-4x;
|
gap: $unit-half;
|
||||||
padding: 0 $unit-4x $unit-2x;
|
|
||||||
|
|
||||||
section {
|
&.inline {
|
||||||
display: flex;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: $unit-half;
|
justify-content: space-between;
|
||||||
|
|
||||||
&.inline {
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
color: $grey-55;
|
margin: 0;
|
||||||
font-size: $font-small;
|
|
||||||
margin-bottom: $unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background-color: $grey-90;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button {
|
h3 {
|
||||||
font-size: $font-regular;
|
color: $grey-55;
|
||||||
padding: ($unit * 1.5) ($unit-2x);
|
font-size: $font-small;
|
||||||
width: 100%;
|
margin-bottom: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
&.btn-disabled {
|
select {
|
||||||
background: $grey-90;
|
background-color: $grey-90;
|
||||||
color: $grey-70;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,11 @@
|
||||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import classNames from 'classnames'
|
import isEqual from 'lodash/isEqual'
|
||||||
|
|
||||||
// UI dependencies
|
// UI dependencies
|
||||||
import {
|
import Alert from '~components/common/Alert'
|
||||||
Dialog,
|
import { Dialog, DialogTrigger } from '~components/common/Dialog'
|
||||||
DialogClose,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '~components/common/Dialog'
|
|
||||||
import DialogContent from '~components/common/DialogContent'
|
import DialogContent from '~components/common/DialogContent'
|
||||||
import Button from '~components/common/Button'
|
import Button from '~components/common/Button'
|
||||||
import SelectWithInput from '~components/common/SelectWithInput'
|
import SelectWithInput from '~components/common/SelectWithInput'
|
||||||
|
|
@ -29,7 +25,6 @@ const emptyExtendedMastery: ExtendedMastery = {
|
||||||
const MAX_AWAKENING_LEVEL = 9
|
const MAX_AWAKENING_LEVEL = 9
|
||||||
|
|
||||||
// Styles and icons
|
// Styles and icons
|
||||||
import CrossIcon from '~public/icons/Cross.svg'
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -39,6 +34,8 @@ import {
|
||||||
GridCharacterObject,
|
GridCharacterObject,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
|
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
|
||||||
|
import DialogHeader from '~components/common/DialogHeader'
|
||||||
|
import DialogFooter from '~components/common/DialogFooter'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
gridCharacter: GridCharacter
|
gridCharacter: GridCharacter
|
||||||
|
|
@ -54,6 +51,7 @@ const CharacterModal = ({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
updateCharacter,
|
updateCharacter,
|
||||||
}: PropsWithChildren<Props>) => {
|
}: PropsWithChildren<Props>) => {
|
||||||
|
// Router and localization
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const locale =
|
const locale =
|
||||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
@ -67,39 +65,27 @@ const CharacterModal = ({
|
||||||
const headerRef = React.createRef<HTMLDivElement>()
|
const headerRef = React.createRef<HTMLDivElement>()
|
||||||
const footerRef = React.createRef<HTMLDivElement>()
|
const footerRef = React.createRef<HTMLDivElement>()
|
||||||
|
|
||||||
// Classes
|
// State: Component
|
||||||
const headerClasses = classNames({
|
const [alertOpen, setAlertOpen] = useState(false)
|
||||||
DialogHeader: true,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Callbacks and Hooks
|
// State: Data
|
||||||
useEffect(() => {
|
|
||||||
setOpen(modalOpen)
|
|
||||||
}, [modalOpen])
|
|
||||||
|
|
||||||
// Character properties: Perpetuity
|
|
||||||
const [perpetuity, setPerpetuity] = useState(false)
|
const [perpetuity, setPerpetuity] = useState(false)
|
||||||
|
|
||||||
// Character properties: Ring
|
|
||||||
const [rings, setRings] = useState<CharacterOverMastery>({
|
const [rings, setRings] = useState<CharacterOverMastery>({
|
||||||
1: { ...emptyExtendedMastery, modifier: 1 },
|
1: { ...emptyExtendedMastery, modifier: 1 },
|
||||||
2: { ...emptyExtendedMastery, modifier: 2 },
|
2: { ...emptyExtendedMastery, modifier: 2 },
|
||||||
3: emptyExtendedMastery,
|
3: emptyExtendedMastery,
|
||||||
4: emptyExtendedMastery,
|
4: emptyExtendedMastery,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Character properties: Earrings
|
|
||||||
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
|
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
|
||||||
|
|
||||||
// Character properties: Awakening
|
|
||||||
const [awakening, setAwakening] = useState<Awakening>()
|
const [awakening, setAwakening] = useState<Awakening>()
|
||||||
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
||||||
|
|
||||||
// Character properties: Transcendence
|
|
||||||
const [transcendenceStep, setTranscendenceStep] = useState(0)
|
const [transcendenceStep, setTranscendenceStep] = useState(0)
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(modalOpen)
|
||||||
|
}, [modalOpen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gridCharacter.aetherial_mastery) {
|
if (gridCharacter.aetherial_mastery) {
|
||||||
setEarring({
|
setEarring({
|
||||||
|
|
@ -150,10 +136,80 @@ const CharacterModal = ({
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methods: Convenience
|
||||||
|
function hasBeenModified() {
|
||||||
|
const rings = ringsChanged()
|
||||||
|
const aetherialMastery = aetherialMasteryChanged()
|
||||||
|
const awakening = awakeningChanged()
|
||||||
|
|
||||||
|
return (
|
||||||
|
rings ||
|
||||||
|
aetherialMastery ||
|
||||||
|
awakening ||
|
||||||
|
gridCharacter.perpetuity !== perpetuity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ringsChanged() {
|
||||||
|
// Create an empty ExtendedMastery object
|
||||||
|
const emptyRingset: CharacterOverMastery = {
|
||||||
|
1: { ...emptyExtendedMastery, modifier: 1 },
|
||||||
|
2: { ...emptyExtendedMastery, modifier: 2 },
|
||||||
|
3: emptyExtendedMastery,
|
||||||
|
4: emptyExtendedMastery,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current ringset is empty on the current GridCharacter and our local state
|
||||||
|
const isEmptyRingset =
|
||||||
|
gridCharacter.over_mastery === undefined && isEqual(emptyRingset, rings)
|
||||||
|
|
||||||
|
// Check if the ringset in local state is different from the one on the current GridCharacter
|
||||||
|
const ringsChanged = !isEqual(gridCharacter.over_mastery, rings)
|
||||||
|
|
||||||
|
// Return true if the ringset has been modified and is not empty
|
||||||
|
return ringsChanged && !isEmptyRingset
|
||||||
|
}
|
||||||
|
|
||||||
|
function aetherialMasteryChanged() {
|
||||||
|
// Create an empty ExtendedMastery object
|
||||||
|
const emptyAetherialMastery: ExtendedMastery = {
|
||||||
|
modifier: 0,
|
||||||
|
strength: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current earring is empty on the current GridCharacter and our local state
|
||||||
|
const isEmptyRingset =
|
||||||
|
gridCharacter.aetherial_mastery === undefined &&
|
||||||
|
isEqual(emptyAetherialMastery, earring)
|
||||||
|
|
||||||
|
// Check if the earring in local state is different from the one on the current GridCharacter
|
||||||
|
const aetherialMasteryChanged = !isEqual(
|
||||||
|
gridCharacter.aetherial_mastery,
|
||||||
|
earring
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return true if the earring has been modified and is not empty
|
||||||
|
return aetherialMasteryChanged && !isEmptyRingset
|
||||||
|
}
|
||||||
|
|
||||||
|
function awakeningChanged() {
|
||||||
|
// Check if the awakening in local state is different from the one on the current GridCharacter
|
||||||
|
const awakeningChanged =
|
||||||
|
!isEqual(gridCharacter.awakening.type, awakening) ||
|
||||||
|
gridCharacter.awakening.level !== awakeningLevel
|
||||||
|
|
||||||
|
// Return true if the awakening has been modified and is not empty
|
||||||
|
return awakeningChanged
|
||||||
|
}
|
||||||
|
|
||||||
// Methods: UI state management
|
// Methods: UI state management
|
||||||
function handleOpenChange(open: boolean) {
|
function handleOpenChange(open: boolean) {
|
||||||
setOpen(open)
|
if (hasBeenModified()) {
|
||||||
onOpenChange(open)
|
setAlertOpen(!open)
|
||||||
|
} else {
|
||||||
|
setOpen(open)
|
||||||
|
onOpenChange(open)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods: Receive data from components
|
// Methods: Receive data from components
|
||||||
|
|
@ -167,21 +223,10 @@ const CharacterModal = ({
|
||||||
) {
|
) {
|
||||||
setEarring({
|
setEarring({
|
||||||
modifier: earringModifier,
|
modifier: earringModifier,
|
||||||
strength: earringStrength,
|
strength: earringModifier > 0 ? earringStrength : 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheckedChange(checked: boolean) {
|
|
||||||
setPerpetuity(checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpdateCharacter() {
|
|
||||||
await updateCharacter(prepareObject())
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
if (onOpenChange) onOpenChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveAwakeningValues(id: string, level: number) {
|
function receiveAwakeningValues(id: string, level: number) {
|
||||||
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
|
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
|
||||||
setAwakeningLevel(level)
|
setAwakeningLevel(level)
|
||||||
|
|
@ -191,113 +236,145 @@ const CharacterModal = ({
|
||||||
setFormValid(isValid)
|
setFormValid(isValid)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ringSelect = () => {
|
// Methods: Event handlers
|
||||||
return (
|
function handleCheckedChange(checked: boolean) {
|
||||||
<section>
|
setPerpetuity(checked)
|
||||||
<h3>{t('modals.characters.subtitles.ring')}</h3>
|
|
||||||
<RingSelect
|
|
||||||
gridCharacter={gridCharacter}
|
|
||||||
sendValues={receiveRingValues}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const earringSelect = () => {
|
async function handleUpdateCharacter() {
|
||||||
const earringData = elementalizeAetherialMastery(gridCharacter)
|
await updateCharacter(prepareObject())
|
||||||
|
|
||||||
return (
|
setOpen(false)
|
||||||
<section>
|
if (onOpenChange) onOpenChange(false)
|
||||||
<h3>{t('modals.characters.subtitles.earring')}</h3>
|
|
||||||
<SelectWithInput
|
|
||||||
object="earring"
|
|
||||||
dataSet={earringData}
|
|
||||||
selectValue={earring.modifier ? earring.modifier : 0}
|
|
||||||
inputValue={earring.strength ? earring.strength : 0}
|
|
||||||
sendValidity={receiveValidity}
|
|
||||||
sendValues={receiveEarringValues}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const awakeningSelect = () => {
|
function close() {
|
||||||
return (
|
setAlertOpen(false)
|
||||||
<section>
|
setOpen(false)
|
||||||
<h3>{t('modals.characters.subtitles.awakening')}</h3>
|
onOpenChange(false)
|
||||||
<AwakeningSelectWithInput
|
|
||||||
dataSet={gridCharacter.object.awakenings}
|
|
||||||
awakening={gridCharacter.awakening.type}
|
|
||||||
level={gridCharacter.awakening.level}
|
|
||||||
defaultAwakening={
|
|
||||||
gridCharacter.object.awakenings.find(
|
|
||||||
(a) => a.slug === 'character-balanced'
|
|
||||||
)!
|
|
||||||
}
|
|
||||||
maxLevel={MAX_AWAKENING_LEVEL}
|
|
||||||
sendValidity={receiveValidity}
|
|
||||||
sendValues={receiveAwakeningValues}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const perpetuitySwitch = () => {
|
// Constants: Rendering
|
||||||
return (
|
const confirmationAlert = (
|
||||||
<section className="inline">
|
<Alert
|
||||||
<h3>{t('modals.characters.subtitles.permanent')}</h3>
|
message={
|
||||||
<Switch onCheckedChange={handleCheckedChange} checked={perpetuity} />
|
<span>
|
||||||
</section>
|
You will lose all changes to{' '}
|
||||||
)
|
<strong>{gridCharacter.object.name[locale]}</strong> if you continue.
|
||||||
}
|
<br />
|
||||||
|
<br />
|
||||||
|
Are you sure you want to continue without saving?
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
open={alertOpen}
|
||||||
|
primaryActionText="Close"
|
||||||
|
primaryAction={close}
|
||||||
|
cancelActionText="Nevermind"
|
||||||
|
cancelAction={() => setAlertOpen(false)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ringSelect = (
|
||||||
|
<section>
|
||||||
|
<h3>{t('modals.characters.subtitles.ring')}</h3>
|
||||||
|
<RingSelect
|
||||||
|
gridCharacter={gridCharacter}
|
||||||
|
sendValues={receiveRingValues}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
const earringSelect = (
|
||||||
|
<section>
|
||||||
|
<h3>{t('modals.characters.subtitles.earring')}</h3>
|
||||||
|
<SelectWithInput
|
||||||
|
object="earring"
|
||||||
|
dataSet={elementalizeAetherialMastery(gridCharacter)}
|
||||||
|
selectValue={
|
||||||
|
gridCharacter.aetherial_mastery
|
||||||
|
? gridCharacter.aetherial_mastery.modifier
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
inputValue={
|
||||||
|
gridCharacter.aetherial_mastery
|
||||||
|
? gridCharacter.aetherial_mastery.strength
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
sendValidity={receiveValidity}
|
||||||
|
sendValues={receiveEarringValues}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
const awakeningSelect = (
|
||||||
|
<section>
|
||||||
|
<h3>{t('modals.characters.subtitles.awakening')}</h3>
|
||||||
|
<AwakeningSelectWithInput
|
||||||
|
dataSet={gridCharacter.object.awakenings}
|
||||||
|
awakening={gridCharacter.awakening.type}
|
||||||
|
level={gridCharacter.awakening.level}
|
||||||
|
defaultAwakening={
|
||||||
|
gridCharacter.object.awakenings.find(
|
||||||
|
(a) => a.slug === 'character-balanced'
|
||||||
|
)!
|
||||||
|
}
|
||||||
|
maxLevel={MAX_AWAKENING_LEVEL}
|
||||||
|
sendValidity={receiveValidity}
|
||||||
|
sendValues={receiveAwakeningValues}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
const perpetuitySwitch = (
|
||||||
|
<section className={styles.inline}>
|
||||||
|
<h3>{t('modals.characters.subtitles.permanent')}</h3>
|
||||||
|
<Switch onCheckedChange={handleCheckedChange} checked={perpetuity} />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Methods: Rendering
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
{confirmationAlert}
|
||||||
<DialogContent
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
className="Character"
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
headerref={headerRef}
|
<DialogContent
|
||||||
footerref={footerRef}
|
className="character"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
headerref={headerRef}
|
||||||
onEscapeKeyDown={() => {}}
|
footerref={footerRef}
|
||||||
>
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
<div className={headerClasses} ref={headerRef}>
|
onEscapeKeyDown={() => {}}
|
||||||
<img
|
>
|
||||||
alt={gridCharacter.object.name[locale]}
|
<DialogHeader
|
||||||
className="DialogImage"
|
ref={headerRef}
|
||||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`}
|
title={gridCharacter.object.name[locale]}
|
||||||
|
subtitle={t('modals.characters.title')}
|
||||||
|
image={{
|
||||||
|
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-square/${gridCharacter.object.granblue_id}_01.jpg`,
|
||||||
|
alt: gridCharacter.object.name[locale],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="DialogTop">
|
<section className={styles.mods}>
|
||||||
<DialogTitle className="SubTitle">
|
{perpetuitySwitch}
|
||||||
{t('modals.characters.title')}
|
{ringSelect}
|
||||||
</DialogTitle>
|
{earringSelect}
|
||||||
<DialogTitle className="DialogTitle">
|
{awakeningSelect}
|
||||||
{gridCharacter.object.name[locale]}
|
</section>
|
||||||
</DialogTitle>
|
<DialogFooter
|
||||||
</div>
|
ref={footerRef}
|
||||||
<DialogClose className="DialogClose" asChild>
|
rightElements={[
|
||||||
<span>
|
<Button
|
||||||
<CrossIcon />
|
bound={true}
|
||||||
</span>
|
onClick={handleUpdateCharacter}
|
||||||
</DialogClose>
|
key="confirm"
|
||||||
</div>
|
disabled={!formValid}
|
||||||
|
text={t('modals.characters.buttons.confirm')}
|
||||||
<div className="mods">
|
/>,
|
||||||
{perpetuitySwitch()}
|
]}
|
||||||
{ringSelect()}
|
|
||||||
{earringSelect()}
|
|
||||||
{awakeningSelect()}
|
|
||||||
</div>
|
|
||||||
<div className="DialogFooter" ref={footerRef}>
|
|
||||||
<Button
|
|
||||||
bound={true}
|
|
||||||
onClick={handleUpdateCharacter}
|
|
||||||
disabled={!formValid}
|
|
||||||
text={t('modals.characters.buttons.confirm')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</DialogContent>
|
||||||
</DialogContent>
|
</Dialog>
|
||||||
</Dialog>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue