([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
@@ -24,13 +28,17 @@ export function AdminUsersPage() {
totalPages: 0,
});
const [error, setError] = useState('');
- const [banDialog, setBanDialog] = useState<
- {
- user: AdminUser;
- reason: string;
- processing: boolean;
- } | null
- >(null);
+ const {
+ banDialogUser,
+ processing: banProcessing,
+ error: banError,
+ openBanDialog,
+ confirmBan,
+ unban,
+ closeDialog,
+ } = useBanUser((userId, isBanned) => {
+ setUsers((prev) => prev.map((u) => u.id === userId ? { ...u, isBanned } : u));
+ });
useEffect(() => {
log.debug({}, 'AdminUsersPage mounted');
@@ -54,7 +62,7 @@ export function AdminUsersPage() {
}));
}).catch((err) => {
log.error({ err }, 'AdminUsersPage fetchUsers failed');
- setError((err as { message?: string })?.message || 'Failed to load users.');
+ setError((err as { message?: string })?.message || t('admin.users.loadError'));
setUsers([]);
setPagination((prev) => ({ ...prev, page: 1, total: 0, totalPages: 0 }));
}).finally(() => setLoading(false));
@@ -69,38 +77,14 @@ export function AdminUsersPage() {
fetchUsers(1, search);
}
- async function handleBan(userId: string) {
- if (!banDialog || !banDialog.reason.trim()) return;
- setBanDialog({ ...banDialog, processing: true });
- try {
- await adminApi.banUser(userId, banDialog.reason);
- setUsers((prev) => prev.map((u) => u.id === userId ? { ...u, isBanned: true } : u));
- setBanDialog(null);
- setError('');
- } catch (err) {
- setBanDialog({ ...banDialog, processing: false });
- setError((err as { message?: string })?.message || 'Failed to ban user.');
- }
- }
-
- async function handleUnban(userId: string) {
- try {
- await adminApi.unbanUser(userId);
- setUsers((prev) => prev.map((u) => u.id === userId ? { ...u, isBanned: false } : u));
- setError('');
- } catch (err) {
- setError((err as { message?: string })?.message || 'Failed to unban user.');
- }
- }
-
return (
- User Management
+ {t('admin.users.management')}
- + New User
+ {t('admin.users.new')}
@@ -113,16 +97,25 @@ export function AdminUsersPage() {
)}
+ {banError && (
+
+ {t(banError)}
+
+ )}
+
@@ -141,7 +134,7 @@ export function AdminUsersPage() {
: users.length === 0
? (
- {search ? 'No users match your search.' : 'No users found.'}
+ {search ? t('admin.users.noSearchResults') : t('admin.users.noUsers')}
)
: (
@@ -150,22 +143,22 @@ export function AdminUsersPage() {
- Username
+ {t('auth.username')}
- Email
+ {t('auth.email')}
- Role
+ {t('admin.users.role')}
- Status
+ {t('admin.users.status')}
- Joined
+ {t('admin.users.joined')}
- Actions
+ {t('common.actions')}
@@ -184,12 +177,14 @@ export function AdminUsersPage() {
{user.email}
- {user.isAdmin ? Admin : 'User'}
+ {user.isAdmin
+ ? {t('admin.users.adminBadge')}
+ : t('admin.users.userRole')}
{user.isBanned
- ? Banned
- : 'Active'}
+ ? {t('admin.users.banned')}
+ : t('admin.users.active')}
{new Date(user.createdAt).toLocaleDateString()}
@@ -201,34 +196,34 @@ export function AdminUsersPage() {
className='text-xs'
style={{ color: 'var(--accent-primary)' }}
>
- View
+ {t('common.view')}
- Edit
+ {t('common.edit')}
{user.isBanned
? (
handleUnban(user.id)}
+ onClick={() => unban(user.id)}
className='text-xs'
style={{ color: 'var(--success)' }}
>
- Unban
+ {t('admin.users.unban')}
)
: (
setBanDialog({ user, reason: '', processing: false })}
+ onClick={() => openBanDialog(user)}
className='text-xs'
style={{ color: 'var(--error)' }}
>
- Ban
+ {t('admin.users.ban')}
)}
{user.isAdmin
@@ -244,14 +239,14 @@ export function AdminUsersPage() {
} catch (err) {
setError(
(err as { message?: string })?.message ||
- 'Failed to remove admin privileges.',
+ t('admin.users.removeAdminError'),
);
}
}}
className='text-xs'
style={{ color: 'var(--warning)' }}
>
- Remove Admin
+ {t('admin.users.removeAdmin')}
)
: (
@@ -266,14 +261,14 @@ export function AdminUsersPage() {
} catch (err) {
setError(
(err as { message?: string })?.message ||
- 'Failed to grant admin privileges.',
+ t('admin.users.makeAdminError'),
);
}
}}
className='text-xs'
style={{ color: 'var(--accent-primary)' }}
>
- Make Admin
+ {t('admin.users.makeAdmin')}
)}
@@ -293,7 +288,7 @@ export function AdminUsersPage() {
disabled={pagination.page <= 1}
className='btn-secondary'
>
- Previous
+ {t('common.previous')}
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((pageNum) => (
= pagination.totalPages}
className='btn-secondary'
>
- Next
+ {t('common.next')}
)}
- {banDialog && (
-
-
-
- Ban User: {banDialog.user.displayName || banDialog.user.username}
-
-
-
- Ban Reason *
-
-
-
- setBanDialog(null)}
- className='btn-secondary'
- >
- Cancel
-
- handleBan(banDialog.user.id)}
- disabled={banDialog.processing || !banDialog.reason.trim()}
- className='btn-primary'
- style={{ backgroundColor: 'var(--error)' }}
- >
- {banDialog.processing ? 'Banning...' : 'Confirm Ban'}
-
-
-
-
+ {banDialogUser && (
+
)}
);
diff --git a/apps/web/src/pages/admin/AdminVendorsPage.test.tsx b/apps/web/src/pages/admin/AdminVendorsPage.test.tsx
new file mode 100644
index 0000000..ddb6af0
--- /dev/null
+++ b/apps/web/src/pages/admin/AdminVendorsPage.test.tsx
@@ -0,0 +1,66 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+
+vi.mock('@/utils/logger.ts', () => ({
+ createLogger: () => ({
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ trace: vi.fn(),
+ fatal: vi.fn(),
+ }),
+}));
+
+vi.mock('../../contexts/I18nContext.tsx', () => ({
+ useTranslation: () => ({
+ t: (k: string) => {
+ const map: Record = {
+ 'admin.vendors.management': 'Satıcı Yönetimi',
+ 'admin.vendors.add': '+ Satıcı Ekle',
+ 'admin.vendors.editTitle': 'Satıcıyı Düzenle',
+ 'admin.vendors.addTitle': 'Satıcı Ekle',
+ 'admin.vendors.website': 'Web Sitesi',
+ 'admin.vendors.deleteConfirm': 'Bu satıcı silinsin mi?',
+ 'common.loading': 'Yükleniyor...',
+ 'common.name': 'Ad',
+ 'common.description': 'Açıklama',
+ 'common.actions': 'Eylemler',
+ 'common.edit': 'Düzenle',
+ 'common.delete': 'Sil',
+ 'common.cancel': 'İptal',
+ 'common.save': 'Kaydet',
+ 'common.saving': 'Kaydediliyor...',
+ 'common.cancelEdit': 'Düzenlemeyi İptal Et',
+ };
+ return map[k] ?? k;
+ },
+ locale: 'tr',
+ setLocale: vi.fn(),
+ availableLocales: ['en', 'tr'],
+ }),
+}));
+
+vi.mock('../../api/client.ts', () => ({
+ api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() },
+}));
+
+import { api } from '../../api/client.ts';
+import { AdminVendorsPage } from './AdminVendorsPage.tsx';
+
+const mockApi = vi.mocked(api);
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ mockApi.get.mockResolvedValue([]);
+});
+
+describe('AdminVendorsPage — tr locale spot-check', () => {
+ it('renders the Turkish vendor management heading', async () => {
+ render( );
+
+ await waitFor(() => {
+ expect(screen.getByText('Satıcı Yönetimi')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/apps/web/src/pages/admin/AdminVendorsPage.tsx b/apps/web/src/pages/admin/AdminVendorsPage.tsx
index 5884d95..956311f 100644
--- a/apps/web/src/pages/admin/AdminVendorsPage.tsx
+++ b/apps/web/src/pages/admin/AdminVendorsPage.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { api } from '../../api/client.ts';
+import { useTranslation } from '../../contexts/I18nContext.tsx';
import { createLogger } from '../../utils/logger.ts';
const log = createLogger('AdminVendorsPage');
@@ -13,6 +14,7 @@ interface Vendor {
/** Admin page: vendor CRUD with inline form. */
export function AdminVendorsPage() {
+ const { t } = useTranslation();
const [vendors, setVendors] = useState([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
@@ -62,7 +64,7 @@ export function AdminVendorsPage() {
}
async function handleDelete(id: string) {
- if (!globalThis.confirm('Delete this vendor?')) return;
+ if (!globalThis.confirm(t('admin.vendors.deleteConfirm'))) return;
try {
await api.delete(`/admin/vendors/${id}`);
setVendors((prev) => prev.filter((v) => v.id !== id));
@@ -90,17 +92,17 @@ export function AdminVendorsPage() {
- Vendor Management
+ {t('admin.vendors.management')}
setShowForm(!showForm)} className='btn-primary'>
- {showForm ? 'Cancel' : '+ Add Vendor'}
+ {showForm ? t('common.cancel') : t('admin.vendors.add')}
{showForm && (