lifekline / components /share /SharePanel.tsx
xiaobo ren
Rebrand to 姣旇抗: Update brand name, colors (primrose yellow #FFF143, fresh green #98FB98), logo icons, and theme throughout application
339fff1
import React, { useState, useRef } from 'react';
import { Share2, Download, Twitter, Send, Copy, Check, QrCode, X, Image, Eye, Plus, Gift } from 'lucide-react';
import html2canvas from 'html2canvas';
import { QRCodeSVG } from 'qrcode.react';
interface SharePanelProps {
userName?: string;
resultRef: React.RefObject<HTMLDivElement>;
shareUrl?: string;
onShareSuccess?: (points: number) => void;
userId?: string;
analysisId?: string;
}
const SharePanel: React.FC<SharePanelProps> = ({
userName,
resultRef,
shareUrl,
onShareSuccess,
userId,
analysisId
}) => {
const [isOpen, setIsOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [showQR, setShowQR] = useState(false);
const [exporting, setExporting] = useState(false);
const [showRewardNotification, setShowRewardNotification] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [previewImage, setPreviewImage] = useState<string>('');
const [trackingShare, setTrackingShare] = useState<string | null>(null);
const currentUrl = shareUrl || window.location.href;
const shareText = userName
? `查看${userName}的比迹命理报告 - 洞悉命运起伏,预见人生轨迹`
: '比迹 - 结合八字命理与现代数据可视化,洞悉命运起伏';
// Generate unique share ID
const generateShareId = () => {
return `share_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Track share reward
const trackShareReward = async (platform: string) => {
if (!userId || !analysisId) return;
const shareId = generateShareId();
setTrackingShare(platform);
try {
const response = await fetch('/api/share/reward', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
analysisId,
shareId,
platform,
points: 300,
timestamp: new Date().toISOString()
}),
});
if (response.ok) {
setShowRewardNotification(true);
onShareSuccess?.(300);
setTimeout(() => setShowRewardNotification(false), 3000);
}
} catch (error) {
console.error('Failed to track share reward:', error);
} finally {
setTrackingShare(null);
}
};
// Preview image before export/share
const handlePreviewImage = async () => {
if (!resultRef.current) return;
try {
const canvas = await html2canvas(resultRef.current, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false,
});
setPreviewImage(canvas.toDataURL('image/png'));
setShowPreview(true);
} catch (error) {
console.error('Preview failed:', error);
}
};
// 导出为图片
const handleExportImage = async (isXCom: boolean = false) => {
if (!resultRef.current) return;
setExporting(true);
try {
const canvas = await html2canvas(resultRef.current, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false,
width: isXCom ? 1200 : undefined,
height: isXCom ? 630 : undefined,
});
const link = document.createElement('a');
link.download = `${userName || 'LifeKLine'}_Report_${isXCom ? 'Xcom_' : ''}${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
} catch (error) {
console.error('Export failed:', error);
alert('导出图片失败,请尝试使用截图功能');
} finally {
setExporting(false);
}
};
// 分享到 X.com (Twitter)
const handleShareTwitter = async () => {
const url = `https://x.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(currentUrl)}&via=laoshiline`;
window.open(url, '_blank', 'width=550,height=420');
// Track reward after share
await trackShareReward('xcom');
};
// 分享到 Telegram
const handleShareTelegram = async () => {
const url = `https://t.me/share/url?url=${encodeURIComponent(currentUrl)}&text=${encodeURIComponent(shareText)}`;
window.open(url, '_blank', 'width=550,height=420');
// Track reward after share
await trackShareReward('telegram');
};
// 复制链接
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(currentUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
// Track reward after copy
await trackShareReward('copy');
} catch (error) {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = currentUrl;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
// Track reward after copy
await trackShareReward('copy');
}
};
// 显示微信二维码
const handleShowQR = async () => {
setShowQR(true);
setIsOpen(false);
// Track reward for QR share
await trackShareReward('wechat');
};
return (
<>
{/* Share Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-medium text-sm shadow-sm"
>
<Share2 className="w-4 h-4" />
分享
{userId && (
<span className="text-xs bg-white/20 px-2 py-0.5 rounded-full flex items-center gap-1">
<Plus className="w-3 h-3" />
300积分
</span>
)}
</button>
{/* Share Panel Dropdown */}
{isOpen && (
<div className="absolute right-0 top-full mt-2 bg-white rounded-xl shadow-2xl border border-gray-200 py-3 min-w-[240px] z-50 animate-fade-in">
<div className="px-4 pb-2 mb-2 border-b border-gray-100">
<p className="text-sm font-bold text-gray-800">分享此报告</p>
<p className="text-xs text-gray-500">
让更多人了解比迹
{userId && (
<span className="text-green-600 font-medium ml-1">• 获得积分奖励</span>
)}
</p>
</div>
{/* Preview Button */}
<button
onClick={handlePreviewImage}
className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors"
>
<Eye className="w-5 h-5 text-indigo-600" />
<span>预览图片</span>
</button>
{/* Export Options */}
<div className="px-4 py-1">
<p className="text-xs text-gray-500 font-medium mb-2">导出图片</p>
<div className="space-y-1">
{/* Standard Export */}
<button
onClick={() => handleExportImage(false)}
disabled={exporting}
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50 rounded-lg"
>
{exporting ? (
<div className="w-5 h-5 border-2 border-indigo-600 border-t-transparent rounded-full animate-spin" />
) : (
<Image className="w-5 h-5 text-green-600" />
)}
<span>{exporting ? '正在生成...' : '标准尺寸'}</span>
</button>
{/* X.com Export */}
<button
onClick={() => handleExportImage(true)}
disabled={exporting}
className="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50 rounded-lg"
>
{exporting ? (
<div className="w-5 h-5 border-2 border-indigo-600 border-t-transparent rounded-full animate-spin" />
) : (
<Twitter className="w-5 h-5 text-sky-500" />
)}
<span>{exporting ? '正在生成...' : 'X.com优化 (1200×630)'}</span>
</button>
</div>
</div>
{/* 分享到 X.com */}
<button
onClick={handleShareTwitter}
disabled={trackingShare === 'xcom'}
className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50"
>
{trackingShare === 'xcom' ? (
<div className="w-5 h-5 border-2 border-sky-500 border-t-transparent rounded-full animate-spin" />
) : (
<Twitter className="w-5 h-5 text-sky-500" />
)}
<span className="flex-1">分享到 X.com</span>
{userId && !trackingShare && (
<span className="text-xs text-green-600 flex items-center gap-1">
<Plus className="w-3 h-3" />
300
</span>
)}
</button>
{/* 分享到 Telegram */}
<button
onClick={handleShareTelegram}
disabled={trackingShare === 'telegram'}
className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50"
>
{trackingShare === 'telegram' ? (
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
) : (
<Send className="w-5 h-5 text-blue-500" />
)}
<span className="flex-1">分享到 Telegram</span>
{userId && !trackingShare && (
<span className="text-xs text-green-600 flex items-center gap-1">
<Plus className="w-3 h-3" />
300
</span>
)}
</button>
{/* 微信二维码 */}
<button
onClick={handleShowQR}
disabled={trackingShare === 'wechat'}
className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50"
>
{trackingShare === 'wechat' ? (
<div className="w-5 h-5 border-2 border-green-500 border-t-transparent rounded-full animate-spin" />
) : (
<QrCode className="w-5 h-5 text-green-500" />
)}
<span className="flex-1">微信扫码分享</span>
{userId && !trackingShare && (
<span className="text-xs text-green-600 flex items-center gap-1">
<Plus className="w-3 h-3" />
300
</span>
)}
</button>
<div className="my-2 border-t border-gray-100" />
{/* 复制链接 */}
<button
onClick={handleCopyLink}
disabled={trackingShare === 'copy'}
className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3 transition-colors disabled:opacity-50"
>
{trackingShare === 'copy' ? (
<div className="w-5 h-5 border-2 border-gray-500 border-t-transparent rounded-full animate-spin" />
) : copied ? (
<Check className="w-5 h-5 text-green-600" />
) : (
<Copy className="w-5 h-5 text-gray-500" />
)}
<span className="flex-1">{copied ? '已复制!' : '复制链接'}</span>
{userId && !trackingShare && !copied && (
<span className="text-xs text-green-600 flex items-center gap-1">
<Plus className="w-3 h-3" />
300
</span>
)}
</button>
</div>
)}
{/* QR Code Modal */}
{showQR && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl p-6 mx-4 max-w-sm w-full animate-fade-in">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-gray-800">微信扫码分享</h3>
<button
onClick={() => setShowQR(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex justify-center p-6 bg-gray-50 rounded-xl">
<QRCodeSVG
value={currentUrl}
size={180}
level="M"
includeMargin
bgColor="#ffffff"
fgColor="#000000"
/>
</div>
<p className="text-center text-sm text-gray-500 mt-4">
使用微信扫描二维码分享给好友
</p>
</div>
</div>
)}
{/* Reward Notification */}
{showRewardNotification && (
<div className="fixed top-4 right-4 z-50 animate-slide-in">
<div className="bg-white rounded-lg shadow-lg border border-green-200 px-4 py-3 flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full flex items-center justify-center">
<Gift className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-bold text-gray-800">🎉 获得300积分奖励!</p>
<p className="text-sm text-gray-600">感谢您的分享</p>
</div>
</div>
</div>
)}
{/* Preview Modal */}
{showPreview && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-4xl max-h-[90vh] overflow-auto animate-fade-in">
<div className="sticky top-0 bg-white border-b border-gray-200 p-4 flex justify-between items-center">
<h3 className="text-lg font-bold text-gray-800">图片预览</h3>
<button
onClick={() => setShowPreview(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4">
<img
src={previewImage}
alt="Preview"
className="w-full h-auto rounded-lg shadow-sm"
/>
</div>
<div className="sticky bottom-0 bg-white border-t border-gray-200 p-4 flex gap-2">
<button
onClick={() => {
const link = document.createElement('a');
link.download = `${userName || 'LifeKLine'}_Report_${Date.now()}.png`;
link.href = previewImage;
link.click();
}}
className="flex-1 px-4 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-medium text-sm flex items-center justify-center gap-2"
>
<Download className="w-4 h-4" />
下载图片
</button>
<button
onClick={() => setShowPreview(false)}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all font-medium text-sm"
>
关闭
</button>
</div>
</div>
</div>
)}
{/* Click outside to close */}
{isOpen && (
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
)}
</>
);
};
export default SharePanel;