Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
156 lines
6.0 KiB
TypeScript
156 lines
6.0 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useRef } from "react";
|
||
|
||
const ChatWidget = () => {
|
||
const [messages, setMessages] = useState([
|
||
{ id: 1, sender: "System", text: "Welcome to the encrypted channel. All messages are end‑to‑end encrypted.", time: "12:00" },
|
||
{ id: 2, sender: "Ghost", text: "Has anyone tested the new stimulant batch?", time: "12:05" },
|
||
{ id: 3, sender: "Vendor_X", text: "Batch #8 passes all purity tests. Available for bulk.", time: "12:07" },
|
||
{ id: 4, sender: "Anonymous", text: "Need a EU passport within 48 hours. DM if you can deliver.", time: "12:10" },
|
||
]);
|
||
const [input, setInput] = useState("");
|
||
/** Set after mount so SSR HTML matches first client paint (no Math.random in initial state). */
|
||
const [encryptionKey, setEncryptionKey] = useState("");
|
||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
setEncryptionKey("session_" + Math.random().toString(36).substring(2, 9));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (chatContainerRef.current) {
|
||
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
||
}
|
||
}, [messages]);
|
||
|
||
const handleSend = () => {
|
||
if (!input.trim()) return;
|
||
const newMessage = {
|
||
id: messages.length + 1,
|
||
sender: "You",
|
||
text: input,
|
||
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
||
};
|
||
setMessages([...messages, newMessage]);
|
||
setInput("");
|
||
|
||
// Simulate a reply after a delay
|
||
setTimeout(() => {
|
||
const replies = [
|
||
"Received. Encryption verified.",
|
||
"I can help with that. Check your vault.",
|
||
"New drop incoming in 24 hours.",
|
||
"Your request has been logged.",
|
||
];
|
||
const randomReply = replies[Math.floor(Math.random() * replies.length)];
|
||
const replyMessage = {
|
||
id: messages.length + 2,
|
||
sender: "System",
|
||
text: randomReply,
|
||
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
||
};
|
||
setMessages(prev => [...prev, replyMessage]);
|
||
}, 1000);
|
||
};
|
||
|
||
const handleEncrypt = () => {
|
||
alert(`Session key: ${encryptionKey}\nAll messages are encrypted with this key.`);
|
||
};
|
||
|
||
return (
|
||
<div className="glass rounded-3xl border border-white/10 p-8">
|
||
<div className="mb-8 flex items-center justify-between">
|
||
<div>
|
||
<h2 className="font-orbitron text-3xl font-bold">ENCRYPTED CHAT</h2>
|
||
<p className="text-foreground/60">End‑to‑end encrypted messaging. No logs, no traces.</p>
|
||
</div>
|
||
<button
|
||
onClick={handleEncrypt}
|
||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 font-bold text-background"
|
||
>
|
||
🔐 SHOW SESSION KEY
|
||
</button>
|
||
</div>
|
||
|
||
{/* Chat messages */}
|
||
<div
|
||
ref={chatContainerRef}
|
||
className="glass mb-6 h-96 overflow-y-auto rounded-2xl border border-white/10 p-6"
|
||
>
|
||
{messages.map((msg) => (
|
||
<div
|
||
key={msg.id}
|
||
className={`mb-4 ${msg.sender === "You" ? "text-right" : ""}`}
|
||
>
|
||
<div className="mb-1 flex items-center gap-2">
|
||
<span className={`inline-block rounded-full px-3 py-1 text-xs font-medium ${msg.sender === "You"
|
||
? "bg-neon-cyan/20 text-neon-cyan"
|
||
: msg.sender === "System"
|
||
? "bg-neon-purple/20 text-neon-purple"
|
||
: "bg-white/10"
|
||
}`}>
|
||
{msg.sender}
|
||
</span>
|
||
<span className="text-xs text-foreground/40">{msg.time}</span>
|
||
</div>
|
||
<div className={`glass inline-block max-w-[80%] rounded-2xl px-4 py-3 ${msg.sender === "You"
|
||
? "bg-gradient-to-r from-neon-cyan/20 to-neon-cyan/10"
|
||
: "bg-white/5"
|
||
}`}>
|
||
{msg.text}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Input area */}
|
||
<div className="flex gap-4">
|
||
<input
|
||
type="text"
|
||
className="glass flex-1 rounded-2xl border border-white/10 bg-transparent px-6 py-4"
|
||
placeholder="Type your encrypted message..."
|
||
value={input}
|
||
onChange={(e) => setInput(e.target.value)}
|
||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||
/>
|
||
<button
|
||
onClick={handleSend}
|
||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-4 font-bold text-background"
|
||
>
|
||
SEND
|
||
</button>
|
||
</div>
|
||
|
||
{/* Chat features */}
|
||
<div className="mt-8 grid grid-cols-2 gap-6 md:grid-cols-4">
|
||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||
<div className="text-2xl">🔒</div>
|
||
<div className="mt-2 text-sm font-bold">AES‑256‑GCM</div>
|
||
<div className="text-xs text-foreground/60">Encryption</div>
|
||
</div>
|
||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||
<div className="text-2xl">⚡</div>
|
||
<div className="mt-2 text-sm font-bold">Zero‑Knowledge</div>
|
||
<div className="text-xs text-foreground/60">No server storage</div>
|
||
</div>
|
||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||
<div className="text-2xl">🌐</div>
|
||
<div className="mt-2 text-sm font-bold">WebRTC</div>
|
||
<div className="text-xs text-foreground/60">P2P possible</div>
|
||
</div>
|
||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||
<div className="text-2xl">🕶️</div>
|
||
<div className="mt-2 text-sm font-bold">Self‑Destruct</div>
|
||
<div className="text-xs text-foreground/60">Messages expire</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-8 text-center text-xs text-foreground/40">
|
||
<p>Ephemeral client-side queue — nothing leaves this browser profile until a real transport is configured.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ChatWidget; |