Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ec25586
Add react testing library, jestd-dom and configure setupFile
ralfting May 23, 2025
3fbc7d3
Ad MUI (Material UI)
ralfting May 23, 2025
5c74a5d
Create SignUp component to route steps, add placeholder for each step
ralfting May 23, 2025
bfd03a2
Add button navigation passing props to the steps
ralfting May 25, 2025
2bc7cfa
Add react hook form
ralfting May 25, 2025
0be37d1
Add test to navigation
ralfting May 25, 2025
ddd035f
Add validation (Zod) to react hook form
ralfting May 25, 2025
a734fa8
Add validation test for user details
ralfting May 25, 2025
f8b5d5f
Add component to the more info page
ralfting May 25, 2025
bb8bf7f
Add sevice to colors, add eslint + prettier
ralfting May 26, 2025
719243d
Fix tests
ralfting May 26, 2025
f5b40b4
Add MSW configuration
ralfting May 26, 2025
0c0953d
Fix warnings
ralfting May 26, 2025
0ebbada
Implemnent Confirmation page and test it
ralfting May 26, 2025
f66cfd4
add api to create an user
ralfting May 26, 2025
a309895
Add Feedback component
ralfting May 26, 2025
8498147
ADd test for Feedback component page
ralfting May 26, 2025
2cbd7fe
Add AlertFieldError component
ralfting May 26, 2025
125ad0f
Adjust UI
ralfting May 26, 2025
97060c5
Create signup success and error test
ralfting May 26, 2025
deb16f5
Fix layout
ralfting May 27, 2025
dc2586c
Adjust code
ralfting May 27, 2025
e70e72e
Add introduction markdown
ralfting May 27, 2025
cf6b246
disabled back button when submit form
ralfting May 27, 2025
0439575
Remove extra stack
ralfting May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "prettier"],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"prettier/prettier": "error",
"react-hooks/exhaustive-deps": "off"
},
"settings": {
"react": {
"version": "detect"
}
}
}
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false
}
64 changes: 64 additions & 0 deletions INTRODUCTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Introduction

## Folder strucutre

```
src/
├── components/
├── features/
│ └── user/
│ └── SignUpForm/
├── services/
├── test/
├── App.jsx
└── index.jsx
```

| **Folder** | **Description** |
| ------------- | ----------------------------------------------------------------------------------------- |
| `components/` | Contains reusable and generic UI components (e.g., buttons, inputs, modals). |
| `features/` | Contains the feature related to the user sign-up form. Follows a feature-based structure. |
| `services/` | Contains functions for API communication |
| `test/` | Contains configuration for test files |

## The idea

The idea behind this structure is to clearly separate responsibilities by type: **reusable components**, **feature-specific components**, **API communication (services)**, and **configuration**. This separation helps keep the codebase organized and easier to maintain. If our project grows to include more complex `domain` logic related to the user, we could add a domain folder to house domain rules and related services.

## Libraries

| **Library** | **Why use it** |
| ----------------------------- | -------------------------------------------------------------------------- |
| **MSW** | To simulate APIs in tests in a realistic way. |
| **react-hook-form** | To handle forms in a efficient way. |
| **Zod** | To validate form data |
| **Material UI (MUI)** | To use nice-looking components that are ready to use and easy to change. |
| **React Testing Library** | To write tests focused on what the user sees and does. |
| **@testing-library/jest-dom** | To use matchers that make tests clearer and closer to real user behavior. |
| **Prettier + ESLint** | To keep the code clean and consistent. |
| **React Router** | To handle navigation and routing in a React application with dynamic URLs. |

## Tests

These tests are keep close to the place that is used, following the [co-locate strategy](https://kentcdodds.com/blog/colocation#tests) by Ken C. Dodds.
Choose to follow the strategy to not test [implementing details](https://kentcdodds.com/blog/testing-implementation-details) by Kent C Dodds, It makes refactoring easier since the behavior remains the same and only the implementation code changes.

## Requests

Using `React Query`, we create custom hooks to implement the service layer responsible for communicating with the backend.
We avoid using `useQuery` directly in component files. Instead, we encapsulate **React Query** logic inside custom hooks located in the `service/(user|color).js` files.
This approach helps isolate logic, making it easier to switch to a different client or update the request implementation in the future without affecting the components.

## Form

Using `react-hopok-form` with zod to create a simple validation. React Hook Form make easy to check form state using the [useFormContext](https://react-hook-form.com/docs/useformcontext) provinding useful helpers and state.

## New scripts

All new scripts were created to improve the developer experience, with a focus on saving time by automating simple formatting tasks.

| Command | Description |
| ---------- | -------------------------------------------------------------- |
| `lint` | Check `.js` and `.jsx` files for code issues using ESLint. |
| `lint:fix` | Same as `lint`, but automatically fixes problems if possible. |
| `format` | Format `.js`, `.jsx`, `.json`, and `.md` files using Prettier. |
5 changes: 1 addition & 4 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
module.exports = {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
],
presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }]],
};
8 changes: 7 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
/>
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
/>
<title>Upgrade challenge</title>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<div id="app"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
35 changes: 32 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,40 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@hookform/resolvers": "^5.0.1",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@tanstack/react-query": "^5.77.0",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-hook-form": "^7.56.4",
"react-router": "^7.6.0",
"react-router-dom": "^7.6.0",
"zod": "^3.25.28"
},
"devDependencies": {
"@babel/eslint-parser": "^7.27.1",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.0.3",
"cross-fetch": "^4.1.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.6.1",
"jest-environment-jsdom": "^29.6.1",
"msw": "^1.3.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"supertest": "^6.3.3",
"vite": "^4.4.4"
},
Expand All @@ -22,7 +45,10 @@
"start:dev": "vite",
"start": "run-p start:*",
"build": "vite build",
"test": "jest --watch"
"test": "jest --watch",
"lint": "eslint . --ext .js,.jsx",
"lint:fix": "eslint . --ext .js,.jsx --fix",
"format": "prettier --write \"**/*.{js,jsx,json,md}\""
},
"browserslist": {
"production": [
Expand All @@ -37,6 +63,9 @@
]
},
"jest": {
"testEnvironment": "jsdom"
"testEnvironment": "jsdom",
"setupFilesAfterEnv": [
"<rootDir>/src/test/setup-jest.js"
]
}
}
28 changes: 15 additions & 13 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { Component } from "react";
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import SignUpForm from './features/user/SignUpForm';

const App = () => {
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default function App() {
return (
<div>
<header>
<h1>Welcome to Upgrade challenge</h1>
</header>
<p>
To get started, edit <code>src/App.jsx</code> and save to reload.
</p>
</div>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
<Route path="/*" element={<SignUpForm />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
);
};

export default App;
}
12 changes: 0 additions & 12 deletions src/App.test.jsx

This file was deleted.

17 changes: 17 additions & 0 deletions src/components/AlertFieldError/AlertFieldError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Alert, AlertTitle } from '@mui/material';

export default function AlertFieldError({ errors }) {
if (Object.keys(errors).length === 0) return null;

return (
<Alert severity="error">
<AlertTitle>Some fields need your attention</AlertTitle>

<ul>
{Object.entries(errors).map(([field]) => (
<li key={field}>{field}</li>
))}
</ul>
</Alert>
);
}
28 changes: 28 additions & 0 deletions src/components/AlertFieldError/AlertFieldError.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react';
import AlertFieldError from './AlertFieldError';

describe('AlertFieldError', () => {
it('shows errors from props', async () => {
const errors = {
name: {
message: 'First Name cannot be empty',
},
email: {
message: 'E-mail cannot be empty',
},
};
render(<AlertFieldError errors={errors} />);

expect(await screen.findByText(/Some fields need your attention/)).toBeVisible();
expect(await screen.findByText(/name/)).toBeVisible();
expect(await screen.findByText(/email/)).toBeVisible();
});

it('does not show alert component if no error', async () => {
const errors = {};

const { container } = render(<AlertFieldError errors={errors} />);

expect(container.firstChild).toBeNull();
});
});
41 changes: 41 additions & 0 deletions src/components/FeedbackPage/Feedback.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Typography, Button, Stack } from '@mui/material';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';

export default function Feedback({ onRestart, isSuccess, message }) {
return (
<Stack minHeight="70vh" alignItems="center" justifyContent="center">
<Typography
textTransform="uppercase"
color={isSuccess ? 'success' : 'error'}
variant="h5"
component="h2"
align="center"
gutterBottom
marginBottom={3}
>
{isSuccess ? 'Success!' : 'Error!'}
</Typography>

{isSuccess ? (
<CheckCircleOutlineIcon
aria-label="Check circle success icon"
fontSize="large"
color="success"
/>
) : (
<ErrorOutlineIcon aria-label="Error Outline icon" color="error" fontSize="large" />
)}

<Typography variant="body1" component="p" align="center" marginBottom={3} marginTop={3}>
{message}
</Typography>

<Stack justifyContent="center">
<Button variant="outlined" onClick={onRestart}>
Restart
</Button>
</Stack>
</Stack>
);
}
46 changes: 46 additions & 0 deletions src/components/FeedbackPage/Feedback.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { render, screen, waitFor } from '@testing-library/react';
import Feedback from './Feedback';
import userEvent from '@testing-library/user-event';

describe('Feedback', () => {
it.each([
{
isSuccess: true,
message: 'Operation successuly conclude',
onRestart: jest.fn(),
},
{
isSuccess: false,
message: 'Something went wrong',
onRestart: jest.fn(),
},
])('shows feedback for isSuccess=${props.isSuccess}', async (props) => {
render(
<Feedback isSuccess={props.isSuccess} onRestart={props.onRestart} message={props.message} />
);

expect(await screen.findByText(props.message)).toBeVisible();

if (props.isSuccess) {
expect(await screen.findByText('Success!')).toBeVisible();
expect(await screen.findByLabelText('Check circle success icon')).toBeVisible();
expect(screen.queryByLabelText('Error Outline icon')).not.toBeInTheDocument();
}
if (props.isSuccess === false) {
expect(await screen.findByText('Error!')).toBeVisible();
expect(await screen.findByLabelText('Error Outline icon')).toBeVisible();
expect(screen.queryByLabelText('Check circle success icon')).not.toBeInTheDocument();
}
});

it('calls restart function', async () => {
const onRestartMock = jest.fn();
render(<Feedback message="Hello World" onRestart={onRestartMock} isSuccess />);

await userEvent.click(await screen.findByRole('button', { name: /Restart/ }));

await waitFor(() => {
expect(onRestartMock).toHaveBeenCalled();
});
});
});
Loading