website refactor

This commit is contained in:
2026-01-12 19:24:59 +01:00
parent 1f0c4f7fa6
commit 5ea95eaf51
54 changed files with 2894 additions and 2342 deletions

View File

@@ -0,0 +1,58 @@
'use client';
import { useState, useEffect } from 'react';
import { AdminDashboardTemplate } from '@/templates/AdminDashboardTemplate';
import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import { AdminDashboardPageQuery } from '@/lib/page-queries/AdminDashboardPageQuery';
export function AdminDashboardClient() {
const [viewData, setViewData] = useState<AdminDashboardViewData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadStats = async () => {
try {
setLoading(true);
const query = new AdminDashboardPageQuery();
const result = await query.execute();
if (result.status === 'ok') {
// Page Query already returns View Data via builder
setViewData(result.dto);
} else if (result.status === 'notFound') {
// Handle not found - could show a message or redirect
console.error('Access denied - You must be logged in as an Owner or Admin');
} else {
// Handle error - could show a toast or error message
console.error('Failed to load dashboard stats');
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load stats';
console.error(message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadStats();
}, []);
if (!viewData) {
return (
<div className="flex flex-col items-center justify-center py-20 space-y-3">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-blue"></div>
<div className="text-gray-400">Loading dashboard...</div>
</div>
);
}
return (
<AdminDashboardTemplate
viewData={viewData}
onRefresh={loadStats}
isLoading={loading}
/>
);
}

View File

@@ -0,0 +1,115 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { AdminUsersTemplate } from '@/templates/AdminUsersTemplate';
import { AdminUsersViewData } from '@/templates/AdminUsersViewData';
import { AdminUsersPresenter } from '@/lib/presenters/AdminUsersPresenter';
import { AdminUsersPageQuery } from '@/lib/page-queries/AdminUsersPageQuery';
import { updateUserStatus, deleteUser } from './actions';
export function AdminUsersClient() {
const [viewData, setViewData] = useState<AdminUsersViewData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [roleFilter, setRoleFilter] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [deletingUser, setDeletingUser] = useState<string | null>(null);
const loadUsers = useCallback(async () => {
try {
setLoading(true);
setError(null);
const query = new AdminUsersPageQuery();
const result = await query.execute({
search: search || undefined,
role: roleFilter || undefined,
status: statusFilter || undefined,
page: 1,
limit: 50,
});
if (result.status === 'ok') {
const data = AdminUsersPresenter.present(result.dto);
setViewData(data);
} else if (result.status === 'notFound') {
setError('Access denied - You must be logged in as an Owner or Admin');
} else {
setError('Failed to load users');
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load users';
setError(message);
} finally {
setLoading(false);
}
}, [search, roleFilter, statusFilter]);
useEffect(() => {
const timeout = setTimeout(() => {
loadUsers();
}, 300);
return () => clearTimeout(timeout);
}, [loadUsers]);
const handleUpdateStatus = async (userId: string, newStatus: string) => {
try {
await updateUserStatus(userId, newStatus);
await loadUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update status');
}
};
const handleDeleteUser = async (userId: string) => {
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
return;
}
try {
setDeletingUser(userId);
await deleteUser(userId);
await loadUsers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete user');
} finally {
setDeletingUser(null);
}
};
const handleClearFilters = () => {
setSearch('');
setRoleFilter('');
setStatusFilter('');
};
if (!viewData) {
return (
<div className="flex flex-col items-center justify-center py-20 space-y-3">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-blue"></div>
<div className="text-gray-400">Loading users...</div>
</div>
);
}
return (
<AdminUsersTemplate
viewData={viewData}
onRefresh={loadUsers}
onSearch={setSearch}
onFilterRole={setRoleFilter}
onFilterStatus={setStatusFilter}
onClearFilters={handleClearFilters}
onUpdateStatus={handleUpdateStatus}
onDeleteUser={handleDeleteUser}
search={search}
roleFilter={roleFilter}
statusFilter={statusFilter}
loading={loading}
error={error}
deletingUser={deletingUser}
/>
);
}

View File

@@ -0,0 +1,45 @@
'use server';
import { UpdateUserStatusMutation } from '@/lib/mutations/admin/UpdateUserStatusMutation';
import { DeleteUserMutation } from '@/lib/mutations/admin/DeleteUserMutation';
import { revalidatePath } from 'next/cache';
/**
* Server actions for admin operations
*
* All write operations must enter through server actions.
* Actions are thin wrappers that handle framework concerns (revalidation).
* Business logic is handled by Mutations.
*/
/**
* Update user status
*/
export async function updateUserStatus(userId: string, status: string): Promise<void> {
try {
const mutation = new UpdateUserStatusMutation();
await mutation.execute({ userId, status });
// Revalidate the users page
revalidatePath('/admin/users');
} catch (error) {
console.error('updateUserStatus failed:', error);
throw new Error('Failed to update user status');
}
}
/**
* Delete user
*/
export async function deleteUser(userId: string): Promise<void> {
try {
const mutation = new DeleteUserMutation();
await mutation.execute({ userId });
// Revalidate the users page
revalidatePath('/admin/users');
} catch (error) {
console.error('deleteUser failed:', error);
throw new Error('Failed to delete user');
}
}

View File

@@ -12,15 +12,11 @@ interface AdminLayoutProps {
* Uses RouteGuard to enforce access control server-side.
*/
export default async function AdminLayout({ children }: AdminLayoutProps) {
console.log('[ADMIN LAYOUT] ========== ADMIN LAYOUT CALLED ==========');
const headerStore = await headers();
const pathname = headerStore.get('x-pathname') || '/';
console.log('[ADMIN LAYOUT] Pathname:', pathname);
const guard = createRouteGuard();
console.log('[ADMIN LAYOUT] About to call guard.enforce');
await guard.enforce({ pathname });
console.log('[ADMIN LAYOUT] guard.enforce completed successfully');
return (
<div className="min-h-screen bg-deep-graphite">

View File

@@ -1,10 +1,5 @@
import { AdminLayout } from '@/components/admin/AdminLayout';
import { AdminDashboardPage } from '@/components/admin/AdminDashboardPage';
import { AdminDashboardClient } from './AdminDashboardClient';
export default function AdminPage() {
return (
<AdminLayout>
<AdminDashboardPage />
</AdminLayout>
);
return <AdminDashboardClient />;
}

View File

@@ -1,10 +1,5 @@
import { AdminLayout } from '@/components/admin/AdminLayout';
import { AdminUsersPage } from '@/components/admin/AdminUsersPage';
import { AdminUsersClient } from '../AdminUsersClient';
export default function AdminUsers() {
return (
<AdminLayout>
<AdminUsersPage />
</AdminLayout>
);
return <AdminUsersClient />;
}