ui(step-4): apply reference layout to remaining 7 pages
- catering-services, fitness-trainers, graphic-designers, social-media-managers, video-editors, verification, kb: white header, data-table/table-card, navy buttons, orange tab underlines, inline styles removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5cfa4b89be
commit
3ffed6c813
7 changed files with 743 additions and 731 deletions
|
|
@ -36,89 +36,90 @@ export default function CateringServicesPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Catering Services Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage all catering services accounts on the platform.</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Catering Services Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all catering services accounts on the platform.</p>
|
||||
</div>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No catering services users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No catering services users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{item.name || item.full_name || '—'}</td>
|
||||
<td style="color:#475569">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,89 +36,90 @@ export default function FitnessTrainersPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Fitness Trainer Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage all fitness trainer accounts on the platform.</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Fitness Trainer Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all fitness trainer accounts on the platform.</p>
|
||||
</div>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No fitness trainer users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No fitness trainer users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{item.name || item.full_name || '—'}</td>
|
||||
<td style="color:#475569">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,89 +36,90 @@ export default function GraphicDesignersPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Graphic Designer Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage all graphic designer accounts on the platform.</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Graphic Designer Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all graphic designer accounts on the platform.</p>
|
||||
</div>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No graphic designer users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No graphic designer users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{item.name || item.full_name || '—'}</td>
|
||||
<td style="color:#475569">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,334 +215,338 @@ export default function KbPage(props: KbPageProps = {}) {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Knowledge Base</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage help articles and categories</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Knowledge Base</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage help articles and categories</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style="display:flex;border-bottom:2px solid #e2e8f0;margin-bottom:24px;gap:0;overflow-x:auto;">
|
||||
<button
|
||||
type="button"
|
||||
class={`admin-tab${tab() === 'categories' ? ' active' : ''}`}
|
||||
onClick={() => setTab('categories')}
|
||||
>
|
||||
Categories
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`admin-tab${tab() === 'articles' ? ' active' : ''}`}
|
||||
onClick={() => setTab('articles')}
|
||||
>
|
||||
Articles
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`admin-tab${tab() === 'create-article' ? ' active' : ''}`}
|
||||
onClick={() => setTab('create-article')}
|
||||
>
|
||||
Create Article
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Categories Tab */}
|
||||
<Show when={tab() === 'categories'}>
|
||||
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:16px">
|
||||
<div />
|
||||
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => setShowCatForm(!showCatForm())}>
|
||||
{showCatForm() ? 'Cancel' : 'Add Category'}
|
||||
{/* Tabs */}
|
||||
<div class="bg-white border-b border-gray-200 px-6 flex gap-6 overflow-x-auto">
|
||||
<button
|
||||
type="button"
|
||||
class={tab() === 'categories' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
||||
onClick={() => setTab('categories')}
|
||||
>
|
||||
Categories
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={tab() === 'articles' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
||||
onClick={() => setTab('articles')}
|
||||
>
|
||||
Articles
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={tab() === 'create-article' ? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium' : 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
|
||||
onClick={() => setTab('create-article')}
|
||||
>
|
||||
Create Article
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
|
||||
</Show>
|
||||
<div class="flex-1 p-6">
|
||||
|
||||
<Show when={showCatForm()}>
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;max-width:480px">
|
||||
<h2 style="margin:0 0 16px;font-size:15px;font-weight:700">New Category</h2>
|
||||
<Show when={catError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{catError()}</div>
|
||||
{/* Categories Tab */}
|
||||
<Show when={tab() === 'categories'}>
|
||||
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:16px">
|
||||
<div />
|
||||
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" onClick={() => setShowCatForm(!showCatForm())}>
|
||||
{showCatForm() ? 'Cancel' : 'Add Category'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={actionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
|
||||
</Show>
|
||||
<form onSubmit={handleAddCategory} style="display:flex;flex-direction:column;gap:12px">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={catName()}
|
||||
onInput={(e) => setCatName(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. Getting Started"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={catSlug()}
|
||||
onInput={(e) => setCatSlug(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. getting-started"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
|
||||
<textarea
|
||||
value={catDesc()}
|
||||
onInput={(e) => setCatDesc(e.currentTarget.value)}
|
||||
rows="3"
|
||||
placeholder="Brief description..."
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={catSaving()}>
|
||||
{catSaving() ? 'Saving...' : 'Create Category'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Article Count</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={categories.loading}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
<Show when={showCatForm()}>
|
||||
<div class="table-card" style="margin-bottom:16px;max-width:480px">
|
||||
<h2 style="margin:0 0 16px;font-size:15px;font-weight:700">New Category</h2>
|
||||
<Show when={catError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{catError()}</div>
|
||||
</Show>
|
||||
<Show when={!categories.loading && categories.error}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) === 0}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">No categories found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) > 0}>
|
||||
<For each={categories()}>
|
||||
{(cat) => (
|
||||
<>
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{cat.name}</td>
|
||||
<td style="color:#475569;font-family:monospace;font-size:13px">{cat.slug}</td>
|
||||
<td style="color:#475569">{cat.article_count ?? '—'}</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => startEditCat(cat)}>Edit</button>
|
||||
<button
|
||||
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
|
||||
disabled={deletingCatId() === cat.id}
|
||||
onClick={() => deleteCategory(cat.id, cat.name)}
|
||||
>
|
||||
{deletingCatId() === cat.id ? '...' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<Show when={editingCatId() === cat.id}>
|
||||
<tr>
|
||||
<td colspan="4" style="background:#f8fafc;padding:14px">
|
||||
<Show when={editCatError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:8px">{editCatError()}</div>
|
||||
</Show>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editCatName()}
|
||||
onInput={(e) => setEditCatName(e.currentTarget.value)}
|
||||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editCatSlug()}
|
||||
onInput={(e) => setEditCatSlug(e.currentTarget.value)}
|
||||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||||
/>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={editCatSaving()} onClick={() => saveEditCat(cat.id)}>
|
||||
{editCatSaving() ? 'Saving...' : 'Save'}
|
||||
<form onSubmit={handleAddCategory} style="display:flex;flex-direction:column;gap:12px">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={catName()}
|
||||
onInput={(e) => setCatName(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. Getting Started"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={catSlug()}
|
||||
onInput={(e) => setCatSlug(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. getting-started"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
|
||||
<textarea
|
||||
value={catDesc()}
|
||||
onInput={(e) => setCatDesc(e.currentTarget.value)}
|
||||
rows="3"
|
||||
placeholder="Brief description..."
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={catSaving()}>
|
||||
{catSaving() ? 'Saving...' : 'Create Category'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Article Count</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={categories.loading}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!categories.loading && categories.error}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) === 0}>
|
||||
<tr><td colspan="4" style="text-align:center;padding:32px;color:#94a3b8">No categories found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!categories.loading && !categories.error && (categories()?.length ?? 0) > 0}>
|
||||
<For each={categories()}>
|
||||
{(cat) => (
|
||||
<>
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{cat.name}</td>
|
||||
<td class="text-slate-500" style="font-family:monospace;font-size:13px">{cat.slug}</td>
|
||||
<td class="text-slate-500">{cat.article_count ?? '—'}</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => startEditCat(cat)}>Edit</button>
|
||||
<button
|
||||
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
|
||||
disabled={deletingCatId() === cat.id}
|
||||
onClick={() => deleteCategory(cat.id, cat.name)}
|
||||
>
|
||||
{deletingCatId() === cat.id ? '...' : 'Delete'}
|
||||
</button>
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={cancelEditCat}>Cancel</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<Show when={editingCatId() === cat.id}>
|
||||
<tr>
|
||||
<td colspan="4" style="background:#f8fafc;padding:14px">
|
||||
<Show when={editCatError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:8px">{editCatError()}</div>
|
||||
</Show>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editCatName()}
|
||||
onInput={(e) => setEditCatName(e.currentTarget.value)}
|
||||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:12px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editCatSlug()}
|
||||
onInput={(e) => setEditCatSlug(e.currentTarget.value)}
|
||||
style="padding:7px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;width:180px"
|
||||
/>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" disabled={editCatSaving()} onClick={() => saveEditCat(cat.id)}>
|
||||
{editCatSaving() ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={cancelEditCat}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Articles Tab */}
|
||||
<Show when={tab() === 'articles'}>
|
||||
<Show when={articleActionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{articleActionError()}</div>
|
||||
</Show>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search articles by title..."
|
||||
value={articleSearch()}
|
||||
onInput={(e) => setArticleSearch(e.currentTarget.value)}
|
||||
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:280px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Updated At</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={articles.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && articles.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && !articles.error && filteredArticles().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No articles found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && !articles.error && filteredArticles().length > 0}>
|
||||
<For each={filteredArticles()}>
|
||||
{(article) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{article.title}</td>
|
||||
<td class="text-slate-500">{article.category || '—'}</td>
|
||||
<td>
|
||||
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${article.status === 'PUBLISHED' ? 'active' : 'draft'}`}>
|
||||
{article.status || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{article.updated_at ? new Date(article.updated_at).toLocaleString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/kb/articles/${article.id}/edit`}>Edit</A>
|
||||
<button
|
||||
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
|
||||
disabled={deletingArticleId() === article.id}
|
||||
onClick={() => deleteArticle(article.id, article.title)}
|
||||
>
|
||||
{deletingArticleId() === article.id ? '...' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
{/* Articles Tab */}
|
||||
<Show when={tab() === 'articles'}>
|
||||
<Show when={articleActionError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{articleActionError()}</div>
|
||||
</Show>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search articles by title..."
|
||||
value={articleSearch()}
|
||||
onInput={(e) => setArticleSearch(e.currentTarget.value)}
|
||||
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:280px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Updated At</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={articles.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && articles.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && !articles.error && filteredArticles().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No articles found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!articles.loading && !articles.error && filteredArticles().length > 0}>
|
||||
<For each={filteredArticles()}>
|
||||
{(article) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{article.title}</td>
|
||||
<td style="color:#475569">{article.category || '—'}</td>
|
||||
<td>
|
||||
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${article.status === 'PUBLISHED' ? 'active' : 'draft'}`}>
|
||||
{article.status || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{article.updated_at ? new Date(article.updated_at).toLocaleString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/kb/articles/${article.id}/edit`}>Edit</A>
|
||||
<button
|
||||
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
|
||||
disabled={deletingArticleId() === article.id}
|
||||
onClick={() => deleteArticle(article.id, article.title)}
|
||||
>
|
||||
{deletingArticleId() === article.id ? '...' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
{/* Create Article Tab */}
|
||||
<Show when={tab() === 'create-article'}>
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:640px">
|
||||
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Article</h2>
|
||||
<Show when={artError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{artError()}</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<form onSubmit={handleCreateArticle} style="display:flex;flex-direction:column;gap:14px">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={artTitle()}
|
||||
onInput={(e) => setArtTitle(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. How to reset your password"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
|
||||
{/* Create Article Tab */}
|
||||
<Show when={tab() === 'create-article'}>
|
||||
<div class="table-card" style="max-width:640px">
|
||||
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Article</h2>
|
||||
<Show when={artError()}>
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{artError()}</div>
|
||||
</Show>
|
||||
<form onSubmit={handleCreateArticle} style="display:flex;flex-direction:column;gap:14px">
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={artTitle()}
|
||||
onInput={(e) => setArtTitle(e.currentTarget.value)}
|
||||
required
|
||||
placeholder="e.g. How to reset your password"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={artSlug()}
|
||||
onInput={(e) => setArtSlug(e.currentTarget.value)}
|
||||
placeholder="e.g. how-to-reset-password"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Category</label>
|
||||
<select
|
||||
value={artCategoryId()}
|
||||
onChange={(e) => setArtCategoryId(e.currentTarget.value)}
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
>
|
||||
<option value="">— Select category —</option>
|
||||
<Show when={!categories.loading}>
|
||||
<For each={categories() ?? []}>
|
||||
{(cat) => <option value={cat.id}>{cat.name}</option>}
|
||||
</For>
|
||||
</Show>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Content</label>
|
||||
<textarea
|
||||
value={artContent()}
|
||||
onInput={(e) => setArtContent(e.currentTarget.value)}
|
||||
required
|
||||
rows="12"
|
||||
placeholder="Write article content here..."
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Status</label>
|
||||
<select
|
||||
value={artStatus()}
|
||||
onChange={(e) => setArtStatus(e.currentTarget.value)}
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
>
|
||||
<option value="DRAFT">DRAFT</option>
|
||||
<option value="PUBLISHED">PUBLISHED</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" type="submit" disabled={artSaving()}>
|
||||
{artSaving() ? 'Creating...' : 'Create Article'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
value={artSlug()}
|
||||
onInput={(e) => setArtSlug(e.currentTarget.value)}
|
||||
placeholder="e.g. how-to-reset-password"
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Category</label>
|
||||
<select
|
||||
value={artCategoryId()}
|
||||
onChange={(e) => setArtCategoryId(e.currentTarget.value)}
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
>
|
||||
<option value="">— Select category —</option>
|
||||
<Show when={!categories.loading}>
|
||||
<For each={categories() ?? []}>
|
||||
{(cat) => <option value={cat.id}>{cat.name}</option>}
|
||||
</For>
|
||||
</Show>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Content</label>
|
||||
<textarea
|
||||
value={artContent()}
|
||||
onInput={(e) => setArtContent(e.currentTarget.value)}
|
||||
required
|
||||
rows="12"
|
||||
placeholder="Write article content here..."
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Status</label>
|
||||
<select
|
||||
value={artStatus()}
|
||||
onChange={(e) => setArtStatus(e.currentTarget.value)}
|
||||
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
|
||||
>
|
||||
<option value="DRAFT">DRAFT</option>
|
||||
<option value="PUBLISHED">PUBLISHED</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={artSaving()}>
|
||||
{artSaving() ? 'Creating...' : 'Create Article'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,89 +36,90 @@ export default function SocialMediaManagersPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Social Media Manager Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage all social media manager accounts on the platform.</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Social Media Manager Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all social media manager accounts on the platform.</p>
|
||||
</div>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No social media manager users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No social media manager users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{item.name || item.full_name || '—'}</td>
|
||||
<td style="color:#475569">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import AdminShell from '~/components/AdminShell';
|
|||
export default function VerificationManagementPage() {
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="page-hero-card">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Approval Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500" style="margin-top:8px">
|
||||
Admin review now lives under Approval Management. Verification remains a user-facing status concept.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<p class="notice" style="margin:0">
|
||||
Use Approval Management to review companies, customers, candidates, and professional submissions.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/approval">Open Approval Management</A>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Approval Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Admin review now lives under Approval Management. Verification remains a user-facing status concept.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<p class="notice" style="margin:0">
|
||||
Use Approval Management to review companies, customers, candidates, and professional submissions.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<A class="inline-flex items-center rounded-lg bg-[#0a1d37] px-4 py-2 text-sm font-semibold text-white hover:bg-[#0f2a4e] transition-colors" href="/admin/approval">Open Approval Management</A>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,89 +36,90 @@ export default function VideoEditorsPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Video Editor Management</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage all video editor accounts on the platform.</p>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Video Editor Management</h1>
|
||||
<p class="text-sm text-gray-500 mt-0.5">Manage all video editor accounts on the platform.</p>
|
||||
</div>
|
||||
<div class="flex-1 p-6">
|
||||
<div class="table-card">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table data-table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No video editor users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="font-semibold text-slate-900">{item.name || item.full_name || '—'}</td>
|
||||
<td class="text-slate-500">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td class="text-slate-500">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
||||
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;width:260px;"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter()}
|
||||
onChange={(e) => setStatusFilter(e.currentTarget.value)}
|
||||
style="border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;font-size:14px;"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">ACTIVE</option>
|
||||
<option value="INACTIVE">INACTIVE</option>
|
||||
<option value="PENDING">PENDING</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Registered</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={users.loading}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && users.error}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load. Is the backend running?</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length === 0}>
|
||||
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No video editor users found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!users.loading && !users.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(item) => (
|
||||
<tr>
|
||||
<td style="font-weight:600;color:#0f172a">{item.name || item.full_name || '—'}</td>
|
||||
<td style="color:#475569">{item.email}</td>
|
||||
<td>
|
||||
{item.status?.toUpperCase() === 'ACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'INACTIVE' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
|
||||
)}
|
||||
{item.status?.toUpperCase() === 'PENDING' && (
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
|
||||
)}
|
||||
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">—</span>}
|
||||
</td>
|
||||
<td style="color:#475569">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue