Skip to content

Commit

Permalink
Facelift: Alerts (#120)
Browse files Browse the repository at this point in the history
* wip: create custom snackbar component

* wip: improve styles

* wip: improve styles

* wip: improve styles

* wip: better comment

* chore: clean up

* chore: clean up

* chore: fix interface

* chore: fix reset paragraph margins

* refactor: reset

* refactor: simplify

* styles: clean-up

* chore: add comments
  • Loading branch information
JuroUhlar authored Feb 23, 2024
1 parent 59621bf commit 4110862
Show file tree
Hide file tree
Showing 20 changed files with 284 additions and 117 deletions.
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

0 comments on commit 4110862

Please sign in to comment.