├── .eslintrc.json ├── .gitignore ├── README.md ├── __tests__ ├── HomePage.test.tsx ├── LoginForm.test.tsx ├── UserList.test.tsx └── UserProfile.test.tsx ├── jest.config.ts ├── jest.setup.js ├── mocks ├── handlers.js └── server.js ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── route.js │ │ └── users │ │ │ └── route.js │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ └── page.tsx └── components │ ├── LoginForm.jsx │ ├── UserList.jsx │ └── UserProfile.jsx └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:testing-library/react", 5 | "plugin:jest-dom/recommended" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | This project has `msw` as a dependency. As of right now (6/9/2023), installing `msw` in a Typescrupt project errors out if the version is less than 5.0.x. The current version of Typescript in this project is 5.1.3. You can just install `msw` with the `--force` flag like this: 4 | 5 | `npm install -D msw --force` 6 | -------------------------------------------------------------------------------- /__tests__/HomePage.test.tsx: -------------------------------------------------------------------------------- 1 | import Home from '@/app/page'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | 5 | describe('Home Page ', () => { 6 | describe('Rendering', () => { 7 | it('should have Home Page text', () => { 8 | render(); 9 | expect(screen.getByText('Home Page')).toBeInTheDocument(); 10 | }); 11 | 12 | it('should have button with text Click Me', () => { 13 | render(); 14 | expect( 15 | screen.getByRole('button', { name: 'Click Me' }) 16 | ).toBeInTheDocument(); 17 | }); 18 | 19 | it('should have input field with label Enter Random Text', () => { 20 | render(); 21 | expect(screen.getByLabelText(/Enter Random Text/)).toBeInTheDocument(); 22 | }); 23 | 24 | it('should have input field with label Enter Specific Text', () => { 25 | render(); 26 | expect(screen.getByLabelText(/Specific/)).toBeInTheDocument(); 27 | }); 28 | 29 | it('should find input field by placeholder text Search', () => { 30 | render(); 31 | expect(screen.getByPlaceholderText(/Search/)).toBeInTheDocument(); 32 | }); 33 | 34 | it('should find input field by display value', () => { 35 | render(); 36 | screen.getByDisplayValue(/AUDI/); 37 | }); 38 | 39 | it('should not find element with text: This is the text!', () => { 40 | render(); 41 | expect(screen.queryByText('This is the text!')).not.toBeInTheDocument(); 42 | }); 43 | }); 44 | 45 | describe('Behavior', () => { 46 | it('should click on Show Text button and find new text', async () => { 47 | render(); 48 | expect(screen.queryByText('This is the text!')).not.toBeInTheDocument(); 49 | const showTextButton = screen.getByRole('button', { name: 'Show Text' }); 50 | await userEvent.click(showTextButton); 51 | expect( 52 | await screen.findByText('This is the text!', {}, { timeout: 5000 }) 53 | ).toBeInTheDocument(); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /__tests__/LoginForm.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import { LoginForm } from '../src/components/LoginForm'; 3 | import userEvent from '@testing-library/user-event'; 4 | import { server } from '../mocks/server'; 5 | import { rest } from 'msw'; 6 | 7 | describe('LoginForm', () => { 8 | it('should enter username and password and click on login button', async () => { 9 | render(); 10 | const loginButton = screen.getByRole('button', { name: 'Login' }); 11 | expect(loginButton).toBeDisabled(); 12 | await userEvent.type(screen.getByLabelText(/Username/), 'anson'); 13 | await userEvent.type(screen.getByLabelText(/Password/), 'password'); 14 | expect(loginButton).toBeEnabled(); 15 | await userEvent.click(loginButton); 16 | await waitFor(() => { 17 | expect(screen.getByText('Success Logging In')).toBeInTheDocument(); 18 | }); 19 | }); 20 | 21 | it('should login user and display error message', async () => { 22 | server.use( 23 | rest.post('/api/auth', (req, res, ctx) => { 24 | return res(ctx.status(400)); 25 | }) 26 | ); 27 | render(); 28 | const loginButton = screen.getByRole('button', { name: 'Login' }); 29 | expect(loginButton).toBeDisabled(); 30 | await userEvent.type(screen.getByLabelText(/Username/), 'anson'); 31 | await userEvent.type(screen.getByLabelText(/Password/), 'password'); 32 | expect(loginButton).toBeEnabled(); 33 | await userEvent.click(loginButton); 34 | await waitFor(() => { 35 | expect(screen.getByText('Error Logging In')).toBeInTheDocument(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/UserList.test.tsx: -------------------------------------------------------------------------------- 1 | import { UserList } from '@/components/UserList'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { server } from '../mocks/server'; 4 | import { rest } from 'msw'; 5 | 6 | describe('UserList - Rendering', () => { 7 | it('should have the text anson', async () => { 8 | render(); 9 | expect(await screen.findByText('anson')).toBeInTheDocument(); 10 | expect(screen.queryByText('No Users')).not.toBeInTheDocument(); 11 | }); 12 | 13 | it('should have username mike rendered', async () => { 14 | server.use( 15 | rest.get('/api/users', (req, res, ctx) => { 16 | return res(ctx.json([{ id: 2, username: 'mike' }])); 17 | }) 18 | ); 19 | render(); 20 | expect(await screen.findByText('mike')).toBeInTheDocument(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/UserProfile.test.tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from '@/components/UserProfile'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | describe('UserProfile - Rendering', () => { 5 | it('should have text Email Verified when isEmailVerified is true', () => { 6 | render( 7 | 13 | ); 14 | expect(screen.getByText('Email Verified')).toBeInTheDocument(); 15 | }); 16 | 17 | it('should have text Email Not Verified when isEmailVerified is false', () => { 18 | render( 19 | 25 | ); 26 | expect(screen.queryByText('Email Verified')).not.toBeInTheDocument(); 27 | expect(screen.getByText('Email Not Verified')).toBeInTheDocument(); 28 | }); 29 | 30 | it('should have display name end with three dots when length is greater than 30 characters', () => { 31 | render( 32 | 38 | ); 39 | const displayNameSpanElement = screen.getByText(/Display Name: /); 40 | expect(displayNameSpanElement).toHaveTextContent(/.*\.\.\./); 41 | }); 42 | 43 | it('should not have three dots at end of display name when less than 30 characters', () => { 44 | render( 45 | 51 | ); 52 | const displayNameSpanElement = screen.getByText(/Display Name: /); 53 | expect(displayNameSpanElement).not.toHaveTextContent(/.*\.\.\./); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import nextJest from 'next/jest.js'; 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: './', 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | /** @type {import('jest').Config} */ 10 | const config = { 11 | // Add more setup options before each test is run 12 | setupFilesAfterEnv: ['/jest.setup.js'], 13 | testEnvironment: 'jest-environment-jsdom', 14 | preset: 'ts-jest', 15 | }; 16 | 17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 18 | export default createJestConfig(config); 19 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import 'whatwg-fetch'; 3 | import { server } from './mocks/server'; 4 | 5 | beforeAll(() => { 6 | server.listen(); 7 | }); 8 | 9 | afterEach(() => { 10 | server.resetHandlers(); 11 | }); 12 | 13 | afterAll(() => { 14 | server.close(); 15 | }); 16 | -------------------------------------------------------------------------------- /mocks/handlers.js: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export const handlers = [ 4 | rest.get('/api/users', (req, res, ctx) => { 5 | return res(ctx.json([{ id: 1, username: 'anson' }])); 6 | }), 7 | rest.post('/api/auth', (req, res, ctx) => { 8 | return res(ctx.json({})); 9 | }), 10 | ]; 11 | -------------------------------------------------------------------------------- /mocks/server.js: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-tests-react-ts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest", 11 | "test:watch": "jest --watchAll" 12 | }, 13 | "dependencies": { 14 | "@types/node": "20.2.5", 15 | "@types/react": "18.2.8", 16 | "@types/react-dom": "18.2.4", 17 | "eslint": "8.42.0", 18 | "eslint-config-next": "13.4.4", 19 | "next": "13.4.4", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "typescript": "5.1.3" 23 | }, 24 | "devDependencies": { 25 | "@testing-library/jest-dom": "^5.16.5", 26 | "@testing-library/react": "^14.0.0", 27 | "@testing-library/user-event": "^14.4.3", 28 | "eslint-plugin-jest-dom": "^5.0.1", 29 | "eslint-plugin-testing-library": "^5.11.0", 30 | "jest": "^29.5.0", 31 | "jest-environment-jsdom": "^29.5.0", 32 | "msw": "^1.2.1", 33 | "ts-jest": "^29.1.0", 34 | "whatwg-fetch": "^3.6.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/auth/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST(req) { 4 | const json = await req.json(); 5 | console.log(json.username); 6 | return NextResponse.json({}); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/api/users/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function GET(req) { 4 | return NextResponse.json([ 5 | { 6 | id: 1, 7 | username: 'anson', 8 | }, 9 | { 10 | id: 2, 11 | username: 'jack', 12 | }, 13 | { 14 | id: 3, 15 | username: 'mike', 16 | }, 17 | ]); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuyy/nextjs-react-unit-testing-tutorial-typescript/29b6c9679a95539b5ca8ffe0acfc5de62023a182/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo { 108 | position: relative; 109 | } 110 | /* Enable hover only on non-touch devices */ 111 | @media (hover: hover) and (pointer: fine) { 112 | .card:hover { 113 | background: rgba(var(--card-rgb), 0.1); 114 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 115 | } 116 | 117 | .card:hover span { 118 | transform: translateX(4px); 119 | } 120 | } 121 | 122 | @media (prefers-reduced-motion) { 123 | .card:hover span { 124 | transform: none; 125 | } 126 | } 127 | 128 | /* Mobile */ 129 | @media (max-width: 700px) { 130 | .content { 131 | padding: 4rem; 132 | } 133 | 134 | .grid { 135 | grid-template-columns: 1fr; 136 | margin-bottom: 120px; 137 | max-width: 320px; 138 | text-align: center; 139 | } 140 | 141 | .card { 142 | padding: 1rem 2.5rem; 143 | } 144 | 145 | .card h2 { 146 | margin-bottom: 0.5rem; 147 | } 148 | 149 | .center { 150 | padding: 8rem 0 6rem; 151 | } 152 | 153 | .center::before { 154 | transform: none; 155 | height: 300px; 156 | } 157 | 158 | .description { 159 | font-size: 0.8rem; 160 | } 161 | 162 | .description a { 163 | padding: 1rem; 164 | } 165 | 166 | .description p, 167 | .description div { 168 | display: flex; 169 | justify-content: center; 170 | position: fixed; 171 | width: 100%; 172 | } 173 | 174 | .description p { 175 | align-items: center; 176 | inset: 0 0 auto; 177 | padding: 2rem 1rem 1.4rem; 178 | border-radius: 0; 179 | border: none; 180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(var(--background-start-rgb), 1), 184 | rgba(var(--callout-rgb), 0.5) 185 | ); 186 | background-clip: padding-box; 187 | backdrop-filter: blur(24px); 188 | } 189 | 190 | .description div { 191 | align-items: flex-end; 192 | pointer-events: none; 193 | inset: auto 0 0; 194 | padding: 2rem; 195 | height: 200px; 196 | background: linear-gradient( 197 | to bottom, 198 | transparent 0%, 199 | rgb(var(--background-end-rgb)) 40% 200 | ); 201 | z-index: 1; 202 | } 203 | } 204 | 205 | /* Tablet and Smaller Desktop */ 206 | @media (min-width: 701px) and (max-width: 1120px) { 207 | .grid { 208 | grid-template-columns: repeat(2, 50%); 209 | } 210 | } 211 | 212 | @media (prefers-color-scheme: dark) { 213 | .vercelLogo { 214 | filter: invert(1); 215 | } 216 | 217 | .logo { 218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 219 | } 220 | } 221 | 222 | @keyframes rotate { 223 | from { 224 | transform: rotate(360deg); 225 | } 226 | to { 227 | transform: rotate(0deg); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { LoginForm } from '@/components/LoginForm'; 3 | import { UserList } from '@/components/UserList'; 4 | import { UserProfile } from '@/components/UserProfile'; 5 | import { useState } from 'react'; 6 | 7 | export default function Home() { 8 | const [showText, setShowText] = useState(false); 9 | return ( 10 |
11 |

Home Page

12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | {}} /> 26 |
27 |
28 | {showText && This is the text!} 29 | 38 |
39 | 45 | 46 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const LoginForm = () => { 4 | const [username, setUsername] = useState(''); 5 | const [password, setPassword] = useState(''); 6 | const [error, setError] = useState(''); 7 | const [success, setSuccess] = useState(''); 8 | 9 | const isDisabled = () => !username || !password; 10 | 11 | const handleLogin = (e) => { 12 | setError(''); 13 | setSuccess(''); 14 | e.preventDefault(); 15 | fetch('/api/auth', { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | username, 19 | password, 20 | }), 21 | }) 22 | .then((res) => res.json()) 23 | .then(() => { 24 | setSuccess('Success Logging In'); 25 | }) 26 | .catch((err) => { 27 | setError('Error Logging In'); 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 |
{error}
34 |
{success}
35 | 36 | { 41 | setUsername(e.target.value); 42 | }} 43 | /> 44 | 45 | 46 | { 51 | setPassword(e.target.value); 52 | }} 53 | /> 54 | 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/UserList.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const UserList = () => { 4 | const [users, setUsers] = useState([]); 5 | 6 | useEffect(() => { 7 | fetch('/api/users') 8 | .then((res) => res.json()) 9 | .then((data) => setUsers(data)); 10 | }, []); 11 | 12 | return ( 13 |
14 | {users.length > 0 ? ( 15 |
16 |

List Of Users

17 | {users.map((user) => ( 18 |
{user.username}
19 | ))} 20 |
21 | ) : ( 22 | No Users 23 | )} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/UserProfile.jsx: -------------------------------------------------------------------------------- 1 | export const UserProfile = ({ 2 | displayName, 3 | username, 4 | email, 5 | isEmailVerified, 6 | }) => ( 7 |
8 |
9 | 10 | Display Name:{' '} 11 | {displayName.length > 30 12 | ? displayName.slice(0, 28).concat('...') 13 | : displayName} 14 | 15 |
16 |
17 | Username: {username} 18 |
19 |
20 | Email: {email} 21 |
22 |
23 | Verified: 24 | {isEmailVerified ? 'Email Verified' : 'Email Not Verified'} 25 |
26 |
27 | ); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------