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

Frontend challenge david #49

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions INTRODUCTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Frontend Challenge

## Layout
For this challenge, I decided to use Mantine as the component library. This allowed me to have a consistent UI without worrying about setting up all the styling.

## Async State Management
To manage my state and API calls, I used Tanstack (React) Query. This allowed me to easily handle success and error states as well as the loading overlay.

## Localization
While I didn't implement localization since it wasn't needed in this case, I could have used react-intl to create a formatted message that could take in a key to map to a translated string.

## Testing
Unit testing was done on each page to ensure that fields were correctly filled out. This could be enhanced to include e2e testing such as with Cypress to test out the navigation between pages as well as the form submission.

## Possible Improvements
There are a few things that I could have improved in this solution:
- I could have made each field mandatory since they are required when submitting the form.
- With more time, I could have used typescript to enforce types. This would have been especially useful for the colours which could have been an enum.
13 changes: 13 additions & 0 deletions __mocks__/__setup__/setupTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
1 change: 1 addition & 0 deletions __mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
11 changes: 11 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react";


export default [
{files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
pluginReact.configs.flat.recommended,
];
27 changes: 27 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '@testing-library/jest-dom';

const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);
window.HTMLElement.prototype.scrollIntoView = () => {};

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}

window.ResizeObserver = ResizeObserver;
30 changes: 27 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,31 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@mantine/core": "^7.13.2",
"@mantine/form": "^7.13.2",
"@mantine/hooks": "^7.13.2",
"@tanstack/react-query": "^5.59.0",
"@testing-library/jest-dom": "^6.5.0",
"axios": "^1.7.7",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.12",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router": "^6.26.2",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@eslint/js": "^9.12.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^9.12.0",
"eslint-plugin-react": "^7.37.1",
"globals": "^15.10.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.6.1",
"jest-environment-jsdom": "^29.6.1",
"npm-run-all": "^4.1.5",
Expand All @@ -22,7 +39,8 @@
"start:dev": "vite",
"start": "run-p start:*",
"build": "vite build",
"test": "jest --watch"
"test": "jest --watch",
"lint": "eslint"
},
"browserslist": {
"production": [
Expand All @@ -37,6 +55,12 @@
]
},
"jest": {
"testEnvironment": "jsdom"
"testEnvironment": "jsdom",
"moduleNameMapper": {
"\\.(css|less|scss|sass)$": "identity-obj-proxy"
},
"setupFilesAfterEnv": [
"<rootDir>/jest.setup.js"
]
}
}
41 changes: 32 additions & 9 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
import React, { Component } from "react";
import { MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import React, { createContext } from "react";
import { RouterProvider } from "react-router-dom";
import { router } from "./navigation/router";
import { isEmail, useForm } from "@mantine/form";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export const FormContext = createContext(null);

const App = () => {
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
password: '',
color: '',
terms: false
},
validate: {
email: isEmail('Invalid email')
}
});

const queryClient = new QueryClient()

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>
<MantineProvider>
<QueryClientProvider client={queryClient}>
<FormContext.Provider value={{form}}>
<RouterProvider router={router} />
</FormContext.Provider>
</QueryClientProvider>
</MantineProvider>
);
};

Expand Down
1 change: 0 additions & 1 deletion src/App.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import App from "./App";

Expand Down
4 changes: 4 additions & 0 deletions src/apis/getColors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const fetchColors = async () => {
const res = await fetch("http://localhost:3001/api/colors");
return await res.json();
}
11 changes: 11 additions & 0 deletions src/apis/submitForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import axios from "axios";

export const submitForm = async ({ name, email, password, color, terms}) => {
await axios.post("http://localhost:3001/api/submit", {
name,
email,
password,
color,
terms
});
}
54 changes: 54 additions & 0 deletions src/components/AdditionalInfo/AdditionalInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Anchor, Button, Center, Checkbox, LoadingOverlay, Paper, Select, Title } from "@mantine/core";
import React, { useContext } from "react";
import { FormContext } from "../../App";
import { useQuery } from "@tanstack/react-query";
import { fetchColors } from "../../apis/getColors";
import { useNavigate } from "react-router";

const AdditionalInfo = () => {

const { data, isLoading } = useQuery({queryKey: ['colors'], queryFn: fetchColors});
const { form } = useContext(FormContext);
const navigate = useNavigate();
return <>
<Center>
<Paper shadow="xs" p="xl" pos="relative">
<LoadingOverlay visible={isLoading} />
<Title mb="md">ADDITIONAL INFO</Title>
<form onSubmit={form.onSubmit(() => navigate("/confirmation"))}>
<Select
data-testid="dropdown"
label="Favorite color"
placeholder="SELECT YOUR FAVORITE COLOR"
data={data}
pb="md"
key={form.key('color')}
{...form.getInputProps('color')}
/>
<Checkbox
data-testid="checkbox"
label={
<>
I AGREE TO{' '}
<Anchor onClick={() => navigate("/terms")}>
TERMS AND CONDITIONS
</Anchor>
.
</>
}
key={form.key('terms')}
{...form.getInputProps('terms', { type: "checkbox" })}
/>
<Button mt="xl" mb="xl" variant="subtle" mr="md" onClick={() => navigate("/")}>
BACK
</Button>
<Button mt="xl" mb="xl" onClick={() => navigate("/confirmation")}>
NEXT
</Button>
</form>
</Paper>
</Center>
</>
}

export default AdditionalInfo;
18 changes: 18 additions & 0 deletions src/components/AdditionalInfo/AdditionalInfo.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import AdditionalInfo from "./AdditionalInfo";
import { fireEvent, render, screen } from "../../../test-utils";

test("displays the additional info page", () => {
render(<AdditionalInfo />);
expect(screen.getByText("ADDITIONAL INFO")).toBeTruthy();
});

test("Correctly fills out all fields", () => {
render(<AdditionalInfo />);
const color = screen.getByTestId("dropdown");
const checkbox = screen.getByTestId("checkbox");
fireEvent.change(color, {target: {value:"red"}});
fireEvent.click(checkbox);
expect(color.value).toBe("red");
expect(checkbox.checked).toEqual(true);
});
54 changes: 54 additions & 0 deletions src/components/Confirmation/Confirmation.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, Center, LoadingOverlay, Paper, Text, Title } from "@mantine/core";
import React, { useContext } from "react";
import { FormContext } from "../../App";
import { useMutation } from "@tanstack/react-query";
import { submitForm } from "../../apis/submitForm";
import { useNavigate } from "react-router";

const Confirmation = () => {
const { form } = useContext(FormContext);
const navigate = useNavigate();
const mutation = useMutation({
mutationFn: submitForm
})
if (mutation.isError) {
navigate("/error")
} else {
if (mutation.isSuccess) {
navigate("/success")
}
}

return <>
<Center>
<Paper shadow="xs" p="xl" pos="relative">
<LoadingOverlay visible={mutation.isPending} />
<Title mb="md">CONFIRMATION</Title>
<form onSubmit={form.onSubmit(({ name, email, password, color, terms }) => {
console.log('test');
mutation.mutate({
name,
email,
password,
color,
terms
});
})}>
<Text>FIRST NAME: {form.getValues().name}</Text>
<Text>E-MAIL: {form.getValues().email}</Text>
<Text>PASSWORD: *****</Text>
<Text>FAVORITE COLOR: {form.getValues().color}</Text>
<Text>TERMS AND CONDITIONS: {form.getValues().terms.toString() == "true" ? "AGREED" : "NOT AGREED"}</Text>
<Button mt="xl" mb="xl" mr="md" variant="subtle" onClick={() => navigate("/more-info")}>
BACK
</Button>
<Button mt="xl" mb="xl" type="submit">
SUBMIT
</Button>
</form>
</Paper>
</Center>
</>
}

export default Confirmation;
8 changes: 8 additions & 0 deletions src/components/Confirmation/Confirmation.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";
import { render, screen } from "../../../test-utils";
import Confirmation from "./Confirmation";

test("displays the confirmation page", () => {
render(<Confirmation />);
expect(screen.getByText("CONFIRMATION")).toBeTruthy();
});
32 changes: 32 additions & 0 deletions src/components/Error/Error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Button, Center, CloseIcon, Paper, Text, Title } from "@mantine/core";
import React, { useContext } from "react";
import { useNavigate } from "react-router-dom";
import { FormContext } from "../../App";

const Error = () => {
const navigate = useNavigate();

const { form } = useContext(FormContext);

return <>
<Center>
<Paper shadow="xs" p="xl">
<Center>
<Title mb="md">ERROR</Title>
</Center>
<CloseIcon size={20} />
<Text>
UH OH, SOMETHING WENT WRONG. PLEASE TRY AGAIN LATER.
</Text>
<Button mt="xl" mb="xl" onClick={() => {
form.reset();
navigate("/");
}}>
RESTART
</Button>
</Paper>
</Center>
</>
}

export default Error;
8 changes: 8 additions & 0 deletions src/components/Error/Error.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";
import { render, screen } from "../../../test-utils";
import Error from "./Error";

test("displays the error page page", () => {
render(<Error />);
expect(screen.getByText("ERROR")).toBeTruthy();
});
Loading