@greatapps/common 1.1.791 → 1.1.792
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/account/AccountUnsavedChangesDialog.mjs +34 -0
- package/dist/components/account/AccountUnsavedChangesDialog.mjs.map +1 -0
- package/dist/components/account/MyAccountModal.mjs +28 -22
- package/dist/components/account/MyAccountModal.mjs.map +1 -1
- package/dist/components/account/sections/MyProfileSection.mjs +20 -22
- package/dist/components/account/sections/MyProfileSection.mjs.map +1 -1
- package/dist/components/account/sections/PreferencesSection.mjs +15 -20
- package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +5 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +5 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +5 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/middlewares/create-auth-middleware.mjs +1 -1
- package/dist/middlewares/create-auth-middleware.mjs.map +1 -1
- package/dist/store/useAccountUnsavedChanges.mjs +31 -0
- package/dist/store/useAccountUnsavedChanges.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/account/AccountUnsavedChangesDialog.tsx +49 -0
- package/src/components/account/MyAccountModal.tsx +41 -34
- package/src/components/account/sections/MyProfileSection.tsx +24 -20
- package/src/components/account/sections/PreferencesSection.tsx +17 -21
- package/src/i18n/messages/en-us.ts +5 -0
- package/src/i18n/messages/es-es.ts +5 -0
- package/src/i18n/messages/pt-br.ts +5 -0
- package/src/middlewares/create-auth-middleware.ts +1 -1
- package/src/store/useAccountUnsavedChanges.ts +52 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl';
|
|
4
|
+
import { IconAlertTriangle } from '@tabler/icons-react';
|
|
5
|
+
import { Button } from '../ui/buttons/Button';
|
|
6
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/overlay/Dialog';
|
|
7
|
+
import { Separator } from '../ui/data-display/Separator';
|
|
8
|
+
import { useAccountUnsavedChanges } from '../../store/useAccountUnsavedChanges';
|
|
9
|
+
|
|
10
|
+
export default function AccountUnsavedChangesDialog() {
|
|
11
|
+
const translate = useTranslations();
|
|
12
|
+
const { pendingAction, save, discard, dismiss } = useAccountUnsavedChanges();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Dialog open={pendingAction !== null} onOpenChange={(nextOpen) => !nextOpen && dismiss()}>
|
|
16
|
+
<DialogContent className="flex flex-col p-0 gap-0 md:w-[386px]! lg:w-[386px]! max-w-[386px]! max-[386px]:w-[calc(100%-32px)]">
|
|
17
|
+
<DialogHeader className="sr-only">
|
|
18
|
+
<DialogTitle>{translate('common.account.unsavedChanges.title')}</DialogTitle>
|
|
19
|
+
<DialogDescription>{translate('common.account.unsavedChanges.description')}</DialogDescription>
|
|
20
|
+
</DialogHeader>
|
|
21
|
+
|
|
22
|
+
<div className="flex flex-col gap-3 p-5">
|
|
23
|
+
<div className="flex items-center justify-center bg-gray-50 rounded-lg size-10">
|
|
24
|
+
<IconAlertTriangle size={24} className="text-gray-950" />
|
|
25
|
+
</div>
|
|
26
|
+
<div className="flex flex-col gap-2">
|
|
27
|
+
<span className="paragraph-medium-semibold text-gray-950">
|
|
28
|
+
{translate('common.account.unsavedChanges.title')}
|
|
29
|
+
</span>
|
|
30
|
+
<span className="paragraph-small-regular text-gray-600">
|
|
31
|
+
{translate('common.account.unsavedChanges.description')}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<Separator className="w-full" />
|
|
37
|
+
|
|
38
|
+
<div className="flex items-center gap-2 p-5">
|
|
39
|
+
<Button onClick={save} className="h-10!">
|
|
40
|
+
{translate('common.actions.save')}
|
|
41
|
+
</Button>
|
|
42
|
+
<Button variant="secondary" onClick={discard} className="h-10!">
|
|
43
|
+
{translate('common.account.unsavedChanges.discard')}
|
|
44
|
+
</Button>
|
|
45
|
+
</div>
|
|
46
|
+
</DialogContent>
|
|
47
|
+
</Dialog>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -29,9 +29,11 @@ import { ProjectsSection } from './sections/ProjectsSection';
|
|
|
29
29
|
import { TeamSection } from './sections/TeamSection';
|
|
30
30
|
import { AffiliatesSection } from './sections/AffiliatesSection';
|
|
31
31
|
import useMyAccountModal from './hooks/useMyAccountModal';
|
|
32
|
+
import AccountUnsavedChangesDialog from './AccountUnsavedChangesDialog';
|
|
32
33
|
import { AccountSectionType } from '../../enums/AccountSectionType';
|
|
33
34
|
import { WorkspaceSectionType } from '../../enums/WorkspaceSectionType';
|
|
34
35
|
import { useAccountModals, type MyAccountSection } from '../../store/useAccountModals';
|
|
36
|
+
import { useAccountUnsavedChanges } from '../../store/useAccountUnsavedChanges';
|
|
35
37
|
import { useAuth } from '../../providers/auth.provider';
|
|
36
38
|
import { useManagementPermissions } from '../../modules/auth/hooks/useManagementPermissions';
|
|
37
39
|
import { useCurrentSubscription } from '../../modules/subscriptions/hooks/use-current-subscription.hook';
|
|
@@ -55,6 +57,7 @@ export default function MyAccountModal() {
|
|
|
55
57
|
const { isExternalContracting } = useExternalContracting();
|
|
56
58
|
const { whitelabel } = useWhitelabel();
|
|
57
59
|
const { data: { data: [subscription] = [] } = {} } = useCurrentSubscription();
|
|
60
|
+
const guardUnsavedChanges = useAccountUnsavedChanges((state) => state.guard);
|
|
58
61
|
|
|
59
62
|
const open = activeModal === 'account';
|
|
60
63
|
|
|
@@ -82,14 +85,14 @@ export default function MyAccountModal() {
|
|
|
82
85
|
icon: IconUser,
|
|
83
86
|
label: translate('common.account.profile.title'),
|
|
84
87
|
visible: true,
|
|
85
|
-
content: <MyProfileSection
|
|
88
|
+
content: <MyProfileSection />,
|
|
86
89
|
},
|
|
87
90
|
{
|
|
88
91
|
value: AccountSectionType.PREFERENCES,
|
|
89
92
|
icon: IconSettings2,
|
|
90
93
|
label: translate('common.preferences.title'),
|
|
91
94
|
visible: true,
|
|
92
|
-
content: <PreferencesSection
|
|
95
|
+
content: <PreferencesSection />,
|
|
93
96
|
},
|
|
94
97
|
{
|
|
95
98
|
value: AccountSectionType.TOKEN,
|
|
@@ -178,38 +181,42 @@ export default function MyAccountModal() {
|
|
|
178
181
|
));
|
|
179
182
|
|
|
180
183
|
return (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{
|
|
184
|
+
<>
|
|
185
|
+
<AccountModalLayout
|
|
186
|
+
open={open}
|
|
187
|
+
onClose={() => guardUnsavedChanges(close)}
|
|
188
|
+
title={translate('common.account.workspace.title')}
|
|
189
|
+
sectionTitle={sectionTitle}
|
|
190
|
+
activeSection={activeSection ?? ''}
|
|
191
|
+
onSectionChange={(section) => guardUnsavedChanges(() => setActiveSection(section))}
|
|
192
|
+
isLoading={isProfileLoading}
|
|
193
|
+
navigation={
|
|
194
|
+
<>
|
|
195
|
+
{visibleWorkspaceSections.length > 0 && (
|
|
196
|
+
<AccountModalNavGroup label={translate('common.account.workspace.groups.workspace')}>
|
|
197
|
+
{renderNavItems(visibleWorkspaceSections)}
|
|
198
|
+
</AccountModalNavGroup>
|
|
199
|
+
)}
|
|
200
|
+
<AccountModalNavGroup label={translate('common.account.workspace.groups.account')}>
|
|
201
|
+
{renderNavItems(visibleAccountSections)}
|
|
194
202
|
</AccountModalNavGroup>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
</AccountModalLayout>
|
|
203
|
+
</>
|
|
204
|
+
}
|
|
205
|
+
>
|
|
206
|
+
{visibleSections.map((section) => (
|
|
207
|
+
<AccountModalSection key={section.value} value={section.value}>
|
|
208
|
+
{section.content}
|
|
209
|
+
</AccountModalSection>
|
|
210
|
+
))}
|
|
211
|
+
|
|
212
|
+
{changePasswordSection.map((section) => (
|
|
213
|
+
<AccountModalSection key={section.value} value={section.value}>
|
|
214
|
+
{section.content}
|
|
215
|
+
</AccountModalSection>
|
|
216
|
+
))}
|
|
217
|
+
</AccountModalLayout>
|
|
218
|
+
|
|
219
|
+
<AccountUnsavedChangesDialog />
|
|
220
|
+
</>
|
|
214
221
|
);
|
|
215
222
|
}
|
|
@@ -25,6 +25,7 @@ import { ChangeEmailModal } from './ChangeEmailModal';
|
|
|
25
25
|
import { NotificationsStep } from './team/steps/NotificationsStep';
|
|
26
26
|
import { ProjectSelectionStep } from './team/steps/ProjectSelectionStep';
|
|
27
27
|
import { FormSection } from './shared/FormSection';
|
|
28
|
+
import { useAccountUnsavedChanges } from '../../../store/useAccountUnsavedChanges';
|
|
28
29
|
import { buildUserFormDefaults, buildUserFormSchema, type UserFormData } from './team/user-form.schema';
|
|
29
30
|
|
|
30
31
|
/** Espaçamento das seções do modal, aplicado dentro de cada aba. */
|
|
@@ -40,11 +41,10 @@ const tabContentClasses = cn(
|
|
|
40
41
|
const tabTriggerClasses = 'cursor-pointer gap-2 px-3 py-4';
|
|
41
42
|
|
|
42
43
|
interface MyProfileSectionProps {
|
|
43
|
-
onClose: () => void;
|
|
44
44
|
storageUrl?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function MyProfileSection({
|
|
47
|
+
export function MyProfileSection({ storageUrl }: MyProfileSectionProps) {
|
|
48
48
|
const translate = useTranslations();
|
|
49
49
|
const genderOptions = GENDER_OPTIONS.map((option) => ({
|
|
50
50
|
...option,
|
|
@@ -60,6 +60,9 @@ export function MyProfileSection({ onClose, storageUrl }: MyProfileSectionProps)
|
|
|
60
60
|
const [photoRemoved, setPhotoRemoved] = useState(false);
|
|
61
61
|
const [changePhoneOpen, setChangePhoneOpen] = useState(false);
|
|
62
62
|
const [changeEmailOpen, setChangeEmailOpen] = useState(false);
|
|
63
|
+
const setDirtyForm = useAccountUnsavedChanges((state) => state.setDirtyForm);
|
|
64
|
+
const completeSave = useAccountUnsavedChanges((state) => state.completeSave);
|
|
65
|
+
const dismissUnsavedChanges = useAccountUnsavedChanges((state) => state.dismiss);
|
|
63
66
|
|
|
64
67
|
// Mesmo formulário do sheet da equipe: as abas Projetos e Notificações são os passos de lá.
|
|
65
68
|
const profileFormSchema = useMemo(
|
|
@@ -82,7 +85,7 @@ export function MyProfileSection({ onClose, storageUrl }: MyProfileSectionProps)
|
|
|
82
85
|
handleSubmit,
|
|
83
86
|
watch,
|
|
84
87
|
setValue,
|
|
85
|
-
formState: { errors, isSubmitting },
|
|
88
|
+
formState: { errors, isSubmitting, isDirty },
|
|
86
89
|
} = form;
|
|
87
90
|
|
|
88
91
|
// O usuário autenticado chega por query: repõe os valores quando ele carrega depois da montagem.
|
|
@@ -134,7 +137,7 @@ export function MyProfileSection({ onClose, storageUrl }: MyProfileSectionProps)
|
|
|
134
137
|
toastId={toastId}
|
|
135
138
|
/>
|
|
136
139
|
));
|
|
137
|
-
|
|
140
|
+
completeSave();
|
|
138
141
|
} catch (error) {
|
|
139
142
|
toast.custom((toastId) => (
|
|
140
143
|
<Toast
|
|
@@ -143,9 +146,24 @@ export function MyProfileSection({ onClose, storageUrl }: MyProfileSectionProps)
|
|
|
143
146
|
toastId={toastId}
|
|
144
147
|
/>
|
|
145
148
|
));
|
|
149
|
+
dismissUnsavedChanges();
|
|
146
150
|
}
|
|
147
151
|
};
|
|
148
152
|
|
|
153
|
+
const hasChanges = isDirty || selectedPhoto !== null || photoRemoved;
|
|
154
|
+
|
|
155
|
+
const discardChanges = () => {
|
|
156
|
+
form.reset();
|
|
157
|
+
setSelectedPhoto(null);
|
|
158
|
+
setPhotoRemoved(false);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!hasChanges) return;
|
|
163
|
+
setDirtyForm({ submit: handleSubmit(onSubmit), discard: discardChanges });
|
|
164
|
+
return () => setDirtyForm(null);
|
|
165
|
+
});
|
|
166
|
+
|
|
149
167
|
return (
|
|
150
168
|
<>
|
|
151
169
|
<FormProvider {...form}>
|
|
@@ -311,22 +329,8 @@ export function MyProfileSection({ onClose, storageUrl }: MyProfileSectionProps)
|
|
|
311
329
|
</TabsContent>
|
|
312
330
|
</Tabs>
|
|
313
331
|
|
|
314
|
-
<div className="fixed bottom-0 left-0 right-0 lg:absolute flex items-center h-20 px-4 lg:px-8 border-t border-gray-200 justify-
|
|
315
|
-
<Button
|
|
316
|
-
variant="secondary"
|
|
317
|
-
className="h-10!"
|
|
318
|
-
type="button"
|
|
319
|
-
disabled={isSubmitting}
|
|
320
|
-
onClick={() => {
|
|
321
|
-
form.reset();
|
|
322
|
-
setSelectedPhoto(null);
|
|
323
|
-
setPhotoRemoved(false);
|
|
324
|
-
onClose();
|
|
325
|
-
}}
|
|
326
|
-
>
|
|
327
|
-
{translate('common.actions.cancel')}
|
|
328
|
-
</Button>
|
|
329
|
-
<Button className="h-10! cursor-pointer" type="submit" disabled={isSubmitting}>
|
|
332
|
+
<div className="fixed bottom-0 left-0 right-0 lg:absolute flex items-center h-20 px-4 lg:px-8 border-t border-gray-200 justify-end bg-white z-10">
|
|
333
|
+
<Button className="h-10! cursor-pointer" type="submit" disabled={!hasChanges || isSubmitting}>
|
|
330
334
|
{isSubmitting ? translate('common.actions.saving') : translate('common.actions.saveChanges')}
|
|
331
335
|
</Button>
|
|
332
336
|
</div>
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from '../constants';
|
|
33
33
|
import { useTimezones } from '../../../modules/accounts/hooks/useTimezones';
|
|
34
34
|
import { useCurrencies } from '../../../modules/accounts/hooks/useCurrencies';
|
|
35
|
+
import { useAccountUnsavedChanges } from '../../../store/useAccountUnsavedChanges';
|
|
35
36
|
import { getCurrencyForGateway } from '../../../utils/format/currency';
|
|
36
37
|
|
|
37
38
|
interface PreferencesFormValues {
|
|
@@ -44,11 +45,7 @@ interface PreferencesFormValues {
|
|
|
44
45
|
receive_email: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
onClose: () => void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
48
|
+
export function PreferencesSection() {
|
|
52
49
|
// Padrão raiz: um único `translate` chamado pela chave inteira (`common.<modulo>.<chave>`).
|
|
53
50
|
const translate = useTranslations();
|
|
54
51
|
// Exceção: chave dinâmica (option.value) numa lista → namespace escopado.
|
|
@@ -80,8 +77,11 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
80
77
|
accountFields: UpdateAccountRequest;
|
|
81
78
|
} | null>(null);
|
|
82
79
|
const [isSaving, setIsSaving] = useState(false);
|
|
80
|
+
const setDirtyForm = useAccountUnsavedChanges((state) => state.setDirtyForm);
|
|
81
|
+
const completeSave = useAccountUnsavedChanges((state) => state.completeSave);
|
|
82
|
+
const dismissUnsavedChanges = useAccountUnsavedChanges((state) => state.dismiss);
|
|
83
83
|
|
|
84
|
-
const { watch, setValue, handleSubmit, reset, formState: { dirtyFields } } =
|
|
84
|
+
const { watch, setValue, handleSubmit, reset, formState: { dirtyFields, isDirty } } =
|
|
85
85
|
useForm<PreferencesFormValues>({
|
|
86
86
|
defaultValues: {
|
|
87
87
|
companyName: '',
|
|
@@ -129,7 +129,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
129
129
|
toast.custom((toastId) => (
|
|
130
130
|
<Toast variant="success" message={translate('common.preferences.savedSuccess')} toastId={toastId} />
|
|
131
131
|
));
|
|
132
|
-
|
|
132
|
+
completeSave();
|
|
133
133
|
if (data.language !== currentLocale) {
|
|
134
134
|
setLocaleCookie(data.language, whitelabel?.domain);
|
|
135
135
|
router.refresh();
|
|
@@ -142,6 +142,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
142
142
|
toastId={toastId}
|
|
143
143
|
/>
|
|
144
144
|
));
|
|
145
|
+
dismissUnsavedChanges();
|
|
145
146
|
} finally {
|
|
146
147
|
setIsSaving(false);
|
|
147
148
|
}
|
|
@@ -174,8 +175,15 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
174
175
|
const handleCancelGlobal = () => {
|
|
175
176
|
setConfirmGlobalOpen(false);
|
|
176
177
|
setPendingSubmit(null);
|
|
178
|
+
dismissUnsavedChanges();
|
|
177
179
|
};
|
|
178
180
|
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!isDirty) return;
|
|
183
|
+
setDirtyForm({ submit: handleSubmit(onSubmit), discard: () => reset() });
|
|
184
|
+
return () => setDirtyForm(null);
|
|
185
|
+
});
|
|
186
|
+
|
|
179
187
|
return (
|
|
180
188
|
<>
|
|
181
189
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col h-full">
|
|
@@ -254,20 +262,8 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
|
|
|
254
262
|
</FormSection>
|
|
255
263
|
</div>
|
|
256
264
|
|
|
257
|
-
<div className="fixed bottom-0 left-0 right-0 lg:absolute flex items-center h-20 px-4 lg:px-8 border-t border-gray-200 justify-
|
|
258
|
-
<Button
|
|
259
|
-
variant="secondary"
|
|
260
|
-
className="h-10!"
|
|
261
|
-
type="button"
|
|
262
|
-
disabled={isSaving}
|
|
263
|
-
onClick={() => {
|
|
264
|
-
reset();
|
|
265
|
-
onClose();
|
|
266
|
-
}}
|
|
267
|
-
>
|
|
268
|
-
{translate('common.actions.cancel')}
|
|
269
|
-
</Button>
|
|
270
|
-
<Button className="h-10!" type="submit" disabled={isSaving}>
|
|
265
|
+
<div className="fixed bottom-0 left-0 right-0 lg:absolute flex items-center h-20 px-4 lg:px-8 border-t border-gray-200 justify-end bg-white z-10">
|
|
266
|
+
<Button className="h-10!" type="submit" disabled={!isDirty || isSaving}>
|
|
271
267
|
{isSaving ? translate('common.actions.saving') : translate('common.actions.saveChanges')}
|
|
272
268
|
</Button>
|
|
273
269
|
</div>
|
|
@@ -59,6 +59,11 @@ const messages = {
|
|
|
59
59
|
},
|
|
60
60
|
|
|
61
61
|
account: {
|
|
62
|
+
unsavedChanges: {
|
|
63
|
+
title: 'Leave without saving?',
|
|
64
|
+
description: 'You have unsaved changes. Do you want to save before leaving?',
|
|
65
|
+
discard: 'Leave without saving',
|
|
66
|
+
},
|
|
62
67
|
notificationTypes: {
|
|
63
68
|
projectUpdates: 'Project updates',
|
|
64
69
|
securityAlerts: 'Security alerts',
|
|
@@ -59,6 +59,11 @@ const messages = {
|
|
|
59
59
|
},
|
|
60
60
|
|
|
61
61
|
account: {
|
|
62
|
+
unsavedChanges: {
|
|
63
|
+
title: '¿Salir sin guardar?',
|
|
64
|
+
description: 'Tienes cambios sin guardar. ¿Deseas guardar antes de salir?',
|
|
65
|
+
discard: 'Salir sin guardar',
|
|
66
|
+
},
|
|
62
67
|
notificationTypes: {
|
|
63
68
|
projectUpdates: 'Actualizaciones de proyectos',
|
|
64
69
|
securityAlerts: 'Alertas de seguridad',
|
|
@@ -68,6 +68,11 @@ const messages = {
|
|
|
68
68
|
|
|
69
69
|
// Conta — modais e seções. `<b>` = trecho em negrito (rich text); `{count}` = interpolação.
|
|
70
70
|
account: {
|
|
71
|
+
unsavedChanges: {
|
|
72
|
+
title: 'Sair sem salvar?',
|
|
73
|
+
description: 'Você fez alterações que ainda não foram salvas. Deseja salvar antes de sair?',
|
|
74
|
+
discard: 'Sair sem salvar',
|
|
75
|
+
},
|
|
71
76
|
notificationTypes: {
|
|
72
77
|
projectUpdates: 'Atualizações de projetos',
|
|
73
78
|
securityAlerts: 'Alertas de segurança',
|
|
@@ -40,7 +40,7 @@ export function createAuthMiddleware(options?: CreateAuthMiddlewareOptions): Mid
|
|
|
40
40
|
? new URL('/login', accountsUrl)
|
|
41
41
|
: new URL('/login', request.url);
|
|
42
42
|
|
|
43
|
-
if (pathname !== '/') {
|
|
43
|
+
if (pathname !== '/' || request.nextUrl.search) {
|
|
44
44
|
// URL absoluta da página de origem — o login vive em outro domínio (accounts),
|
|
45
45
|
// então só o pathname faria o redirect cair no host do accounts.
|
|
46
46
|
const proto =
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
|
|
5
|
+
interface DirtyAccountForm {
|
|
6
|
+
submit: () => void;
|
|
7
|
+
discard: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AccountUnsavedChangesState {
|
|
11
|
+
dirtyForm: DirtyAccountForm | null;
|
|
12
|
+
pendingAction: (() => void) | null;
|
|
13
|
+
|
|
14
|
+
setDirtyForm: (form: DirtyAccountForm | null) => void;
|
|
15
|
+
guard: (action: () => void) => void;
|
|
16
|
+
save: () => void;
|
|
17
|
+
completeSave: () => void;
|
|
18
|
+
discard: () => void;
|
|
19
|
+
dismiss: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const useAccountUnsavedChanges = create<AccountUnsavedChangesState>((set, get) => ({
|
|
23
|
+
dirtyForm: null,
|
|
24
|
+
pendingAction: null,
|
|
25
|
+
|
|
26
|
+
setDirtyForm: (dirtyForm) => set({ dirtyForm }),
|
|
27
|
+
|
|
28
|
+
guard: (action) => {
|
|
29
|
+
if (get().dirtyForm) {
|
|
30
|
+
set({ pendingAction: action });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
action();
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
save: () => get().dirtyForm?.submit(),
|
|
37
|
+
|
|
38
|
+
completeSave: () => {
|
|
39
|
+
const { pendingAction } = get();
|
|
40
|
+
set({ pendingAction: null });
|
|
41
|
+
pendingAction?.();
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
discard: () => {
|
|
45
|
+
const { dirtyForm, pendingAction } = get();
|
|
46
|
+
dirtyForm?.discard();
|
|
47
|
+
set({ dirtyForm: null, pendingAction: null });
|
|
48
|
+
pendingAction?.();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
dismiss: () => set({ pendingAction: null }),
|
|
52
|
+
}));
|