Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🧪 QA (push) Failing after 1m31s
Build & Deploy / 🏗️ Build (push) Failing after 3m51s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
185 lines
6.7 KiB
TypeScript
185 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState, useRef } from "react";
|
|
import { Copy, Check } from "lucide-react";
|
|
import { useAnalytics } from "./analytics/useAnalytics";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
export function TextSelectionShare() {
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const [selectedText, setSelectedText] = useState("");
|
|
const [copied, setCopied] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
const { trackEvent } = useAnalytics();
|
|
const selectionTimeoutRef = useRef<NodeJS.Timeout>(undefined);
|
|
|
|
useEffect(() => {
|
|
const handleSelection = () => {
|
|
// Clear any pending timeout
|
|
if (selectionTimeoutRef.current) {
|
|
clearTimeout(selectionTimeoutRef.current);
|
|
}
|
|
|
|
// Small delay to ensure selection is complete
|
|
selectionTimeoutRef.current = setTimeout(() => {
|
|
const selection = window.getSelection();
|
|
const text = selection?.toString().trim();
|
|
|
|
if (text && text.length > 10) {
|
|
const range = selection?.getRangeAt(0);
|
|
const rect = range?.getBoundingClientRect();
|
|
|
|
if (rect) {
|
|
// Position menu above selection, centered
|
|
setPosition({
|
|
x: rect.left + rect.width / 2,
|
|
y: rect.top + window.scrollY - 10,
|
|
});
|
|
setSelectedText(text);
|
|
setIsVisible(true);
|
|
}
|
|
} else {
|
|
setIsVisible(false);
|
|
setCopied(false);
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
const selection = window.getSelection();
|
|
if (!selection?.toString()) {
|
|
setIsVisible(false);
|
|
setCopied(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Listen to both mouseup and selectionchange for better reliability
|
|
document.addEventListener("mouseup", handleSelection);
|
|
document.addEventListener("selectionchange", handleSelection);
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
|
|
return () => {
|
|
document.removeEventListener("mouseup", handleSelection);
|
|
document.removeEventListener("selectionchange", handleSelection);
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
if (selectionTimeoutRef.current) {
|
|
clearTimeout(selectionTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleCopy = async () => {
|
|
const url = window.location.href;
|
|
const shareText = `"${selectedText}"\n\n${url}`;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(shareText);
|
|
setCopied(true);
|
|
trackEvent("text_selection_copied", {
|
|
text_length: selectedText.length,
|
|
url: url,
|
|
});
|
|
setTimeout(() => {
|
|
setIsVisible(false);
|
|
setCopied(false);
|
|
}, 1500);
|
|
} catch (err) {
|
|
console.error("Failed to copy:", err);
|
|
}
|
|
};
|
|
|
|
const handleShareX = () => {
|
|
const url = window.location.href;
|
|
const text = encodeURIComponent(`"${selectedText}"\n\n`);
|
|
const shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodeURIComponent(url)}`;
|
|
|
|
window.open(shareUrl, "_blank", "width=550,height=420");
|
|
trackEvent("text_selection_shared_x", {
|
|
text_length: selectedText.length,
|
|
url: url,
|
|
});
|
|
setIsVisible(false);
|
|
};
|
|
|
|
const handleShareLinkedIn = () => {
|
|
const url = window.location.href;
|
|
const shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`;
|
|
|
|
window.open(shareUrl, "_blank", "width=550,height=420");
|
|
trackEvent("text_selection_shared_linkedin", {
|
|
text_length: selectedText.length,
|
|
url: url,
|
|
});
|
|
setIsVisible(false);
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isVisible && (
|
|
<motion.div
|
|
ref={menuRef}
|
|
initial={{ opacity: 0, y: 10, scale: 0.9 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 10, scale: 0.9 }}
|
|
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
className="fixed z-[200]"
|
|
style={{
|
|
left: `${position.x}px`,
|
|
top: `${position.y}px`,
|
|
transform: "translate(-50%, -100%)",
|
|
}}
|
|
>
|
|
<div className="bg-slate-900 text-white rounded-xl shadow-2xl px-2 py-2 flex items-center gap-1">
|
|
<button
|
|
onClick={handleCopy}
|
|
className="px-3 py-2 hover:bg-slate-800 rounded-lg transition-colors flex items-center gap-2 text-sm font-medium"
|
|
title="Zitat kopieren"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check size={16} className="text-green-400" />
|
|
<span>Kopiert!</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy size={16} />
|
|
<span>Kopieren</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<div className="w-px h-6 bg-slate-700" />
|
|
|
|
<button
|
|
onClick={handleShareX}
|
|
className="px-3 py-2 hover:bg-slate-800 rounded-lg transition-colors flex items-center gap-2 text-sm font-medium"
|
|
title="Auf X teilen"
|
|
>
|
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
</svg>
|
|
<span>X</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleShareLinkedIn}
|
|
className="px-3 py-2 hover:bg-slate-800 rounded-lg transition-colors flex items-center gap-2 text-sm font-medium"
|
|
title="Auf LinkedIn teilen"
|
|
>
|
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
|
</svg>
|
|
<span>LinkedIn</span>
|
|
</button>
|
|
</div>
|
|
{/* Arrow */}
|
|
<div className="absolute left-1/2 -translate-x-1/2 -bottom-2 w-0 h-0 border-l-8 border-r-8 border-t-8 border-l-transparent border-r-transparent border-t-slate-900" />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|