"use client"; import React, { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; export type D3ImageInfoProps = { imageUrl?: string; title?: string; description?: string; width?: number; // fallback width if container size not measured yet height?: number; // fallback height }; /** * D3ImageInfo * - Renders an image and an information box beside it using D3 within an SVG. * - Includes a clickable "show" chip that opens a popup dialog with more details. */ export function D3ImageInfo({ imageUrl = "/placeholder.svg", title = "عنوان آیتم", description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده می‌شود.", width = 800, height = 360, }: D3ImageInfoProps) { const containerRef = useRef(null); const svgRef = useRef(null); const [open, setOpen] = useState(false); // Redraw helper const draw = () => { if (!containerRef.current || !svgRef.current) return; const container = containerRef.current; const svg = d3.select(svgRef.current); const W = Math.max(480, container.clientWidth || width); const H = Math.max(260, height); svg.attr("width", W).attr("height", H); // Clear previous content svg.selectAll("*").remove(); // Layout const padding = 16; const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35)); const infoAreaX = padding + imageAreaWidth + padding; const infoAreaWidth = W - infoAreaX - padding; // Image area (with rounded border) const imgGroup = svg .append("g") .attr("transform", `translate(${padding}, ${padding})`); const imgW = imageAreaWidth; const imgH = H - 2 * padding; // Frame imgGroup .append("rect") .attr("width", imgW) .attr("height", imgH) .attr("rx", 10) .attr("ry", 10) .attr("fill", "#1F2937") // gray-800 .attr("stroke", "#4B5563") // gray-600 .attr("stroke-width", 1.5); // Image imgGroup .append("image") .attr("href", imageUrl) .attr("x", 4) .attr("y", 4) .attr("width", imgW - 8) .attr("height", imgH - 8) .attr("preserveAspectRatio", "xMidYMid slice") .attr("clip-path", null); // Info area const infoGroup = svg .append("g") .attr("transform", `translate(${infoAreaX}, ${padding})`); // Info container infoGroup .append("rect") .attr("width", Math.max(220, infoAreaWidth)) .attr("height", imgH) .attr("rx", 12) .attr("ry", 12) .attr("fill", "url(#infoGradient)") .attr("stroke", "#6B7280") // gray-500 .attr("stroke-width", 1); // Background gradient const defs = svg.append("defs"); const gradient = defs .append("linearGradient") .attr("id", "infoGradient") .attr("x1", "0%") .attr("y1", "0%") .attr("x2", "0%") .attr("y2", "100%"); gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900 gradient .append("stop") .attr("offset", "100%") .attr("stop-color", "#374151"); // gray-700 // Title infoGroup .append("text") .attr("x", 16) .attr("y", 36) .attr("fill", "#F9FAFB") // gray-50 .attr("font-weight", 700) .attr("font-size", 18) .text(title); // Description (wrapped) const wrapText = (text: string, maxWidth: number) => { const words = text.split(/\s+/).reverse(); const lines: string[] = []; let line: string[] = []; let t = ""; while (words.length) { const word = words.pop()!; const test = (t + " " + word).trim(); // Approximate measure using character count const tooLong = test.length * 8 > maxWidth; // 8px avg char width if (tooLong && t.length) { lines.push(t); t = word; } else { t = test; } } if (t) lines.push(t); return lines; }; const descMaxWidth = Math.max(200, infoAreaWidth - 32); const descLines = wrapText(description, descMaxWidth); descLines.forEach((line, i) => { infoGroup .append("text") .attr("x", 16) .attr("y", 70 + i * 22) .attr("fill", "#E5E7EB") // gray-200 .attr("font-size", 14) .text(line); }); // Show button-like chip const chipY = Math.min(imgH - 48, 70 + descLines.length * 22 + 16); const chip = infoGroup .append("g") .attr("class", "show-chip") .style("cursor", "pointer"); const chipW = 120; const chipH = 36; chip .append("rect") .attr("x", 16) .attr("y", chipY) .attr("width", chipW) .attr("height", chipH) .attr("rx", 8) .attr("ry", 8) .attr("fill", "#3B82F6") // blue-500 .attr("stroke", "#60A5FA") .attr("stroke-width", 1.5) .attr("opacity", 0.95); chip .append("text") .attr("x", 16 + chipW / 2) .attr("y", chipY + chipH / 2 + 5) .attr("text-anchor", "middle") .attr("fill", "#FFFFFF") .attr("font-weight", 700) .text("نمایش"); // Hover & click chip .on("mouseenter", function () { d3.select(this).select("rect").attr("fill", "#2563EB"); // blue-600 }) .on("mouseleave", function () { d3.select(this).select("rect").attr("fill", "#3B82F6"); // blue-500 }) .on("click", () => setOpen(true)); }; useEffect(() => { const ro = new ResizeObserver(() => draw()); if (containerRef.current) ro.observe(containerRef.current); draw(); return () => ro.disconnect(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [imageUrl, title, description]); return (
{title} {description}
{title}
); }