Skip to content
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
17 changes: 17 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"quill": "^2.0.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.72.0",
"react-hot-toast": "^2.5.2",
"react-router-dom": "^7.6.0",
"tailwindcss": "^4.1.7"
Expand Down
29 changes: 13 additions & 16 deletions client/src/components/admin/auth/Login/Login.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { useAppContext } from '../../../../context/AppContext'
import { adminApi } from '../../../../api'
import { Button, Input } from '../../../ui'
Expand All @@ -7,16 +8,14 @@ import { MESSAGES } from '../../../../constants/messages'

function Login() {
const { setToken } = useAppContext()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const { register, handleSubmit } = useForm()

const handleSubmit = async (e) => {
e.preventDefault()
const onSubmit = async (data) => {
setLoading(true)

try {
const response = await adminApi.login({ email, password })
const response = await adminApi.login(data)

if (response.data.success) {
setToken(response.data.token)
Expand All @@ -42,30 +41,28 @@ function Login() {
</h1>
<p className='font-light'>Enter your credentials to access the admin panel</p>
</div>
<form onSubmit={handleSubmit} className='mt-6 w-full sm:max-w-md text-gray-600'>

<form onSubmit={handleSubmit(onSubmit)} className='mt-6 w-full sm:max-w-md text-gray-600'>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General Reviewer · Login and CommentForm can silently do nothing on whitespace-only required inputs (warning)

<Input
label='Email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='your email id'
required
className='border-b-2 border-gray-300 mb-6 rounded-none border-x-0 border-t-0'
{...register('email', { required: true })}
/>

<Input
label='Password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='your password'
required
className='border-b-2 border-gray-300 mb-6 rounded-none border-x-0 border-t-0'
{...register('password', { required: true })}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General Reviewer · Login form keeps native validation path, so RHF required rules never run (suggestion)

The form has no noValidate, and the Input components still receive native required. As a result, browsers block invalid submits before React's onSubmit runs, so the new RHF register(..., { required: true }) validation is effectively dead code. Current behavior is preserved by native validation, but a future developer adding RHF errors UI could be misled. The same pattern applies to CommentForm.jsx lines 21-34.

Suggestion: Either add noValidate and explicitly handle RHF errors, or remove the redundant RHF required rules to make the intended validation path clear.

<Button
type='submit'

<Button
type='submit'
variant='primary'
fullWidth
loading={loading}
Expand Down
106 changes: 46 additions & 60 deletions client/src/components/admin/auth/Login/Login.test.jsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,80 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import toast from 'react-hot-toast'
import Login from '.'

vi.mock('react-hot-toast', () => ({
default: { success: vi.fn(), error: vi.fn() },
}))
import { describe, it, expect, vi, beforeEach } from 'vitest'
import Login from './Login'

const mockSetToken = vi.fn()

vi.mock('../../../../context/AppContext', () => ({
useAppContext: () => ({
setToken: mockSetToken,
}),
useAppContext: () => ({ setToken: mockSetToken })
}))

const server = setupServer()
const mockLogin = vi.fn()
vi.mock('../../../../api', () => ({
adminApi: { login: (...args) => mockLogin(...args) }
}))

beforeAll(() => server.listen())
afterEach(() => {
server.resetHandlers()
vi.clearAllMocks()
localStorage.clear()
})
afterAll(() => server.close())
vi.mock('react-hot-toast', () => ({
default: { success: vi.fn(), error: vi.fn() }
}))

vi.mock('../../../../constants/messages', () => ({
MESSAGES: {
SUCCESS_LOGIN: 'Login successful',
ERROR_LOGIN: 'Login failed'
}
}))

describe('Login', () => {
it('successful login sets token and shows success toast', async () => {
server.use(
http.post('http://localhost:5001/api/admin/login', () =>
HttpResponse.json({ success: true, token: 'jwt123' })
)
)
beforeEach(() => {
vi.clearAllMocks()
})

const user = userEvent.setup()
it('renders email and password inputs', () => {
render(<Login />)

await user.type(screen.getByPlaceholderText('your email id'), 'admin@quickblog.com')
await user.type(screen.getByPlaceholderText('your password'), 'admin123')
await user.click(screen.getByRole('button', { name: /login/i }))

await waitFor(() => {
expect(mockSetToken).toHaveBeenCalledWith('jwt123')
})

expect(localStorage.getItem('token')).toBe('jwt123')
expect(toast.success).toHaveBeenCalled()
expect(screen.getByPlaceholderText('your email id')).toBeInTheDocument()
expect(screen.getByPlaceholderText('your password')).toBeInTheDocument()
})

it('shows error toast on invalid credentials', async () => {
server.use(
http.post('http://localhost:5001/api/admin/login', () =>
HttpResponse.json({ message: 'Invalid credentials' }, { status: 401 })
)
)
it('renders login button', () => {
render(<Login />)
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument()
})

it('submits form with email and password', async () => {
const user = userEvent.setup()
mockLogin.mockResolvedValue({ data: { success: true, token: 'abc123' } })

render(<Login />)

await user.type(screen.getByPlaceholderText('your email id'), 'wrong@email.com')
await user.type(screen.getByPlaceholderText('your password'), 'wrongpass')
await user.type(screen.getByPlaceholderText('your email id'), 'test@example.com')
await user.type(screen.getByPlaceholderText('your password'), 'password123')
await user.click(screen.getByRole('button', { name: /login/i }))

await waitFor(() => {
expect(toast.error).toHaveBeenCalled()
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
})
})

expect(mockSetToken).not.toHaveBeenCalled()
await waitFor(() => {
expect(mockSetToken).toHaveBeenCalledWith('abc123')
})
})

it('disables submit button while loading', async () => {
server.use(
http.post('http://localhost:5001/api/admin/login', async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
return HttpResponse.json({ success: true, token: 'jwt123' })
})
)

it('shows error toast on failed login', async () => {
const user = userEvent.setup()
const toast = (await import('react-hot-toast')).default
mockLogin.mockRejectedValue({ response: { data: { message: 'Bad creds' } } })

render(<Login />)

await user.type(screen.getByPlaceholderText('your email id'), 'admin@quickblog.com')
await user.type(screen.getByPlaceholderText('your password'), 'admin123')
await user.type(screen.getByPlaceholderText('your email id'), 'test@example.com')
await user.type(screen.getByPlaceholderText('your password'), 'wrong')
await user.click(screen.getByRole('button', { name: /login/i }))

await waitFor(() => {
expect(screen.getByRole('button', { name: /loading/i })).toBeDisabled()
expect(toast.error).toHaveBeenCalledWith('Bad creds')
})
})
})
35 changes: 15 additions & 20 deletions client/src/components/comment/CommentForm/CommentForm.jsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
import React, { useState } from 'react'
import React from 'react'
import { useForm } from 'react-hook-form'
import Button from '../../ui/Button'
import Input from '../../ui/Input'
import Textarea from '../../ui/Textarea'

function CommentForm({ onSubmit, loading = false }) {
const [name, setName] = useState('')
const [content, setContent] = useState('')

const handleSubmit = async (e) => {
e.preventDefault()

const result = await onSubmit({ name, content })

const { register, handleSubmit, reset } = useForm()

const onFormSubmit = async (data) => {
const result = await onSubmit(data)

if (result?.success) {
setName('')
setContent('')
reset()
}
}

return (
<div className='max-w-3xl mx-auto'>
<p className='font-semibold mb-4'>Add your comment</p>
<form onSubmit={handleSubmit} className='flex flex-col items-start gap-4 max-w-lg'>
<form onSubmit={handleSubmit(onFormSubmit)} className='flex flex-col items-start gap-4 max-w-lg'>
<Input
type='text'
placeholder='Name'
value={name}
onChange={(e) => setName(e.target.value)}
required
{...register('name', { required: true })}
/>

<Textarea
placeholder='Comment'
value={content}
onChange={(e) => setContent(e.target.value)}
rows={6}
required
{...register('content', { required: true })}
/>
<Button
type='submit'

<Button
type='submit'
variant='primary'
loading={loading}
>
Expand Down
Loading
Loading