added admin login to frontend + obscurification for contact details
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { listMessages, deleteMessage, ApiError } from '../../../lib/api';
|
||||
import type { Message } from '../../../lib/types';
|
||||
|
||||
export default function Inbox() {
|
||||
const [messages, setMessages] = useState<Message[] | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const data = await listMessages();
|
||||
setMessages(data);
|
||||
setError('');
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? e.message : 'Failed to load messages.');
|
||||
setMessages([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
if (!confirm('Delete this message? This cannot be undone.')) return;
|
||||
try {
|
||||
await deleteMessage(id);
|
||||
setMessages(prev => (prev ?? []).filter(m => m.id !== id));
|
||||
} catch (e) {
|
||||
alert(e instanceof ApiError ? e.message : 'Failed to delete.');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
return d.toLocaleString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
if (messages === null) {
|
||||
return <p className="font-display italic text-[var(--subtext0)]">Loading…</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className="p-4 text-sm font-display italic text-center border bg-[var(--red)]/15 text-[var(--red)] border-[var(--red)]/30"
|
||||
style={{ borderRadius: 1 }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="glass p-12 md:p-16 text-center">
|
||||
<div className="font-display italic text-[var(--subtext0)] text-xs tracking-[0.3em] uppercase mb-3">Inbox empty</div>
|
||||
<p className="font-display italic text-[var(--text)] text-2xl">No messages yet.</p>
|
||||
<p className="font-sans text-sm text-[var(--subtext1)] mt-3">When visitors send a note from the contact page, it'll appear here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-3">
|
||||
{messages.map(m => {
|
||||
const isOpen = expandedId === m.id;
|
||||
return (
|
||||
<li
|
||||
key={m.id}
|
||||
className="border border-[var(--surface2)]/60"
|
||||
style={{ borderRadius: 1 }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedId(isOpen ? null : m.id)}
|
||||
className="w-full flex flex-col md:flex-row md:items-baseline md:justify-between gap-2 md:gap-4 px-5 py-4 text-left hover:bg-[var(--surface0)]/40 transition-colors"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-display italic text-base md:text-lg text-[var(--text)] truncate">
|
||||
{m.subject || m.name || '(no subject)'}
|
||||
</div>
|
||||
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider mt-1 truncate">
|
||||
{m.name ? `${m.name} · ` : ''}
|
||||
{m.email ?? 'no email'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider shrink-0">
|
||||
{formatDate(m.received_at)}
|
||||
</div>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-5 pb-5 pt-2 border-t border-[var(--surface2)]/40 space-y-4">
|
||||
<pre className="font-sans whitespace-pre-wrap text-[var(--text)] text-sm leading-relaxed">
|
||||
{m.body}
|
||||
</pre>
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
{m.email && (
|
||||
<a
|
||||
href={`mailto:${m.email}${m.subject ? `?subject=${encodeURIComponent('Re: ' + m.subject)}` : ''}`}
|
||||
className="chip chip-accent uppercase"
|
||||
>
|
||||
Reply
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(m.id)}
|
||||
className="chip text-[var(--red)]"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{m.ip_hash && (
|
||||
<span className="font-mono text-[10px] text-[var(--overlay0)] ml-auto" title="Hashed sender bucket">
|
||||
sender: {m.ip_hash.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user