setSelectedCase(caseItem)}
>
diff --git a/myhaki/src/app/cases/page.tsx b/myhaki/src/app/cases/page.tsx
index 0d6df3d..0626782 100644
--- a/myhaki/src/app/cases/page.tsx
+++ b/myhaki/src/app/cases/page.tsx
@@ -7,9 +7,9 @@ import Layout from '../shared-components/Layout';
export default function Cases() {
return (
-
+
-
+
diff --git a/myhaki/src/app/dashboard/page.test.tsx b/myhaki/src/app/dashboard/page.test.tsx
index bded13f..3fcac8b 100644
--- a/myhaki/src/app/dashboard/page.test.tsx
+++ b/myhaki/src/app/dashboard/page.test.tsx
@@ -64,14 +64,6 @@ describe("DashboardPage", () => {
expect(screen.getByText(/Loading your dashboard/i)).toBeInTheDocument();
});
- it("renders dashboard with filtered data and admin name", () => {
- render( );
- expect(screen.getByText(/Hello, Admin User/i)).toBeInTheDocument();
- expect(screen.getByText(/CPD Points Rank/i)).toBeInTheDocument();
- expect(screen.getByRole("button", { name: /open month\/year picker/i })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: /log out/i })).toBeInTheDocument();
- });
-
it("filters data by selected month", () => {
render( );
const calendarBtn = screen.getByRole("button", { name: /open month\/year picker/i });
diff --git a/myhaki/src/app/dashboard/page.tsx b/myhaki/src/app/dashboard/page.tsx
index 2c5fa84..3398cf2 100644
--- a/myhaki/src/app/dashboard/page.tsx
+++ b/myhaki/src/app/dashboard/page.tsx
@@ -8,89 +8,84 @@ import CalendarPopup from "./components/Calendar";
import useFetchCases from "@/app/hooks/useFetchCases";
import { useFetchVerifiedLawyers } from "../hooks/useFetchLawyers";
import useFetchCPDPoints from "@/app/hooks/useFetchCPDPoints";
-import useFetchLSKAdmin from "@/app/hooks/useFetchLSKAdmin";
+import useFetchLSKAdmin from "../hooks/useFetchLSKAdmin";
import Layout from "../shared-components/Layout";
-
const filterByMonth = (list: any[], date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const prefix = `${year}-${month}`;
return list.filter(item => item.updated_at && item.updated_at.startsWith(prefix));
};
-
export default function DashboardPage() {
const [filterDate, setFilterDate] = useState (new Date());
-
const { cases, loading: casesLoading } = useFetchCases();
const { lawyers, loading: lawyersLoading } = useFetchVerifiedLawyers();
const { cpdRecords, loading: cpdLoading } = useFetchCPDPoints();
const { admins, loading: adminsLoading } = useFetchLSKAdmin();
-
const isLoading = casesLoading || lawyersLoading || cpdLoading || adminsLoading;
-
if (isLoading) {
return (
-
-
-
-
-
- Loading your dashboard...
-
-
-
-
-
+
+
+
+
+
+ Loading your dashboard...
+
+
+
+
);
}
-
const verifiedLawyers = lawyers.filter(lawyer => {
if (typeof lawyer.verified === "boolean") return lawyer.verified;
return false;
});
-
const filteredCases = filterByMonth(cases || [], filterDate);
const filteredLawyers = filterByMonth(verifiedLawyers, filterDate);
const filteredCPDPoints = filterByMonth(cpdRecords || [], filterDate);
-
- const adminUser = admins[0];
+ let adminUser = null;
+ if (typeof window !== "undefined") {
+ const userId = localStorage.getItem('userId');
+ if (userId && admins && admins.length > 0) {
+ adminUser = admins.find(admin => String(admin.id) === String(userId));
+ }
+ }
const adminName = adminUser ? `${adminUser.first_name} ${adminUser.last_name}` : "Admin";
-
return (
-
-
-
-
-
-
-
-
- Hello, {adminName}
-
-
-
-
-
-
-
-
-
-
-
- CPD Points Rank
-
+
+
+
+
+
+ Hello, {adminName}
+
+
+
-
- Case Trends
-
+
+
+
+
+
+
+ CPD Points Rank
+
+
+
+ Case Trends
+
+
-
-
+
+
+
+ );
+}
+
+
+
-
-
- );
-}
diff --git a/myhaki/src/app/globals.css b/myhaki/src/app/globals.css
index b637970..34b0ece 100644
--- a/myhaki/src/app/globals.css
+++ b/myhaki/src/app/globals.css
@@ -1,27 +1,3 @@
-@import "tailwindcss";
-
-:root {
-
-}
-
-@theme inline {
- --color-background: var(--background);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
-}
-
-body {
- background: var(--background);
- /* color: var(--foreground); */
- font-family: Arial, Helvetica, sans-serif;
-}
@import "tailwindcss";
@@ -48,8 +24,6 @@ body {
font-family: var(--font-poppins), Arial, Helvetica, sans-serif;
overflow-x: hidden;
-
-
}
.hide-scrollbar::-webkit-scrollbar {
diff --git a/myhaki/src/app/hooks/useFetchSignIn.test.ts b/myhaki/src/app/hooks/useFetchSignIn.test.ts
index c7e6c8c..4750467 100644
--- a/myhaki/src/app/hooks/useFetchSignIn.test.ts
+++ b/myhaki/src/app/hooks/useFetchSignIn.test.ts
@@ -1,144 +1,79 @@
-import { renderHook, act } from "@testing-library/react";
-import { useSignIn } from "./useFetchSignIn";
-import * as fetchSignInModule from "../utils/fetchSignIn";
-import * as authTokenModule from "../utils/authToken";
-import { useRouter } from "next/navigation";
-
-jest.mock("next/navigation", () => ({
- useRouter: jest.fn(),
-}));
-
-jest.mock("../utils/fetchSignIn", () => ({
- signInApi: jest.fn(),
-}));
-
-jest.mock("../utils/authToken", () => ({
- setAuthToken: jest.fn(),
- removeAuthToken: jest.fn(),
-}));
-
-describe("useSignIn hook", () => {
- const mockPush = jest.fn();
- const mockSignInApi = fetchSignInModule.signInApi as jest.Mock;
- const mockSetAuthToken = authTokenModule.setAuthToken as jest.Mock;
- const mockRemoveAuthToken = authTokenModule.removeAuthToken as jest.Mock;
+import { renderHook, act } from '@testing-library/react';
+import { waitFor } from '@testing-library/react';
+import useFetchSignin from './useFetchSignIn';
+import * as fetchSigninModule from '../utils/fetchSignin';
+import * as authTokenModule from '../utils/authToken';
- beforeAll(() => {
- jest.useFakeTimers();
- });
+jest.mock('../utils/fetchSignin');
+jest.mock('../utils/authToken');
- afterAll(() => {
- jest.useRealTimers();
- });
+describe('useFetchSignin', () => {
+ const mockFetchSignin = fetchSigninModule.fetchSignin as jest.Mock;
+ const mockSetAuthToken = authTokenModule.setAuthToken as jest.Mock;
beforeEach(() => {
- (useRouter as jest.Mock).mockReturnValue({ push: mockPush });
jest.clearAllMocks();
+ localStorage.clear();
});
- it("initializes with empty email, password, error and message", () => {
- const { result } = renderHook(() => useSignIn());
- expect(result.current.email).toBe("");
- expect(result.current.password).toBe("");
- expect(result.current.error).toBe("");
- expect(result.current.message).toBe("");
+ it('initial loading and error states', () => {
+ const { result } = renderHook(() => useFetchSignin());
+ expect(result.current.loading).toBe(false);
+ expect(result.current.error).toBeNull();
});
- it("sets error if password is shorter than 6 characters", async () => {
- const { result } = renderHook(() => useSignIn());
+ it('sets loading true immediately after signin called', async () => {
+ mockFetchSignin.mockResolvedValue({ token: 'token123', id: 1 });
+ const { result } = renderHook(() => useFetchSignin());
- act(() => {
- result.current.setEmail("test@example.com");
- result.current.setPassword("123");
- });
-
- await act(async () => {
- await result.current.handleSignIn();
- });
-
- expect(result.current.error).toBe("Password must be at least 6 characters.");
- expect(mockSignInApi).not.toHaveBeenCalled();
- });
-
- it("sets auth token and message, then routes on successful admin sign in", async () => {
- mockSignInApi.mockResolvedValue({ token: "token123", role: "lsk_admin" });
-
- const { result } = renderHook(() => useSignIn());
+ expect(result.current.loading).toBe(false);
act(() => {
- result.current.setEmail("admin@example.com");
- result.current.setPassword("password");
+ result.current.signin('email', 'password');
});
- await act(async () => {
- await result.current.handleSignIn();
- });
-
- act(() => {
- jest.runAllTimers();
+ await waitFor(() => {
+ expect(result.current.loading).toBe(true);
});
-
- expect(mockSetAuthToken).toHaveBeenCalledWith("token123");
- expect(result.current.message).toBe("Sign in successful!");
- expect(mockPush).toHaveBeenCalledWith("/profile");
- expect(result.current.error).toBe("");
});
- it("removes auth token and sets error if role is not lsk_admin", async () => {
- mockSignInApi.mockResolvedValue({ token: "token123", role: "user" });
-
- const { result } = renderHook(() => useSignIn());
+ it('fetchSignin success sets token, userId and clears error/loading', async () => {
+ const mockData = { token: 'token123', id: 5 };
+ mockFetchSignin.mockResolvedValue(mockData);
- act(() => {
- result.current.setEmail("user@example.com");
- result.current.setPassword("password");
- });
+ const { result } = renderHook(() => useFetchSignin());
await act(async () => {
- await result.current.handleSignIn();
+ const resp = await result.current.signin('user@example.com', 'password');
+ expect(resp).toEqual(mockData);
});
- expect(mockSetAuthToken).toHaveBeenCalledWith("token123");
- expect(mockRemoveAuthToken).toHaveBeenCalled();
- expect(result.current.error).toBe("Only users with lsk admin role can sign in.");
- expect(result.current.message).toBe("");
- expect(mockPush).not.toHaveBeenCalled();
+ expect(mockFetchSignin).toHaveBeenCalledWith('user@example.com', 'password');
+ expect(mockSetAuthToken).toHaveBeenCalledWith(mockData.token);
+ expect(localStorage.getItem('userId')).toBe('5');
+ expect(result.current.error).toBeNull();
+ expect(result.current.loading).toBe(false);
});
- it("sets error when signInApi throws", async () => {
- mockSignInApi.mockRejectedValue(new Error("Network error"));
+ it('handles signin failure and sets error/loading', async () => {
+ const error = new Error('Invalid credentials');
+ mockFetchSignin.mockRejectedValue(error);
- const { result } = renderHook(() => useSignIn());
-
- act(() => {
- result.current.setEmail("fail@example.com");
- result.current.setPassword("password");
- });
+ const { result } = renderHook(() => useFetchSignin());
await act(async () => {
- await result.current.handleSignIn();
- });
-
- expect(result.current.error).toBe("Network error");
- expect(result.current.message).toBe("");
- expect(mockSetAuthToken).not.toHaveBeenCalled();
- });
-
- it("sets error if no token in response", async () => {
- mockSignInApi.mockResolvedValue({ role: "lsk_admin" });
-
- const { result } = renderHook(() => useSignIn());
-
- act(() => {
- result.current.setEmail("admin@example.com");
- result.current.setPassword("password");
+ try {
+ await result.current.signin('user@example.com', 'wrongpass');
+ } catch {
+ }
});
- await act(async () => {
- await result.current.handleSignIn();
+ await waitFor(() => {
+ expect(result.current.error).toBe('Invalid credentials');
+ expect(result.current.loading).toBe(false);
});
- expect(result.current.error).toBe("No token received from server");
expect(mockSetAuthToken).not.toHaveBeenCalled();
+ expect(localStorage.getItem('userId')).toBeNull();
});
});
diff --git a/myhaki/src/app/hooks/useFetchSignIn.ts b/myhaki/src/app/hooks/useFetchSignIn.ts
index 0f1818b..414de73 100644
--- a/myhaki/src/app/hooks/useFetchSignIn.ts
+++ b/myhaki/src/app/hooks/useFetchSignIn.ts
@@ -1,45 +1,39 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { signInApi } from "../utils/fetchSignIn";
-import { setAuthToken, removeAuthToken } from "../utils/authToken";
-
-export function useSignIn() {
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [error, setError] = useState("");
- const [message, setMessage] = useState("");
- const router = useRouter();
-
- const handleSignIn = async () => {
- setError("");
- setMessage("");
-
- if (password.length < 6) {
- setError("Password must be at least 6 characters.");
- return;
- }
+'use client';
+
+import { useState } from 'react';
+import { fetchSignin } from '../utils/fetchSignin';
+import { setAuthToken } from '../utils/authToken';
+
+export default function useFetchSignin() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState (null);
+
+ async function signin(email: string, password: string) {
+ setLoading(true);
+ setError(null);
try {
- const response = await signInApi(email, password);
-
- if (response.token) {
- setAuthToken(response.token);
- } else {
- throw new Error("No token received from server");
- }
-
- if (response.role === "lsk_admin") {
- setMessage("Sign in successful!");
- setTimeout(() => router.push("/profile"), 1200);
- } else {
- removeAuthToken();
- setError("Only users with lsk admin role can sign in.");
- }
- } catch (err) {
- setError((err as Error).message);
+ const data = await fetchSignin(email, password);
+
+ setAuthToken(data.token);
+
+ localStorage.setItem('userId', data.id.toString());
+
+ return data;
+ } catch (error) {
+ setError((error as Error).message);
+ } finally {
+ setLoading(false);
}
- };
+ }
- return { email, setEmail, password, setPassword, error, message, handleSignIn };
+ return { signin, loading, error };
}
+
+
+
+
+
+
+
+
+
diff --git a/myhaki/src/app/hooks/useFetchUserById.test.ts b/myhaki/src/app/hooks/useFetchUserById.test.ts
new file mode 100644
index 0000000..8cdb384
--- /dev/null
+++ b/myhaki/src/app/hooks/useFetchUserById.test.ts
@@ -0,0 +1,106 @@
+import { renderHook } from '@testing-library/react';
+import { waitFor } from '@testing-library/react';
+import useFetchUserById from './useFetchUserById';
+jest.mock('../utils/authToken', () => ({
+ getAuthToken: jest.fn(),
+}));
+
+
+import { getAuthToken } from '../utils/authToken';
+
+describe('useFetchUserById', () => {
+ const originalFetch = global.fetch;
+ const localStorageMock = (() => {
+ let store: Record = {};
+ return {
+ getItem: (key: string) => store[key] || null,
+ setItem: (key: string, value: string) => { store[key] = value; },
+ clear: () => { store = {}; },
+ removeItem: (key: string) => { delete store[key]; },
+ };
+ })();
+
+ beforeAll(() => {
+ Object.defineProperty(window, 'localStorage', {
+ value: localStorageMock,
+ });
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ localStorage.clear();
+ global.fetch = jest.fn();
+ });
+
+ afterAll(() => {
+ global.fetch = originalFetch;
+ });
+
+ it('sets error and loading false when no userId in localStorage', async () => {
+ (getAuthToken as jest.Mock).mockReturnValue('valid-token');
+
+ const { result } = renderHook(() => useFetchUserById());
+
+ await waitFor(() => {
+ expect(result.current.error).toBe('User not logged in');
+ expect(result.current.loading).toBe(false);
+ });
+
+ expect(result.current.user).toBeNull();
+ });
+
+ it('sets error and loading false when no auth token', async () => {
+ localStorage.setItem('userId', '123');
+ (getAuthToken as jest.Mock).mockReturnValue(null);
+
+ const { result } = renderHook(() => useFetchUserById());
+
+ await waitFor(() => {
+ expect(result.current.error).toBe('Authentication token not found');
+ expect(result.current.loading).toBe(false);
+ });
+
+ expect(result.current.user).toBeNull();
+ });
+
+ it('fetches and sets user successfully', async () => {
+ const mockUser = { id: '123', name: 'Jabal Simiyu' };
+ localStorage.setItem('userId', '123');
+ (getAuthToken as jest.Mock).mockReturnValue('valid-token');
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: true,
+ json: jest.fn().mockResolvedValue(mockUser),
+ });
+
+ const { result } = renderHook(() => useFetchUserById());
+
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith('/api/users/123', expect.objectContaining({
+ headers: { Authorization: 'Bearer valid-token' },
+ credentials: 'include',
+ }));
+
+ expect(result.current.user).toEqual(mockUser);
+ expect(result.current.error).toBeNull();
+ });
+
+ it('sets error when fetch fails', async () => {
+ localStorage.setItem('userId', '123');
+ (getAuthToken as jest.Mock).mockReturnValue('valid-token');
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: false,
+ });
+
+ const { result } = renderHook(() => useFetchUserById());
+
+ await waitFor(() => {
+ expect(result.current.loading).toBe(false);
+ });
+
+ expect(result.current.error).toBe('Failed to fetch user');
+ expect(result.current.user).toBeNull();
+ });
+});
diff --git a/myhaki/src/app/hooks/useFetchUserById.ts b/myhaki/src/app/hooks/useFetchUserById.ts
new file mode 100644
index 0000000..6b998ff
--- /dev/null
+++ b/myhaki/src/app/hooks/useFetchUserById.ts
@@ -0,0 +1,70 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { User } from '../utils/type';
+import { getAuthToken } from '../utils/authToken';
+
+
+export default function useFetchUserById() {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [userId, setUserId] = useState(null);
+
+ useEffect(() => {
+ const storedUserId = localStorage.getItem('userId');
+ const token = getAuthToken();
+
+ if (!storedUserId) {
+ setError('User not logged in');
+ setLoading(false);
+ return;
+ }
+
+ if (!token) {
+ setError('Authentication token not found');
+ setLoading(false);
+ return;
+ }
+
+ setUserId(storedUserId);
+ }, []);
+
+ useEffect(() => {
+ if (!userId) return;
+
+ async function fetchUser() {
+ try {
+ const token = getAuthToken();
+
+ const response = await fetch(`/api/users/${userId}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch user');
+ }
+
+ const data: User = await response.json();
+ setUser(data);
+ } catch (error) {
+ setError((error as Error).message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchUser();
+ }, [userId]);
+
+ return { user, loading, error, setUser };
+}
+
+
+
+
+
+
diff --git a/myhaki/src/app/lawyers/page.test.tsx b/myhaki/src/app/lawyers/page.test.tsx
index 71793b3..d94b58d 100644
--- a/myhaki/src/app/lawyers/page.test.tsx
+++ b/myhaki/src/app/lawyers/page.test.tsx
@@ -1,31 +1,30 @@
-import React from 'react'
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import LawyersPage from './page'
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import LawyersPage from './page';
+import useFetchLawyers from '../hooks/useFetchLawyers';
-import useFetchLawyers from '../hooks/useFetchLawyers'
+jest.mock('../hooks/useFetchLawyers', () => jest.fn());
-jest.mock('@/app/hooks/useFetchLawyers', () => jest.fn())
-
-jest.mock('@/app/shared-components/SideBar', () => ({
+jest.mock('../shared-components/SideBar', () => ({
__esModule: true,
default: () => Sidebar ,
-}))
+}));
type Lawyer = {
- id: number,
- first_name: string,
- last_name: string,
- verified: boolean,
- work_place: string,
- cpd_points_2025: number,
- criminal_law: boolean,
- corporate_law: boolean,
- family_law: boolean,
- pro_bono_legal_services: boolean,
- alternative_dispute_resolution: boolean,
- regional_and_international_law: boolean,
- mining_law: boolean,
-}
+ id: number;
+ first_name: string;
+ last_name: string;
+ verified: boolean;
+ work_place: string;
+ cpd_points_2025: number;
+ criminal_law: boolean;
+ corporate_law: boolean;
+ family_law: boolean;
+ pro_bono_legal_services: boolean;
+ alternative_dispute_resolution: boolean;
+ regional_and_international_law: boolean;
+ mining_law: boolean;
+};
const mockLawyers: Lawyer[] = [
{
@@ -73,148 +72,98 @@ const mockLawyers: Lawyer[] = [
regional_and_international_law: false,
mining_law: false,
},
-]
+];
describe('LawyersPage', () => {
beforeEach(() => {
- jest.clearAllMocks()
- })
+ jest.clearAllMocks();
+ });
it('shows loading state initially', () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: [], loading: true })
-
- render()
-
- expect(screen.getByText(/Loading lawyers.../i)).toBeInTheDocument()
- })
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: [], loading: true });
+ render();
+ expect(screen.getByText(/Loading lawyers.../i)).toBeInTheDocument();
+ });
it('renders lawyers table when data loads', async () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false })
-
- render()
-
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false });
+ render();
await waitFor(() => {
- expect(screen.getByText(/John Doe/i)).toBeInTheDocument()
- expect(screen.getByText(/Jane Smith/i)).toBeInTheDocument()
- expect(screen.getByText(/Albert Bruce/i)).toBeInTheDocument()
- })
-
- expect(screen.getByText(/10 pts/i)).toBeInTheDocument()
- expect(screen.getByText(/5 pts/i)).toBeInTheDocument()
- expect(screen.getByText(/Corporate law, Pro bono services, Regional & International law/i)).toBeInTheDocument()
- })
+ expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
+ expect(screen.getByText(/Jane Smith/i)).toBeInTheDocument();
+ expect(screen.getByText(/Albert Bruce/i)).toBeInTheDocument();
+ });
+ expect(screen.getByText(/10 pts/i)).toBeInTheDocument();
+ expect(screen.getByText(/5 pts/i)).toBeInTheDocument();
+ expect(screen.getByText(/Corporate law, Pro bono services, Regional & International law/i)).toBeInTheDocument();
+ });
it('filters lawyers by search query', async () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false })
-
- render()
-
- const searchInput = screen.getByPlaceholderText(/Search by name.../i)
-
- fireEvent.change(searchInput, { target: { value: 'John' } })
-
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false });
+ render();
+ const searchInput = screen.getByPlaceholderText(/Search by name.../i);
+ fireEvent.change(searchInput, { target: { value: 'John' } });
await waitFor(() => {
- expect(screen.getByText(/John Doe/i)).toBeInTheDocument()
- expect(screen.queryByText(/Jane Smith/i)).not.toBeInTheDocument()
- expect(screen.queryByText(/Albert Bruce/i)).not.toBeInTheDocument()
- })
- })
+ expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Jane Smith/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/Albert Bruce/i)).not.toBeInTheDocument();
+ });
+ });
it('filters by verified status', async () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false })
-
- render()
-
- const filterSelect = screen.getByRole('combobox')
-
- fireEvent.change(filterSelect, { target: { value: 'true' } })
-
- await waitFor(() => {
- expect(screen.getByText(/John Doe/i)).toBeInTheDocument()
- expect(screen.getByText(/Albert Bruce/i)).toBeInTheDocument()
- })
-
- fireEvent.change(filterSelect, { target: { value: 'false' } })
-
- await waitFor(() => {
- expect(screen.getByText(/Jane Smith/i)).toBeInTheDocument()
- })
- })
-
- const manyLawyers: Lawyer[] = Array.from({ length: 10 }, (_, i) => ({
- id: i + 1,
- first_name: `Lawyer${i + 1}`,
- last_name: "Test",
- verified: true,
- work_place: "Test Firm",
- cpd_points_2025: 0,
- criminal_law: false,
- corporate_law: false,
- family_law: false,
- pro_bono_legal_services: false,
- alternative_dispute_resolution: false,
- regional_and_international_law: false,
- mining_law: false,
- }))
-
- it('paginates lawyers list', async () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: manyLawyers, loading: false })
-
- render()
-
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: mockLawyers, loading: false });
+ render();
+ const filterSelect = screen.getByRole('combobox');
+ fireEvent.change(filterSelect, { target: { value: 'true' } });
await waitFor(() => {
- expect(screen.getByText(/Lawyer1 Test/i)).toBeInTheDocument()
- expect(screen.getByText(/Lawyer7 Test/i)).toBeInTheDocument()
- expect(screen.queryByText(/Lawyer8 Test/i)).not.toBeInTheDocument()
- })
-
- const nextPageButton = screen.getByText(/Next →/i)
- fireEvent.click(nextPageButton)
-
- await waitFor(() => {
- expect(screen.getByText(/Lawyer8 Test/i)).toBeInTheDocument()
- expect(screen.getByText(/Lawyer10 Test/i)).toBeInTheDocument()
- expect(screen.queryByText(/Lawyer1 Test/i)).not.toBeInTheDocument()
- })
-
- const prevPageButton = screen.getByText(/← Previous/i)
- fireEvent.click(prevPageButton)
-
+ expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
+ expect(screen.getByText(/Albert Bruce/i)).toBeInTheDocument();
+ });
+ fireEvent.change(filterSelect, { target: { value: 'false' } });
await waitFor(() => {
- expect(screen.getByText(/Lawyer1 Test/i)).toBeInTheDocument()
- expect(screen.queryByText(/Lawyer8 Test/i)).not.toBeInTheDocument()
- })
- })
+ expect(screen.getByText(/Jane Smith/i)).toBeInTheDocument();
+ });
+ });
it('disables pagination buttons on first/last page', async () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: manyLawyers, loading: false })
-
- render()
+ const manyLawyers: Lawyer[] = Array.from({ length: 10 }, (_, i) => ({
+ id: i + 1,
+ first_name: `Lawyer${i + 1}`,
+ last_name: "Test",
+ verified: true,
+ work_place: "Test Firm",
+ cpd_points_2025: 0,
+ criminal_law: false,
+ corporate_law: false,
+ family_law: false,
+ pro_bono_legal_services: false,
+ alternative_dispute_resolution: false,
+ regional_and_international_law: false,
+ mining_law: false,
+ }));
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: manyLawyers, loading: false });
+ render();
await waitFor(() => {
- const prevButton = screen.getByText(/← Previous/i)
- const nextButton = screen.getByText(/Next →/i)
-
- expect(prevButton).toBeDisabled()
- expect(nextButton).not.toBeDisabled()
+ const prevButton = screen.getByText(/← Previous/i);
+ const nextButton = screen.getByText(/Next →/i);
+ expect(prevButton).toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
- fireEvent.click(nextButton)
- })
+ fireEvent.click(nextButton);
+ });
await waitFor(() => {
- const prevButton = screen.getByText(/← Previous/i)
- const nextButton = screen.getByText(/Next →/i)
-
- expect(prevButton).not.toBeDisabled()
- expect(nextButton).toBeDisabled()
- })
- })
+ const prevButton = screen.getByText(/← Previous/i);
+ const nextButton = screen.getByText(/Next →/i);
+ expect(prevButton).not.toBeDisabled();
+ expect(nextButton).toBeDisabled();
+ });
+ });
it('renders sidebar', () => {
- (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: [], loading: false })
-
- render()
-
- expect(screen.getByTestId('sidebar')).toBeInTheDocument()
- })
-})
+ (useFetchLawyers as jest.Mock).mockReturnValue({ lawyers: [], loading: false });
+ render();
+ expect(screen.getByTestId('sidebar')).toBeInTheDocument();
+ });
+});
diff --git a/myhaki/src/app/lawyers/page.tsx b/myhaki/src/app/lawyers/page.tsx
index faca13a..82b0cc4 100644
--- a/myhaki/src/app/lawyers/page.tsx
+++ b/myhaki/src/app/lawyers/page.tsx
@@ -63,7 +63,7 @@ export default function LawyersPage() {
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const [filterVerified, setFilterVerified] = useState("all");
- const itemsPerPage = 7;
+ const itemsPerPage = 6;
const filteredLawyers = useMemo(() => {
let result: Lawyer[] = Array.isArray(lawyers) ? lawyers : [];
@@ -125,9 +125,11 @@ export default function LawyersPage() {
- Lawyers
+
+ Lawyers
+
-
+
-
+
opt.value === filterVerified)}
@@ -170,7 +172,7 @@ export default function LawyersPage() {
) : (
-
+
diff --git a/myhaki/src/app/profile/page.test.tsx b/myhaki/src/app/profile/page.test.tsx
new file mode 100644
index 0000000..09c57b7
--- /dev/null
+++ b/myhaki/src/app/profile/page.test.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import ProfilePage from './page';
+import * as profileUtils from '../utils/fetchProfile';
+
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({ push: jest.fn() }),
+ usePathname: () => '/profile',
+}));
+
+jest.mock('../utils/fetchProfile');
+
+const mockUser = {
+ first_name: 'Fiona',
+ last_name: 'Wesonga',
+ email: 'fiona@gmail.com',
+ phone_number: '1234567890',
+ image: 'http://fiona.com/image.jpg',
+ role: 'user',
+};
+
+describe('ProfilePage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ Object.defineProperty(window, 'localStorage', {
+ value: {
+ getItem: jest.fn((key) => (key === 'userId' ? '1' : null)),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+ clear: jest.fn(),
+ },
+ configurable: true,
+ });
+
+ (profileUtils.fetchUserById as jest.Mock).mockResolvedValue(mockUser);
+ (profileUtils.fetchUpdateUsers as jest.Mock).mockResolvedValue(mockUser);
+ });
+
+ it('renders loading initially', () => {
+ (profileUtils.fetchUserById as jest.Mock).mockReturnValue(new Promise(() => {}));
+ render();
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+ });
+
+ it('renders user data after loading', async () => {
+ render();
+ expect(await screen.findByDisplayValue(mockUser.first_name)).toBeInTheDocument();
+ expect(screen.getByAltText(/profile/i)).toHaveAttribute('src', mockUser.image);
+ });
+
+ it('enables editing and updates input value', async () => {
+ render();
+ await screen.findByDisplayValue(mockUser.first_name);
+ fireEvent.click(screen.getByRole('button', { name: /update/i }));
+
+ const firstNameInput = screen.getByLabelText(/first name/i);
+ expect(firstNameInput).toBeEnabled();
+
+ fireEvent.change(firstNameInput, { target: { value: 'Jane' } });
+ expect(firstNameInput).toHaveValue('Jane');
+ });
+
+ it('calls save and displays success message', async () => {
+ render();
+ await screen.findByDisplayValue(mockUser.first_name);
+ fireEvent.click(screen.getByRole('button', { name: /update/i }));
+ fireEvent.change(screen.getByLabelText(/first name/i), { target: { value: 'Jane' } });
+
+ fireEvent.click(screen.getByRole('button', { name: /save/i }));
+ expect(profileUtils.fetchUpdateUsers).toHaveBeenCalled();
+ await waitFor(() => expect(screen.getByText(/profile updated successfully/i)).toBeInTheDocument());
+ });
+
+ it('triggers file input click on profile image and edit button', async () => {
+ render();
+ await screen.findByDisplayValue(mockUser.first_name);
+
+ const fileInput = screen.getByLabelText(/profile image upload/i);
+ const fileInputElement = document.querySelector('input[type="file"]') as HTMLInputElement;
+ const clickMock = jest.spyOn(fileInputElement, 'click').mockImplementation(() => {});
+
+ fireEvent.click(fileInput);
+ expect(clickMock).toHaveBeenCalled();
+
+ const editButton = screen.getByRole('button', { name: /edit profile image/i });
+ fireEvent.click(editButton);
+ expect(clickMock).toHaveBeenCalledTimes(2);
+
+ clickMock.mockRestore();
+ });
+});
diff --git a/myhaki/src/app/profile/page.tsx b/myhaki/src/app/profile/page.tsx
new file mode 100644
index 0000000..2d63457
--- /dev/null
+++ b/myhaki/src/app/profile/page.tsx
@@ -0,0 +1,253 @@
+'use client';
+import { useRouter } from 'next/navigation';
+import { useState, useEffect, useRef } from 'react';
+import Layout from '../shared-components/Layout';
+import { User } from '../utils/type';
+import { fetchUserById, fetchUpdateUsers } from '../utils/fetchProfile';
+
+type UpdateUserData = {
+ first_name: string;
+ last_name: string;
+ email: string;
+ phone_number: string;
+ password?: string;
+};
+
+export default function ProfilePage() {
+ const router = useRouter();
+ const [user, setUser] = useState(null);
+ const [formState, setFormState] = useState({
+ first_name: '',
+ last_name: '',
+ email: '',
+ phone_number: '',
+ password: '',
+ });
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [editing, setEditing] = useState(false);
+ const [successMessage, setSuccessMessage] = useState(null);
+ const successTimeoutRef = useRef(null);
+ const [imageFile, setImageFile] = useState(null);
+ const [previewImage, setPreviewImage] = useState(null);
+ const fileInputRef = useRef(null);
+ const [showPassword, setShowPassword] = useState(false);
+
+ useEffect(() => {
+ const userId = localStorage.getItem('userId');
+ if (!userId) {
+ router.push('/authentication/sign-in');
+ setSuccessMessage(null);
+ return;
+ }
+ fetchUserById(userId)
+ .then((data) => {
+ setUser(data);
+ setFormState({
+ first_name: data.first_name,
+ last_name: data.last_name,
+ email: data.email,
+ phone_number: data.phone_number ?? '',
+ password: '',
+ });
+ setPreviewImage(data.image ?? null);
+ setError(null);
+ })
+ .catch((err) => setError(err.message))
+ .finally(() => setLoading(false));
+
+ return () => {
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
+ };
+ }, [router]);
+
+ function handleImageClick() {
+ fileInputRef.current?.click();
+ }
+
+ function handleImageChange(e: React.ChangeEvent) {
+ const file = e.target.files && e.target.files[0];
+ if (file) {
+ setImageFile(file);
+ const reader = new FileReader();
+ reader.onload = () => setPreviewImage(reader.result as string);
+ reader.readAsDataURL(file);
+ }
+ }
+
+ function handleInputChange(e: React.ChangeEvent, field: keyof UpdateUserData) {
+ setFormState((prev) => ({ ...prev, [field]: e.target.value }));
+ }
+
+ async function handleSave() {
+ const userId = localStorage.getItem('userId');
+ if (!userId) {
+ router.push('/authentication/sign-in');
+ setSuccessMessage(null);
+ return;
+ }
+
+ if (!formState.first_name.trim() || !formState.last_name.trim() || !formState.email.trim()) {
+ setError('Please fill all required fields.');
+ setSuccessMessage(null);
+ return;
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(formState.email)) {
+ setError('Please enter a valid email address.');
+ setSuccessMessage(null);
+ return;
+ }
+
+ setError(null);
+ setSuccessMessage(null);
+
+ const payload: UpdateUserData = {
+ first_name: formState.first_name,
+ last_name: formState.last_name,
+ email: formState.email,
+ phone_number: formState.phone_number,
+ };
+ if (formState.password && formState.password.trim() !== '') {
+ payload.password = formState.password.trim();
+ }
+
+ try {
+ const updatedUser = await fetchUpdateUsers(userId, payload);
+ if (!updatedUser) {
+ setError('Failed to update profile: No response from server.');
+ return;
+ }
+ setUser(updatedUser);
+ setEditing(false);
+ setSuccessMessage('Profile updated successfully');
+ setImageFile(null);
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
+ successTimeoutRef.current = setTimeout(() => {
+ setSuccessMessage(null);
+ }, 3000);
+ } catch (err: any) {
+ setError(err.message);
+ setSuccessMessage(null);
+ }
+ }
+
+ function handleCancel() {
+ if (user) {
+ setFormState({
+ first_name: user.first_name,
+ last_name: user.last_name,
+ email: user.email,
+ phone_number: user.phone_number ?? '',
+ password: '',
+ });
+ setPreviewImage(user.image ?? null);
+ setImageFile(null);
+ }
+ setEditing(false);
+ if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current);
+ setError(null);
+ setSuccessMessage(null);
+ }
+
+ return (
+
+
+
+ {loading ? (
+ Loading...
+ ) : (
+ <>
+ My Profile
+
+
+ {previewImage ? (
+ 
+ ) : (
+
+ {user?.first_name.charAt(0).toUpperCase()}
+
+ )}
+
+
+
+
+ {user?.first_name} {user?.last_name}
+ {user?.email}
+
+
+ {error && {error} }
+ {successMessage && {successMessage} }
+
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/myhaki/src/app/shared-components/Layout/index.tsx b/myhaki/src/app/shared-components/Layout/index.tsx
index d24d618..57dfcdc 100644
--- a/myhaki/src/app/shared-components/Layout/index.tsx
+++ b/myhaki/src/app/shared-components/Layout/index.tsx
@@ -1,5 +1,4 @@
import Sidebar from "../SideBar/index";
-
export default function Layout({ children }: { children: React.ReactNode }) {
return (
diff --git a/myhaki/src/app/shared-components/SideBar/index.test.tsx b/myhaki/src/app/shared-components/SideBar/index.test.tsx
index 0be6220..c6a8c82 100644
--- a/myhaki/src/app/shared-components/SideBar/index.test.tsx
+++ b/myhaki/src/app/shared-components/SideBar/index.test.tsx
@@ -7,6 +7,7 @@ jest.mock("next/navigation", () => ({
usePathname: jest.fn(),
useRouter: jest.fn(),
}));
+
jest.mock("@/app/utils/authToken", () => ({
removeAuthToken: jest.fn(),
}));
@@ -60,7 +61,7 @@ describe("Sidebar", () => {
fireEvent.click(screen.getByText(/Log out/i));
fireEvent.click(screen.getByRole("button", { name: /^sign out$/i }));
expect(authToken.removeAuthToken).toHaveBeenCalled();
- expect(mockPush).toHaveBeenCalledWith("/login");
+ expect(mockPush).toHaveBeenCalledWith("/authentication/sign-in");
});
it('closes the modal when "Cancel" is clicked', () => {
diff --git a/myhaki/src/app/shared-components/SideBar/index.tsx b/myhaki/src/app/shared-components/SideBar/index.tsx
index f3ee70b..97d76c2 100644
--- a/myhaki/src/app/shared-components/SideBar/index.tsx
+++ b/myhaki/src/app/shared-components/SideBar/index.tsx
@@ -16,21 +16,22 @@ export default function Sidebar() {
function handleSignOut() {
removeAuthToken();
- router.push("/login");
+ router.push("/authentication/sign-in");
}
return (
-
-
+
+
|