"use client"; import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Terminal, Send, Loader2, ShieldCheck, ShieldAlert, } from "lucide-react"; import { pb } from "@/lib/pb"; import { Dictionary } from "@/lib/IDict"; export function TerminalClient({ dict }: { dict: Dictionary["terminal"] }) { const [messages, setMessages] = useState([]); const [nickname, setNickname] = useState(""); const [inputMsg, setInputMsg] = useState(""); const [errorText, setErrorText] = useState(null); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoading, setIsLoading] = useState(true); const [isFetchingMore, setIsFetchingMore] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [cooldown, setCooldown] = useState(0); const scrollRef = useRef(null); const prevScrollHeight = useRef(0); useEffect(() => { fetchHistory(1, true); pb.collection("comments").subscribe("*", (e) => { if (e.action === "create") { setMessages((prev) => [...prev, e.record]); setTimeout(scrollToBottom, 50); } }); const lastPost = localStorage.getItem("last_comment_time"); if (lastPost) { const timePassed = Math.floor((Date.now() - parseInt(lastPost)) / 1000); if (timePassed < 60) setCooldown(60 - timePassed); } return () => { pb.collection("comments").unsubscribe("*"); }; }, []); useEffect(() => { if (cooldown > 0) { const timer = setTimeout(() => setCooldown((c) => c - 1), 1000); return () => clearTimeout(timer); } }, [cooldown]); useEffect(() => { if (prevScrollHeight.current > 0 && scrollRef.current) { const newHeight = scrollRef.current.scrollHeight; scrollRef.current.scrollTop += newHeight - prevScrollHeight.current; prevScrollHeight.current = 0; } }, [messages]); const fetchHistory = async (pageToLoad: number, isInitial = false) => { try { if (isInitial) setIsLoading(true); else setIsFetchingMore(true); const res = await pb .collection("comments") .getList(pageToLoad, 30, { sort: "-created", requestKey: null }); const fetchedItems = res.items.reverse(); if (isInitial) { setMessages(fetchedItems); setTimeout(scrollToBottom, 100); } else { if (scrollRef.current) { prevScrollHeight.current = scrollRef.current.scrollHeight; } setMessages((prev) => [...fetchedItems, ...prev]); } setHasMore(res.page < res.totalPages); setPage(pageToLoad); } catch (e) { console.error(e); } finally { setIsLoading(false); setIsFetchingMore(false); } }; const handleScroll = () => { if (!scrollRef.current || isFetchingMore || !hasMore) return; if (scrollRef.current.scrollTop === 0) { fetchHistory(page + 1); } }; const scrollToBottom = () => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorText(null); if (!nickname.trim() || !inputMsg.trim() || cooldown > 0 || isSubmitting) return; try { setIsSubmitting(true); const proxyUrl = "/api/proxy"; const res = await fetch(`${proxyUrl}/api/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nickname: nickname.trim(), message: inputMsg.trim(), }), }); if (!res.ok) { if (res.status === 406) { throw new Error("FILTER_VIOLATION"); } throw new Error("SERVER_ERROR"); } // Успешная отправка setInputMsg(""); setCooldown(60); localStorage.setItem("last_comment_time", Date.now().toString()); setTimeout(scrollToBottom, 50); } catch (err: any) { console.error(err); // Выдаем разные ошибки в зависимости от ответа сервера if (err.message === "FILTER_VIOLATION") { setErrorText( (dict as any).statusErrorFilter || "⛔ Сообщение отклонено ИИ-фильтром (нарушение политики: мат, спам или токсичность).", ); } else { setErrorText( (dict as any).statusErrorOffline || "⚠️ Ошибка связи с сервером. Попробуйте повторить запрос позже.", ); } // Убираем ошибку через 8 секунд setTimeout(() => setErrorText(null), 8000); } finally { setIsSubmitting(false); } }; return (
{/* Header */}
{dict.title}
{dict.systemReady}
{/* Message Area */}
{isFetchingMore && (
{" "} {dict.loadingHistory}
)} {!hasMore && !isLoading && (
--- {dict.noMoreLogs} ---
)} {/* MANIFESTO */} {!isFetchingMore && !hasMore && !isLoading && (
{dict.manifestoTitle}

{dict.manifestoText}

{dict.rulesTitle}

{dict.rule1}

{dict.rule2}

{dict.rule3}

{dict.statusReady}
)} {messages.map((msg, idx) => (
guest@{msg.nickname} :~# {new Date(msg.created).toLocaleTimeString()}
{msg.message}
))} {isLoading && (
)}
{/* Input Panel */}
{/* Блок вывода ошибок */} {errorText && ( {errorText} )}
setNickname(e.target.value)} className="w-full sm:w-40 bg-background/50 border border-border rounded-xl px-4 py-3 font-mono text-sm focus:border-primary outline-none transition-colors" disabled={isSubmitting || cooldown > 0} />
0 ? dict.cooldown.replace("{{time}}", cooldown.toString()) : dict.message } value={inputMsg} onChange={(e) => setInputMsg(e.target.value)} className="w-full bg-background/50 border border-border rounded-xl pl-4 pr-12 py-3 font-mono text-sm focus:border-primary outline-none transition-colors" disabled={isSubmitting || cooldown > 0} />
); }