"use client"; import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Terminal, Send, ShieldAlert, CheckCircle2, User, MessageSquare, Zap, } from "lucide-react"; import { pb } from "@/lib/pb"; import { Button } from "@/components/ui/button"; import { Dictionary } from "@/lib/IDict"; import { CyberButton } from "../ui/cyber-button"; import { cn } from "@/lib/utils"; export function TerminalGuestbook({ dict }: { dict: Dictionary["guestbook"] }) { const [comments, setComments] = useState([]); const [nickname, setNickname] = useState(""); const [message, setMessage] = useState(""); const [honeypot, setHoneypot] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [cooldown, setCooldown] = useState(0); const [status, setStatus] = useState<{ type: "success" | "error" | null; text: string; }>({ type: null, text: "" }); const scrollRef = useRef(null); useEffect(() => { pb.autoCancellation(false); // Начальная загрузка const fetchComments = async () => { try { const records = await pb .collection("comments") .getList(1, 50, { sort: "-created", requestKey: null }); setComments(records.items); } catch (e) { console.error(e); } }; fetchComments(); // Кулдаун const lastPost = localStorage.getItem("last_comment_time"); if (lastPost) { const timePassed = Math.floor((Date.now() - parseInt(lastPost)) / 1000); if (timePassed < 60) setCooldown(60 - timePassed); } // Реалтайм pb.collection("comments").subscribe("*", (e) => { if (e.action === "create") { setComments((prev) => [e.record, ...prev].slice(0, 50)); } }); return () => { pb.collection("comments").unsubscribe("*"); }; }, []); useEffect(() => { if (cooldown > 0) { const timer = setTimeout(() => setCooldown((c) => c - 1), 1000); return () => clearTimeout(timer); } }, [cooldown]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (honeypot) return setStatus({ type: "error", text: dict.statusErrorBreach }); if (!nickname.trim() || !message.trim()) return setStatus({ type: "error", text: dict.statusErrorEmpty }); if (nickname.length > 20 || message.length > 150) return setStatus({ type: "error", text: dict.statusErrorSize }); if (cooldown > 0) return setStatus({ type: "error", text: dict.statusErrorFirewall.replace("{{time}}", cooldown.toString()), }); try { setIsSubmitting(true); const proxyUrl = process.env.NODE_ENV === "production" ? "/api/proxy" : "http://localhost:3001"; const res = await fetch(`${proxyUrl}/api/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nickname: nickname.trim(), message: message.trim(), }), }); if (!res.ok) { // Если Rust вернул 406 Not Acceptable (грязь в тексте) if (res.status === 406) { throw new Error(dict.statusErrorBreach); // или кастомная ошибка "Мат запрещен" } throw new Error("Server error"); } setNickname(""); setMessage(""); setStatus({ type: "success", text: dict.statusSuccess }); setCooldown(60); localStorage.setItem("last_comment_time", Date.now().toString()); } catch (err) { setStatus({ type: "error", text: dict.statusErrorOffline }); } finally { setIsSubmitting(false); setTimeout(() => setStatus({ type: null, text: "" }), 5000); } }; return (
{/* Мягкие свечения — увеличили размер и размытие для плавности */}
{/* Заголовок секции */}
{dict.subtitle}

{dict.title}

{/* ЛЕВАЯ КОЛОНКА (4/12): Философия и Форма */}
{/* Блок философии (Тот самый "Плюс") */}

{dict.philosophyTitle}

"{dict.philosophyText}"

{/* Форма отправки */}
setNickname(e.target.value)} placeholder={dict.placeholderAlias} disabled={isSubmitting || cooldown > 0} // Фрагмент классов для input и textarea в TerminalGuestbook.tsx className={cn( "w-full rounded-2xl px-5 py-4 text-sm outline-none transition-all font-mono", "disabled:opacity-50 text-foreground", // Светлая тема: более плотный плейсхолдер и четкая рамка "bg-background/50 border border-border/80 placeholder:text-muted-foreground/60 focus:border-primary focus:bg-background shadow-sm", // Темная тема: стеклянный эффект "dark:bg-background/50 dark:border-border/50 dark:placeholder:text-muted-foreground/30 dark:focus:border-primary/50", )} />