127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import type { MessageDTO } from '@/lib/types';
|
|
|
|
const POLL_MS = 4000;
|
|
|
|
function formatTime(iso: string) {
|
|
return new Date(iso).toLocaleString(undefined, {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
export default function Chat({
|
|
proofId,
|
|
role,
|
|
otherPartyLabel,
|
|
}: {
|
|
proofId: string;
|
|
role: 'ADMIN' | 'CUSTOMER';
|
|
otherPartyLabel: string;
|
|
}) {
|
|
const [messages, setMessages] = useState<MessageDTO[]>([]);
|
|
const [input, setInput] = useState('');
|
|
const [sending, setSending] = useState(false);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const load = async () => {
|
|
try {
|
|
const res = await fetch(`/api/proofs/${proofId}/messages`);
|
|
if (!res.ok || cancelled) return;
|
|
const data: MessageDTO[] = await res.json();
|
|
if (!cancelled) setMessages(data);
|
|
} catch {
|
|
// silently retry on next poll
|
|
}
|
|
};
|
|
|
|
load();
|
|
const interval = setInterval(load, POLL_MS);
|
|
return () => {
|
|
cancelled = true;
|
|
clearInterval(interval);
|
|
};
|
|
}, [proofId]);
|
|
|
|
useEffect(() => {
|
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
|
|
}, [messages.length]);
|
|
|
|
const send = async () => {
|
|
const trimmed = input.trim();
|
|
if (!trimmed || sending) return;
|
|
setSending(true);
|
|
setInput('');
|
|
try {
|
|
const res = await fetch(`/api/proofs/${proofId}/messages`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ sender: role, body: trimmed }),
|
|
});
|
|
if (res.ok) {
|
|
const created: MessageDTO = await res.json();
|
|
setMessages((prev) => [...prev, created]);
|
|
}
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="border border-line bg-surface">
|
|
<div className="border-b border-line px-4 py-3">
|
|
<p className="tag-label">Chat with {otherPartyLabel}</p>
|
|
</div>
|
|
|
|
<div ref={scrollRef} className="max-h-72 space-y-3 overflow-y-auto p-4">
|
|
{messages.length === 0 ? (
|
|
<p className="text-sm text-muted">No messages yet — say hello.</p>
|
|
) : (
|
|
messages.map((m) => {
|
|
const mine = m.sender === role;
|
|
return (
|
|
<div key={m.id} className={`flex ${mine ? 'justify-end' : 'justify-start'}`}>
|
|
<div className={`max-w-[75%] ${mine ? 'bg-clay text-paper' : 'bg-paper border border-line text-ink'} px-3 py-2`}>
|
|
<p className="text-sm">{m.body}</p>
|
|
<p className={`mt-1 text-[10px] ${mine ? 'text-paper/60' : 'text-muted'}`}>
|
|
{formatTime(m.createdAt)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-2 border-t border-line p-3">
|
|
<input
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
send();
|
|
}
|
|
}}
|
|
placeholder="Type a message…"
|
|
className="flex-1 border border-line px-3 py-2 text-sm"
|
|
/>
|
|
<button
|
|
onClick={send}
|
|
disabled={sending || !input.trim()}
|
|
className="border border-ink px-4 py-2 text-sm hover:border-clay hover:bg-clay hover:text-paper disabled:opacity-50"
|
|
>
|
|
Send
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|