Skip to content

Commit 3c39a29

Browse files
committed
feat(alert): implement global alert container for toast notifications
1 parent 0790b17 commit 3c39a29

File tree

13 files changed

+292
-140
lines changed

13 files changed

+292
-140
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ jobs:
3333
- name: Install dependencies
3434
run: pnpm install --frozen-lockfile
3535

36+
- name: Generate Type
37+
run: pnpm generate:type
38+
3639
- name: Lint
3740
run: pnpm lint
3841
build:
@@ -58,5 +61,8 @@ jobs:
5861
- name: Install dependencies
5962
run: pnpm install --frozen-lockfile
6063

64+
- name: Generate Type
65+
run: pnpm generate:type
66+
6167
- name: Build
6268
run: pnpm build

app/components/Navbar/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { gql } from 'urql';
1212

1313
import UserContext from '#contexts/UserContext';
1414
import { useLogoutMutation } from '#generated/types/graphql';
15+
import useAlert from '#hooks/useAlert';
1516

1617
import styles from './styles.module.css';
1718

@@ -24,18 +25,20 @@ const LOGOUT = gql`
2425

2526
function Navbar() {
2627
const { user, setUser } = use(UserContext);
28+
const alert = useAlert();
2729
const navigate = useNavigate();
2830

29-
const [{ fetching }, triggerLogout] = useLogoutMutation();
31+
const [{ fetching: pendingLogout }, triggerLogout] = useLogoutMutation();
3032

3133
const handleLogout = useCallback(async () => {
3234
const res = await triggerLogout({});
3335
const logoutResponse = res.data?.logout;
3436
if (logoutResponse) {
3537
setUser(undefined);
3638
navigate('/login');
39+
alert.show('Logout Successful', { variant: 'success' });
3740
}
38-
}, [navigate, triggerLogout, setUser]);
41+
}, [navigate, triggerLogout, setUser, alert]);
3942

4043
return (
4144
<nav className={styles.navbar}>
@@ -61,8 +64,9 @@ function Navbar() {
6164
variant="tertiary"
6265
className={styles.dropdownOption}
6366
onClick={handleLogout}
67+
disabled={pendingLogout}
6468
>
65-
{fetching ? 'Logging out' : 'Logout'}
69+
{pendingLogout ? 'Logging out' : 'Logout'}
6670
</Button>
6771
</React.Fragment>
6872
</DropdownMenu>

app/components/Navbar/styles.module.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
}
2121
.dropdown-option {
2222
padding: var(--go-ui-spacing-xs);
23-
margin-left: var(--go-ui-spacing-md);
23+
padding-left: var(--go-ui-spacing-md);
2424
cursor: pointer;
25+
overflow: hidden;
26+
width: 100%;
2527
&:hover {
2628
color: var(--go-ui-color-primary-red);
2729
}

app/components/Navigation/index.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ReactNode,
3+
useCallback,
34
useState,
45
} from 'react';
56
import {
@@ -20,21 +21,24 @@ export interface NavigationItem {
2021
children?: NavigationItem[];
2122

2223
}
23-
function Navigation({ navigationItem }: { navigationItem: NavigationItem[] }) {
24-
const [openIndexes, setOpenIndexes] = useState(
24+
interface NavigationProps {
25+
navigationItem: NavigationItem[];
26+
}
27+
function Navigation({ navigationItem }: NavigationProps) {
28+
const [openAccordion, setOpenAccordion] = useState(
2529
navigationItem.map((_, i) => i),
2630
);
2731

28-
const toggleAccordion = (index: number) => {
29-
setOpenIndexes((prev) => (prev.includes(index)
32+
const toggleAccordion = useCallback((index: number) => {
33+
setOpenAccordion((prev) => (prev.includes(index)
3034
? prev.filter((i) => i !== index)
3135
: [...prev, index]));
32-
};
36+
}, []);
3337

3438
return (
3539
<nav className={styles.nav}>
3640
{navigationItem.map((item, index) => {
37-
const isOpen = openIndexes.includes(index);
41+
const isOpen = openAccordion.includes(index);
3842
return (
3943
<div
4044
key={item.title}
@@ -43,11 +47,11 @@ function Navigation({ navigationItem }: { navigationItem: NavigationItem[] }) {
4347
)}
4448
>
4549
<Button
46-
name={item.title}
50+
name={index}
4751
type="button"
4852
className={styles.navHeaderContainer}
4953
childrenContainerClassName={styles.navHeader}
50-
onClick={() => toggleAccordion(index)}
54+
onClick={toggleAccordion}
5155
variant="tertiary"
5256
icons={item.icon}
5357
>

app/components/Page/styles.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
.leftPane {
88
display: flex;
99
flex-direction: column;
10-
box-shadow: 0px 4px 4px 0px rgba(227, 227, 227, 0.25);
1110
background-color: var(--go-ui-color-white);
1211
gap: var(--go-ui-spacing-md);
1312
}

app/hooks/useAlert.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
use,
3+
useCallback,
4+
useMemo,
5+
} from 'react';
6+
import {
7+
AlertContext,
8+
AlertType,
9+
} from '@ifrc-go/ui/contexts';
10+
import { randomString } from '@togglecorp/fujs';
11+
12+
const DEFAULT_ALERT_DISMISS_DURATION = 4500;
13+
14+
interface AddAlertOption {
15+
name?: string;
16+
variant?: AlertType;
17+
duration?: number;
18+
description?: React.ReactNode;
19+
nonDismissable?: boolean;
20+
debugMessage?: string;
21+
}
22+
23+
function useAlert() {
24+
const {
25+
addAlert,
26+
// removeAlert,
27+
// updateAlert,
28+
} = use(AlertContext);
29+
30+
const show = useCallback((title: React.ReactNode, options?: AddAlertOption) => {
31+
const name = options?.name ?? randomString(16);
32+
addAlert({
33+
variant: options?.variant ?? 'info',
34+
duration: options?.duration ?? DEFAULT_ALERT_DISMISS_DURATION,
35+
name: options?.name ?? name,
36+
title,
37+
description: options?.description,
38+
nonDismissable: options?.nonDismissable ?? false,
39+
debugMessage: options?.debugMessage,
40+
});
41+
42+
return name;
43+
}, [addAlert]);
44+
45+
return useMemo(() => ({
46+
show,
47+
}), [show]);
48+
}
49+
50+
export default useAlert;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
useCallback,
3+
useMemo,
4+
useState,
5+
} from 'react';
6+
import {
7+
AlertContextProps,
8+
AlertParams,
9+
} from '@ifrc-go/ui/contexts';
10+
import { unique } from '@togglecorp/fujs';
11+
12+
function useAlertContextProviderValue() {
13+
const [alerts, setAlerts] = useState<AlertParams[]>([]);
14+
15+
const addAlert = useCallback((alert: AlertParams) => {
16+
setAlerts((prevAlerts) => unique(
17+
[...prevAlerts, alert],
18+
(a) => a.name,
19+
) ?? prevAlerts);
20+
}, [setAlerts]);
21+
22+
const removeAlert = useCallback((name: AlertParams['name']) => {
23+
setAlerts((prevAlerts) => {
24+
const i = prevAlerts.findIndex((a) => a.name === name);
25+
if (i === -1) {
26+
return prevAlerts;
27+
}
28+
29+
const newAlerts = [...prevAlerts];
30+
newAlerts.splice(i, 1);
31+
32+
return newAlerts;
33+
});
34+
}, [setAlerts]);
35+
36+
const updateAlert = useCallback((name: AlertParams['name'], paramsWithoutName: Omit<AlertParams, 'name'>) => {
37+
setAlerts((prevAlerts) => {
38+
const i = prevAlerts.findIndex((a) => a.name === name);
39+
if (i === -1) {
40+
return prevAlerts;
41+
}
42+
43+
const updatedAlert = {
44+
...prevAlerts[i],
45+
paramsWithoutName,
46+
};
47+
48+
const newAlerts = [...prevAlerts];
49+
newAlerts.splice(i, 1, updatedAlert);
50+
51+
return newAlerts;
52+
});
53+
}, [setAlerts]);
54+
55+
const alertContextValue: AlertContextProps = useMemo(() => ({
56+
alerts,
57+
addAlert,
58+
updateAlert,
59+
removeAlert,
60+
}), [alerts, addAlert, updateAlert, removeAlert]);
61+
62+
return alertContextValue;
63+
}
64+
65+
export default useAlertContextProviderValue;

app/root/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
} from 'react';
55
import { Cookies } from 'react-cookie';
66
import { Outlet } from 'react-router';
7+
import { AlertContainer } from '@ifrc-go/ui';
8+
import { AlertContext } from '@ifrc-go/ui/contexts';
79
import {
810
cacheExchange,
911
Client,
@@ -12,6 +14,7 @@ import {
1214
} from 'urql';
1315

1416
import UserContext, { type UserContextInterface } from '#contexts/UserContext';
17+
import useAlertContextProviderValue from '#hooks/useAlertContextProviderValue';
1518

1619
import type { User } from './types/user';
1720

@@ -32,6 +35,7 @@ const gqlClient = new Client({
3235
credentials: 'include',
3336
}),
3437
requestPolicy: 'cache-and-network',
38+
suspense: false,
3539
});
3640

3741
function Root() {
@@ -50,10 +54,15 @@ function Root() {
5054
setUser,
5155
],
5256
);
57+
const alertContextValue = useAlertContextProviderValue();
58+
5359
return (
5460
<UrqlProvider value={gqlClient}>
5561
<UserContext.Provider value={userContext}>
56-
<Outlet />
62+
<AlertContext.Provider value={alertContextValue}>
63+
<AlertContainer />
64+
<Outlet />
65+
</AlertContext.Provider>
5766
</UserContext.Provider>
5867
</UrqlProvider>
5968
);

0 commit comments

Comments
 (0)