Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Facelift: Alerts #120

Merged
merged 14 commits into from
Feb 23, 2024
81 changes: 71 additions & 10 deletions src/client/components/common/Alert/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,103 @@ import { Severity } from '../../../../server/checkResult';
import SuccessIcon from './sucess.svg';
import ErrorIcon from './error.svg';
import WarningIcon from './warning.svg';
import InfoIcon from '../../../../client/img/InfoIcon.svg';
import styles from './alert.module.scss';
import Image from 'next/image';
import classNames from 'classnames';
import { TEST_IDS } from '../../../testIDs';
import { CustomContentProps, VariantType, useSnackbar, SnackbarKey } from 'notistack';
import React from 'react';
import Button from '../Button/Button';
import { CrossIconSvg } from '../../../img/crossIconSvg';

type AlertProps = {
severity: Severity;
className?: string;
dataTestId?: string;
} & PropsWithChildren;

const STYLES_MAP: Record<Severity, keyof typeof styles> = {
const STYLES_MAP: Record<VariantType, keyof typeof styles> = {
error: styles.error,
warning: styles.warning,
success: styles.success,
default: styles.info,
info: styles.info,
};

const ICON_MAP: Record<Severity, any> = {
error: ErrorIcon,
warning: WarningIcon,
success: SuccessIcon,
export const ALERT_ICON_MAP: Record<VariantType, any> = {
error: <Image src={ErrorIcon} alt="" />,
warning: <Image src={WarningIcon} alt="" />,
success: <Image src={SuccessIcon} alt="" />,
default: <Image src={InfoIcon} alt="" />,
info: <Image src={InfoIcon} alt="" width={24} height={24} style={{ padding: '2px' }} />,
};

const Alert: FunctionComponent<AlertProps> = ({ severity, children, className, dataTestId }) => {
/**
* Static on-page alert/notification
*/
export const Alert: FunctionComponent<AlertProps> = ({ severity, children, className, dataTestId }) => {
return (
<div
className={classNames(styles.alert, STYLES_MAP[severity], className)}
data-testid={classNames(TEST_IDS.common.alert, dataTestId)}
data-test-severity={severity}
>
<div className={styles.iconWrapper}>
<Image src={ICON_MAP[severity]} alt="" />
</div>
<div className={styles.iconWrapper}>{ALERT_ICON_MAP[severity]}</div>
<div>{children}</div>
</div>
);
};

export default Alert;
/**
* Custom `notistack` snackbar/popup (visually similar to the static alert above, but a separate component)
*/
export const CustomSnackbar = React.forwardRef<HTMLDivElement, CustomContentProps>((props, ref) => {
const {
// You have access to notistack props and options 👇🏼
id,
action,
message,
variant,
className,
...other
} = props;

return (
<div
ref={ref}
role="alert"
className={classNames(styles.snackbar, styles.withBorder, STYLES_MAP[variant], className)}
{...other}
>
<div className={styles.snackbarContent}>
<div className={styles.iconWrapper}>{ALERT_ICON_MAP[variant]}</div>
<div className={styles.message}>{message}</div>
</div>
<div className={styles.snackbarActions}>{typeof action === 'function' ? action(id) : action}</div>
</div>
);
});

CustomSnackbar.displayName = 'CustomSnackbar';

export function CloseSnackbarButton({ snackbarId }: { snackbarId: SnackbarKey }) {
const { closeSnackbar } = useSnackbar();

return (
<>
<div className={styles.closeIcon}>
<CrossIconSvg onClick={() => closeSnackbar(snackbarId)} />
</div>
<Button
onClick={() => closeSnackbar(snackbarId)}
className={styles.closeButton}
variant="ghost"
outlined
size="small"
>
CLOSE
</Button>
</>
);
}
86 changes: 86 additions & 0 deletions src/client/components/common/Alert/alert.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,100 @@
.success {
background: v('success-background');
color: v('success-text');

&.withBorder {
border: 1px solid v('success-text');
}
}

.error {
background: v('error-background');
color: v('error-text');

&.withBorder {
border: 1px solid v('error-text');
}
}

.warning {
background: v('warning-background');
color: v('warning-text');

&.withBorder {
border: 1px solid v('warning-text');
}
}

.info {
background: v('info-background');
color: v('info-text');

&.withBorder {
border: 1px solid v('info-text');
}
}

.snackbar {
display: flex;
flex-direction: row;
padding: 12px;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
border-radius: 6px;
font-size: 14px;
line-height: 160%;
letter-spacing: 0.14px;
font-weight: 500;

@include media('<=phone') {
flex-direction: column;
align-items: flex-start;
}
}

.snackbarContent {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px;

.iconWrapper {
display: flex;
}

.message {
align-self: center;
}
}

.snackbarActions {
display: flex;
flex-direction: row;
margin-left: auto;
align-self: flex-start;
gap: 12px;
}

.closeButton {
color: inherit;
margin-left: auto;
text-transform: capitalize !important;
@include media('>=phone') {
display: none;
}
}

.closeIcon {
cursor: pointer;
padding: 0px 3px;
margin-left: auto;
// height matching action button (even if it's not there) to keep the snackbar height consistent and everything aligned
min-height: 37px;
display: flex;
align-items: center;

@include media('<=phone') {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback } from 'react';
import { SnackbarKey, useSnackbar } from 'notistack';
import Button from '@mui/material/Button';
import { useCopyToClipboard } from 'react-use';
import Button from '../../components/common/Button/Button';
import { CloseSnackbarButton } from '../../components/common/Alert/Alert';

export function usePersonalizationNotification() {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
Expand All @@ -26,18 +27,16 @@ export function usePersonalizationNotification() {
action: (snackbarId) => (
<>
<Button
color="inherit"
variant="green"
size="small"
onClick={() => {
copyToClipboard(window.location.href);

showLinkCopiedSnackbar(snackbarId);
}}
>
Copy link
</Button>
<Button color="inherit" onClick={() => closeSnackbar(snackbarId)}>
Close
COPY LINK
</Button>
<CloseSnackbarButton snackbarId={snackbarId} />
</>
),
});
Expand Down
17 changes: 14 additions & 3 deletions src/client/hooks/useReset/useReset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export const useReset = ({ onError, onSuccess }: UseResetParams) => {
const resetMutation = useMutation<ResetResponse>(
'resetMutation',
async () => {
const { visitorId, requestId } = await getData();
const body: ResetRequest = { visitorId, requestId };
const { requestId } = await getData();
const body: ResetRequest = { requestId };

return fetch('/api/admin/reset', {
method: 'POST',
Expand All @@ -46,7 +46,18 @@ export const useReset = ({ onError, onSuccess }: UseResetParams) => {
((data) => {
enqueueSnackbar(
<div data-testid={TEST_IDS.reset.resetSuccess}>
<p>Scenarios reset successfully!</p> <p>{data.message}</p>
<p>
<b>Scenarios reset successfully!</b>
</p>{' '}
<ul>
<li>Deleted {data.result?.deletedCouponsClaims} coupon claims.</li>
<li>Deleted {data.result?.deletedLoginAttempts} login attempts.</li>
<li>Deleted {data.result?.deletedLoanRequests} loan requests.</li>
<li>Deleted {data.result?.deletedPaymentAttempts} payment attempts.</li>
<li>Deleted {data.result?.deletedArticleViews} article views.</li>
<li>Deleted {data.result?.deletedPersonalizationRecords} personalization records.</li>
<li>Deleted {data.result?.deletedBlockedIps} blocked IPs.</li>
</ul>
</div>,
{
variant: 'success',
Expand Down
4 changes: 4 additions & 0 deletions src/client/hooks/useReset/userReset.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.snackbar {
max-width: rem(600px);

p:first-child {
margin-top: 2px;
}
}
14 changes: 14 additions & 0 deletions src/client/img/crossIconSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FunctionComponent, ComponentProps } from 'react';

export const CrossIconSvg: FunctionComponent<ComponentProps<'svg'>> = (props) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M2.6665 2.66602L13.3332 13.3327M2.6665 13.3327L13.3332 2.66602"
stroke="currentColor"
stroke-opacity="0.6"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
14 changes: 11 additions & 3 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import '../styles/global-styles.scss';
import { QueryClient, QueryClientProvider } from 'react-query';
import Head from 'next/head';
import { SnackbarProvider } from 'notistack';
import { SnackbarAction } from '../client/components/snackbar-action';
import { FpjsProvider, FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react';
import { AppProps } from 'next/app';
import Header from '../client/components/common/Header/Header';
Expand All @@ -12,6 +11,8 @@ import Footer from '../client/components/common/Footer/Footer';
import styles from '../styles/layout.module.scss';
import { PUBLIC_API_KEY, SCRIPT_URL_PATTERN, ENDPOINT, FRONTEND_REGION, CUSTOM_TLS_ENDPOINT } from '../server/const';

import { CloseSnackbarButton, CustomSnackbar } from '../client/components/common/Alert/Alert';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand Down Expand Up @@ -44,13 +45,20 @@ function CustomApp({ Component, pageProps }: AppProps<CustomPageProps>) {
return (
<QueryClientProvider client={queryClient}>
<SnackbarProvider
action={(snackbarId) => <SnackbarAction snackbarId={snackbarId} />}
maxSnack={3}
action={(snackbarId) => <CloseSnackbarButton snackbarId={snackbarId} />}
maxSnack={4}
autoHideDuration={5000}
anchorOrigin={{
horizontal: 'left',
vertical: 'bottom',
}}
Components={{
default: CustomSnackbar,
success: CustomSnackbar,
error: CustomSnackbar,
warning: CustomSnackbar,
info: CustomSnackbar,
}}
>
<FpjsProvider loadOptions={FP_LOAD_OPTIONS}>
<Head>
Expand Down
Loading
Loading