Fix/csv import (#1397)

* feat: add ability to enable or disable header selection

* feat: limit to max of 200 records for now

* fix: bigger modal

* feat: add missing standard fields for company

* fix: person fields

* feat: add hotkeys on dialog

* feat: mobile device

* fix: company import error

* fix: csv import crash

* fix: use scoped hotkey
This commit is contained in:
Jérémy M 2023-09-04 11:50:12 +02:00 committed by GitHub
parent f29d843db9
commit c0cb3a47f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 213 additions and 86 deletions

View File

@ -3,12 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { SpreadsheetOptions } from '@/spreadsheet-import/types'; import { SpreadsheetOptions } from '@/spreadsheet-import/types';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; import { useInsertManyCompanyMutation } from '~/generated/graphql';
import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds';
import {
GetPeopleDocument,
useInsertManyCompanyMutation,
} from '~/generated/graphql';
import { fieldsForCompany } from '../utils/fieldsForCompany'; import { fieldsForCompany } from '../utils/fieldsForCompany';
@ -16,8 +11,6 @@ export type FieldCompanyMapping = (typeof fieldsForCompany)[number]['key'];
export function useSpreadsheetCompanyImport() { export function useSpreadsheetCompanyImport() {
const { openSpreadsheetImport } = useSpreadsheetImport<FieldCompanyMapping>(); const { openSpreadsheetImport } = useSpreadsheetImport<FieldCompanyMapping>();
const upsertEntityTableItems = useUpsertEntityTableItems();
const upsertTableRowIds = useUpsertTableRowIds();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const [createManyCompany] = useInsertManyCompanyMutation(); const [createManyCompany] = useInsertManyCompanyMutation();
@ -34,11 +27,11 @@ export function useSpreadsheetCompanyImport() {
// TODO: Add better type checking in spreadsheet import later // TODO: Add better type checking in spreadsheet import later
const createInputs = data.validData.map((company) => ({ const createInputs = data.validData.map((company) => ({
id: uuidv4(), id: uuidv4(),
name: company.name as string, name: (company.name ?? '') as string,
domainName: company.domainName as string, domainName: (company.domainName ?? '') as string,
address: company.address as string, address: (company.address ?? '') as string,
employees: parseInt(company.employees as string, 10), employees: parseInt((company.employees ?? '') as string, 10),
linkedinUrl: company.linkedinUrl as string | undefined, linkedinUrl: (company.linkedinUrl ?? '') as string | undefined,
})); }));
try { try {
@ -46,15 +39,12 @@ export function useSpreadsheetCompanyImport() {
variables: { variables: {
data: createInputs, data: createInputs,
}, },
refetchQueries: [GetPeopleDocument], refetchQueries: 'active',
}); });
if (result.errors) { if (result.errors) {
throw result.errors; throw result.errors;
} }
upsertTableRowIds(createInputs.map((company) => company.id));
upsertEntityTableItems(createInputs);
} catch (error: any) { } catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', { enqueueSnackBar(error?.message || 'Something went wrong', {
variant: 'error', variant: 'error',

View File

@ -1,8 +1,11 @@
import { import {
IconBrandLinkedin, IconBrandLinkedin,
IconBrandX,
IconBuildingSkyscraper, IconBuildingSkyscraper,
IconMail, IconMail,
IconMap, IconMap,
IconMoneybag,
IconTarget,
IconUsers, IconUsers,
} from '@/ui/icon'; } from '@/ui/icon';
@ -16,13 +19,6 @@ export const fieldsForCompany = [
type: 'input', type: 'input',
}, },
example: 'Tim', example: 'Tim',
validations: [
{
rule: 'required',
errorMessage: 'Name is required',
level: 'error',
},
],
}, },
{ {
icon: <IconMail />, icon: <IconMail />,
@ -33,13 +29,6 @@ export const fieldsForCompany = [
type: 'input', type: 'input',
}, },
example: 'apple.dev', example: 'apple.dev',
validations: [
{
rule: 'required',
errorMessage: 'Domain name is required',
level: 'error',
},
],
}, },
{ {
icon: <IconBrandLinkedin />, icon: <IconBrandLinkedin />,
@ -51,6 +40,61 @@ export const fieldsForCompany = [
}, },
example: 'https://www.linkedin.com/in/apple', example: 'https://www.linkedin.com/in/apple',
}, },
{
icon: <IconMoneybag />,
label: 'ARR',
key: 'annualRecurringRevenue',
alternateMatches: [
'arr',
'annual revenue',
'revenue',
'recurring revenue',
'annual recurring revenue',
],
fieldType: {
type: 'input',
},
validation: [
{
regex: /^(\d+)?$/,
errorMessage: 'Annual recurring revenue must be a number',
level: 'error',
},
],
example: '1000000',
},
{
icon: <IconTarget />,
label: 'ICP',
key: 'idealCustomerProfile',
alternateMatches: [
'icp',
'ideal profile',
'ideal customer profile',
'ideal customer',
],
fieldType: {
type: 'input',
},
validation: [
{
regex: /^(true|false)?$/,
errorMessage: 'Ideal custoner profile must be a boolean',
level: 'error',
},
],
example: 'true/false',
},
{
icon: <IconBrandX />,
label: 'x URL',
key: 'xUrl',
alternateMatches: ['x', 'twitter', 'twitter url', 'x url'],
fieldType: {
type: 'input',
},
example: 'https://x.com/tim_cook',
},
{ {
icon: <IconMap />, icon: <IconMap />,
label: 'Address', label: 'Address',
@ -59,13 +103,6 @@ export const fieldsForCompany = [
type: 'input', type: 'input',
}, },
example: 'Maple street', example: 'Maple street',
validations: [
{
rule: 'required',
errorMessage: 'Address is required',
level: 'error',
},
],
}, },
{ {
icon: <IconUsers />, icon: <IconUsers />,
@ -75,6 +112,13 @@ export const fieldsForCompany = [
fieldType: { fieldType: {
type: 'input', type: 'input',
}, },
validation: [
{
regex: /^\d+$/,
errorMessage: 'Employees must be a number',
level: 'error',
},
],
example: '150', example: '150',
}, },
] as const; ] as const;

View File

@ -3,12 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { SpreadsheetOptions } from '@/spreadsheet-import/types'; import { SpreadsheetOptions } from '@/spreadsheet-import/types';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; import { useInsertManyPersonMutation } from '~/generated/graphql';
import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds';
import {
GetPeopleDocument,
useInsertManyPersonMutation,
} from '~/generated/graphql';
import { fieldsForPerson } from '../utils/fieldsForPerson'; import { fieldsForPerson } from '../utils/fieldsForPerson';
@ -16,8 +11,6 @@ export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key'];
export function useSpreadsheetPersonImport() { export function useSpreadsheetPersonImport() {
const { openSpreadsheetImport } = useSpreadsheetImport<FieldPersonMapping>(); const { openSpreadsheetImport } = useSpreadsheetImport<FieldPersonMapping>();
const upsertEntityTableItems = useUpsertEntityTableItems();
const upsertTableRowIds = useUpsertTableRowIds();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const [createManyPerson] = useInsertManyPersonMutation(); const [createManyPerson] = useInsertManyPersonMutation();
@ -49,15 +42,12 @@ export function useSpreadsheetPersonImport() {
variables: { variables: {
data: createInputs, data: createInputs,
}, },
refetchQueries: [GetPeopleDocument], refetchQueries: 'active',
}); });
if (result.errors) { if (result.errors) {
throw result.errors; throw result.errors;
} }
upsertTableRowIds(createInputs.map((person) => person.id));
upsertEntityTableItems(createInputs);
} catch (error: any) { } catch (error: any) {
enqueueSnackBar(error?.message || 'Something went wrong', { enqueueSnackBar(error?.message || 'Something went wrong', {
variant: 'error', variant: 'error',

View File

@ -2,7 +2,7 @@ import { isValidPhoneNumber } from 'libphonenumber-js';
import { import {
IconBrandLinkedin, IconBrandLinkedin,
IconBrandTwitter, IconBrandX,
IconBriefcase, IconBriefcase,
IconMail, IconMail,
IconMap, IconMap,
@ -19,13 +19,6 @@ export const fieldsForPerson = [
type: 'input', type: 'input',
}, },
example: 'Tim', example: 'Tim',
validations: [
{
rule: 'required',
errorMessage: 'Firstname is required',
level: 'error',
},
],
}, },
{ {
icon: <IconUser />, icon: <IconUser />,
@ -36,13 +29,6 @@ export const fieldsForPerson = [
type: 'input', type: 'input',
}, },
example: 'Cook', example: 'Cook',
validations: [
{
rule: 'required',
errorMessage: 'Lastname is required',
level: 'error',
},
],
}, },
{ {
icon: <IconMail />, icon: <IconMail />,
@ -53,13 +39,6 @@ export const fieldsForPerson = [
type: 'input', type: 'input',
}, },
example: 'tim@apple.dev', example: 'tim@apple.dev',
validations: [
{
rule: 'required',
errorMessage: 'email is required',
level: 'error',
},
],
}, },
{ {
icon: <IconBrandLinkedin />, icon: <IconBrandLinkedin />,
@ -72,7 +51,7 @@ export const fieldsForPerson = [
example: 'https://www.linkedin.com/in/timcook', example: 'https://www.linkedin.com/in/timcook',
}, },
{ {
icon: <IconBrandTwitter />, icon: <IconBrandX />,
label: 'X URL', label: 'X URL',
key: 'xUrl', key: 'xUrl',
alternateMatches: ['x', 'x url'], alternateMatches: ['x', 'x url'],

View File

@ -45,7 +45,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
message: 'Are you sure? Your current information will not be saved.', message: 'Are you sure? Your current information will not be saved.',
buttons: [ buttons: [
{ title: 'Cancel' }, { title: 'Cancel' },
{ title: 'Exit', onClick: onClose, accent: 'danger' }, { title: 'Exit', onClick: onClose, accent: 'danger', role: 'confirm' },
], ],
}); });
} }

View File

@ -3,15 +3,22 @@ import styled from '@emotion/styled';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { ModalCloseButton } from './ModalCloseButton'; import { ModalCloseButton } from './ModalCloseButton';
const StyledModal = styled(Modal)` const StyledModal = styled(Modal)`
height: 61%; height: 61%;
min-height: 500px; min-height: 600px;
min-width: 600px; min-width: 800px;
position: relative; position: relative;
width: 53%; width: 63%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
min-width: auto;
min-height: auto;
width: 100%;
height: 100%;
}
`; `;
const StyledRtlLtr = styled.div` const StyledRtlLtr = styled.div`

View File

@ -12,6 +12,8 @@ export const defaultSpreadsheetImportProps: Partial<SpreadsheetOptions<any>> = {
matchColumnsStepHook: async (table) => table, matchColumnsStepHook: async (table) => table,
dateFormat: 'yyyy-mm-dd', // ISO 8601, dateFormat: 'yyyy-mm-dd', // ISO 8601,
parseRaw: true, parseRaw: true,
selectHeader: false,
maxRecords: 200,
} as const; } as const;
export const SpreadsheetImport = <T extends string>( export const SpreadsheetImport = <T extends string>(

View File

@ -21,6 +21,8 @@ import { UserTableColumn } from './components/UserTableColumn';
const StyledContent = styled(Modal.Content)` const StyledContent = styled(Modal.Content)`
align-items: center; align-items: center;
padding-left: ${({ theme }) => theme.spacing(6)};
padding-right: ${({ theme }) => theme.spacing(6)};
`; `;
const StyledColumnsContainer = styled.div` const StyledColumnsContainer = styled.div`
@ -224,6 +226,7 @@ export const MatchColumnsStep = <T extends string>({
title: 'Continue', title: 'Continue',
onClick: handleAlertOnContinue, onClick: handleAlertOnContinue,
variant: 'primary', variant: 'primary',
role: 'confirm',
}, },
], ],
}); });

View File

@ -19,7 +19,7 @@ const StyledGrid = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-top: ${({ theme }) => theme.spacing(8)}; margin-top: ${({ theme }) => theme.spacing(8)};
width: 75%; width: 100%;
`; `;
type HeightProps = { type HeightProps = {

View File

@ -9,6 +9,8 @@ import { Modal } from '@/ui/modal/components/Modal';
const StyledContent = styled(Modal.Content)` const StyledContent = styled(Modal.Content)`
align-items: center; align-items: center;
padding-left: ${({ theme }) => theme.spacing(6)};
padding-right: ${({ theme }) => theme.spacing(6)};
`; `;
const StyledHeading = styled(Heading)` const StyledHeading = styled(Heading)`

View File

@ -5,6 +5,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { StepBar } from '@/ui/step-bar/components/StepBar'; import { StepBar } from '@/ui/step-bar/components/StepBar';
import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; import { useStepBar } from '@/ui/step-bar/hooks/useStepBar';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { UploadFlow } from './UploadFlow'; import { UploadFlow } from './UploadFlow';
@ -15,6 +16,10 @@ const StyledHeader = styled(Modal.Header)`
padding: 0px; padding: 0px;
padding-left: ${({ theme }) => theme.spacing(30)}; padding-left: ${({ theme }) => theme.spacing(30)};
padding-right: ${({ theme }) => theme.spacing(30)}; padding-right: ${({ theme }) => theme.spacing(30)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(4)};
}
`; `;
const stepTitles = { const stepTitles = {

View File

@ -72,6 +72,7 @@ export const UploadFlow = ({ nextStep }: Props) => {
uploadStepHook, uploadStepHook,
selectHeaderStepHook, selectHeaderStepHook,
matchColumnsStepHook, matchColumnsStepHook,
selectHeader,
} = useSpreadsheetImportInternal(); } = useSpreadsheetImportInternal();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -109,10 +110,27 @@ export const UploadFlow = ({ nextStep }: Props) => {
const mappedWorkbook = await uploadStepHook( const mappedWorkbook = await uploadStepHook(
mapWorkbook(workbook), mapWorkbook(workbook),
); );
setState({
type: StepType.selectHeader, if (selectHeader) {
data: mappedWorkbook, setState({
}); type: StepType.selectHeader,
data: mappedWorkbook,
});
} else {
// Automatically select first row as header
const trimmedData = mappedWorkbook.slice(1);
const { data, headerValues } = await selectHeaderStepHook(
mappedWorkbook[0],
trimmedData,
);
setState({
type: StepType.matchColumns,
data,
headerValues,
});
}
} catch (e) { } catch (e) {
errorToast((e as Error).message); errorToast((e as Error).message);
} }

View File

@ -17,6 +17,11 @@ import { Modal } from '@/ui/modal/components/Modal';
import { generateColumns } from './components/columns'; import { generateColumns } from './components/columns';
import type { Meta } from './types'; import type { Meta } from './types';
const StyledContent = styled(Modal.Content)`
padding-left: ${({ theme }) => theme.spacing(6)};
padding-right: ${({ theme }) => theme.spacing(6)};
`;
const StyledToolbar = styled.div` const StyledToolbar = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -175,6 +180,7 @@ export const ValidationStep = <T extends string>({
title: 'Submit', title: 'Submit',
variant: 'primary', variant: 'primary',
onClick: submitData, onClick: submitData,
role: 'confirm',
}, },
], ],
}); });
@ -183,7 +189,7 @@ export const ValidationStep = <T extends string>({
return ( return (
<> <>
<Modal.Content> <StyledContent>
<Heading <Heading
title="Review your import" title="Review your import"
description="Correct the issues and fill the missing data." description="Correct the issues and fill the missing data."
@ -225,7 +231,7 @@ export const ValidationStep = <T extends string>({
}} }}
/> />
</StyledScrollContainer> </StyledScrollContainer>
</Modal.Content> </StyledContent>
<ContinueButton onContinue={onContinue} title="Confirm" /> <ContinueButton onContinue={onContinue} title="Confirm" />
</> </>
); );

View File

@ -50,6 +50,8 @@ export type SpreadsheetOptions<Keys extends string> = {
parseRaw?: boolean; parseRaw?: boolean;
// Use for right-to-left (RTL) support // Use for right-to-left (RTL) support
rtl?: boolean; rtl?: boolean;
// Allow header selection
selectHeader?: boolean;
}; };
export type RawData = Array<string | undefined>; export type RawData = Array<string | undefined>;

View File

@ -1,8 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Key } from 'ts-key-enum';
import { Button } from '@/ui/button/components/Button'; import { Button } from '@/ui/button/components/Button';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
const StyledDialogOverlay = styled(motion.div)` const StyledDialogOverlay = styled(motion.div)`
align-items: center; align-items: center;
@ -52,7 +56,12 @@ const StyledDialogButton = styled(Button)`
export type DialogButtonOptions = Omit< export type DialogButtonOptions = Omit<
React.ComponentProps<typeof Button>, React.ComponentProps<typeof Button>,
'fullWidth' 'fullWidth'
>; > & {
onClick?: (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> | KeyboardEvent,
) => void;
role?: 'confirm';
};
export type DialogProps = React.ComponentPropsWithoutRef<typeof motion.div> & { export type DialogProps = React.ComponentPropsWithoutRef<typeof motion.div> & {
title?: string; title?: string;
@ -86,6 +95,32 @@ export function Dialog({
closed: { y: '50vh' }, closed: { y: '50vh' },
}; };
useScopedHotkeys(
Key.Enter,
(event: KeyboardEvent) => {
const confirmButton = buttons.find((button) => button.role === 'confirm');
event.preventDefault();
if (confirmButton) {
confirmButton?.onClick?.(event);
closeSnackbar();
}
},
DialogHotkeyScope.Dialog,
[],
);
useScopedHotkeys(
Key.Escape,
(event: KeyboardEvent) => {
event.preventDefault();
closeSnackbar();
},
DialogHotkeyScope.Dialog,
[],
);
return ( return (
<StyledDialogOverlay <StyledDialogOverlay
variants={dialogVariants} variants={dialogVariants}

View File

@ -1,20 +1,38 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { dialogInternalState } from '../states/dialogState'; import { dialogInternalState } from '../states/dialogState';
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
import { Dialog } from './Dialog'; import { Dialog } from './Dialog';
export function DialogProvider({ children }: React.PropsWithChildren) { export function DialogProvider({ children }: React.PropsWithChildren) {
const [dialogState, setDialogState] = useRecoilState(dialogInternalState); const [dialogState, setDialogState] = useRecoilState(dialogInternalState);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// Handle dialog close event // Handle dialog close event
const handleDialogClose = (id: string) => { const handleDialogClose = (id: string) => {
setDialogState((prevState) => ({ setDialogState((prevState) => ({
...prevState, ...prevState,
queue: prevState.queue.filter((snackBar) => snackBar.id !== id), queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
})); }));
goBackToPreviousHotkeyScope();
}; };
useEffect(() => {
if (dialogState.queue.length === 0) {
return;
}
setHotkeyScopeAndMemorizePreviousScope(DialogHotkeyScope.Dialog);
}, [dialogState.queue, setHotkeyScopeAndMemorizePreviousScope]);
return ( return (
<> <>
{children} {children}

View File

@ -0,0 +1,3 @@
export enum DialogHotkeyScope {
Dialog = 'dialog',
}

View File

@ -3,11 +3,16 @@ import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { AnimatedCheckmark } from '@/ui/checkmark/components/AnimatedCheckmark'; import { AnimatedCheckmark } from '@/ui/checkmark/components/AnimatedCheckmark';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
const StyledContainer = styled.div<{ isLast: boolean }>` const StyledContainer = styled.div<{ isLast: boolean }>`
align-items: center; align-items: center;
display: flex; display: flex;
flex-grow: ${({ isLast }) => (isLast ? '0' : '1')}; flex-grow: ${({ isLast }) => (isLast ? '0' : '1')};
@media (max-width: ${MOBILE_VIEWPORT}px) {
flex-grow: 0;
}
`; `;
const StyledStepCircle = styled(motion.div)` const StyledStepCircle = styled(motion.div)`
@ -64,6 +69,7 @@ export const Step = ({
children, children,
}: StepProps) => { }: StepProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useIsMobile();
const variantsCircle = { const variantsCircle = {
active: { active: {
@ -104,7 +110,7 @@ export const Step = ({
{!isActive && <StyledStepIndex>{index + 1}</StyledStepIndex>} {!isActive && <StyledStepIndex>{index + 1}</StyledStepIndex>}
</StyledStepCircle> </StyledStepCircle>
<StyledStepLabel isActive={isActive}>{label}</StyledStepLabel> <StyledStepLabel isActive={isActive}>{label}</StyledStepLabel>
{!isLast && ( {!isLast && !isMobile && (
<StyledStepLine <StyledStepLine
variants={variantsLine} variants={variantsLine}
animate={isActive ? 'active' : 'inactive'} animate={isActive ? 'active' : 'inactive'}

View File

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Step, StepProps } from './Step'; import { Step, StepProps } from './Step';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: space-between; justify-content: space-between;
@media (max-width: ${MOBILE_VIEWPORT}px) {
align-items: center;
justify-content: center;
}
`; `;
export type StepsProps = React.PropsWithChildren & export type StepsProps = React.PropsWithChildren &
@ -15,6 +22,8 @@ export type StepsProps = React.PropsWithChildren &
}; };
export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => { export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => {
const isMobile = useIsMobile();
return ( return (
<StyledContainer {...restProps}> <StyledContainer {...restProps}>
{React.Children.map(children, (child, index) => { {React.Children.map(children, (child, index) => {
@ -29,6 +38,14 @@ export const StepBar = ({ children, activeStep, ...restProps }: StepsProps) => {
return child; return child;
} }
// We should only render the active step, and if activeStep is -1, we should only render the first step only when it's mobile device
if (
isMobile &&
(activeStep === -1 ? index !== 0 : index !== activeStep)
) {
return null;
}
return React.cloneElement<StepProps>(child as any, { return React.cloneElement<StepProps>(child as any, {
index, index,
isActive: index <= activeStep, isActive: index <= activeStep,