fix: reduce code duplication and add tests for SonarQube quality gate
- Consolidate API logic: hooks/useApi.ts now uses services/api.ts - Create BaseFormField component to reduce form duplication - Refactor FormField, FormSelect, FormTextarea to use BaseFormField - Add centralized theme utility (utils/theme.ts) for colors/styles - Add comprehensive tests for api, auth.service, useApi hooks, AuthContext - Add tests for theme utility This reduces duplication from 11.45% and improves test coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
467696e5b8
commit
0a00c04b54
437
__tests__/contexts/AuthContext.test.tsx
Normal file
437
__tests__/contexts/AuthContext.test.tsx
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
/**
|
||||
* Tests for the AuthContext
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
||||
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
||||
import { authService } from '@/services/auth.service';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// Mock authService
|
||||
jest.mock('@/services/auth.service', () => ({
|
||||
authService: {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
getCurrentUser: jest.fn(),
|
||||
googleLogin: jest.fn(),
|
||||
facebookLogin: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast
|
||||
jest.mock('react-hot-toast', () => ({
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock utils/helpers
|
||||
jest.mock('@/utils/helpers', () => ({
|
||||
setToken: jest.fn(),
|
||||
removeToken: jest.fn(),
|
||||
getToken: jest.fn(),
|
||||
storage: {
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { setToken, removeToken, getToken, storage } from '@/utils/helpers';
|
||||
|
||||
// Mock next/navigation
|
||||
const mockPush = jest.fn();
|
||||
const mockReplace = jest.fn();
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
replace: mockReplace,
|
||||
prefetch: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Test component that uses the hook
|
||||
const TestComponent: React.FC = () => {
|
||||
const { user, isLoading, isAuthenticated, login, logout, register } = useAuth();
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await login({ email: 'test@test.com', password: 'password' });
|
||||
} catch {
|
||||
// Error is handled in AuthContext
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
try {
|
||||
await register({
|
||||
email: 'new@test.com',
|
||||
password: 'password',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
});
|
||||
} catch {
|
||||
// Error is handled in AuthContext
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="authenticated">{isAuthenticated ? 'yes' : 'no'}</div>
|
||||
<div data-testid="user">{user ? JSON.stringify(user) : 'null'}</div>
|
||||
<button onClick={handleLogin}>Login</button>
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
<button onClick={handleRegister}>Register</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AuthContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getToken as jest.Mock).mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should show loading state when token exists', async () => {
|
||||
// When token exists, it fetches user data which shows loading
|
||||
(getToken as jest.Mock).mockReturnValue('valid-token');
|
||||
|
||||
// Delay the response to ensure loading state is visible
|
||||
let resolveUser: (value: unknown) => void;
|
||||
const userPromise = new Promise((resolve) => {
|
||||
resolveUser = resolve;
|
||||
});
|
||||
(authService.getCurrentUser as jest.Mock).mockReturnValue(userPromise);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
// Should show loading while fetching user
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
|
||||
// Resolve the promise
|
||||
await act(async () => {
|
||||
resolveUser!({ id: '1', email: 'test@test.com', role: 'CLIENT' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have null user when no token exists', async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
// When no token, loading completes immediately
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('null');
|
||||
expect(screen.getByTestId('authenticated')).toHaveTextContent('no');
|
||||
});
|
||||
});
|
||||
|
||||
it('should load user when token exists', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
};
|
||||
|
||||
(getToken as jest.Mock).mockReturnValue('valid-token');
|
||||
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove token if getCurrentUser fails', async () => {
|
||||
(getToken as jest.Mock).mockReturnValue('invalid-token');
|
||||
(authService.getCurrentUser as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('Invalid token')
|
||||
);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeToken).toHaveBeenCalled();
|
||||
expect(storage.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login successfully and redirect CLIENT', async () => {
|
||||
const mockResponse = {
|
||||
token: 'new-token',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByText('Login');
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setToken).toHaveBeenCalledWith('new-token');
|
||||
expect(toast.success).toHaveBeenCalledWith('Connexion réussie !');
|
||||
expect(mockReplace).toHaveBeenCalledWith('/client');
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect ADMIN to admin dashboard', async () => {
|
||||
const mockResponse = {
|
||||
token: 'admin-token',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'admin@test.com',
|
||||
firstName: 'Admin',
|
||||
lastName: 'User',
|
||||
role: 'ADMIN',
|
||||
},
|
||||
};
|
||||
|
||||
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByText('Login');
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/admin/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect EMPLOYEE to employee dashboard', async () => {
|
||||
const mockResponse = {
|
||||
token: 'employee-token',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'employee@test.com',
|
||||
firstName: 'Employee',
|
||||
lastName: 'User',
|
||||
role: 'EMPLOYEE',
|
||||
},
|
||||
};
|
||||
|
||||
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByText('Login');
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/employe/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast on login failure', async () => {
|
||||
const errorMessage = 'Invalid credentials';
|
||||
(authService.login as jest.Mock).mockRejectedValueOnce(
|
||||
new Error(errorMessage)
|
||||
);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const loginButton = screen.getByText('Login');
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register successfully', async () => {
|
||||
const mockResponse = {
|
||||
token: 'new-user-token',
|
||||
user: {
|
||||
id: '2',
|
||||
email: 'new@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(authService.register as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const registerButton = screen.getByText('Register');
|
||||
await act(async () => {
|
||||
fireEvent.click(registerButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setToken).toHaveBeenCalledWith('new-user-token');
|
||||
expect(toast.success).toHaveBeenCalledWith('Inscription réussie !');
|
||||
expect(mockPush).toHaveBeenCalledWith('/client');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should logout and redirect to login page', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
};
|
||||
|
||||
(getToken as jest.Mock).mockReturnValue('valid-token');
|
||||
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);
|
||||
(authService.logout as jest.Mock).mockResolvedValueOnce(undefined);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByText('Logout');
|
||||
await act(async () => {
|
||||
fireEvent.click(logoutButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeToken).toHaveBeenCalled();
|
||||
expect(storage.remove).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith('Déconnexion réussie');
|
||||
expect(mockPush).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('should still logout even if API call fails', async () => {
|
||||
(getToken as jest.Mock).mockReturnValue('valid-token');
|
||||
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce({
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
role: 'CLIENT',
|
||||
});
|
||||
(authService.logout as jest.Mock).mockRejectedValueOnce(
|
||||
new Error('Logout failed')
|
||||
);
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<TestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByText('Logout');
|
||||
await act(async () => {
|
||||
fireEvent.click(logoutButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeToken).toHaveBeenCalled();
|
||||
expect(mockPush).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAuth hook error', () => {
|
||||
it('should throw error when used outside AuthProvider', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
render(<TestComponent />);
|
||||
}).toThrow('useAuth must be used within an AuthProvider');
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
333
__tests__/hooks/useApi.test.ts
Normal file
333
__tests__/hooks/useApi.test.ts
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
/**
|
||||
* Tests for the useApi hook
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useApi, useFetchData } from '@/hooks/useApi';
|
||||
import { api } from '@/services/api';
|
||||
|
||||
// Mock the api module
|
||||
jest.mock('@/services/api', () => ({
|
||||
api: {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
ApiError: class ApiError extends Error {
|
||||
constructor(public status: number, message: string, public data?: unknown) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast - define mock inside factory to avoid hoisting issues
|
||||
jest.mock('react-hot-toast', () => {
|
||||
const mockToast = {
|
||||
error: jest.fn(),
|
||||
success: jest.fn(),
|
||||
};
|
||||
return {
|
||||
__esModule: true,
|
||||
default: mockToast,
|
||||
toast: mockToast,
|
||||
error: mockToast.error,
|
||||
success: mockToast.success,
|
||||
};
|
||||
});
|
||||
|
||||
// Import to access the mock for assertions
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
describe('useApi Hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should have correct initial state', () => {
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute GET request successfully', async () => {
|
||||
const mockData = { id: 1, name: 'Test' };
|
||||
(api.get as jest.Mock).mockResolvedValueOnce(mockData);
|
||||
|
||||
const { result } = renderHook(() => useApi<typeof mockData>());
|
||||
|
||||
let response: typeof mockData | null = null;
|
||||
await act(async () => {
|
||||
response = await result.current.execute('get', '/test');
|
||||
});
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith('/test');
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(response).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('should execute POST request with data', async () => {
|
||||
const postData = { email: 'test@test.com' };
|
||||
const mockResponse = { success: true };
|
||||
(api.post as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('post', '/auth/login', postData);
|
||||
});
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith('/auth/login', postData);
|
||||
expect(result.current.data).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should execute PUT request', async () => {
|
||||
const putData = { name: 'Updated' };
|
||||
(api.put as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('put', '/users/1', putData);
|
||||
});
|
||||
|
||||
expect(api.put).toHaveBeenCalledWith('/users/1', putData);
|
||||
});
|
||||
|
||||
it('should execute PATCH request', async () => {
|
||||
const patchData = { status: 'active' };
|
||||
(api.patch as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('patch', '/users/1', patchData);
|
||||
});
|
||||
|
||||
expect(api.patch).toHaveBeenCalledWith('/users/1', patchData);
|
||||
});
|
||||
|
||||
it('should execute DELETE request', async () => {
|
||||
(api.delete as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('delete', '/users/1');
|
||||
});
|
||||
|
||||
expect(api.delete).toHaveBeenCalledWith('/users/1');
|
||||
});
|
||||
|
||||
it('should handle errors and show toast', async () => {
|
||||
const errorMessage = 'Something went wrong';
|
||||
(api.get as jest.Mock).mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('get', '/test');
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBe(errorMessage);
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
(api.get as jest.Mock).mockRejectedValueOnce(new Error('Error'));
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
let response: unknown = 'initial';
|
||||
await act(async () => {
|
||||
response = await result.current.execute('get', '/test');
|
||||
});
|
||||
|
||||
expect(response).toBeNull();
|
||||
});
|
||||
|
||||
it('should set loading state during request', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
(api.get as jest.Mock).mockReturnValueOnce(promise);
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
act(() => {
|
||||
result.current.execute('get', '/test');
|
||||
});
|
||||
|
||||
// Should be loading immediately after calling execute
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
// Resolve the promise
|
||||
await act(async () => {
|
||||
resolvePromise!({ data: 'test' });
|
||||
await promise;
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset state to initial values', async () => {
|
||||
const mockData = { id: 1 };
|
||||
(api.get as jest.Mock).mockResolvedValueOnce(mockData);
|
||||
|
||||
const { result } = renderHook(() => useApi());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.execute('get', '/test');
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useFetchData Hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have correct initial state before refetch', () => {
|
||||
const { result } = renderHook(() => useFetchData('/test'));
|
||||
|
||||
// Before calling refetch, data should be null and loading should be true
|
||||
expect(result.current.data).toBeNull();
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(typeof result.current.refetch).toBe('function');
|
||||
});
|
||||
|
||||
it('should extract data from response.data', async () => {
|
||||
const mockData = { id: 1, name: 'Test' };
|
||||
(api.get as jest.Mock).mockResolvedValue({ data: mockData });
|
||||
|
||||
const { result } = renderHook(() => useFetchData<typeof mockData>('/test'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should use response directly if no data field', async () => {
|
||||
const mockData = { id: 1, name: 'Test' };
|
||||
(api.get as jest.Mock).mockResolvedValue(mockData);
|
||||
|
||||
const { result } = renderHook(() => useFetchData<typeof mockData>('/test'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('should call onSuccess callback', async () => {
|
||||
const mockData = { id: 1 };
|
||||
const onSuccess = jest.fn();
|
||||
(api.get as jest.Mock).mockResolvedValue({ data: mockData });
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFetchData('/test', { onSuccess })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith(mockData);
|
||||
});
|
||||
|
||||
it('should call onError callback on error', async () => {
|
||||
const errorMessage = 'Fetch failed';
|
||||
const onError = jest.fn();
|
||||
(api.get as jest.Mock).mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFetchData('/test', { onError, showErrorToast: false })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(errorMessage);
|
||||
expect(result.current.error).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('should show error toast by default', async () => {
|
||||
(api.get as jest.Mock).mockRejectedValue(new Error('Error'));
|
||||
|
||||
const { result } = renderHook(() => useFetchData('/test'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not show error toast when showErrorToast is false', async () => {
|
||||
(api.get as jest.Mock).mockRejectedValue(new Error('Error'));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFetchData('/test', { showErrorToast: false })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow refetching data', async () => {
|
||||
const mockData1 = { id: 1 };
|
||||
const mockData2 = { id: 2 };
|
||||
(api.get as jest.Mock)
|
||||
.mockResolvedValueOnce({ data: mockData1 })
|
||||
.mockResolvedValueOnce({ data: mockData2 });
|
||||
|
||||
const { result } = renderHook(() => useFetchData('/test'));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData1);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refetch();
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData2);
|
||||
});
|
||||
});
|
||||
276
__tests__/services/api.test.ts
Normal file
276
__tests__/services/api.test.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/**
|
||||
* Tests for the API service
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { api, ApiError } from '@/services/api';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = jest.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
// Mock helpers
|
||||
jest.mock('@/utils/helpers', () => ({
|
||||
getToken: jest.fn(),
|
||||
}));
|
||||
|
||||
import { getToken } from '@/utils/helpers';
|
||||
|
||||
describe('API Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getToken as jest.Mock).mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe('ApiError class', () => {
|
||||
it('should create an error with status and message', () => {
|
||||
const error = new ApiError(404, 'Not found');
|
||||
expect(error.status).toBe(404);
|
||||
expect(error.message).toBe('Not found');
|
||||
expect(error.name).toBe('ApiError');
|
||||
});
|
||||
|
||||
it('should create an error with data', () => {
|
||||
const errorData = { field: 'email', reason: 'invalid' };
|
||||
const error = new ApiError(400, 'Validation error', errorData);
|
||||
expect(error.data).toEqual(errorData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.get', () => {
|
||||
it('should make a GET request without token', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse,
|
||||
});
|
||||
|
||||
const result = await api.get('/test');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:4000/api/test',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should include Authorization header when token exists', async () => {
|
||||
(getToken as jest.Mock).mockReturnValue('test-token');
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
await api.get('/test');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer test-token',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle full URLs', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
await api.get('http://external.api/endpoint');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://external.api/endpoint',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw ApiError on non-ok response', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
json: async () => ({ message: 'Invalid token' }),
|
||||
});
|
||||
|
||||
await expect(api.get('/test')).rejects.toThrow(ApiError);
|
||||
|
||||
// Need a fresh mock for the second assertion
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
json: async () => ({ message: 'Invalid token' }),
|
||||
});
|
||||
|
||||
await expect(api.get('/test')).rejects.toMatchObject({
|
||||
status: 401,
|
||||
message: 'Invalid token',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(api.get('/test')).rejects.toThrow(ApiError);
|
||||
|
||||
// Fresh mock for the second assertion
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
await expect(api.get('/test')).rejects.toMatchObject({
|
||||
status: 0,
|
||||
message: 'Erreur de connexion au serveur',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.post', () => {
|
||||
it('should make a POST request with data', async () => {
|
||||
const postData = { email: 'test@test.com', password: 'password' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
});
|
||||
|
||||
await api.post('/auth/login', postData);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:4000/api/auth/login',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(postData),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should make a POST request without data', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
});
|
||||
|
||||
await api.post('/auth/logout');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.put', () => {
|
||||
it('should make a PUT request with data', async () => {
|
||||
const updateData = { name: 'New Name' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
});
|
||||
|
||||
await api.put('/users/1', updateData);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:4000/api/users/1',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.patch', () => {
|
||||
it('should make a PATCH request with data', async () => {
|
||||
const patchData = { status: 'active' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
});
|
||||
|
||||
await api.patch('/users/1', patchData);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:4000/api/users/1',
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patchData),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.delete', () => {
|
||||
it('should make a DELETE request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
});
|
||||
|
||||
await api.delete('/users/1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:4000/api/users/1',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should extract error message from response', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
json: async () => ({ message: 'Email already exists' }),
|
||||
});
|
||||
|
||||
try {
|
||||
await api.post('/auth/register', {});
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect((error as ApiError).message).toBe('Email already exists');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use error field if message is not present', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
json: async () => ({ error: 'Validation failed' }),
|
||||
});
|
||||
|
||||
try {
|
||||
await api.post('/auth/register', {});
|
||||
} catch (error) {
|
||||
expect((error as ApiError).message).toBe('Validation failed');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use statusText if JSON parsing fails', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: async () => { throw new Error('Invalid JSON'); },
|
||||
});
|
||||
|
||||
try {
|
||||
await api.get('/test');
|
||||
} catch (error) {
|
||||
expect((error as ApiError).message).toBe('Internal Server Error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
276
__tests__/services/auth.service.test.ts
Normal file
276
__tests__/services/auth.service.test.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/**
|
||||
* Tests for the Auth Service
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { authService } from '@/services/auth.service';
|
||||
import { api } from '@/services/api';
|
||||
import { API_ENDPOINTS } from '@/utils/constants';
|
||||
|
||||
// Mock the api module
|
||||
jest.mock('@/services/api', () => ({
|
||||
api: {
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
ApiError: class ApiError extends Error {
|
||||
constructor(public status: number, message: string, public data?: unknown) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Auth Service', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login with valid credentials', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
token: 'jwt-token-123',
|
||||
user: {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(api.post as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await authService.login({
|
||||
email: 'test@test.com',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.LOGIN,
|
||||
{ email: 'test@test.com', password: 'password123' }
|
||||
);
|
||||
expect(result).toEqual({
|
||||
token: 'jwt-token-123',
|
||||
user: mockResponse.user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on invalid credentials', async () => {
|
||||
(api.post as jest.Mock).mockRejectedValueOnce(new Error('Invalid credentials'));
|
||||
|
||||
await expect(
|
||||
authService.login({ email: 'test@test.com', password: 'wrong' })
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
token: 'jwt-token-new',
|
||||
user: {
|
||||
id: '2',
|
||||
email: 'new@test.com',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(api.post as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const registerData = {
|
||||
email: 'new@test.com',
|
||||
password: 'password123',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
};
|
||||
|
||||
const result = await authService.register(registerData);
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.REGISTER,
|
||||
registerData
|
||||
);
|
||||
expect(result.token).toBe('jwt-token-new');
|
||||
expect(result.user.email).toBe('new@test.com');
|
||||
});
|
||||
|
||||
it('should throw error if email already exists', async () => {
|
||||
(api.post as jest.Mock).mockRejectedValueOnce(new Error('Email already exists'));
|
||||
|
||||
await expect(
|
||||
authService.register({
|
||||
email: 'existing@test.com',
|
||||
password: 'password',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
})
|
||||
).rejects.toThrow('Email already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call logout endpoint', async () => {
|
||||
(api.post as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await authService.logout();
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(API_ENDPOINTS.AUTH.LOGOUT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentUser', () => {
|
||||
it('should return user data from response.data', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
};
|
||||
|
||||
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: mockUser,
|
||||
});
|
||||
|
||||
const result = await authService.getCurrentUser();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(API_ENDPOINTS.AUTH.ME);
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should return user data from response.user', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
};
|
||||
|
||||
(api.get as jest.Mock).mockResolvedValueOnce({
|
||||
success: true,
|
||||
user: mockUser,
|
||||
});
|
||||
|
||||
const result = await authService.getCurrentUser();
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should return response directly if no data or user field', async () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@test.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'CLIENT',
|
||||
};
|
||||
|
||||
(api.get as jest.Mock).mockResolvedValueOnce(mockUser);
|
||||
|
||||
const result = await authService.getCurrentUser();
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('googleLogin', () => {
|
||||
it('should login with Google token', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
token: 'jwt-google-token',
|
||||
user: {
|
||||
id: '3',
|
||||
email: 'google@test.com',
|
||||
firstName: 'Google',
|
||||
lastName: 'User',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(api.post as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await authService.googleLogin('google-oauth-token');
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.GOOGLE,
|
||||
{ token: 'google-oauth-token' }
|
||||
);
|
||||
expect(result.token).toBe('jwt-google-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('facebookLogin', () => {
|
||||
it('should login with Facebook token', async () => {
|
||||
const mockResponse = {
|
||||
success: true,
|
||||
token: 'jwt-facebook-token',
|
||||
user: {
|
||||
id: '4',
|
||||
email: 'facebook@test.com',
|
||||
firstName: 'Facebook',
|
||||
lastName: 'User',
|
||||
role: 'CLIENT',
|
||||
},
|
||||
};
|
||||
|
||||
(api.post as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await authService.facebookLogin('facebook-oauth-token');
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.FACEBOOK,
|
||||
{ token: 'facebook-oauth-token' }
|
||||
);
|
||||
expect(result.token).toBe('jwt-facebook-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyEmail', () => {
|
||||
it('should verify email with token', async () => {
|
||||
(api.post as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await authService.verifyEmail('verification-token');
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.VERIFY_EMAIL,
|
||||
{ token: 'verification-token' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgotPassword', () => {
|
||||
it('should send forgot password request', async () => {
|
||||
(api.post as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await authService.forgotPassword('test@test.com');
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.FORGOT_PASSWORD,
|
||||
{ email: 'test@test.com' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset password with token', async () => {
|
||||
(api.post as jest.Mock).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await authService.resetPassword('reset-token', 'newPassword123');
|
||||
|
||||
expect(api.post).toHaveBeenCalledWith(
|
||||
API_ENDPOINTS.AUTH.RESET_PASSWORD,
|
||||
{ token: 'reset-token', password: 'newPassword123' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
174
__tests__/utils/theme.test.ts
Normal file
174
__tests__/utils/theme.test.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Tests for the theme utility
|
||||
*/
|
||||
|
||||
import {
|
||||
STATUS_COLORS,
|
||||
BADGE_COLORS,
|
||||
BUTTON_STYLES,
|
||||
INPUT_STYLES,
|
||||
CARD_STYLES,
|
||||
ALERT_STYLES,
|
||||
ROLE_COLORS,
|
||||
PRIZE_COLORS,
|
||||
getStatusColor,
|
||||
getButtonStyle,
|
||||
getInputStyle,
|
||||
getRoleColor,
|
||||
getPrizeColor,
|
||||
} from '@/utils/theme';
|
||||
|
||||
describe('Theme Utility', () => {
|
||||
describe('Constants', () => {
|
||||
it('should have all status colors defined', () => {
|
||||
expect(STATUS_COLORS.PENDING).toBe('bg-yellow-100 text-yellow-800');
|
||||
expect(STATUS_COLORS.CLAIMED).toBe('bg-green-100 text-green-800');
|
||||
expect(STATUS_COLORS.REJECTED).toBe('bg-red-100 text-red-800');
|
||||
expect(STATUS_COLORS.ACTIVE).toBe('bg-green-100 text-green-800');
|
||||
expect(STATUS_COLORS.INACTIVE).toBe('bg-gray-100 text-gray-800');
|
||||
expect(STATUS_COLORS.EXPIRED).toBe('bg-red-100 text-red-800');
|
||||
});
|
||||
|
||||
it('should have all badge colors defined', () => {
|
||||
expect(BADGE_COLORS.info).toBe('bg-blue-100 text-blue-800');
|
||||
expect(BADGE_COLORS.success).toBe('bg-green-100 text-green-800');
|
||||
expect(BADGE_COLORS.warning).toBe('bg-yellow-100 text-yellow-800');
|
||||
expect(BADGE_COLORS.error).toBe('bg-red-100 text-red-800');
|
||||
expect(BADGE_COLORS.purple).toBe('bg-purple-100 text-purple-800');
|
||||
expect(BADGE_COLORS.pink).toBe('bg-pink-100 text-pink-800');
|
||||
expect(BADGE_COLORS.amber).toBe('bg-amber-100 text-amber-800');
|
||||
expect(BADGE_COLORS.gray).toBe('bg-gray-100 text-gray-800');
|
||||
});
|
||||
|
||||
it('should have all button styles defined', () => {
|
||||
expect(BUTTON_STYLES.primary).toContain('bg-blue-600');
|
||||
expect(BUTTON_STYLES.secondary).toContain('bg-gray-600');
|
||||
expect(BUTTON_STYLES.success).toContain('bg-green-600');
|
||||
expect(BUTTON_STYLES.danger).toContain('bg-red-600');
|
||||
expect(BUTTON_STYLES.warning).toContain('bg-yellow-600');
|
||||
expect(BUTTON_STYLES.outline).toContain('border-blue-600');
|
||||
expect(BUTTON_STYLES.ghost).toContain('text-gray-600');
|
||||
});
|
||||
|
||||
it('should have input styles defined', () => {
|
||||
expect(INPUT_STYLES.base).toContain('w-full');
|
||||
expect(INPUT_STYLES.base).toContain('rounded-lg');
|
||||
expect(INPUT_STYLES.error).toBe('border-red-500');
|
||||
expect(INPUT_STYLES.normal).toBe('border-gray-300');
|
||||
});
|
||||
|
||||
it('should have card styles defined', () => {
|
||||
expect(CARD_STYLES.base).toContain('bg-white');
|
||||
expect(CARD_STYLES.base).toContain('rounded-lg');
|
||||
expect(CARD_STYLES.hover).toContain('hover:shadow-lg');
|
||||
expect(CARD_STYLES.bordered).toContain('border-gray-200');
|
||||
});
|
||||
|
||||
it('should have alert styles defined', () => {
|
||||
expect(ALERT_STYLES.info).toContain('bg-blue-50');
|
||||
expect(ALERT_STYLES.success).toContain('bg-green-50');
|
||||
expect(ALERT_STYLES.warning).toContain('bg-yellow-50');
|
||||
expect(ALERT_STYLES.error).toContain('bg-red-50');
|
||||
});
|
||||
|
||||
it('should have role colors defined', () => {
|
||||
expect(ROLE_COLORS.ADMIN).toBe('bg-red-100 text-red-800');
|
||||
expect(ROLE_COLORS.EMPLOYEE).toBe('bg-blue-100 text-blue-800');
|
||||
expect(ROLE_COLORS.CLIENT).toBe('bg-green-100 text-green-800');
|
||||
});
|
||||
|
||||
it('should have prize colors defined', () => {
|
||||
expect(PRIZE_COLORS.INFUSEUR).toBe('bg-blue-100 text-blue-800');
|
||||
expect(PRIZE_COLORS.THE_SIGNATURE).toBe('bg-green-100 text-green-800');
|
||||
expect(PRIZE_COLORS.COFFRET_DECOUVERTE).toBe('bg-purple-100 text-purple-800');
|
||||
expect(PRIZE_COLORS.COFFRET_PRESTIGE).toBe('bg-amber-100 text-amber-800');
|
||||
expect(PRIZE_COLORS.THE_GRATUIT).toBe('bg-pink-100 text-pink-800');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatusColor', () => {
|
||||
it('should return correct color for known statuses', () => {
|
||||
expect(getStatusColor('PENDING')).toBe('bg-yellow-100 text-yellow-800');
|
||||
expect(getStatusColor('CLAIMED')).toBe('bg-green-100 text-green-800');
|
||||
expect(getStatusColor('REJECTED')).toBe('bg-red-100 text-red-800');
|
||||
});
|
||||
|
||||
it('should handle lowercase statuses', () => {
|
||||
expect(getStatusColor('pending')).toBe('bg-yellow-100 text-yellow-800');
|
||||
expect(getStatusColor('claimed')).toBe('bg-green-100 text-green-800');
|
||||
});
|
||||
|
||||
it('should return gray for unknown statuses', () => {
|
||||
expect(getStatusColor('UNKNOWN')).toBe('bg-gray-100 text-gray-800');
|
||||
expect(getStatusColor('random')).toBe('bg-gray-100 text-gray-800');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getButtonStyle', () => {
|
||||
it('should return primary style by default', () => {
|
||||
expect(getButtonStyle()).toContain('bg-blue-600');
|
||||
});
|
||||
|
||||
it('should return correct style for variant', () => {
|
||||
expect(getButtonStyle('primary')).toContain('bg-blue-600');
|
||||
expect(getButtonStyle('secondary')).toContain('bg-gray-600');
|
||||
expect(getButtonStyle('success')).toContain('bg-green-600');
|
||||
expect(getButtonStyle('danger')).toContain('bg-red-600');
|
||||
expect(getButtonStyle('warning')).toContain('bg-yellow-600');
|
||||
expect(getButtonStyle('outline')).toContain('border-blue-600');
|
||||
expect(getButtonStyle('ghost')).toContain('text-gray-600');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputStyle', () => {
|
||||
it('should return normal input style when no error', () => {
|
||||
const style = getInputStyle(false);
|
||||
expect(style).toContain('w-full');
|
||||
expect(style).toContain('border-gray-300');
|
||||
expect(style).not.toContain('border-red-500');
|
||||
});
|
||||
|
||||
it('should return error input style when has error', () => {
|
||||
const style = getInputStyle(true);
|
||||
expect(style).toContain('w-full');
|
||||
expect(style).toContain('border-red-500');
|
||||
expect(style).not.toContain('border-gray-300');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoleColor', () => {
|
||||
it('should return correct color for known roles', () => {
|
||||
expect(getRoleColor('ADMIN')).toBe('bg-red-100 text-red-800');
|
||||
expect(getRoleColor('EMPLOYEE')).toBe('bg-blue-100 text-blue-800');
|
||||
expect(getRoleColor('CLIENT')).toBe('bg-green-100 text-green-800');
|
||||
});
|
||||
|
||||
it('should handle lowercase roles', () => {
|
||||
expect(getRoleColor('admin')).toBe('bg-red-100 text-red-800');
|
||||
expect(getRoleColor('employee')).toBe('bg-blue-100 text-blue-800');
|
||||
expect(getRoleColor('client')).toBe('bg-green-100 text-green-800');
|
||||
});
|
||||
|
||||
it('should return gray for unknown roles', () => {
|
||||
expect(getRoleColor('UNKNOWN')).toBe('bg-gray-100 text-gray-800');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrizeColor', () => {
|
||||
it('should return correct color for known prize types', () => {
|
||||
expect(getPrizeColor('INFUSEUR')).toBe('bg-blue-100 text-blue-800');
|
||||
expect(getPrizeColor('THE_SIGNATURE')).toBe('bg-green-100 text-green-800');
|
||||
expect(getPrizeColor('COFFRET_DECOUVERTE')).toBe('bg-purple-100 text-purple-800');
|
||||
expect(getPrizeColor('COFFRET_PRESTIGE')).toBe('bg-amber-100 text-amber-800');
|
||||
expect(getPrizeColor('THE_GRATUIT')).toBe('bg-pink-100 text-pink-800');
|
||||
});
|
||||
|
||||
it('should handle lowercase prize types', () => {
|
||||
expect(getPrizeColor('infuseur')).toBe('bg-blue-100 text-blue-800');
|
||||
});
|
||||
|
||||
it('should return gray for unknown prize types', () => {
|
||||
expect(getPrizeColor('UNKNOWN')).toBe('bg-gray-100 text-gray-800');
|
||||
});
|
||||
});
|
||||
});
|
||||
57
components/forms/BaseFormField.tsx
Normal file
57
components/forms/BaseFormField.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface BaseFormFieldProps {
|
||||
label: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
touched?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base form field wrapper component that provides consistent styling
|
||||
* for labels, error messages, and layout across all form components.
|
||||
*/
|
||||
export default function BaseFormField({
|
||||
label,
|
||||
name,
|
||||
required = false,
|
||||
error,
|
||||
touched,
|
||||
className = '',
|
||||
children,
|
||||
}: BaseFormFieldProps) {
|
||||
const showError = touched && error;
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
{children}
|
||||
{showError && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Common input class names for consistency
|
||||
export const inputBaseClasses = `
|
||||
w-full px-4 py-2 border rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
`;
|
||||
|
||||
export const getInputClasses = (hasError: boolean): string => `
|
||||
${inputBaseClasses}
|
||||
${hasError ? 'border-red-500' : 'border-gray-300'}
|
||||
`;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import BaseFormField, { getInputClasses } from './BaseFormField';
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
|
|
@ -36,17 +37,15 @@ export default function FormField({
|
|||
className = '',
|
||||
autoComplete,
|
||||
}: FormFieldProps) {
|
||||
const showError = touched && error;
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
<BaseFormField
|
||||
label={label}
|
||||
name={name}
|
||||
required={required}
|
||||
error={error}
|
||||
touched={touched}
|
||||
className={className}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
|
|
@ -58,16 +57,8 @@ export default function FormField({
|
|||
disabled={disabled}
|
||||
required={required}
|
||||
autoComplete={autoComplete}
|
||||
className={`
|
||||
w-full px-4 py-2 border rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
${showError ? 'border-red-500' : 'border-gray-300'}
|
||||
`}
|
||||
className={getInputClasses(!!touched && !!error)}
|
||||
/>
|
||||
{showError && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
</BaseFormField>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import BaseFormField, { getInputClasses } from './BaseFormField';
|
||||
|
||||
interface SelectOption {
|
||||
value: string | number;
|
||||
|
|
@ -39,17 +40,15 @@ export default function FormSelect({
|
|||
disabled = false,
|
||||
className = '',
|
||||
}: FormSelectProps) {
|
||||
const showError = touched && error;
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
<BaseFormField
|
||||
label={label}
|
||||
name={name}
|
||||
required={required}
|
||||
error={error}
|
||||
touched={touched}
|
||||
className={className}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<select
|
||||
id={name}
|
||||
name={name}
|
||||
|
|
@ -58,12 +57,7 @@ export default function FormSelect({
|
|||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-4 py-2 border rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
${showError ? 'border-red-500' : 'border-gray-300'}
|
||||
`}
|
||||
className={getInputClasses(!!touched && !!error)}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
|
|
@ -76,9 +70,6 @@ export default function FormSelect({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
{showError && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
</BaseFormField>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import BaseFormField, { getInputClasses } from './BaseFormField';
|
||||
|
||||
interface FormTextareaProps {
|
||||
label: string;
|
||||
|
|
@ -34,17 +35,15 @@ export default function FormTextarea({
|
|||
rows = 4,
|
||||
className = '',
|
||||
}: FormTextareaProps) {
|
||||
const showError = touched && error;
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ${className}`}>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-700 mb-2"
|
||||
<BaseFormField
|
||||
label={label}
|
||||
name={name}
|
||||
required={required}
|
||||
error={error}
|
||||
touched={touched}
|
||||
className={className}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<textarea
|
||||
id={name}
|
||||
name={name}
|
||||
|
|
@ -55,16 +54,8 @@ export default function FormTextarea({
|
|||
disabled={disabled}
|
||||
required={required}
|
||||
rows={rows}
|
||||
className={`
|
||||
w-full px-4 py-2 border rounded-lg
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-500
|
||||
disabled:bg-gray-100 disabled:cursor-not-allowed
|
||||
${showError ? 'border-red-500' : 'border-gray-300'}
|
||||
`}
|
||||
className={getInputClasses(!!touched && !!error)}
|
||||
/>
|
||||
{showError && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
</BaseFormField>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,43 +2,10 @@
|
|||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { api, ApiError } from '@/services/api';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
||||
|
||||
/**
|
||||
* Get authentication token from localStorage
|
||||
*/
|
||||
export const getAuthToken = (): string | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('auth_token') || localStorage.getItem('token');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic fetch wrapper with authentication
|
||||
*/
|
||||
export const apiFetch = async <T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> => {
|
||||
const token = getAuthToken();
|
||||
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Erreur serveur' }));
|
||||
throw new Error(error.message || `Erreur ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
// Re-export for backward compatibility
|
||||
export { api, ApiError };
|
||||
|
||||
interface UseApiState<T> {
|
||||
data: T | null;
|
||||
|
|
@ -47,12 +14,17 @@ interface UseApiState<T> {
|
|||
}
|
||||
|
||||
interface UseApiReturn<T> extends UseApiState<T> {
|
||||
execute: (endpoint: string, options?: RequestInit) => Promise<T | null>;
|
||||
execute: <R = T>(
|
||||
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
|
||||
endpoint: string,
|
||||
data?: unknown
|
||||
) => Promise<R | null>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for API calls with loading and error states
|
||||
* Uses centralized api service from services/api.ts
|
||||
*/
|
||||
export function useApi<T = unknown>(): UseApiReturn<T> {
|
||||
const [state, setState] = useState<UseApiState<T>>({
|
||||
|
|
@ -61,13 +33,34 @@ export function useApi<T = unknown>(): UseApiReturn<T> {
|
|||
error: null,
|
||||
});
|
||||
|
||||
const execute = useCallback(async (endpoint: string, options?: RequestInit): Promise<T | null> => {
|
||||
const execute = useCallback(async <R = T>(
|
||||
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
|
||||
endpoint: string,
|
||||
data?: unknown
|
||||
): Promise<R | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const data = await apiFetch<T>(endpoint, options);
|
||||
setState({ data, loading: false, error: null });
|
||||
return data;
|
||||
let result: R;
|
||||
switch (method) {
|
||||
case 'get':
|
||||
result = await api.get<R>(endpoint);
|
||||
break;
|
||||
case 'post':
|
||||
result = await api.post<R>(endpoint, data);
|
||||
break;
|
||||
case 'put':
|
||||
result = await api.put<R>(endpoint, data);
|
||||
break;
|
||||
case 'patch':
|
||||
result = await api.patch<R>(endpoint, data);
|
||||
break;
|
||||
case 'delete':
|
||||
result = await api.delete<R>(endpoint);
|
||||
break;
|
||||
}
|
||||
setState({ data: result as unknown as T, loading: false, error: null });
|
||||
return result;
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Une erreur est survenue';
|
||||
setState(prev => ({ ...prev, loading: false, error: message }));
|
||||
|
|
@ -97,7 +90,8 @@ interface UseFetchDataReturn<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching data on mount with auto-refresh support
|
||||
* Hook for fetching data with auto-refresh support
|
||||
* Uses centralized api service from services/api.ts
|
||||
*/
|
||||
export function useFetchData<T>(
|
||||
endpoint: string,
|
||||
|
|
@ -112,7 +106,7 @@ export function useFetchData<T>(
|
|||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await apiFetch<{ data: T } | T>(endpoint);
|
||||
const result = await api.get<{ data: T } | T>(endpoint);
|
||||
const responseData = (result as { data: T }).data ?? result as T;
|
||||
setData(responseData);
|
||||
options?.onSuccess?.(responseData);
|
||||
|
|
|
|||
102
utils/theme.ts
Normal file
102
utils/theme.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Centralized theme and color utility
|
||||
* This file consolidates all color schemes and styling constants
|
||||
* to eliminate duplication across components.
|
||||
*/
|
||||
|
||||
// Status colors for badges and indicators
|
||||
export const STATUS_COLORS = {
|
||||
PENDING: 'bg-yellow-100 text-yellow-800',
|
||||
CLAIMED: 'bg-green-100 text-green-800',
|
||||
REJECTED: 'bg-red-100 text-red-800',
|
||||
ACTIVE: 'bg-green-100 text-green-800',
|
||||
INACTIVE: 'bg-gray-100 text-gray-800',
|
||||
EXPIRED: 'bg-red-100 text-red-800',
|
||||
} as const;
|
||||
|
||||
// Badge color variants
|
||||
export const BADGE_COLORS = {
|
||||
info: 'bg-blue-100 text-blue-800',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
warning: 'bg-yellow-100 text-yellow-800',
|
||||
error: 'bg-red-100 text-red-800',
|
||||
purple: 'bg-purple-100 text-purple-800',
|
||||
pink: 'bg-pink-100 text-pink-800',
|
||||
amber: 'bg-amber-100 text-amber-800',
|
||||
gray: 'bg-gray-100 text-gray-800',
|
||||
} as const;
|
||||
|
||||
// Button style variants
|
||||
export const BUTTON_STYLES = {
|
||||
primary: 'bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-all duration-300',
|
||||
secondary: 'bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-gray-700 transition-all duration-300',
|
||||
success: 'bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-700 transition-all duration-300',
|
||||
danger: 'bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-red-700 transition-all duration-300',
|
||||
warning: 'bg-yellow-600 text-white px-6 py-2 rounded-lg hover:bg-yellow-700 transition-all duration-300',
|
||||
outline: 'border border-blue-600 text-blue-600 px-6 py-2 rounded-lg hover:bg-blue-50 transition-all duration-300',
|
||||
ghost: 'text-gray-600 px-6 py-2 rounded-lg hover:bg-gray-100 transition-all duration-300',
|
||||
} as const;
|
||||
|
||||
// Common input styling
|
||||
export const INPUT_STYLES = {
|
||||
base: 'w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed',
|
||||
error: 'border-red-500',
|
||||
normal: 'border-gray-300',
|
||||
} as const;
|
||||
|
||||
// Card and container styling
|
||||
export const CARD_STYLES = {
|
||||
base: 'bg-white rounded-lg shadow-md p-6',
|
||||
hover: 'bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow duration-300',
|
||||
bordered: 'bg-white rounded-lg border border-gray-200 p-6',
|
||||
} as const;
|
||||
|
||||
// Alert/notification styles
|
||||
export const ALERT_STYLES = {
|
||||
info: 'bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg',
|
||||
success: 'bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg',
|
||||
warning: 'bg-yellow-50 border border-yellow-200 text-yellow-800 px-4 py-3 rounded-lg',
|
||||
error: 'bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded-lg',
|
||||
} as const;
|
||||
|
||||
// Utility function to get status color
|
||||
export function getStatusColor(status: string): string {
|
||||
const upperStatus = status.toUpperCase();
|
||||
return STATUS_COLORS[upperStatus as keyof typeof STATUS_COLORS] || BADGE_COLORS.gray;
|
||||
}
|
||||
|
||||
// Utility function to get button style
|
||||
export function getButtonStyle(variant: keyof typeof BUTTON_STYLES = 'primary'): string {
|
||||
return BUTTON_STYLES[variant];
|
||||
}
|
||||
|
||||
// Utility function to get input class with error state
|
||||
export function getInputStyle(hasError: boolean): string {
|
||||
return `${INPUT_STYLES.base} ${hasError ? INPUT_STYLES.error : INPUT_STYLES.normal}`;
|
||||
}
|
||||
|
||||
// Role-based colors for user badges
|
||||
export const ROLE_COLORS = {
|
||||
ADMIN: 'bg-red-100 text-red-800',
|
||||
EMPLOYEE: 'bg-blue-100 text-blue-800',
|
||||
CLIENT: 'bg-green-100 text-green-800',
|
||||
} as const;
|
||||
|
||||
export function getRoleColor(role: string): string {
|
||||
const upperRole = role.toUpperCase();
|
||||
return ROLE_COLORS[upperRole as keyof typeof ROLE_COLORS] || BADGE_COLORS.gray;
|
||||
}
|
||||
|
||||
// Prize type colors (from constants.ts PRIZE_CONFIG)
|
||||
export const PRIZE_COLORS = {
|
||||
INFUSEUR: 'bg-blue-100 text-blue-800',
|
||||
THE_SIGNATURE: 'bg-green-100 text-green-800',
|
||||
COFFRET_DECOUVERTE: 'bg-purple-100 text-purple-800',
|
||||
COFFRET_PRESTIGE: 'bg-amber-100 text-amber-800',
|
||||
THE_GRATUIT: 'bg-pink-100 text-pink-800',
|
||||
} as const;
|
||||
|
||||
export function getPrizeColor(prizeType: string): string {
|
||||
const upperType = prizeType.toUpperCase();
|
||||
return PRIZE_COLORS[upperType as keyof typeof PRIZE_COLORS] || BADGE_COLORS.gray;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user