170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useI18n } from '../../components/I18nProvider';
|
|
|
|
type PendingUser = { id: string; email: string; status: string; emailVerifiedAt: string | null; approvedAt: string | null; role: string };
|
|
type PendingListing = { id: string; status: string; createdAt: string; owner: { email: string }; translations: { title: string; slug: string; locale: string }[] };
|
|
|
|
export default function PendingAdminPage() {
|
|
const { t } = useI18n();
|
|
const [pendingUsers, setPendingUsers] = useState<PendingUser[]>([]);
|
|
const [pendingListings, setPendingListings] = useState<PendingListing[]>([]);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function loadPending() {
|
|
setMessage(null);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch('/api/admin/pending', { cache: 'no-store' });
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to load');
|
|
return;
|
|
}
|
|
setPendingUsers(data.users ?? []);
|
|
setPendingListings(data.listings ?? []);
|
|
} catch (e) {
|
|
setError('Failed to load');
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetch('/api/auth/me', { cache: 'no-store' })
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data.user?.role === 'ADMIN') {
|
|
loadPending();
|
|
} else {
|
|
setError(t('adminRequired'));
|
|
}
|
|
})
|
|
.catch(() => setError(t('adminRequired')));
|
|
}, [t]);
|
|
|
|
async function approveUser(userId: string, makeAdmin: boolean) {
|
|
setMessage(null);
|
|
setError(null);
|
|
const res = await fetch('/api/admin/users/approve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId, makeAdmin }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to approve user');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
async function approveListing(listingId: string, action: 'approve' | 'reject' | 'remove') {
|
|
setMessage(null);
|
|
setError(null);
|
|
const reason =
|
|
action === 'reject'
|
|
? window.prompt(`${t('reject')}? (optional)`)
|
|
: action === 'remove'
|
|
? window.prompt(`${t('remove')}? (optional)`)
|
|
: null;
|
|
const res = await fetch('/api/admin/listings/approve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ listingId, action, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to update listing');
|
|
} else {
|
|
setMessage(t('approvalsMessage'));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
async function rejectUser(userId: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
const reason = window.prompt(`${t('reject')}? (optional)`);
|
|
const res = await fetch('/api/admin/users/reject', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userId, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || 'Failed to reject user');
|
|
} else {
|
|
setMessage(t('userUpdated'));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="panel" style={{ maxWidth: 960, margin: '40px auto' }}>
|
|
<h1>{t('pendingAdminTitle')}</h1>
|
|
<div style={{ display: 'grid', gap: 16 }}>
|
|
<section>
|
|
<h3>{t('pendingUsersTitle')}</h3>
|
|
{pendingUsers.length === 0 ? (
|
|
<p>{t('noPendingUsers')}</p>
|
|
) : (
|
|
<ul style={{ display: 'grid', gap: 8, padding: 0, listStyle: 'none' }}>
|
|
{pendingUsers.map((u) => (
|
|
<li key={u.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 12 }}>
|
|
<div>
|
|
<strong>{u.email}</strong> — {t('statusLabel')}: {u.status} — {t('verifiedLabel')}: {u.emailVerifiedAt ? t('yes') : t('no')}
|
|
</div>
|
|
<div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
|
|
<button className="button" onClick={() => approveUser(u.id, false)}>
|
|
{t('approve')}
|
|
</button>
|
|
<button className="button secondary" onClick={() => approveUser(u.id, true)}>
|
|
{t('approveAdmin')}
|
|
</button>
|
|
<button className="button secondary" onClick={() => rejectUser(u.id)}>
|
|
{t('reject')}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
<section>
|
|
<h3>{t('pendingListingsTitle')}</h3>
|
|
{pendingListings.length === 0 ? (
|
|
<p>{t('noPendingListings')}</p>
|
|
) : (
|
|
<ul style={{ display: 'grid', gap: 8, padding: 0, listStyle: 'none' }}>
|
|
{pendingListings.map((l) => (
|
|
<li key={l.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 12 }}>
|
|
<div>
|
|
<strong>{l.translations[0]?.title ?? 'Listing'}</strong> — owner: {l.owner.email}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: '#666' }}>
|
|
{t('slugsLabel')}: {l.translations.map((t) => `${t.slug} (${t.locale})`).join(', ')}
|
|
</div>
|
|
<div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
|
|
<button className="button" onClick={() => approveListing(l.id, 'approve')}>
|
|
{t('publish')}
|
|
</button>
|
|
<button className="button secondary" onClick={() => approveListing(l.id, 'reject')}>
|
|
{t('reject')}
|
|
</button>
|
|
<button className="button secondary" onClick={() => approveListing(l.id, 'remove')}>
|
|
{t('remove')}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
</div>
|
|
{message ? <p style={{ marginTop: 12, color: 'green' }}>{message}</p> : null}
|
|
{error ? <p style={{ marginTop: 12, color: 'red' }}>{error}</p> : null}
|
|
</main>
|
|
);
|
|
}
|