Husr's picture
first commit
d988ae4
'use client';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { apiUrl } from '@/lib/constants';
interface ClipboardSummary {
id: string;
createdAt: string;
lastActivity: string;
hasPassword: boolean;
entryCount: number;
fileCount: number;
ttl: number | null;
}
export default function AdminDashboard() {
const [token, setToken] = useState('');
const [inputToken, setInputToken] = useState('');
const [clipboards, setClipboards] = useState<ClipboardSummary[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
const saved = localStorage.getItem('adminToken');
if (saved) {
setToken(saved);
setInputToken(saved);
}
}, []);
useEffect(() => {
if (token) {
fetchClipboards();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const fetchClipboards = async () => {
setLoading(true);
setError('');
try {
const response = await fetch(`${apiUrl}/admin/clipboards`, {
headers: { 'x-admin-token': token },
});
if (response.status === 401) {
setError('Invalid token. Please log in again.');
return;
}
if (!response.ok) {
throw new Error('Failed to fetch clipboards');
}
const data = await response.json();
setClipboards(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
const handleLogin = () => {
if (!inputToken) {
setError('Please enter an admin token');
return;
}
localStorage.setItem('adminToken', inputToken);
setToken(inputToken);
setError('');
};
const handleDelete = async (roomCode: string) => {
if (!confirm(`Delete clipboard ${roomCode}? This cannot be undone.`)) {
return;
}
try {
const response = await fetch(`${apiUrl}/admin/clipboards/${roomCode}`, {
method: 'DELETE',
headers: { 'x-admin-token': token },
});
if (!response.ok) {
throw new Error('Failed to delete clipboard');
}
await fetchClipboards();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
};
const tokenValid = useMemo(() => Boolean(token), [token]);
return (
<div className="w-full max-w-6xl mx-auto p-6 md:p-10">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
<p className="text-text-secondary">Manage clipboards and inspect activity</p>
</div>
<Link href="/" className="btn btn-ghost">
Back to Home
</Link>
</div>
<div className="card mb-6">
<div className="flex flex-col sm:flex-row sm:items-end gap-4">
<div className="flex-1">
<label className="block text-sm text-text-secondary mb-2">Admin Token</label>
<input
type="password"
value={inputToken}
onChange={(e) => setInputToken(e.target.value)}
className="input w-full"
placeholder="Enter admin token"
/>
</div>
<button className="btn btn-primary self-start sm:self-auto" onClick={handleLogin}>
{tokenValid ? 'Update Token' : 'Login'}
</button>
</div>
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
</div>
{tokenValid && (
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">Clipboards</h2>
<div className="flex gap-2">
<button className="btn btn-ghost" onClick={fetchClipboards} disabled={loading}>
Refresh
</button>
</div>
</div>
{loading ? (
<p className="text-text-secondary">Loading clipboards...</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="text-left text-text-secondary border-b border-surface-hover">
<tr>
<th className="py-2 pr-4">ID</th>
<th className="py-2 pr-4">Entries</th>
<th className="py-2 pr-4">Files</th>
<th className="py-2 pr-4">Password</th>
<th className="py-2 pr-4">TTL</th>
<th className="py-2 pr-4">Last Activity</th>
<th className="py-2 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody>
{clipboards.length === 0 && (
<tr>
<td className="py-4 text-text-secondary" colSpan={7}>
No clipboards found.
</td>
</tr>
)}
{clipboards.map((clipboard) => (
<tr key={clipboard.id} className="border-b border-surface-hover">
<td className="py-3 pr-4 font-mono">{clipboard.id}</td>
<td className="py-3 pr-4">{clipboard.entryCount}</td>
<td className="py-3 pr-4">{clipboard.fileCount}</td>
<td className="py-3 pr-4">{clipboard.hasPassword ? 'Yes' : 'No'}</td>
<td className="py-3 pr-4">{formatTTL(clipboard.ttl)}</td>
<td className="py-3 pr-4 text-text-secondary">{new Date(clipboard.lastActivity).toLocaleString()}</td>
<td className="py-3 pr-4 text-right">
<div className="flex justify-end gap-2">
<Link href={`/admin/${clipboard.id}`} className="btn btn-secondary btn-sm">
View
</Link>
<button className="btn btn-ghost btn-sm" onClick={() => handleDelete(clipboard.id)}>
Delete
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
}
function formatTTL(ttl: number | null): string {
if (ttl === null) return 'None';
if (ttl < 60) return `${ttl}s`;
if (ttl < 3600) return `${Math.round(ttl / 60)}m`;
const hours = Math.round(ttl / 3600);
return `${hours}h`;
}