added admin login to frontend + obscurification for contact details

This commit is contained in:
2026-05-14 17:21:34 +02:00
parent 0102c89d81
commit 244dc076cb
13 changed files with 722 additions and 44 deletions
@@ -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>
);
}