"use client"; import React, { useState, useEffect, useMemo } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { MessageSquare, X, Check, Plus, List, Send, User } from "lucide-react"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import html2canvas from "html2canvas"; function cn(...inputs: any[]) { return twMerge(clsx(inputs)); } interface FeedbackComment { id: string; userName: string; text: string; createdAt: string; } interface Feedback { id: string; x: number; y: number; selector: string; text: string; type: "design" | "content"; elementRect: DOMRect | null; userName: string; comments: FeedbackComment[]; } export function FeedbackOverlay() { const [isActive, setIsActive] = useState(false); const [hoveredElement, setHoveredElement] = useState( null, ); const [selectedElement, setSelectedElement] = useState( null, ); const [feedbacks, setFeedbacks] = useState([]); const [currentComment, setCurrentComment] = useState(""); const [currentType, setCurrentType] = useState<"design" | "content">( "design", ); const [showList, setShowList] = useState(false); const [currentUser, setCurrentUser] = useState<{ identity: string; isDevFallback?: boolean; } | null>(null); const [newCommentTexts, setNewCommentTexts] = useState<{ [feedbackId: string]: string; }>({}); const [isCapturing, setIsCapturing] = useState(false); // 1. Fetch Identity and Existing Feedback useEffect(() => { const checkAuth = async () => { try { const urlParams = new URLSearchParams(window.location.search); const bypass = urlParams.get("gatekeeper_bypass"); const apiUrl = bypass ? `/api/whoami?gatekeeper_bypass=${bypass}` : "/api/whoami"; const res = await fetch(apiUrl); if (res.ok) { const data = await res.json(); setCurrentUser(data); } else { setCurrentUser({ identity: "Guest" }); } } catch (_e) { setCurrentUser({ identity: "Guest" }); } }; const fetchFeedback = async () => { try { const res = await fetch("/api/feedback"); if (res.ok) { const data = await res.json(); const mapped = data.map((fb: any) => ({ id: fb.id, x: fb.x, y: fb.y, selector: fb.selector, text: fb.text, type: fb.type, userName: fb.user_name, comments: (fb.comments || []).map((c: any) => ({ id: c.id, userName: c.user_name, text: c.text, createdAt: c.date_created, })), })); setFeedbacks(mapped); } } catch (e) { console.error("Failed to fetch feedbacks", e); } }; checkAuth(); fetchFeedback(); }, []); const getSelector = (el: HTMLElement): string => { if (el.id) return `#${el.id}`; const path = []; let curr: HTMLElement | null = el; while (curr && curr.parentElement) { const index = Array.from(curr.parentElement.children).indexOf(curr) + 1; path.unshift(`${curr.tagName.toLowerCase()}:nth-child(${index})`); curr = curr.parentElement; } return path.join(" > "); }; useEffect(() => { if (!isActive) { setHoveredElement(null); return; } const handleMouseMove = (e: MouseEvent) => { if (selectedElement) return; const target = e.target as HTMLElement; if (target.closest(".feedback-ui-ignore")) { setHoveredElement(null); return; } setHoveredElement(target); }; const handleClick = (e: MouseEvent) => { if (selectedElement) return; const target = e.target as HTMLElement; if (target.closest(".feedback-ui-ignore")) return; e.preventDefault(); e.stopPropagation(); setSelectedElement(target); setHoveredElement(null); }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("click", handleClick, true); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("click", handleClick, true); }; }, [isActive, selectedElement]); const captureScreenshot = async (): Promise => { try { setIsCapturing(true); const canvas = await html2canvas(document.body, { useCORS: true, scale: 1, ignoreElements: (el) => el.classList.contains("feedback-ui-ignore"), }); return canvas.toDataURL("image/png"); } catch (e) { console.error("Screenshot failed", e); return null; } finally { setIsCapturing(false); } }; const saveFeedback = async () => { if (!selectedElement || !currentComment) return; const rect = selectedElement.getBoundingClientRect(); const screenshot = await captureScreenshot(); const feedbackData = { url: window.location.href, x: rect.left + rect.width / 2 + window.scrollX, y: rect.top + rect.height / 2 + window.scrollY, selector: getSelector(selectedElement), text: currentComment, type: currentType, userName: currentUser?.identity || "Unknown", userIdentity: currentUser?.identity === "Admin" ? "admin" : "user", screenshot_base64: screenshot, }; try { const res = await fetch("/api/feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(feedbackData), }); if (res.ok) { const savedFb = await res.json(); const newFeedback: Feedback = { id: savedFb.id, x: savedFb.x, y: savedFb.y, selector: savedFb.selector, text: savedFb.text, type: savedFb.type, elementRect: rect, userName: savedFb.user_name, comments: [], }; setFeedbacks([...feedbacks, newFeedback]); setSelectedElement(null); setCurrentComment(""); } } catch (e) { console.error("Failed to save feedback", e); } }; const addReply = async (feedbackId: string) => { const text = newCommentTexts[feedbackId]; if (!text) return; if (!currentUser?.identity || currentUser.identity === "Guest") { alert("Nur angemeldete Benutzer können antworten."); return; } try { const res = await fetch("/api/feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "reply", feedbackId, userName: currentUser?.identity || "Unknown", text, }), }); if (res.ok) { const savedReply = await res.json(); setFeedbacks( feedbacks.map((f) => { if (f.id === feedbackId) { return { ...f, comments: [ ...f.comments, { id: savedReply.id, userName: savedReply.user_name, text: savedReply.text, createdAt: savedReply.date_created, }, ], }; } return f; }), ); setNewCommentTexts({ ...newCommentTexts, [feedbackId]: "" }); } } catch (e) { console.error("Failed to save reply", e); } }; const hoveredRect = useMemo( () => hoveredElement?.getBoundingClientRect(), [hoveredElement], ); const selectedRect = useMemo( () => selectedElement?.getBoundingClientRect(), [selectedElement], ); return (
{/* 1. Global Toolbar */}
{currentUser?.identity || "Loading..."} {currentUser?.isDevFallback && " (Local Dev Bypass)"}
{/* 2. Feedback Markers & Highlights */} {isActive && ( <> {/* Fixed Overlay for real-time highlights */}
{hoveredRect && ( )} {selectedRect && ( )}
{/* Absolute Overlay for persistent pins */}
{feedbacks.map((fb) => (
))}
)}
{/* 3. Feedback Modal */} {selectedElement && (

Feedback geben

{(["design", "content"] as const).map((type) => ( ))}