diff --git a/app/src/components/QuoteSharer.jsx b/app/src/components/QuoteSharer.jsx new file mode 100644 index 0000000..c0b15f4 --- /dev/null +++ b/app/src/components/QuoteSharer.jsx @@ -0,0 +1,335 @@ +// QuoteSharer — renders a floating "share quote" tooltip on text selection inside the article. +// Uses Canvas API for branded card rendering; Web Share API or download as PNG fallback. +import { useEffect, useRef, useState, useCallback } from 'react'; +import { contentApi } from '../lib/content-api.js'; +import shareIcon from '../assets/icons/share-2.svg'; +import clipboardIcon from '../assets/icons/clipboard-copy.svg'; + +const BRAND_BG1 = '#0C1E22'; +const BRAND_BG2 = '#0F6B73'; +const BRAND_GOLD = '#D49A2A'; +const FONT = '\'Vazirmatn\', Tahoma, sans-serif'; + +function loadFont() { + if (typeof document === 'undefined') return Promise.resolve(); + return document.fonts.load(`700 18px Vazirmatn`).catch(() => {}); +} + +function wrapText(ctx, text, x, y, maxWidth, lineHeight) { + const words = text.split(' ').filter(Boolean); + let line = ''; + let currentY = y; + const lines = []; + for (const word of words) { + const test = line ? `${line} ${word}` : word; + const m = ctx.measureText(test); + if (m.width > maxWidth && line) { + lines.push(line); + line = word; + } else { + line = test; + } + } + if (line) lines.push(line); + for (const l of lines.slice(0, 4)) { + ctx.fillText(l, x, currentY); + currentY += lineHeight; + } + return currentY; +} + +function renderCard(canvas, text, articleTitle, authorName) { + const W = 1080, H = 1080; + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + + // Background gradient + const grad = ctx.createLinearGradient(0, 0, W, H); + grad.addColorStop(0, BRAND_BG1); + grad.addColorStop(1, '#0F3540'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + + // Decorative corner accent + ctx.fillStyle = BRAND_BG2; + ctx.beginPath(); + ctx.moveTo(W, 0); + ctx.lineTo(W, 220); + ctx.lineTo(W - 220, 0); + ctx.closePath(); + ctx.fill(); + + // Subtle grid pattern overlay + ctx.strokeStyle = 'rgba(255,255,255,0.03)'; + ctx.lineWidth = 1; + for (let i = 0; i < W; i += 60) { + ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, H); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(W, i); ctx.stroke(); + } + + // Gold separator + ctx.fillStyle = BRAND_GOLD; + ctx.fillRect(80, 90, 5, 80); + + // TEKNAV logo text + ctx.fillStyle = BRAND_GOLD; + ctx.font = `700 32px ${FONT}`; + ctx.textAlign = 'right'; + ctx.direction = 'rtl'; + ctx.fillText('تکناو', W - 60, 80); + + // Tagline next to logo + ctx.fillStyle = 'rgba(212,154,42,0.55)'; + ctx.font = `400 16px ${FONT}`; + ctx.fillText('teknav.ir', W - 60, 106); + + // Quote marks + ctx.fillStyle = 'rgba(212,154,42,0.18)'; + ctx.font = `700 280px ${FONT}`; + ctx.textAlign = 'right'; + ctx.fillText('«', W - 40, 420); + + // Quote text + ctx.fillStyle = '#FFFFFF'; + ctx.font = `400 38px ${FONT}`; + ctx.textAlign = 'right'; + ctx.direction = 'rtl'; + const nextY = wrapText(ctx, text, W - 80, 220, W - 160, 60); + + // Gold line + ctx.fillStyle = BRAND_GOLD; + ctx.fillRect(80, nextY + 20, W - 160, 2); + + // Article title + ctx.fillStyle = 'rgba(255,255,255,0.65)'; + ctx.font = `400 26px ${FONT}`; + ctx.textAlign = 'right'; + const titleTrunc = articleTitle.length > 60 ? `${articleTitle.slice(0, 59)}…` : articleTitle; + ctx.fillText(titleTrunc, W - 80, nextY + 70); + + // Author + ctx.fillStyle = 'rgba(255,255,255,0.45)'; + ctx.font = `400 22px ${FONT}`; + ctx.fillText(authorName, W - 80, H - 60); +} + +export function QuoteSharer({ containerRef, article }) { + const [tooltip, setTooltip] = useState(null); // { x, y, text } + const [modal, setModal] = useState(false); + const [sharing, setSharing] = useState(false); + const [copied, setCopied] = useState(false); + const canvasRef = useRef(null); + const fontReady = useRef(false); + + useEffect(() => { loadFont().then(() => { fontReady.current = true; }); }, []); + + // Close modal on Escape + useEffect(() => { + if (!modal) return; + const onKey = (e) => { if (e.key === 'Escape') { setModal(false); setTooltip(null); } }; + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [modal]); + + const handleSelection = useCallback(() => { + const sel = window.getSelection(); + if (!sel || sel.isCollapsed || !sel.toString().trim()) { + setTooltip(null); + return; + } + const container = containerRef?.current; + if (!container) return; + let node = sel.anchorNode; + while (node && node !== container) node = node.parentNode; + if (!node) { setTooltip(null); return; } + + const text = sel.toString().trim(); + if (text.length < 20 || text.length > 400) { setTooltip(null); return; } + + const range = sel.getRangeAt(0); + const rect = range.getBoundingClientRect(); + setTooltip({ + x: rect.left + rect.width / 2, + y: rect.top + window.scrollY - 48, + text, + }); + }, [containerRef]); + + useEffect(() => { + document.addEventListener('mouseup', handleSelection); + document.addEventListener('touchend', handleSelection); + return () => { + document.removeEventListener('mouseup', handleSelection); + document.removeEventListener('touchend', handleSelection); + }; + }, [handleSelection]); + + const openModal = useCallback(() => { + if (!tooltip) return; + setModal(true); + try { + contentApi.trackEvent({ type: 'share_clicked', metadata: { channel: 'quote_card', slug: article?.slug } }); + } catch {} + requestAnimationFrame(() => { + if (canvasRef.current) { + renderCard( + canvasRef.current, + tooltip.text, + article?.title || 'تکناو', + article?.authorName || 'تکناو', + ); + } + }); + }, [tooltip, article]); + + const download = useCallback(() => { + if (!canvasRef.current) return; + canvasRef.current.toBlob((blob) => { + if (!blob) return; + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'teknav-quote.png'; + a.click(); + URL.revokeObjectURL(url); + }, 'image/png'); + }, []); + + const copyText = useCallback(() => { + if (!tooltip?.text) return; + const fullText = `«${tooltip.text}»\n\n— از مقاله: ${article?.title || 'تکناو'}\nhttps://www.teknav.ir/article/${article?.slug || ''}`; + try { + navigator.clipboard.writeText(fullText).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }).catch(() => {}); + } catch {} + }, [tooltip, article]); + + const share = useCallback(async () => { + if (!canvasRef.current || sharing) return; + setSharing(true); + try { + if (navigator.share && navigator.canShare) { + const blob = await new Promise((res) => canvasRef.current.toBlob(res, 'image/png')); + const file = new File([blob], 'teknav-quote.png', { type: 'image/png' }); + if (navigator.canShare({ files: [file] })) { + await navigator.share({ + files: [file], + title: article?.title || 'تکناو', + url: `https://www.teknav.ir/article/${article?.slug || ''}`, + }); + setSharing(false); + return; + } + } + download(); + } catch {} + setSharing(false); + }, [sharing, download, article]); + + if (!tooltip && !modal) return null; + + return ( + <> + {tooltip && !modal && ( +
Esc برای بستن
+