├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── setupTests.js ├── src ├── App.jsx ├── __mocks__ │ └── msw │ │ ├── handlers.js │ │ └── server.js ├── __tests__ │ ├── App.test.jsx │ ├── AppWithRouter.test.jsx │ ├── PostContainer.test.jsx │ ├── TestInputField.test.jsx │ ├── UsernameDisplay.test.jsx │ ├── __snapshots__ │ │ └── PostContainer.test.jsx.snap │ └── utils │ │ └── helpers.jsx ├── components │ ├── LoginForm.jsx │ ├── PostContainer.jsx │ ├── PostContent.jsx │ ├── PostContentButtons.jsx │ ├── RegisterForm.jsx │ ├── TestInputField.jsx │ ├── UserDetails.jsx │ └── UsernameDisplay.jsx ├── globals.css ├── main.jsx ├── pages │ ├── BlogPostsPage.jsx │ └── UsersPage.jsx └── utils │ ├── constants.jsx │ ├── contexts │ └── UserContext.js │ └── hooks │ ├── useDocumentClick.js │ └── useFetchUser.js └── vite.config.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tutorial", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "test": "vitest" 12 | }, 13 | "dependencies": { 14 | "prop-types": "^15.8.1", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.22.3" 18 | }, 19 | "devDependencies": { 20 | "@testing-library/jest-dom": "^6.4.2", 21 | "@testing-library/react": "^14.2.2", 22 | "@testing-library/user-event": "^14.5.2", 23 | "@types/react": "^18.2.56", 24 | "@types/react-dom": "^18.2.19", 25 | "@vitejs/plugin-react": "^4.2.1", 26 | "eslint": "^8.56.0", 27 | "eslint-plugin-react": "^7.33.2", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "eslint-plugin-react-refresh": "^0.4.5", 30 | "jest": "^29.7.0", 31 | "jsdom": "^24.0.0", 32 | "msw": "^2.2.13", 33 | "sass": "^1.71.1", 34 | "vite": "^5.1.4", 35 | "vitest": "^1.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeAll, afterAll } from "vitest"; 2 | import { cleanup, configure } from "@testing-library/react"; 3 | import { server } from "./src/__mocks__/msw/server"; 4 | import "@testing-library/jest-dom/vitest"; 5 | 6 | configure({ asyncUtilTimeout: 5000 }); 7 | 8 | beforeAll(() => { 9 | server.listen(); 10 | }); 11 | 12 | afterEach(() => { 13 | cleanup(); 14 | server.resetHandlers(); 15 | }); 16 | 17 | afterAll(() => { 18 | server.close(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense, useEffect, useState } from "react"; 2 | import { PostContainer } from "./components/PostContainer"; 3 | import { UserDetails } from "./components/UserDetails"; 4 | import { UserContext } from "./utils/contexts/UserContext"; 5 | import { useFetchUser } from "./utils/hooks/useFetchUser"; 6 | import { Outlet, Link, useNavigate } from "react-router-dom"; 7 | import { TestInputField } from "./components/TestInputField"; 8 | 9 | export default function App({ usersData }) { 10 | const { user, loading, error } = useFetchUser(2); 11 | const [userData, setUserData] = useState(); 12 | // const navigate = useNavigate(); 13 | 14 | const [users, setUsers] = useState(usersData); 15 | 16 | useEffect(() => { 17 | if (!loading && !error && user) { 18 | setUserData(user); 19 | } 20 | }, [loading, error, user]); 21 | 22 | return ( 23 | Loading...}> 24 | 25 | {users.map((user) => ( 26 | 27 | ))} 28 | 41 | {/*
42 | 43 | { 47 | if (e.target.value.length > 10) { 48 | navigate("/blog-posts", { 49 | state: { 50 | posts: [ 51 | { 52 | id: 1, 53 | title: "hello world", 54 | content: "welcome to my first post", 55 | }, 56 | ], 57 | }, 58 | }); 59 | } 60 | }} 61 | /> 62 |
*/} 63 | 64 | 65 | 66 | 67 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/__mocks__/msw/handlers.js: -------------------------------------------------------------------------------- 1 | import { http } from "msw"; 2 | 3 | export const handlers = [ 4 | http.get("https://jsonplaceholder.typicode.com/users/*", ({ params }) => { 5 | return Response.json({ 6 | id: params.id, 7 | username: "josh", 8 | name: "josh", 9 | email: "josh@josh.com", 10 | }); 11 | }), 12 | ]; 13 | -------------------------------------------------------------------------------- /src/__mocks__/msw/server.js: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers) -------------------------------------------------------------------------------- /src/__tests__/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from "vitest"; 2 | import { render, screen, waitFor } from "@testing-library/react"; 3 | import userEvent from "@testing-library/user-event"; 4 | import { within } from "@testing-library/dom"; 5 | import App from "../App"; 6 | import { server } from "../__mocks__/msw/server"; 7 | import { http } from "msw"; 8 | 9 | describe("when there is only 1 user", () => { 10 | describe("Edit Button is Clicked", () => { 11 | it("should render save button", async () => { 12 | render( 13 | 22 | ); 23 | const editButton = screen.getByRole("button", { name: "Edit" }); 24 | await userEvent.click(editButton); 25 | const saveButton = screen.getByRole("button", { name: "Save" }); 26 | expect(saveButton).toBeInTheDocument(); 27 | }); 28 | it("should display username & email input fields", async () => { 29 | render( 30 | 39 | ); 40 | const editButton = screen.getByRole("button", { name: "Edit" }); 41 | await userEvent.click(editButton); 42 | expect(screen.getByLabelText("Username:")).toBeInTheDocument(); 43 | expect(screen.getByLabelText("Email:")).toBeInTheDocument(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe("when there are 2 users", () => { 49 | it("should have two users", () => { 50 | render( 51 | 65 | ); 66 | 67 | expect(screen.getByText("ansonthedev")).toBeInTheDocument(); 68 | expect(screen.getByText("michael")).toBeInTheDocument(); 69 | }); 70 | 71 | it("should click edit button for 1st user and display save button", async () => { 72 | render( 73 | 87 | ); 88 | const userDetails = screen.getByTestId("user-details-1"); 89 | expect(within(userDetails).queryByText("michael")).toBeNull(); 90 | const editBtn = within(userDetails).getByRole("button", { name: "Edit" }); 91 | await userEvent.click(editBtn); 92 | expect( 93 | within(userDetails).getByRole("button", { name: "Save" }) 94 | ).toBeInTheDocument(); 95 | }); 96 | 97 | it("should edit 2nd username and save", async () => { 98 | render( 99 | 113 | ); 114 | const userDetails = screen.getByTestId("user-details-2"); 115 | await userEvent.click( 116 | within(userDetails).getByRole("button", { name: "Edit" }) 117 | ); 118 | await userEvent.type( 119 | within(userDetails).getByLabelText("Username:"), 120 | "123" 121 | ); 122 | await userEvent.click( 123 | within(userDetails).getByRole("button", { name: "Save" }) 124 | ); 125 | expect(within(userDetails).queryByLabelText("Username:")).toBeNull(); 126 | expect(within(userDetails).getByText("michael123")).toBeInTheDocument(); 127 | }); 128 | }); 129 | 130 | describe("rendering context data", () => { 131 | it("should render correct Email", async () => { 132 | server.use( 133 | http.get( 134 | "https://jsonplaceholder.typicode.com/users/*", 135 | async ({ params }) => { 136 | return Response.json({ 137 | id: params.id, 138 | username: "joshua", 139 | name: "joshua", 140 | email: "joshua@yahoo.com", 141 | }); 142 | } 143 | ) 144 | ); 145 | 146 | render(); 147 | await waitFor(async () => 148 | expect( 149 | await screen.findByText("Email: joshua@yahoo.com") 150 | ).toBeInTheDocument() 151 | ); 152 | }); 153 | }); 154 | describe("updating UserContext", () => { 155 | it("should update display name", async () => { 156 | render(); 157 | await userEvent.type( 158 | screen.getByLabelText("Update Name:"), 159 | "Jonathan The Dev" 160 | ); 161 | await userEvent.click( 162 | screen.getByRole("button", { name: "Save Display Name" }) 163 | ); 164 | expect( 165 | screen.getByText("Display Name: Jonathan The Dev") 166 | ).toBeInTheDocument(); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /src/__tests__/AppWithRouter.test.jsx: -------------------------------------------------------------------------------- 1 | import { renderWithRouter } from "./utils/helpers"; 2 | import { expect, it } from "vitest"; 3 | import { screen } from "@testing-library/react"; 4 | import userEvent from "@testing-library/user-event"; 5 | 6 | it("should click on Users link and navigate to users route", async () => { 7 | renderWithRouter({ initialEntries: ["/"] }); 8 | await userEvent.click(screen.getByRole("link", { name: "Users" })); 9 | 10 | expect(screen.getByText("Welcome to Users Dashboard")).toBeInTheDocument(); 11 | }); 12 | 13 | it("should navigate to /blog-posts and back to /", async () => { 14 | renderWithRouter({ initialEntries: ["/"] }); 15 | await userEvent.click(screen.getByRole("link", { name: "Blogs" })); 16 | expect(screen.getByText("Welcome to BlogPosts Page")).toBeInTheDocument(); 17 | await userEvent.click(screen.getByRole("link", { name: "Home" })); 18 | expect(screen.queryByText("Welcome to BlogPosts Page")).toBeNull(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/PostContainer.test.jsx: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { PostContainer } from "../components/PostContainer"; 4 | import { UserContext } from "../utils/contexts/UserContext"; 5 | 6 | describe("render context values", () => { 7 | const mockUserContextData = { 8 | id: 1001, 9 | username: "johnny", 10 | email: "johnny@gmail.com", 11 | name: "Johnny", 12 | setUserData: () => {}, 13 | }; 14 | 15 | it("should match snapshot", () => { 16 | const { container } = render( 17 | 18 | 19 | 20 | ); 21 | expect(container).toMatchSnapshot(); 22 | }); 23 | 24 | it("should display correct username", () => { 25 | render( 26 | 27 | 28 | 29 | ); 30 | 31 | expect(screen.getByText("Username: johnny")).toBeInTheDocument(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/TestInputField.test.jsx: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { TestInputField } from "../components/TestInputField"; 4 | 5 | it("should find input by placeholder value", () => { 6 | render(); 7 | expect(screen.getByPlaceholderText(/enter data/)).toBeInTheDocument(); 8 | }); 9 | 10 | it("should find input by display value", () => { 11 | render(); 12 | expect(screen.getByDisplayValue("hello")).toBeInTheDocument(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/UsernameDisplay.test.jsx: -------------------------------------------------------------------------------- 1 | import { describe, it, test, expect } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { UsernameDisplay } from "../components/UsernameDisplay"; 4 | 5 | describe("UsernameDisplay", () => { 6 | it("should render username", async () => { 7 | render(); 8 | expect( 9 | await screen.findByText("ansonthedev", {}, { timeout: 2000 }) 10 | ).toBeInTheDocument(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/PostContainer.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`render context values > should match snapshot 1`] = ` 4 |
5 |
6 |
7 |

8 | PostContainer 9 |

10 |
11 |
12 | Display Name: 13 | Johnny 14 |
15 |
16 | ID: 17 | 1001 18 |
19 |
20 | Email: 21 | johnny@gmail.com 22 |
23 |
24 | Username: 25 | johnny 26 |
27 |
28 |
29 |

30 | PostContent 31 |

32 |
33 |
34 |
35 |

36 | PostContentButtons 37 |

38 |
39 | 1001 40 |
41 | 46 | 50 | 53 |
54 | johnny@gmail.com 55 |
56 |
57 |
58 | `; 59 | -------------------------------------------------------------------------------- /src/__tests__/utils/helpers.jsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createMemoryRouter } from "react-router-dom"; 2 | import { routes } from "../../utils/constants"; 3 | import { render } from "@testing-library/react"; 4 | 5 | export const renderWithRouter = ({ initialEntries = [], initialIndex = 0 }) => { 6 | const router = createMemoryRouter(routes, { 7 | initialEntries, 8 | initialIndex, 9 | }); 10 | 11 | return render(); 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import { useDocumentClick } from "../utils/hooks/useDocumentClick"; 3 | import { UserDetails } from "./UserDetails"; 4 | import { AppContext } from "../utils/contexts/AppContext"; 5 | import { RegisterForm } from "./RegisterForm"; 6 | 7 | export function LoginForm() { 8 | const appContext = useContext(AppContext); 9 | 10 | console.log(appContext); 11 | 12 | useEffect(() => { 13 | const resizeEventHandler = (e) => { 14 | console.log("Window/ViewPort Resized!"); 15 | }; 16 | 17 | window.addEventListener("resize", resizeEventHandler); 18 | 19 | return () => { 20 | console.log("Unmounting LoginForm"); 21 | console.log("Removing Resize Event Listener"); 22 | window.removeEventListener("resize", resizeEventHandler); 23 | }; 24 | }, []); 25 | 26 | useDocumentClick(); 27 | 28 | return ( 29 |
{ 31 | e.preventDefault(); 32 | const formData = new FormData(e.target); 33 | const username = formData.get("username"); 34 | const password = formData.get("password"); 35 | fetch("http://localhost:3001/api/login", { 36 | body: { 37 | username, 38 | password, 39 | }, 40 | method: "POST", 41 | }); 42 | }} 43 | > 44 | 45 |
46 | { 49 | console.log(`Username: ${e.target.value}`); 50 | }} 51 | name="username" 52 | /> 53 |
54 | 55 |
56 | { 60 | console.log(`Password: ${e.target.value}`); 61 | }} 62 | name="password" 63 | /> 64 |
65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/PostContainer.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { PostContent } from "./PostContent"; 3 | import { UserContext } from "../utils/contexts/UserContext"; 4 | 5 | export function PostContainer() { 6 | const userContextData = useContext(UserContext); 7 | 8 | return ( 9 |
10 |
11 |

PostContainer

12 |
13 |
Display Name: {userContextData.name}
14 |
ID: {userContextData.id}
15 |
Email: {userContextData.email}
16 |
Username: {userContextData.username}
17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/PostContent.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { PostContentButtons } from "./PostContentButtons"; 3 | import { UserContext } from "../utils/contexts/UserContext"; 4 | 5 | export function PostContent() { 6 | const userContextData = useContext(UserContext); 7 | 8 | return ( 9 |
10 |
11 |

PostContent

12 |
13 | 14 | {userContextData.email} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/PostContentButtons.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { UserContext } from "../utils/contexts/UserContext"; 3 | 4 | export function PostContentButtons() { 5 | const { id, setUserData } = useContext(UserContext); 6 | const [value, setValue] = useState(""); 7 | 8 | return ( 9 |
10 |
11 |

PostContentButtons

12 |
13 | {id} 14 |
15 | 16 | setValue(e.target.value)} 20 | /> 21 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/RegisterForm.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { AppContext } from "../utils/contexts/AppContext"; 3 | 4 | export function RegisterForm() { 5 | const [formFields, setFormFields] = useState({ 6 | username: "", 7 | password: "", 8 | displayName: "", 9 | }); 10 | 11 | const appContext = useContext(AppContext); 12 | 13 | return ( 14 |
15 |
16 | 17 | { 21 | setFormFields((currentState) => ({ 22 | ...currentState, 23 | username: e.target.value, 24 | })); 25 | }} 26 | /> 27 |
28 |
29 | 30 | { 34 | setFormFields((currentState) => ({ 35 | ...currentState, 36 | password: e.target.value, 37 | })); 38 | }} 39 | /> 40 |
41 |
42 | 43 | { 47 | setFormFields((currentState) => ({ 48 | ...currentState, 49 | displayName: e.target.value, 50 | })); 51 | }} 52 | /> 53 |
54 |
55 | Username: {formFields.username} 56 |
57 |
58 | Password: {formFields.password} 59 |
60 |
61 | Display Name: {formFields.displayName} 62 |
63 | {/* */} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/TestInputField.jsx: -------------------------------------------------------------------------------- 1 | export function TestInputField() { 2 | return ( 3 | {}} /> 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/UserDetails.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useState } from "react"; 3 | 4 | export function UserDetails({ user, setUsers }) { 5 | const [isEditing, setIsEditing] = useState(false); 6 | const [username, setUsername] = useState(user.username); 7 | const [email, setEmail] = useState(user.email); 8 | 9 | return ( 10 |
11 |
12 | 19 | 30 | {isEditing && ( 31 | 45 | )} 46 |
47 |
48 | ID: 49 | {user.id} 50 |
51 | {isEditing ? ( 52 | 53 | ) : ( 54 | Username: 55 | )} 56 | {isEditing ? ( 57 | { 63 | setUsername(e.target.value); 64 | }} 65 | /> 66 | ) : ( 67 | {user.username} 68 | )} 69 |
70 | {isEditing ? : Email: } 71 | {isEditing ? ( 72 | { 77 | setEmail(e.target.value); 78 | }} 79 | /> 80 | ) : ( 81 | {user.email} 82 | )} 83 |
84 |
85 |
86 | ); 87 | } 88 | 89 | UserDetails.propTypes = { 90 | user: PropTypes.shape({ 91 | id: PropTypes.number.isRequired, 92 | username: PropTypes.string.isRequired, 93 | email: PropTypes.string.isRequired, 94 | }).isRequired, 95 | setUsers: PropTypes.func.isRequired, 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/UsernameDisplay.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function UsernameDisplay({ username }) { 4 | const [timerPassed, setTimerPassed] = useState(false); 5 | 6 | useEffect(() => { 7 | setTimeout(() => { 8 | setTimerPassed(true); 9 | }, 1500); 10 | }, []); 11 | 12 | return ( 13 |
14 | {timerPassed && username} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #1e1e1e; 3 | color: #e3e3e3; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 4 | import "./globals.css"; 5 | import { routes } from "./utils/constants"; 6 | 7 | export const router = createBrowserRouter(routes); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/pages/BlogPostsPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | 4 | export function BlogPostsPage() { 5 | const [posts, setPosts] = useState([]); 6 | const { state } = useLocation(); 7 | 8 | useEffect(() => { 9 | if (state && state.posts) { 10 | setPosts(state.posts); 11 | } 12 | }, [state]); 13 | 14 | return ( 15 |
16 |

Welcome to BlogPosts Page

17 | {posts.map((post) => ( 18 |
19 |

{post.title}

20 |
21 |

{post.content}

22 |
23 |
24 | ))} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/UsersPage.jsx: -------------------------------------------------------------------------------- 1 | export function UsersPage() { 2 | return ( 3 |
4 |

Welcome to Users Dashboard

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/constants.jsx: -------------------------------------------------------------------------------- 1 | import App from "../App"; 2 | import { BlogPostsPage } from "../pages/BlogPostsPage"; 3 | import { UsersPage } from "../pages/UsersPage"; 4 | 5 | export const routes = [ 6 | { 7 | path: "/", 8 | element: ( 9 | 23 | ), 24 | children: [ 25 | { 26 | path: "/blog-posts", 27 | element: , 28 | }, 29 | ], 30 | }, 31 | { 32 | path: "users", 33 | element: , 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/utils/contexts/UserContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export const UserContext = createContext({ 4 | id: 0, 5 | username: "", 6 | email: "", 7 | name: "", 8 | setUserData: () => {}, 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/hooks/useDocumentClick.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export function useDocumentClick() { 4 | useEffect(() => { 5 | console.log("useDocumentClick"); 6 | 7 | const handleDocumentClick = (e) => { 8 | console.log("Clicked Document"); 9 | }; 10 | document.addEventListener("click", handleDocumentClick); 11 | 12 | return () => { 13 | document.removeEventListener("click", handleDocumentClick); 14 | }; 15 | }, []); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/hooks/useFetchUser.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const userApiUrl = "https://jsonplaceholder.typicode.com/users"; 4 | 5 | export function useFetchUser(userId) { 6 | const [userData, setUserData] = useState({}); 7 | const [loading, setLoading] = useState(false); 8 | const [error, setError] = useState(); 9 | 10 | useEffect(() => { 11 | const controller = new AbortController(); 12 | setLoading(true); 13 | fetch(`${userApiUrl}/${userId}`, { signal: controller.signal }) 14 | .then((response) => response.json()) 15 | .then((data) => { 16 | setUserData(data); 17 | setError(undefined); 18 | }) 19 | .catch((err) => { 20 | setError(err); 21 | }) 22 | .finally(() => { 23 | setTimeout(() => { 24 | setLoading(false); 25 | }, 2000); 26 | }); 27 | return () => { 28 | controller.abort(); 29 | setTimeout(() => { 30 | setLoading(false); 31 | }, 2000); 32 | }; 33 | }, [userId]); 34 | 35 | return { user: userData, loading, error }; 36 | } 37 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | environment: "jsdom", 9 | setupFiles: "./setupTests.js", 10 | testTimeout: 10000, 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------