@greatapps/common 1.1.154 → 1.1.156
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/ConfirmDeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/DeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -1
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -1
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -1
- package/dist/components/layouts/AppNavBar.mjs.map +1 -1
- package/dist/components/layouts/NavBar.mjs.map +1 -1
- package/dist/components/layouts/UsersSelectorPopover.mjs +3 -6
- package/dist/components/layouts/UsersSelectorPopover.mjs.map +1 -1
- package/dist/components/navigation/AppNavigation.mjs +3 -1
- package/dist/components/navigation/AppNavigation.mjs.map +1 -1
- package/dist/components/navigation/CancelledSubscriptionBanner.mjs +59 -0
- package/dist/components/navigation/CancelledSubscriptionBanner.mjs.map +1 -0
- package/dist/components/navigation/NavItemsContext.mjs.map +1 -1
- package/dist/components/navigation/shells/DesktopShell.mjs +2 -14
- package/dist/components/navigation/shells/DesktopShell.mjs.map +1 -1
- package/dist/components/navigation/shells/MdShell.mjs +2 -14
- package/dist/components/navigation/shells/MdShell.mjs.map +1 -1
- package/dist/components/navigation/shells/TabsMobileShell.mjs.map +1 -1
- package/dist/components/pages/notifications/Notifications.mjs.map +1 -1
- package/dist/components/ui/buttons/Button.mjs.map +1 -1
- package/dist/components/ui/buttons/CancelButton.mjs.map +1 -1
- package/dist/components/ui/buttons/CopyButton.mjs.map +1 -1
- package/dist/components/ui/data-display/Accordion.mjs.map +1 -1
- package/dist/components/ui/data-display/Badge.mjs.map +1 -1
- package/dist/components/ui/data-display/Card.mjs.map +1 -1
- package/dist/components/ui/data-display/Pagination.mjs.map +1 -1
- package/dist/components/ui/data-display/ScrollArea.mjs.map +1 -1
- package/dist/components/ui/data-display/Separator.mjs.map +1 -1
- package/dist/components/ui/data-display/Table.mjs.map +1 -1
- package/dist/components/ui/data-display/Tabs.mjs.map +1 -1
- package/dist/components/ui/feedback/CircularProgress.mjs.map +1 -1
- package/dist/components/ui/feedback/DefaultCircularProgress.mjs.map +1 -1
- package/dist/components/ui/feedback/LoadingOverlay.mjs.map +1 -1
- package/dist/components/ui/feedback/Progress.mjs.map +1 -1
- package/dist/components/ui/feedback/Toast.mjs.map +1 -1
- package/dist/components/ui/form/Calendar.mjs.map +1 -1
- package/dist/components/ui/form/Checkbox.mjs.map +1 -1
- package/dist/components/ui/form/DatePicker.mjs.map +1 -1
- package/dist/components/ui/form/DateRangePicker.mjs.map +1 -1
- package/dist/components/ui/form/Input.mjs.map +1 -1
- package/dist/components/ui/form/InputOtp.mjs.map +1 -1
- package/dist/components/ui/form/RadioGroup.mjs.map +1 -1
- package/dist/components/ui/form/Select.mjs +1 -1
- package/dist/components/ui/form/Select.mjs.map +1 -1
- package/dist/components/ui/form/Switch.mjs.map +1 -1
- package/dist/components/ui/form/Textarea.mjs.map +1 -1
- package/dist/components/ui/overlay/Command.mjs.map +1 -1
- package/dist/components/ui/overlay/Dialog.mjs.map +1 -1
- package/dist/components/ui/overlay/Popover.mjs.map +1 -1
- package/dist/components/ui/overlay/Sheet.mjs.map +1 -1
- package/dist/components/ui/overlay/Tooltip.mjs.map +1 -1
- package/dist/hooks/copy-to-clipboard.hook.mjs.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.mjs.map +1 -1
- package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -1
- package/dist/modules/accounts/actions/find-current-account.action.mjs.map +1 -1
- package/dist/modules/accounts/actions/list-currencies.action.mjs.map +1 -1
- package/dist/modules/accounts/actions/list-timezones.action.mjs.map +1 -1
- package/dist/modules/accounts/hooks/current-account.hook.mjs.map +1 -1
- package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -1
- package/dist/modules/accounts/hooks/useCurrencies.mjs.map +1 -1
- package/dist/modules/accounts/hooks/useTimezones.mjs.map +1 -1
- package/dist/modules/auth/hooks/login.hook.mjs.map +1 -1
- package/dist/modules/auth/hooks/logout.hook.mjs.map +1 -1
- package/dist/modules/auth/hooks/register.hook.mjs.map +1 -1
- package/dist/modules/ia-credits/actions/list-ia-credits.action.mjs.map +1 -1
- package/dist/modules/projects/actions/add-project-user.action.mjs.map +1 -1
- package/dist/modules/projects/actions/list-available-users.action.mjs.map +1 -1
- package/dist/modules/projects/actions/list-project-users.action.mjs.map +1 -1
- package/dist/modules/projects/actions/remove-project-user.action.mjs.map +1 -1
- package/dist/modules/projects/hooks/list-available-users.hook.mjs.map +1 -1
- package/dist/modules/projects/hooks/list-project-users.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/actions/list-subscriptions.action.mjs.map +1 -1
- package/dist/modules/subscriptions/hooks/find-active-subscription.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/hooks/list-subscriptions.hook.mjs.map +1 -1
- package/dist/modules/users/action/find-user-by-id.action.mjs.map +1 -1
- package/dist/modules/users/action/list-messages.action.mjs.map +1 -1
- package/dist/modules/users/action/mark-all-messages-as-read.action.mjs.map +1 -1
- package/dist/modules/users/action/mark-message-as-read.action.mjs.map +1 -1
- package/dist/modules/users/hooks/messages.hook.mjs.map +1 -1
- package/dist/modules/whitelabel/actions/find-whitelabel-styles.action.mjs.map +1 -1
- package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
- package/dist/modules/whitelabel/services/whitelabel-styles.service.mjs.map +1 -1
- package/dist/providers/whitelabel.provider.mjs.map +1 -1
- package/dist/utils/safeServerAction.mjs.map +1 -1
- package/dist/utils/withAction.mjs.map +1 -1
- package/package.json +90 -89
- package/src/components/account/ConfirmDeleteAccountModal.tsx +55 -55
- package/src/components/account/DeleteAccountModal.tsx +130 -130
- package/src/components/account/IsntPossibleDeleteAccountModal.tsx +49 -49
- package/src/components/account/hooks/useTwoFactorAuthForm.tsx +101 -101
- package/src/components/layouts/AppMobileNavBar.tsx +125 -125
- package/src/components/layouts/AppNavBar.tsx +57 -57
- package/src/components/layouts/NavBar.tsx +112 -112
- package/src/components/layouts/UsersSelectorPopover.tsx +6 -10
- package/src/components/navigation/AppNavigation.tsx +4 -1
- package/src/components/navigation/CancelledSubscriptionBanner.tsx +69 -0
- package/src/components/navigation/NavItemsContext.ts +20 -20
- package/src/components/navigation/shells/DesktopShell.tsx +0 -7
- package/src/components/navigation/shells/MdShell.tsx +0 -6
- package/src/components/navigation/shells/TabsMobileShell.tsx +72 -72
- package/src/components/pages/notifications/Notifications.tsx +160 -160
- package/src/components/ui/buttons/Button.tsx +57 -57
- package/src/components/ui/buttons/CancelButton.tsx +14 -14
- package/src/components/ui/buttons/CopyButton.tsx +54 -54
- package/src/components/ui/data-display/Accordion.tsx +81 -81
- package/src/components/ui/data-display/Badge.tsx +47 -47
- package/src/components/ui/data-display/Card.tsx +93 -93
- package/src/components/ui/data-display/Pagination.tsx +126 -126
- package/src/components/ui/data-display/ScrollArea.tsx +59 -59
- package/src/components/ui/data-display/Separator.tsx +29 -29
- package/src/components/ui/data-display/Table.tsx +117 -117
- package/src/components/ui/data-display/Tabs.tsx +67 -67
- package/src/components/ui/feedback/CircularProgress.tsx +75 -75
- package/src/components/ui/feedback/DefaultCircularProgress.tsx +53 -53
- package/src/components/ui/feedback/LoadingOverlay.tsx +31 -31
- package/src/components/ui/feedback/Progress.tsx +40 -40
- package/src/components/ui/feedback/Toast.tsx +101 -101
- package/src/components/ui/form/Calendar.tsx +228 -228
- package/src/components/ui/form/Checkbox.tsx +31 -31
- package/src/components/ui/form/DatePicker.tsx +97 -97
- package/src/components/ui/form/DateRangePicker.tsx +108 -108
- package/src/components/ui/form/Input.tsx +20 -20
- package/src/components/ui/form/InputOtp.tsx +80 -80
- package/src/components/ui/form/RadioGroup.tsx +45 -45
- package/src/components/ui/form/Select.tsx +200 -200
- package/src/components/ui/form/Switch.tsx +32 -32
- package/src/components/ui/form/Textarea.tsx +19 -19
- package/src/components/ui/overlay/Command.tsx +185 -185
- package/src/components/ui/overlay/Dialog.tsx +151 -151
- package/src/components/ui/overlay/Popover.tsx +49 -49
- package/src/components/ui/overlay/Sheet.tsx +159 -159
- package/src/components/ui/overlay/Tooltip.tsx +62 -62
- package/src/hooks/copy-to-clipboard.hook.tsx +67 -67
- package/src/index.ts +347 -347
- package/src/middleware.ts +4 -4
- package/src/modules/accounts/actions/account-management.action.ts +90 -90
- package/src/modules/accounts/actions/find-current-account.action.ts +9 -9
- package/src/modules/accounts/actions/list-currencies.action.ts +8 -8
- package/src/modules/accounts/actions/list-timezones.action.ts +8 -8
- package/src/modules/accounts/hooks/current-account.hook.tsx +22 -22
- package/src/modules/accounts/hooks/useAccountManagement.ts +85 -85
- package/src/modules/accounts/hooks/useCurrencies.ts +15 -15
- package/src/modules/accounts/hooks/useTimezones.ts +15 -15
- package/src/modules/auth/hooks/login.hook.tsx +13 -13
- package/src/modules/auth/hooks/logout.hook.tsx +11 -11
- package/src/modules/auth/hooks/register.hook.tsx +11 -11
- package/src/modules/ia-credits/actions/list-ia-credits.action.ts +8 -8
- package/src/modules/projects/actions/add-project-user.action.ts +9 -9
- package/src/modules/projects/actions/list-available-users.action.ts +9 -9
- package/src/modules/projects/actions/list-project-users.action.ts +9 -9
- package/src/modules/projects/actions/remove-project-user.action.ts +9 -9
- package/src/modules/projects/hooks/list-available-users.hook.ts +33 -33
- package/src/modules/projects/hooks/list-project-users.hook.ts +33 -33
- package/src/modules/subscriptions/actions/list-subscriptions.action.ts +9 -9
- package/src/modules/subscriptions/hooks/find-active-subscription.hook.ts +19 -19
- package/src/modules/subscriptions/hooks/list-subscriptions.hook.ts +17 -17
- package/src/modules/users/action/find-user-by-id.action.ts +8 -8
- package/src/modules/users/action/list-messages.action.ts +12 -12
- package/src/modules/users/action/mark-all-messages-as-read.action.ts +8 -8
- package/src/modules/users/action/mark-message-as-read.action.ts +8 -8
- package/src/modules/users/hooks/messages.hook.ts +60 -60
- package/src/modules/whitelabel/actions/find-whitelabel-styles.action.ts +14 -14
- package/src/modules/whitelabel/actions/find-whitelabel.action.ts +23 -23
- package/src/modules/whitelabel/services/whitelabel-styles.service.ts +98 -98
- package/src/modules/whitelabel/styles-schema.ts +37 -37
- package/src/providers/whitelabel.provider.tsx +46 -46
- package/src/utils/safeServerAction.ts +21 -21
- package/src/utils/withAction.ts +13 -13
|
@@ -5,6 +5,7 @@ import { DesktopShell } from './shells/DesktopShell';
|
|
|
5
5
|
import { MdShell } from './shells/MdShell';
|
|
6
6
|
import { MobileShell } from './shells/MobileShell';
|
|
7
7
|
import { TabsMobileShell } from './shells/TabsMobileShell';
|
|
8
|
+
import { CancelledSubscriptionBanner } from './CancelledSubscriptionBanner';
|
|
8
9
|
import { NavItems } from './subcomponents/NavItems';
|
|
9
10
|
import { ProjectSelector } from './subcomponents/ProjectSelector';
|
|
10
11
|
import { PlanCard } from './subcomponents/PlanCard';
|
|
@@ -21,13 +22,15 @@ interface AppNavigationProps {
|
|
|
21
22
|
children: ReactNode;
|
|
22
23
|
mobileVariant?: 'sheet' | 'tabs';
|
|
23
24
|
mobileTabsOffset?: string;
|
|
25
|
+
onCancelledReactivate?: () => void;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
function AppNavigationRoot({ children, mobileVariant = 'sheet', mobileTabsOffset }: AppNavigationProps) {
|
|
28
|
+
function AppNavigationRoot({ children, mobileVariant = 'sheet', mobileTabsOffset, onCancelledReactivate }: AppNavigationProps) {
|
|
27
29
|
const [navItems, setNavItems] = useState<NavItemConfig[]>([]);
|
|
28
30
|
|
|
29
31
|
return (
|
|
30
32
|
<NavItemsContext.Provider value={{ navItems, setNavItems, mobileVariant }}>
|
|
33
|
+
<CancelledSubscriptionBanner onReactivate={onCancelledReactivate} />
|
|
31
34
|
<DesktopShell>{children}</DesktopShell>
|
|
32
35
|
<MdShell>{children}</MdShell>
|
|
33
36
|
{mobileVariant === 'sheet' ? (
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { IconAlertOctagon, IconX } from '@tabler/icons-react';
|
|
5
|
+
import { useActiveSubscription } from '../../modules/subscriptions/hooks/find-active-subscription.hook';
|
|
6
|
+
|
|
7
|
+
function daysBetween(a: Date, b: Date): number {
|
|
8
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
9
|
+
const da = new Date(a.getFullYear(), a.getMonth(), a.getDate());
|
|
10
|
+
const db = new Date(b.getFullYear(), b.getMonth(), b.getDate());
|
|
11
|
+
return Math.round((db.getTime() - da.getTime()) / msPerDay);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CancelledSubscriptionBannerProps {
|
|
15
|
+
onReactivate?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CancelledSubscriptionBanner({ onReactivate }: CancelledSubscriptionBannerProps) {
|
|
19
|
+
const { data: { data: [subscription] = [] } = {} } = useActiveSubscription();
|
|
20
|
+
const [visible, setVisible] = useState(true);
|
|
21
|
+
const [animated, setAnimated] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const timer = setTimeout(() => setAnimated(true), 50);
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const dismiss = useCallback(() => setVisible(false), []);
|
|
29
|
+
|
|
30
|
+
const cancellationDate = subscription?.date_cancellation;
|
|
31
|
+
if (!cancellationDate || !visible) return null;
|
|
32
|
+
|
|
33
|
+
const today = new Date();
|
|
34
|
+
const daysSinceCancellation = daysBetween(new Date(cancellationDate), today);
|
|
35
|
+
if (daysSinceCancellation < 0 || daysSinceCancellation > 5) return null;
|
|
36
|
+
|
|
37
|
+
const daysRemaining = Math.max(0, 5 - daysSinceCancellation);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="fixed top-0 lg:left-[273px] left-0 right-0 z-50 flex justify-center pointer-events-none px-4 lg:px-0">
|
|
41
|
+
<div
|
|
42
|
+
className={`flex flex-col sm:flex-row items-start sm:items-center justify-between bg-red-500 rounded-lg px-3 py-2 sm:py-1 w-full lg:w-[805px] max-w-full pointer-events-auto transition-transform duration-500 ease-out gap-2 sm:gap-0 ${animated ? 'translate-y-9' : '-translate-y-full'}`}
|
|
43
|
+
>
|
|
44
|
+
{daysRemaining > 0 && (
|
|
45
|
+
<div className="flex items-center gap-2 flex-1 w-full sm:w-auto">
|
|
46
|
+
<IconAlertOctagon size={16} className="text-white shrink-0" />
|
|
47
|
+
<span className="paragraph-xsmall-semibold text-white">
|
|
48
|
+
Assinatura cancelada! Em {daysRemaining} dias suas páginas serão tiradas do ar!
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="flex items-center gap-1.5 w-full sm:w-auto justify-between sm:justify-start">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className="flex items-center justify-center p-1.5 cursor-pointer"
|
|
56
|
+
onClick={onReactivate}
|
|
57
|
+
>
|
|
58
|
+
<span className="paragraph-xsmall-semibold text-white underline">
|
|
59
|
+
Reativar assinatura
|
|
60
|
+
</span>
|
|
61
|
+
</button>
|
|
62
|
+
<button type="button" className="cursor-pointer shrink-0" onClick={dismiss}>
|
|
63
|
+
<IconX size={18} className="text-red-200" />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext } from 'react';
|
|
4
|
-
import type { NavItemConfig } from './subcomponents/NavItems';
|
|
5
|
-
|
|
6
|
-
interface NavItemsContextValue {
|
|
7
|
-
navItems: NavItemConfig[];
|
|
8
|
-
setNavItems: (items: NavItemConfig[]) => void;
|
|
9
|
-
mobileVariant: 'sheet' | 'tabs';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const NavItemsContext = createContext<NavItemsContextValue>({
|
|
13
|
-
navItems: [],
|
|
14
|
-
setNavItems: () => {},
|
|
15
|
-
mobileVariant: 'sheet',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export function useNavItemsContext(): NavItemsContextValue {
|
|
19
|
-
return useContext(NavItemsContext);
|
|
20
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { NavItemConfig } from './subcomponents/NavItems';
|
|
5
|
+
|
|
6
|
+
interface NavItemsContextValue {
|
|
7
|
+
navItems: NavItemConfig[];
|
|
8
|
+
setNavItems: (items: NavItemConfig[]) => void;
|
|
9
|
+
mobileVariant: 'sheet' | 'tabs';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const NavItemsContext = createContext<NavItemsContextValue>({
|
|
13
|
+
navItems: [],
|
|
14
|
+
setNavItems: () => {},
|
|
15
|
+
mobileVariant: 'sheet',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function useNavItemsContext(): NavItemsContextValue {
|
|
19
|
+
return useContext(NavItemsContext);
|
|
20
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import Image from 'next/image';
|
|
5
4
|
import { SideBarNavigation } from '../../layouts/SideBarNavigation';
|
|
6
5
|
import { AppNavigationContext } from '../AppNavigationContext';
|
|
7
6
|
|
|
@@ -9,12 +8,6 @@ export function DesktopShell({ children }: { children: ReactNode }) {
|
|
|
9
8
|
return (
|
|
10
9
|
<AppNavigationContext.Provider value={{ breakpoint: 'desktop' }}>
|
|
11
10
|
<SideBarNavigation className="justify-between gap-5">
|
|
12
|
-
<Image
|
|
13
|
-
src="/icons/great-icon.svg"
|
|
14
|
-
alt="GreatPages"
|
|
15
|
-
width={28}
|
|
16
|
-
height={28}
|
|
17
|
-
/>
|
|
18
11
|
{children}
|
|
19
12
|
</SideBarNavigation>
|
|
20
13
|
</AppNavigationContext.Provider>
|
|
@@ -9,12 +9,6 @@ export function MdShell({ children }: { children: ReactNode }) {
|
|
|
9
9
|
return (
|
|
10
10
|
<AppNavigationContext.Provider value={{ breakpoint: 'md' }}>
|
|
11
11
|
<MdSideBarNavigation className="justify-between gap-5">
|
|
12
|
-
<Image
|
|
13
|
-
src="/icons/great-icon.svg"
|
|
14
|
-
alt="GreatPages"
|
|
15
|
-
width={28}
|
|
16
|
-
height={28}
|
|
17
|
-
/>
|
|
18
12
|
{children}
|
|
19
13
|
</MdSideBarNavigation>
|
|
20
14
|
</AppNavigationContext.Provider>
|
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { ReactNode, useRef, useEffect } from 'react';
|
|
4
|
-
import { AppNavigationContext } from '../AppNavigationContext';
|
|
5
|
-
import { useNavItemsContext } from '../NavItemsContext';
|
|
6
|
-
|
|
7
|
-
function InternalTabsBar({ topOffset = '72px' }: { topOffset?: string }) {
|
|
8
|
-
const { navItems } = useNavItemsContext();
|
|
9
|
-
const tabsListRef = useRef<HTMLDivElement>(null);
|
|
10
|
-
const activeTabRef = useRef<HTMLButtonElement>(null);
|
|
11
|
-
|
|
12
|
-
const activeLabel = navItems.find((i) => i.isActive)?.label;
|
|
13
|
-
|
|
14
|
-
// Scroll active tab into center when active item changes
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (activeTabRef.current && tabsListRef.current) {
|
|
17
|
-
const tabsList = tabsListRef.current;
|
|
18
|
-
const activeTabElement = activeTabRef.current;
|
|
19
|
-
const tabsListRect = tabsList.getBoundingClientRect();
|
|
20
|
-
const activeTabRect = activeTabElement.getBoundingClientRect();
|
|
21
|
-
const scrollLeft =
|
|
22
|
-
activeTabElement.offsetLeft - tabsListRect.width / 2 + activeTabRect.width / 2;
|
|
23
|
-
tabsList.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' });
|
|
24
|
-
}
|
|
25
|
-
}, [activeLabel]);
|
|
26
|
-
|
|
27
|
-
if (!navItems.length) return null;
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
ref={tabsListRef}
|
|
32
|
-
className="md:hidden fixed left-0 right-0 z-40 bg-white border-b border-gray-200 flex overflow-x-auto scrollbar-hide"
|
|
33
|
-
style={{ top: topOffset }}
|
|
34
|
-
>
|
|
35
|
-
{navItems.map((item) => (
|
|
36
|
-
<button
|
|
37
|
-
key={item.label}
|
|
38
|
-
ref={item.isActive ? activeTabRef : undefined}
|
|
39
|
-
onClick={item.onClick}
|
|
40
|
-
className={`flex items-center gap-2 px-4 py-3 shrink-0 border-b-2 transition-colors ${
|
|
41
|
-
item.isActive
|
|
42
|
-
? 'border-gray-950 text-gray-950'
|
|
43
|
-
: 'border-transparent text-gray-500 hover:text-gray-900'
|
|
44
|
-
}`}
|
|
45
|
-
>
|
|
46
|
-
{item.icon && <item.icon size={16} className="shrink-0" />}
|
|
47
|
-
<span
|
|
48
|
-
className={
|
|
49
|
-
item.isActive ? 'paragraph-small-semibold' : 'paragraph-small-medium'
|
|
50
|
-
}
|
|
51
|
-
>
|
|
52
|
-
{item.label}
|
|
53
|
-
</span>
|
|
54
|
-
</button>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function TabsMobileShell({ children, topOffset }: { children: ReactNode; topOffset?: string }) {
|
|
61
|
-
return (
|
|
62
|
-
<AppNavigationContext.Provider value={{ breakpoint: 'mobile' }}>
|
|
63
|
-
{/*
|
|
64
|
-
Children are hidden visually, but NavItems' useEffect still fires inside
|
|
65
|
-
display:none — React effects are tied to commit phase, not layout visibility.
|
|
66
|
-
This registers navItems in NavItemsContext so InternalTabsBar can read them.
|
|
67
|
-
*/}
|
|
68
|
-
<div className="hidden">{children}</div>
|
|
69
|
-
<InternalTabsBar topOffset={topOffset} />
|
|
70
|
-
</AppNavigationContext.Provider>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useRef, useEffect } from 'react';
|
|
4
|
+
import { AppNavigationContext } from '../AppNavigationContext';
|
|
5
|
+
import { useNavItemsContext } from '../NavItemsContext';
|
|
6
|
+
|
|
7
|
+
function InternalTabsBar({ topOffset = '72px' }: { topOffset?: string }) {
|
|
8
|
+
const { navItems } = useNavItemsContext();
|
|
9
|
+
const tabsListRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const activeTabRef = useRef<HTMLButtonElement>(null);
|
|
11
|
+
|
|
12
|
+
const activeLabel = navItems.find((i) => i.isActive)?.label;
|
|
13
|
+
|
|
14
|
+
// Scroll active tab into center when active item changes
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (activeTabRef.current && tabsListRef.current) {
|
|
17
|
+
const tabsList = tabsListRef.current;
|
|
18
|
+
const activeTabElement = activeTabRef.current;
|
|
19
|
+
const tabsListRect = tabsList.getBoundingClientRect();
|
|
20
|
+
const activeTabRect = activeTabElement.getBoundingClientRect();
|
|
21
|
+
const scrollLeft =
|
|
22
|
+
activeTabElement.offsetLeft - tabsListRect.width / 2 + activeTabRect.width / 2;
|
|
23
|
+
tabsList.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' });
|
|
24
|
+
}
|
|
25
|
+
}, [activeLabel]);
|
|
26
|
+
|
|
27
|
+
if (!navItems.length) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
ref={tabsListRef}
|
|
32
|
+
className="md:hidden fixed left-0 right-0 z-40 bg-white border-b border-gray-200 flex overflow-x-auto scrollbar-hide"
|
|
33
|
+
style={{ top: topOffset }}
|
|
34
|
+
>
|
|
35
|
+
{navItems.map((item) => (
|
|
36
|
+
<button
|
|
37
|
+
key={item.label}
|
|
38
|
+
ref={item.isActive ? activeTabRef : undefined}
|
|
39
|
+
onClick={item.onClick}
|
|
40
|
+
className={`flex items-center gap-2 px-4 py-3 shrink-0 border-b-2 transition-colors ${
|
|
41
|
+
item.isActive
|
|
42
|
+
? 'border-gray-950 text-gray-950'
|
|
43
|
+
: 'border-transparent text-gray-500 hover:text-gray-900'
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{item.icon && <item.icon size={16} className="shrink-0" />}
|
|
47
|
+
<span
|
|
48
|
+
className={
|
|
49
|
+
item.isActive ? 'paragraph-small-semibold' : 'paragraph-small-medium'
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
{item.label}
|
|
53
|
+
</span>
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function TabsMobileShell({ children, topOffset }: { children: ReactNode; topOffset?: string }) {
|
|
61
|
+
return (
|
|
62
|
+
<AppNavigationContext.Provider value={{ breakpoint: 'mobile' }}>
|
|
63
|
+
{/*
|
|
64
|
+
Children are hidden visually, but NavItems' useEffect still fires inside
|
|
65
|
+
display:none — React effects are tied to commit phase, not layout visibility.
|
|
66
|
+
This registers navItems in NavItemsContext so InternalTabsBar can read them.
|
|
67
|
+
*/}
|
|
68
|
+
<div className="hidden">{children}</div>
|
|
69
|
+
<InternalTabsBar topOffset={topOffset} />
|
|
70
|
+
</AppNavigationContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Bell, Check, Loader2 } from "lucide-react";
|
|
4
|
-
import { Button } from "../../ui/buttons/Button";
|
|
5
|
-
import {
|
|
6
|
-
Tabs,
|
|
7
|
-
TabsContent,
|
|
8
|
-
TabsList,
|
|
9
|
-
TabsTrigger,
|
|
10
|
-
} from "../../ui/data-display/Tabs";
|
|
11
|
-
import { useInfiniteMessages, useMarkAllMessagesAsRead } from "../../../modules/users/hooks/messages.hook";
|
|
12
|
-
import NotificationCard from "../../widgets/notifications/NotificationCard";
|
|
13
|
-
import { Toast } from "../../ui/feedback/Toast";
|
|
14
|
-
import { formatDistanceToNow } from "date-fns";
|
|
15
|
-
import { ptBR } from "date-fns/locale";
|
|
16
|
-
import { toast } from "sonner";
|
|
17
|
-
import React from "react";
|
|
18
|
-
|
|
19
|
-
function NotificationsList({
|
|
20
|
-
pages,
|
|
21
|
-
isFetchingNextPage,
|
|
22
|
-
hasNextPage,
|
|
23
|
-
fetchNextPage,
|
|
24
|
-
}: {
|
|
25
|
-
pages: ReturnType<typeof useInfiniteMessages>["data"];
|
|
26
|
-
isFetchingNextPage: boolean;
|
|
27
|
-
hasNextPage: boolean;
|
|
28
|
-
fetchNextPage: () => void;
|
|
29
|
-
}) {
|
|
30
|
-
const notifications = pages?.pages.flatMap((p) => p.data) ?? [];
|
|
31
|
-
|
|
32
|
-
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
33
|
-
const target = e.currentTarget;
|
|
34
|
-
const nearBottom =
|
|
35
|
-
target.scrollTop + target.clientHeight >= target.scrollHeight - 50;
|
|
36
|
-
if (!nearBottom) return;
|
|
37
|
-
if (hasNextPage && !isFetchingNextPage) {
|
|
38
|
-
fetchNextPage();
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div
|
|
44
|
-
className="flex flex-col border border-gray-200 rounded-lg w-full bg-white max-h-[70vh] overflow-y-auto custom-scrollbar"
|
|
45
|
-
onScroll={handleScroll}
|
|
46
|
-
>
|
|
47
|
-
{notifications.map((notification, index) => (
|
|
48
|
-
<NotificationCard
|
|
49
|
-
key={notification.id}
|
|
50
|
-
icon={<Bell size={20} className="text-pages-600" />}
|
|
51
|
-
iconBgColor="bg-pages-100"
|
|
52
|
-
title={notification.subject}
|
|
53
|
-
description={notification.message}
|
|
54
|
-
gapPx={16}
|
|
55
|
-
time={formatDistanceToNow(new Date(notification.datetime_add), {
|
|
56
|
-
addSuffix: true,
|
|
57
|
-
locale: ptBR,
|
|
58
|
-
})}
|
|
59
|
-
isUnread={notification.status === 1}
|
|
60
|
-
showBorder={index < notifications.length - 1}
|
|
61
|
-
/>
|
|
62
|
-
))}
|
|
63
|
-
{isFetchingNextPage && (
|
|
64
|
-
<div className="flex items-center justify-center py-4">
|
|
65
|
-
<Loader2 size={20} className="animate-spin text-gray-400" />
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function NotificationPageContent() {
|
|
73
|
-
const unreadNotifications = useInfiniteMessages({
|
|
74
|
-
readed: false,
|
|
75
|
-
type: "notification",
|
|
76
|
-
order: "desc",
|
|
77
|
-
});
|
|
78
|
-
const allNotifications = useInfiniteMessages({
|
|
79
|
-
type: "notification",
|
|
80
|
-
order: "desc",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const totalAll = allNotifications.data?.pages[0]?.total ?? 0;
|
|
84
|
-
const totalUnread = unreadNotifications.data?.pages[0]?.total ?? 0;
|
|
85
|
-
|
|
86
|
-
const { mutate: markAllAsRead, isPending: isMarkingAll } = useMarkAllMessagesAsRead();
|
|
87
|
-
|
|
88
|
-
const handleMarkAllAsRead = () => {
|
|
89
|
-
markAllAsRead(undefined, {
|
|
90
|
-
onSuccess: () => {
|
|
91
|
-
toast.custom((t) => (
|
|
92
|
-
<Toast variant="success" message="Todas as notificações foram marcadas como lidas" toastId={t} />
|
|
93
|
-
));
|
|
94
|
-
},
|
|
95
|
-
onError: () => {
|
|
96
|
-
toast.custom((t) => (
|
|
97
|
-
<Toast variant="error" message="Erro ao marcar notificações como lidas" toastId={t} />
|
|
98
|
-
));
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div className="flex flex-col gap-6 lg:gap-10 px-4 py-6 lg:px-20 lg:py-14">
|
|
105
|
-
<div className="flex items-center justify-between gap-4">
|
|
106
|
-
<div className="flex flex-col gap-2">
|
|
107
|
-
<h1 className="paragraph-xlarge-semibold lg:title-large-semibold text-gray-950">
|
|
108
|
-
Notificações
|
|
109
|
-
</h1>
|
|
110
|
-
<p className="paragraph-medium-regular text-gray-600 hidden lg:block">
|
|
111
|
-
Tenha controle total sobre páginas, experiências e conversões.
|
|
112
|
-
</p>
|
|
113
|
-
</div>
|
|
114
|
-
<Button
|
|
115
|
-
variant={"secondary"}
|
|
116
|
-
className="h-10! lg:h-12! shrink-0"
|
|
117
|
-
onClick={handleMarkAllAsRead}
|
|
118
|
-
disabled={isMarkingAll || totalUnread === 0}
|
|
119
|
-
>
|
|
120
|
-
{isMarkingAll ? <Loader2 size={20} className="animate-spin" /> : <Check size={20} />}
|
|
121
|
-
<span className="lg:block hidden">Marcar todos como lidos</span>
|
|
122
|
-
<span className="lg:hidden">Marcar como lidos</span>
|
|
123
|
-
</Button>
|
|
124
|
-
</div>
|
|
125
|
-
<Tabs
|
|
126
|
-
defaultValue="all"
|
|
127
|
-
className="flex flex-col w-full bg-transparent gap-6 lg:gap-8"
|
|
128
|
-
>
|
|
129
|
-
<TabsList className="h-auto pb-0 bg-transparent">
|
|
130
|
-
<TabsTrigger className="gap-2" value="all">
|
|
131
|
-
Todas as notificações{" "}
|
|
132
|
-
<span className="text-gray-500">({totalAll})</span>
|
|
133
|
-
</TabsTrigger>
|
|
134
|
-
<TabsTrigger className="gap-2" value="unreads" disabled={totalUnread === 0}>
|
|
135
|
-
Não lidas{" "}
|
|
136
|
-
<span className="text-gray-500">({totalUnread})</span>
|
|
137
|
-
</TabsTrigger>
|
|
138
|
-
</TabsList>
|
|
139
|
-
|
|
140
|
-
<TabsContent value="all">
|
|
141
|
-
<NotificationsList
|
|
142
|
-
pages={allNotifications.data}
|
|
143
|
-
isFetchingNextPage={allNotifications.isFetchingNextPage}
|
|
144
|
-
hasNextPage={allNotifications.hasNextPage}
|
|
145
|
-
fetchNextPage={allNotifications.fetchNextPage}
|
|
146
|
-
/>
|
|
147
|
-
</TabsContent>
|
|
148
|
-
|
|
149
|
-
<TabsContent value="unreads">
|
|
150
|
-
<NotificationsList
|
|
151
|
-
pages={unreadNotifications.data}
|
|
152
|
-
isFetchingNextPage={unreadNotifications.isFetchingNextPage}
|
|
153
|
-
hasNextPage={unreadNotifications.hasNextPage}
|
|
154
|
-
fetchNextPage={unreadNotifications.fetchNextPage}
|
|
155
|
-
/>
|
|
156
|
-
</TabsContent>
|
|
157
|
-
</Tabs>
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Bell, Check, Loader2 } from "lucide-react";
|
|
4
|
+
import { Button } from "../../ui/buttons/Button";
|
|
5
|
+
import {
|
|
6
|
+
Tabs,
|
|
7
|
+
TabsContent,
|
|
8
|
+
TabsList,
|
|
9
|
+
TabsTrigger,
|
|
10
|
+
} from "../../ui/data-display/Tabs";
|
|
11
|
+
import { useInfiniteMessages, useMarkAllMessagesAsRead } from "../../../modules/users/hooks/messages.hook";
|
|
12
|
+
import NotificationCard from "../../widgets/notifications/NotificationCard";
|
|
13
|
+
import { Toast } from "../../ui/feedback/Toast";
|
|
14
|
+
import { formatDistanceToNow } from "date-fns";
|
|
15
|
+
import { ptBR } from "date-fns/locale";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import React from "react";
|
|
18
|
+
|
|
19
|
+
function NotificationsList({
|
|
20
|
+
pages,
|
|
21
|
+
isFetchingNextPage,
|
|
22
|
+
hasNextPage,
|
|
23
|
+
fetchNextPage,
|
|
24
|
+
}: {
|
|
25
|
+
pages: ReturnType<typeof useInfiniteMessages>["data"];
|
|
26
|
+
isFetchingNextPage: boolean;
|
|
27
|
+
hasNextPage: boolean;
|
|
28
|
+
fetchNextPage: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
const notifications = pages?.pages.flatMap((p) => p.data) ?? [];
|
|
31
|
+
|
|
32
|
+
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
33
|
+
const target = e.currentTarget;
|
|
34
|
+
const nearBottom =
|
|
35
|
+
target.scrollTop + target.clientHeight >= target.scrollHeight - 50;
|
|
36
|
+
if (!nearBottom) return;
|
|
37
|
+
if (hasNextPage && !isFetchingNextPage) {
|
|
38
|
+
fetchNextPage();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className="flex flex-col border border-gray-200 rounded-lg w-full bg-white max-h-[70vh] overflow-y-auto custom-scrollbar"
|
|
45
|
+
onScroll={handleScroll}
|
|
46
|
+
>
|
|
47
|
+
{notifications.map((notification, index) => (
|
|
48
|
+
<NotificationCard
|
|
49
|
+
key={notification.id}
|
|
50
|
+
icon={<Bell size={20} className="text-pages-600" />}
|
|
51
|
+
iconBgColor="bg-pages-100"
|
|
52
|
+
title={notification.subject}
|
|
53
|
+
description={notification.message}
|
|
54
|
+
gapPx={16}
|
|
55
|
+
time={formatDistanceToNow(new Date(notification.datetime_add), {
|
|
56
|
+
addSuffix: true,
|
|
57
|
+
locale: ptBR,
|
|
58
|
+
})}
|
|
59
|
+
isUnread={notification.status === 1}
|
|
60
|
+
showBorder={index < notifications.length - 1}
|
|
61
|
+
/>
|
|
62
|
+
))}
|
|
63
|
+
{isFetchingNextPage && (
|
|
64
|
+
<div className="flex items-center justify-center py-4">
|
|
65
|
+
<Loader2 size={20} className="animate-spin text-gray-400" />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function NotificationPageContent() {
|
|
73
|
+
const unreadNotifications = useInfiniteMessages({
|
|
74
|
+
readed: false,
|
|
75
|
+
type: "notification",
|
|
76
|
+
order: "desc",
|
|
77
|
+
});
|
|
78
|
+
const allNotifications = useInfiniteMessages({
|
|
79
|
+
type: "notification",
|
|
80
|
+
order: "desc",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const totalAll = allNotifications.data?.pages[0]?.total ?? 0;
|
|
84
|
+
const totalUnread = unreadNotifications.data?.pages[0]?.total ?? 0;
|
|
85
|
+
|
|
86
|
+
const { mutate: markAllAsRead, isPending: isMarkingAll } = useMarkAllMessagesAsRead();
|
|
87
|
+
|
|
88
|
+
const handleMarkAllAsRead = () => {
|
|
89
|
+
markAllAsRead(undefined, {
|
|
90
|
+
onSuccess: () => {
|
|
91
|
+
toast.custom((t) => (
|
|
92
|
+
<Toast variant="success" message="Todas as notificações foram marcadas como lidas" toastId={t} />
|
|
93
|
+
));
|
|
94
|
+
},
|
|
95
|
+
onError: () => {
|
|
96
|
+
toast.custom((t) => (
|
|
97
|
+
<Toast variant="error" message="Erro ao marcar notificações como lidas" toastId={t} />
|
|
98
|
+
));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-col gap-6 lg:gap-10 px-4 py-6 lg:px-20 lg:py-14">
|
|
105
|
+
<div className="flex items-center justify-between gap-4">
|
|
106
|
+
<div className="flex flex-col gap-2">
|
|
107
|
+
<h1 className="paragraph-xlarge-semibold lg:title-large-semibold text-gray-950">
|
|
108
|
+
Notificações
|
|
109
|
+
</h1>
|
|
110
|
+
<p className="paragraph-medium-regular text-gray-600 hidden lg:block">
|
|
111
|
+
Tenha controle total sobre páginas, experiências e conversões.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<Button
|
|
115
|
+
variant={"secondary"}
|
|
116
|
+
className="h-10! lg:h-12! shrink-0"
|
|
117
|
+
onClick={handleMarkAllAsRead}
|
|
118
|
+
disabled={isMarkingAll || totalUnread === 0}
|
|
119
|
+
>
|
|
120
|
+
{isMarkingAll ? <Loader2 size={20} className="animate-spin" /> : <Check size={20} />}
|
|
121
|
+
<span className="lg:block hidden">Marcar todos como lidos</span>
|
|
122
|
+
<span className="lg:hidden">Marcar como lidos</span>
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
<Tabs
|
|
126
|
+
defaultValue="all"
|
|
127
|
+
className="flex flex-col w-full bg-transparent gap-6 lg:gap-8"
|
|
128
|
+
>
|
|
129
|
+
<TabsList className="h-auto pb-0 bg-transparent">
|
|
130
|
+
<TabsTrigger className="gap-2" value="all">
|
|
131
|
+
Todas as notificações{" "}
|
|
132
|
+
<span className="text-gray-500">({totalAll})</span>
|
|
133
|
+
</TabsTrigger>
|
|
134
|
+
<TabsTrigger className="gap-2" value="unreads" disabled={totalUnread === 0}>
|
|
135
|
+
Não lidas{" "}
|
|
136
|
+
<span className="text-gray-500">({totalUnread})</span>
|
|
137
|
+
</TabsTrigger>
|
|
138
|
+
</TabsList>
|
|
139
|
+
|
|
140
|
+
<TabsContent value="all">
|
|
141
|
+
<NotificationsList
|
|
142
|
+
pages={allNotifications.data}
|
|
143
|
+
isFetchingNextPage={allNotifications.isFetchingNextPage}
|
|
144
|
+
hasNextPage={allNotifications.hasNextPage}
|
|
145
|
+
fetchNextPage={allNotifications.fetchNextPage}
|
|
146
|
+
/>
|
|
147
|
+
</TabsContent>
|
|
148
|
+
|
|
149
|
+
<TabsContent value="unreads">
|
|
150
|
+
<NotificationsList
|
|
151
|
+
pages={unreadNotifications.data}
|
|
152
|
+
isFetchingNextPage={unreadNotifications.isFetchingNextPage}
|
|
153
|
+
hasNextPage={unreadNotifications.hasNextPage}
|
|
154
|
+
fetchNextPage={unreadNotifications.fetchNextPage}
|
|
155
|
+
/>
|
|
156
|
+
</TabsContent>
|
|
157
|
+
</Tabs>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|