-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(onboarding-flow): implement onboarding functionality
- Loading branch information
1 parent
da36774
commit 5028221
Showing
8 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
src/components/ModalsContainer/OnboardingFlow/GraphDetailsStep/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { Button } from '@mui/material' | ||
import { FC, useEffect } from 'react' | ||
import { useFormContext } from 'react-hook-form' | ||
import { MdError } from 'react-icons/md' | ||
import styled from 'styled-components' | ||
import { noSpacePattern } from '~/components/AddItemModal/SourceTypeStep/constants' | ||
import { Flex } from '~/components/common/Flex' | ||
import { Text } from '~/components/common/Text' | ||
import { TextInput } from '~/components/common/TextInput' | ||
import { requiredRule } from '~/constants' | ||
import { colors } from '~/utils' | ||
|
||
type Props = { | ||
onSubmit: () => void | ||
error?: string | ||
} | ||
|
||
export const GraphDetailsStep: FC<Props> = ({ onSubmit, error }) => { | ||
const { | ||
formState: { isSubmitting }, | ||
watch, | ||
} = useFormContext() | ||
|
||
const title = watch('title') | ||
const description = watch('description') | ||
|
||
const isFormValid = !!title?.trim() && !!description?.trim() | ||
|
||
useEffect(() => { | ||
const titleInput = document.getElementById('graph-title') as HTMLInputElement | ||
|
||
if (titleInput) { | ||
titleInput.focus() | ||
} | ||
}, []) | ||
|
||
return ( | ||
<Flex> | ||
<Flex direction="column" justify="space-between"> | ||
<StyledText>Welcome to SecondBrain</StyledText> | ||
<StyledSubText>Set a name and short description for your graph.</StyledSubText> | ||
</Flex> | ||
|
||
<StyledWrapper> | ||
<Flex className="input__wrapper"> | ||
<TextInput | ||
id="graph-title" | ||
label="Title" | ||
maxLength={50} | ||
name="title" | ||
placeholder="Type graph title here..." | ||
rules={{ | ||
...requiredRule, | ||
pattern: { | ||
message: 'No leading whitespace allowed', | ||
value: noSpacePattern, | ||
}, | ||
}} | ||
/> | ||
<TextInput | ||
id="graph-description" | ||
label="Description" | ||
maxLength={100} | ||
name="description" | ||
placeholder="Type graph description here..." | ||
rules={{ | ||
...requiredRule, | ||
pattern: { | ||
message: 'No leading whitespace allowed', | ||
value: noSpacePattern, | ||
}, | ||
}} | ||
/> | ||
</Flex> | ||
</StyledWrapper> | ||
|
||
<Flex mt={10}> | ||
<Button | ||
color="secondary" | ||
disabled={isSubmitting || !!error || !isFormValid} | ||
onClick={onSubmit} | ||
size="large" | ||
variant="contained" | ||
> | ||
Confirm | ||
</Button> | ||
</Flex> | ||
{error ? ( | ||
<StyledError> | ||
<StyledErrorText> | ||
<MdError className="errorIcon" /> | ||
<span>{error}</span> | ||
</StyledErrorText> | ||
</StyledError> | ||
) : null} | ||
</Flex> | ||
) | ||
} | ||
|
||
const StyledText = styled(Text)` | ||
font-size: 22px; | ||
font-weight: 600; | ||
font-family: 'Barlow'; | ||
margin-bottom: 10px; | ||
` | ||
|
||
const StyledSubText = styled(Text)` | ||
font-size: 14px; | ||
font-family: 'Barlow'; | ||
margin-bottom: 20px; | ||
` | ||
|
||
const StyledWrapper = styled(Flex)` | ||
width: 100%; | ||
display: flex; | ||
justify-content: center; | ||
gap: 10px; | ||
margin: 0 0 15px 0; | ||
.input__wrapper { | ||
display: flex; | ||
gap: 23px; | ||
max-height: 225px; | ||
overflow-y: auto; | ||
padding-right: 20px; | ||
width: calc(100% + 20px); | ||
} | ||
` | ||
|
||
const StyledErrorText = styled(Flex)` | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
justify-content: center; | ||
gap: 2px; | ||
.errorIcon { | ||
display: block; | ||
font-size: 13px; | ||
min-height: 13px; | ||
min-width: 13px; | ||
} | ||
span { | ||
display: -webkit-box; | ||
-webkit-line-clamp: 1; | ||
-webkit-box-orient: vertical; | ||
overflow: hidden; | ||
white-space: normal; | ||
letter-spacing: 0.2px; | ||
padding-left: 4px; | ||
font-size: 13px; | ||
font-family: Barlow; | ||
line-height: 18px; | ||
} | ||
` | ||
|
||
const StyledError = styled(Flex)` | ||
display: flex; | ||
align-items: center; | ||
color: ${colors.primaryRed}; | ||
position: relative; | ||
margin-top: 20px; | ||
` |
120 changes: 120 additions & 0 deletions
120
src/components/ModalsContainer/OnboardingFlow/__tests__/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import '@testing-library/jest-dom' | ||
import { fireEvent, render, screen, waitFor } from '@testing-library/react' | ||
import React from 'react' | ||
import { postAboutData } from '~/network/fetchSourcesData' | ||
import { useModal } from '~/stores/useModalStore' | ||
import { OnboardingModal } from '../index' | ||
|
||
jest.mock('~/network/fetchSourcesData', () => ({ | ||
postAboutData: jest.fn(), | ||
})) | ||
|
||
jest.mock('~/stores/useModalStore', () => ({ | ||
useModal: jest.fn(), | ||
})) | ||
|
||
const useModalMock = useModal as jest.MockedFunction<typeof useModal> | ||
const postAboutDataMock = postAboutData as jest.MockedFunction<typeof postAboutData> | ||
|
||
describe('OnboardingModal Component', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
|
||
useModalMock.mockReturnValue({ | ||
close: jest.fn(), | ||
visible: true, | ||
}) | ||
}) | ||
|
||
test('renders the onboarding modal', () => { | ||
render(<OnboardingModal />) | ||
expect(screen.getByText('Welcome to SecondBrain')).toBeInTheDocument() | ||
expect(screen.getByText('Set a name and short description for your graph.')).toBeInTheDocument() | ||
}) | ||
|
||
test('submits form successfully', async () => { | ||
postAboutDataMock.mockResolvedValue({ status: 'success' }) | ||
|
||
render(<OnboardingModal />) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { | ||
target: { value: 'Test Description' }, | ||
}) | ||
|
||
fireEvent.click(screen.getByText('Confirm')) | ||
|
||
await waitFor(() => { | ||
expect(postAboutDataMock).toHaveBeenCalledWith({ | ||
title: 'Test Title', | ||
description: 'Test Description', | ||
}) | ||
}) | ||
}) | ||
|
||
test('displays error on form submission failure', async () => { | ||
postAboutDataMock.mockRejectedValue({ status: 400, json: async () => ({ errorCode: 'Error occurred' }) }) | ||
|
||
render(<OnboardingModal />) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { | ||
target: { value: 'Test Description' }, | ||
}) | ||
|
||
fireEvent.click(screen.getByText('Confirm')) | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText('Error occurred')).toBeInTheDocument() | ||
}) | ||
}) | ||
|
||
test('closes modal on successful submission', async () => { | ||
const closeMock = jest.fn() | ||
|
||
useModalMock.mockReturnValue({ | ||
close: closeMock, | ||
visible: true, | ||
}) | ||
|
||
postAboutDataMock.mockResolvedValue({ status: 'success' }) | ||
|
||
render(<OnboardingModal />) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { | ||
target: { value: 'Test Description' }, | ||
}) | ||
|
||
fireEvent.click(screen.getByText('Confirm')) | ||
|
||
await waitFor(() => { | ||
expect(closeMock).toHaveBeenCalled() | ||
}) | ||
}) | ||
|
||
test('resets form and error on modal close', async () => { | ||
const { rerender } = render(<OnboardingModal />) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph title here...'), { target: { value: 'Test Title' } }) | ||
|
||
fireEvent.change(screen.getByPlaceholderText('Type graph description here...'), { | ||
target: { value: 'Test Description' }, | ||
}) | ||
|
||
useModalMock.mockReturnValue({ | ||
close: jest.fn(), | ||
visible: false, | ||
}) | ||
|
||
rerender(<OnboardingModal />) | ||
|
||
waitFor(() => { | ||
expect(screen.getByPlaceholderText('Type graph title here...')).toHaveValue('') | ||
expect(screen.getByPlaceholderText('Type graph description here...')).toHaveValue('') | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.