'use client'; import React, { useState, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { MessageSquare, X, Check, MousePointer2, Plus, List, Send, User } from 'lucide-react'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; 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 }>({}); // 1. Fetch Identity and Existing Feedback useEffect(() => { const checkAuth = async () => { try { // Determine if we have a bypass parameter in the URL 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(); // Map Directus fields back to our interface if necessary 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(); }, []); // Helper to get unique selector const getSelector = (el: HTMLElement): string => { if (el.id) return `#${el.id}`; let path = []; while (el.parentElement) { let index = Array.from(el.parentElement.children).indexOf(el) + 1; path.unshift(`${el.tagName.toLowerCase()}:nth-child(${index})`); el = el.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 saveFeedback = async () => { if (!selectedElement || !currentComment) return; const rect = selectedElement.getBoundingClientRect(); 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' }; 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) => ( ))}