Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🏗️ Build (push) Failing after 16s
Build & Deploy / 🧪 QA (push) Failing after 2m1s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
224 lines
8.9 KiB
TypeScript
224 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Share2 } from "lucide-react";
|
|
import { ShareModal } from "./ShareModal";
|
|
import { useAnalytics } from "./analytics/useAnalytics";
|
|
import * as htmlToImage from "html-to-image";
|
|
import QRCode from "qrcode";
|
|
import LogoBlack from "../assets/logo/Logo-Black-Transparent.svg";
|
|
|
|
interface ComponentShareButtonProps {
|
|
targetId: string;
|
|
title?: string;
|
|
shareText?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export const ComponentShareButton: React.FC<ComponentShareButtonProps> = ({
|
|
targetId,
|
|
title = "Mintel Architektur Insights",
|
|
shareText = "Schauen Sie sich diese spannende interaktive Simulation an:",
|
|
className = ""
|
|
}) => {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [generatedImage, setGeneratedImage] = useState<string | undefined>();
|
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
const { trackEvent } = useAnalytics();
|
|
|
|
const currentUrl =
|
|
typeof window !== "undefined"
|
|
? `${window.location.origin}${window.location.pathname}#${targetId}`
|
|
: "";
|
|
|
|
const applyWatermark = async (base64Img: string, qrCodeSrc: string): Promise<string> => {
|
|
return new Promise((resolve) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement("canvas");
|
|
// html-to-image uses pixelRatio: 2, so img.width/img.height are 2x
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return resolve(base64Img);
|
|
|
|
// Draw main image
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
// Watermark styling for 2x resolution
|
|
const padding = 32;
|
|
const wHeight = 110;
|
|
const wWidth = 360;
|
|
const x = canvas.width - wWidth - padding;
|
|
const y = canvas.height - wHeight - padding;
|
|
|
|
// Draw pill shape background
|
|
ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
|
|
ctx.shadowColor = "rgba(0, 0, 0, 0.05)";
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowOffsetY = 4;
|
|
|
|
if (ctx.roundRect) {
|
|
ctx.beginPath();
|
|
ctx.roundRect(x, y, wWidth, wHeight, 24);
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillRect(x, y, wWidth, wHeight);
|
|
}
|
|
|
|
// Draw text and logo
|
|
ctx.shadowColor = "transparent";
|
|
ctx.textBaseline = "middle";
|
|
|
|
(async () => {
|
|
try {
|
|
const qrImg = new Image();
|
|
qrImg.src = qrCodeSrc;
|
|
await new Promise(r => { qrImg.onload = r; qrImg.onerror = r; });
|
|
|
|
const logoSrc = typeof LogoBlack === 'string' ? LogoBlack : (LogoBlack as any).src;
|
|
const logoImg = new Image();
|
|
logoImg.src = logoSrc;
|
|
await new Promise(r => { logoImg.onload = r; logoImg.onerror = r; });
|
|
|
|
if (logoImg.width > 0) {
|
|
// Calculate logo dimensions
|
|
const logoHeight = 32;
|
|
const logoWidth = logoImg.width * (logoHeight / logoImg.height);
|
|
const logoX = x + 24;
|
|
const logoY = y + (wHeight - logoHeight) / 2;
|
|
|
|
// Draw Logo
|
|
ctx.drawImage(logoImg, logoX, logoY, logoWidth, logoHeight);
|
|
|
|
// Draw "Zum Artikel" text right of the logo
|
|
ctx.font = "600 20px sans-serif";
|
|
ctx.fillStyle = "#64748b";
|
|
ctx.textAlign = "left";
|
|
ctx.fillText("Zum Artikel", logoX + logoWidth + 24, y + wHeight / 2 + 2);
|
|
} else {
|
|
// Fallback if logo fails to load for some reason
|
|
ctx.font = "900 36px sans-serif";
|
|
ctx.fillStyle = "#0f172a";
|
|
ctx.textAlign = "left";
|
|
ctx.fillText("mintel.", x + 24, y + wHeight / 2 + 4);
|
|
}
|
|
|
|
// Draw solid line separator before QR
|
|
const qrSize = wHeight - 24; // padding 12*2 inside pill
|
|
const qrX = x + wWidth - qrSize - 12;
|
|
const qrY = y + 12;
|
|
const sepX = qrX - 12;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(sepX, y + 20);
|
|
ctx.lineTo(sepX, y + wHeight - 20);
|
|
ctx.strokeStyle = "#e2e8f0";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
|
|
// Draw QR Code
|
|
if (qrImg.width > 0) {
|
|
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
|
|
}
|
|
|
|
resolve(canvas.toDataURL("image/png"));
|
|
} catch (e) {
|
|
resolve(base64Img); // Error fallback
|
|
}
|
|
})();
|
|
};
|
|
img.onerror = () => resolve(base64Img); // Error fallback
|
|
img.src = base64Img;
|
|
});
|
|
};
|
|
|
|
const handleOpenModal = async () => {
|
|
setIsCapturing(true);
|
|
try {
|
|
const element = document.getElementById(targetId);
|
|
if (element) {
|
|
// Force explicit dimensions to prevent html-to-image from clipping
|
|
const w = element.offsetWidth;
|
|
const h = element.offsetHeight;
|
|
|
|
const rawDataUrl = await htmlToImage.toPng(element, {
|
|
quality: 1,
|
|
type: 'image/png',
|
|
pixelRatio: 2,
|
|
backgroundColor: 'white',
|
|
skipFonts: true,
|
|
width: w,
|
|
height: h,
|
|
style: {
|
|
transform: 'none',
|
|
margin: '0',
|
|
width: `${w}px`,
|
|
height: `${h}px`
|
|
},
|
|
filter: (node) => {
|
|
const el = node as HTMLElement;
|
|
if (el.tagName === 'BUTTON' || el.dataset?.shareButton === 'true' || el.dataset?.shareWrapper === 'true') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// Generate QR Code
|
|
const qrDataUrl = await QRCode.toDataURL(currentUrl, {
|
|
width: 120, // slightly larger for sharper scaling
|
|
margin: 0,
|
|
color: { dark: '#0f172a', light: '#ffffff' }
|
|
});
|
|
|
|
// Add Watermark
|
|
const watermarkedImage = await applyWatermark(rawDataUrl, qrDataUrl);
|
|
setGeneratedImage(watermarkedImage);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to capture component:", err);
|
|
} finally {
|
|
setIsCapturing(false);
|
|
setIsModalOpen(true);
|
|
trackEvent("component_share_opened", {
|
|
component_id: targetId,
|
|
component_title: title
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
<button
|
|
onClick={handleOpenModal}
|
|
disabled={isCapturing}
|
|
data-share-button="true"
|
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-full font-medium text-xs transition-all shadow-sm
|
|
${isCapturing
|
|
? "bg-slate-100 text-slate-400 cursor-wait border border-slate-200"
|
|
: "bg-white text-slate-600 hover:text-slate-900 border border-slate-200 hover:border-slate-300 hover:shadow-md"
|
|
}`}
|
|
title="Diesen Abschnitt als Bild teilen"
|
|
>
|
|
{isCapturing ? (
|
|
<div className="w-3.5 h-3.5 border-2 border-slate-300 border-t-slate-500 rounded-full animate-spin" />
|
|
) : (
|
|
<Share2 size={14} />
|
|
)}
|
|
<span>Teilen</span>
|
|
</button>
|
|
|
|
<ShareModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
url={currentUrl}
|
|
title={title}
|
|
description={shareText}
|
|
diagramImage={generatedImage}
|
|
qrCodeData={generatedImage} // Let's try sending it to both to be safe depending on ShareModal's internals
|
|
/>
|
|
</div>
|
|
);
|
|
};
|