diff --git a/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png b/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png new file mode 100644 index 0000000..cb5d6ff Binary files /dev/null and b/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png differ diff --git a/frontend/src/index.css b/frontend/src/index.css index 33a8327..7994525 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,7 @@ @import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + :root { --text: #6b6375; --text-h: #08060d; @@ -16,24 +19,14 @@ --heading: system-ui, 'Segoe UI', Roboto, sans-serif; --mono: ui-monospace, Consolas, monospace; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - @media (max-width: 1024px) { font-size: 16px; } } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; +.dark { + /* dark theme — prepared for future, not active yet */ + --text: #9ca3af; --text-h: #f3f4f6; --bg: #16171d; --border: #2e303a; @@ -44,8 +37,7 @@ --social-bg: rgba(47, 48, 58, 0.5); --shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - + #social .button-icon { filter: invert(1) brightness(2); } @@ -63,50 +55,64 @@ box-sizing: border-box; } -body { - margin: 0; -} +@layer base { + html { + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; + body { + margin: 0; } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + + h1, + h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); + } + + h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } + } + h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } + } + p { + margin: 0; + } + + code, + .counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); + } + + code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); + } } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..318fbf8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +document.documentElement.classList.add('light'); +document.documentElement.dataset.theme = 'light'; + createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 2d24284..eea2750 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,9 +1,11 @@ +// frontend/src/pages/LoginPage.tsx import { useState } from 'react'; import { useNavigate, useLocation, Link } from 'react-router-dom'; import { AxiosError } from 'axios'; import { authService } from '../services/authService'; import { useAuth } from '../hooks/useAuth'; import type { LoginPayload, ApiValidationError, UserRole } from '../types/auth'; +import doctorsIllustration from '../assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png'; type FieldErrors = Partial>; @@ -24,6 +26,7 @@ export default function LoginPage() { const [form, setForm] = useState({ email: '', password: '' }); const [rememberMe, setRememberMe] = useState(false); + const [showPassword, setShowPassword] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); const [genericError, setGenericError] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -66,235 +69,184 @@ export default function LoginPage() { } return ( -
-
-
-

QueueNova Health

-

Masuk ke akun Anda

+
+ {/* LEFT PANEL */} +
+
+ + {successMessage && ( +
+ {successMessage} +
+ )} + + {genericError && ( +
+ {genericError} +
+ )} + +

Sign in

+ +

+ Don't have an account?{' '} + + Create now + +

+ +
+ {/* Email */} +
+ + + {fieldErrors.email && ( + + {fieldErrors.email} + + )} +
+ + {/* Password */} +
+ +
+ + +
+ {fieldErrors.password && ( + + {fieldErrors.password} + + )} +
+ + {/* Remember me + Forgot password */} +
+ + + Forgot Password? + +
+ + {/* Submit */} + +
+
- {successMessage && ( -
- {successMessage} -
- )} - - {genericError && ( -
- {genericError} -
- )} - -
-
- - - {fieldErrors.email && ( - - {fieldErrors.email} - - )} + {/* RIGHT PANEL */} +
+ {/* Logo */} +
+
+ + + + +
+ QueueNova +
-
- - +
+ {/* Grey cloud blob behind illustration */} +
+ Doctor and nurse illustration - {fieldErrors.password && ( - - {fieldErrors.password} - - )}
-
- -
+

+ Welcome to QueueNova Health +

+
- - - -

- Belum punya akun?{' '} - - Daftar sekarang - -

+ + + + +
); -} - -const styles: Record = { - container: { - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#f5f7fa', - padding: '24px', - }, - card: { - backgroundColor: '#ffffff', - borderRadius: '8px', - border: '1px solid #e2e8f0', - padding: '40px', - width: '100%', - maxWidth: '400px', - }, - header: { - marginBottom: '28px', - }, - title: { - fontSize: '20px', - fontWeight: 700, - color: '#1a202c', - margin: 0, - }, - subtitle: { - fontSize: '14px', - color: '#64748b', - marginTop: '4px', - marginBottom: 0, - }, - form: { - display: 'flex', - flexDirection: 'column', - gap: '16px', - }, - fieldWrapper: { - display: 'flex', - flexDirection: 'column', - gap: '4px', - }, - label: { - fontSize: '13px', - fontWeight: 500, - color: '#374151', - }, - input: { - padding: '10px 12px', - fontSize: '14px', - border: '1px solid #d1d5db', - borderRadius: '6px', - outline: 'none', - color: '#1a202c', - backgroundColor: '#ffffff', - }, - inputError: { - borderColor: '#ef4444', - }, - fieldError: { - fontSize: '12px', - color: '#ef4444', - }, - successBanner: { - padding: '10px 14px', - backgroundColor: '#f0fdf4', - border: '1px solid #bbf7d0', - borderRadius: '6px', - fontSize: '13px', - color: '#15803d', - marginBottom: '16px', - }, - genericError: { - padding: '10px 14px', - backgroundColor: '#fef2f2', - border: '1px solid #fecaca', - borderRadius: '6px', - fontSize: '13px', - color: '#b91c1c', - marginBottom: '16px', - }, - rememberRow: { - display: 'flex', - alignItems: 'center', - }, - checkboxLabel: { - display: 'flex', - alignItems: 'center', - gap: '8px', - fontSize: '13px', - color: '#374151', - cursor: 'pointer', - }, - checkbox: { - cursor: 'pointer', - }, - button: { - marginTop: '4px', - padding: '11px', - backgroundColor: '#2563eb', - color: '#ffffff', - border: 'none', - borderRadius: '6px', - fontSize: '14px', - fontWeight: 600, - cursor: 'pointer', - }, - buttonDisabled: { - backgroundColor: '#93c5fd', - cursor: 'not-allowed', - }, - footer: { - marginTop: '20px', - fontSize: '13px', - color: '#64748b', - textAlign: 'center', - }, - link: { - color: '#2563eb', - textDecoration: 'none', - fontWeight: 500, - }, -}; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx index b88afc2..24c7944 100644 --- a/frontend/src/pages/RegisterPage.tsx +++ b/frontend/src/pages/RegisterPage.tsx @@ -1,8 +1,10 @@ +// frontend/src/pages/RegisterPage.tsx import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { AxiosError } from 'axios'; import { authService } from '../services/authService'; import type { RegisterPayload, ApiValidationError } from '../types/auth'; +import doctorsIllustration from '../assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png'; type FieldErrors = Partial>; @@ -14,9 +16,10 @@ export default function RegisterPage() { email: '', password: '', password_confirmation: '', - phone: '', }); + const [showPassword, setShowPassword] = useState(false); + const [showPasswordConfirmation, setShowPasswordConfirmation] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); const [genericError, setGenericError] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -58,225 +61,237 @@ export default function RegisterPage() { } return ( -
-
-
-

QueueNova Health

-

Buat akun baru

-
+
+ {/* LEFT PANEL */} +
+
- {genericError && ( -
- {genericError} -
- )} + {genericError && ( +
+ {genericError} +
+ )} -
- - - - - +

Sign up

- - +

+ Already have an account?{' '} + + Sign in + +

+ +
+ + {/* Name */} +
+ + + {fieldErrors.name && ( + + {fieldErrors.name} + + )} +
+ + {/* Email */} +
+ + + {fieldErrors.email && ( + + {fieldErrors.email} + + )} +
-

- Sudah punya akun?{' '} - - Masuk - -

+ {/* Password */} +
+ +
+ + +
+ {fieldErrors.password && ( + + {fieldErrors.password} + + )} +
+ + {/* Confirm Password */} +
+ +
+ + +
+ {fieldErrors.password_confirmation && ( + + {fieldErrors.password_confirmation} + + )} +
+ + {/* Submit */} + +
+
-
- ); -} -interface FieldProps { - label: string; - name: string; - type: string; - value: string; - onChange: (e: React.ChangeEvent) => void; - error?: string; - autoComplete?: string; -} + {/* RIGHT PANEL */} +
+ {/* Logo */} +
+
+ + + + + +
+ QueueNova +
-function Field({ label, name, type, value, onChange, error, autoComplete }: FieldProps) { - return ( -
- - - {error && ( - - {error} - - )} + {/* Illustration */} +
+
+
+ Doctor and nurse illustration +
+ +

+ Welcome to QueueNova Health +

+
+ + {/* Carousel dots */} +
+ + + + + +
+
); -} - -const styles: Record = { - container: { - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#f5f7fa', - padding: '24px', - }, - card: { - backgroundColor: '#ffffff', - borderRadius: '8px', - border: '1px solid #e2e8f0', - padding: '40px', - width: '100%', - maxWidth: '420px', - }, - header: { - marginBottom: '28px', - }, - title: { - fontSize: '20px', - fontWeight: 700, - color: '#1a202c', - margin: 0, - }, - subtitle: { - fontSize: '14px', - color: '#64748b', - marginTop: '4px', - marginBottom: 0, - }, - form: { - display: 'flex', - flexDirection: 'column', - gap: '16px', - }, - fieldWrapper: { - display: 'flex', - flexDirection: 'column', - gap: '4px', - }, - label: { - fontSize: '13px', - fontWeight: 500, - color: '#374151', - }, - input: { - padding: '10px 12px', - fontSize: '14px', - border: '1px solid #d1d5db', - borderRadius: '6px', - outline: 'none', - color: '#1a202c', - backgroundColor: '#ffffff', - transition: 'border-color 0.15s', - }, - inputError: { - borderColor: '#ef4444', - }, - fieldError: { - fontSize: '12px', - color: '#ef4444', - }, - genericError: { - padding: '10px 14px', - backgroundColor: '#fef2f2', - border: '1px solid #fecaca', - borderRadius: '6px', - fontSize: '13px', - color: '#b91c1c', - marginBottom: '16px', - }, - button: { - marginTop: '4px', - padding: '11px', - backgroundColor: '#2563eb', - color: '#ffffff', - border: 'none', - borderRadius: '6px', - fontSize: '14px', - fontWeight: 600, - cursor: 'pointer', - }, - buttonDisabled: { - backgroundColor: '#93c5fd', - cursor: 'not-allowed', - }, - footer: { - marginTop: '20px', - fontSize: '13px', - color: '#64748b', - textAlign: 'center', - }, - link: { - color: '#2563eb', - textDecoration: 'none', - fontWeight: 500, - }, -}; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/tests/pages/LoginPage.test.tsx b/frontend/src/tests/pages/LoginPage.test.tsx index 7eb606b..9eec75a 100644 --- a/frontend/src/tests/pages/LoginPage.test.tsx +++ b/frontend/src/tests/pages/LoginPage.test.tsx @@ -39,7 +39,7 @@ function renderLoginPage() { } const mockLoginResponse: { message: string; data: LoginResponseData } = { - message: 'Login berhasil.', + message: 'Login successful.', data: { token: 'test-token-123', token_type: 'Bearer', @@ -70,7 +70,7 @@ describe('LoginPage', () => { renderLoginPage(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Masuk' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Login' })).toBeInTheDocument(); }); it('calls authService.login with correct payload on submit', async () => { @@ -84,7 +84,7 @@ describe('LoginPage', () => { fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' }, }); - fireEvent.click(screen.getByRole('button', { name: 'Masuk' })); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect(authService.login).toHaveBeenCalledWith({ @@ -104,7 +104,7 @@ describe('LoginPage', () => { fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' }, }); - fireEvent.click(screen.getByRole('button', { name: 'Masuk' })); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect(mockSetAuth).toHaveBeenCalledWith(mockLoginResponse.data); @@ -123,7 +123,7 @@ describe('LoginPage', () => { vi.mocked(authService.login).mockRejectedValueOnce(error); renderLoginPage(); - fireEvent.click(screen.getByRole('button', { name: 'Masuk' })); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect( @@ -146,7 +146,7 @@ describe('LoginPage', () => { vi.mocked(authService.login).mockRejectedValueOnce(error); renderLoginPage(); - fireEvent.click(screen.getByRole('button', { name: 'Masuk' })); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { expect( @@ -161,10 +161,10 @@ describe('LoginPage', () => { ); renderLoginPage(); - fireEvent.click(screen.getByRole('button', { name: 'Masuk' })); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Masuk...' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Logging in...' })).toBeDisabled(); }); }); diff --git a/frontend/src/tests/pages/RegisterPage.test.tsx b/frontend/src/tests/pages/RegisterPage.test.tsx index 4f13cb4..1cc7961 100644 --- a/frontend/src/tests/pages/RegisterPage.test.tsx +++ b/frontend/src/tests/pages/RegisterPage.test.tsx @@ -45,12 +45,11 @@ describe('RegisterPage', () => { it('renders all form fields and submit button', () => { renderRegisterPage(); - expect(screen.getByLabelText('Nama Lengkap')).toBeInTheDocument(); + expect(screen.getByLabelText('Full Name')).toBeInTheDocument(); expect(screen.getByLabelText('Email')).toBeInTheDocument(); - expect(screen.getByLabelText('No. Telepon')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); - expect(screen.getByLabelText('Konfirmasi Password')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Daftar' })).toBeInTheDocument(); + expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument(); }); it('calls authService.register with form values on submit', async () => { @@ -61,29 +60,25 @@ describe('RegisterPage', () => { renderRegisterPage(); - fireEvent.change(screen.getByLabelText('Nama Lengkap'), { + fireEvent.change(screen.getByLabelText('Full Name'), { target: { value: 'Ucok Sitorus' }, }); fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'ucok@example.com' }, }); - fireEvent.change(screen.getByLabelText('No. Telepon'), { - target: { value: '081234567890' }, - }); fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' }, }); - fireEvent.change(screen.getByLabelText('Konfirmasi Password'), { + fireEvent.change(screen.getByLabelText('Confirm Password'), { target: { value: 'password123' }, }); - fireEvent.click(screen.getByRole('button', { name: 'Daftar' })); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { expect(authService.register).toHaveBeenCalledWith({ name: 'Ucok Sitorus', email: 'ucok@example.com', - phone: '081234567890', password: 'password123', password_confirmation: 'password123', }); @@ -100,7 +95,7 @@ describe('RegisterPage', () => { fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'ucok@example.com' }, }); - fireEvent.click(screen.getByRole('button', { name: 'Daftar' })); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/login', { @@ -123,7 +118,7 @@ describe('RegisterPage', () => { vi.mocked(authService.register).mockRejectedValueOnce(error); renderRegisterPage(); - fireEvent.click(screen.getByRole('button', { name: 'Daftar' })); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { expect( @@ -136,7 +131,7 @@ describe('RegisterPage', () => { vi.mocked(authService.register).mockRejectedValueOnce(new Error('Network Error')); renderRegisterPage(); - fireEvent.click(screen.getByRole('button', { name: 'Daftar' })); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { expect( @@ -151,10 +146,10 @@ describe('RegisterPage', () => { ); renderRegisterPage(); - fireEvent.click(screen.getByRole('button', { name: 'Daftar' })); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Mendaftar...' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Register...' })).toBeDisabled(); }); }); }); \ No newline at end of file diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index 8e1aaa6..8d356e8 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -29,7 +29,7 @@ export interface PatientProfile { email: string; password: string; password_confirmation: string; - phone: string; + // phone: string; } export interface LoginPayload {