mono/packages/ui/src/pages/Post/components/ExportDropdown.tsx
2026-02-08 15:09:32 +01:00

241 lines
9.8 KiB
TypeScript

import React, { useState } from "react";
import { FileText, Download, Code, Mail, Archive, MoreVertical, Share2, Link } from 'lucide-react';
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuLabel
} from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { toast } from "sonner";
import { generateOfflineZip } from "@/utils/zipGenerator";
import { T } from "@/i18n";
import { PostItem, UserProfile } from "../types";
import { Picture } from "@/types-server";
interface ExportDropdownProps {
post: PostItem | null;
mediaItems: Picture[];
authorProfile: UserProfile | null;
onExportMarkdown?: () => void;
className?: string;
}
export const ExportDropdown: React.FC<ExportDropdownProps> = ({
post,
mediaItems,
authorProfile,
onExportMarkdown,
className
}) => {
const [isZipping, setIsZipping] = useState(false);
const [isEmbedDialogOpen, setIsEmbedDialogOpen] = useState(false);
const [isEmailDialogOpen, setIsEmailDialogOpen] = useState(false);
const [emailHtml, setEmailHtml] = useState('');
const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
const embedUrl = `${baseUrl}/embed/${post?.id}`;
const embedCode = `<iframe src="${embedUrl}" width="100%" height="600" frameborder="0"></iframe>`;
const loadEmailHtml = async () => {
if (!post?.id) {
console.error('No post ID available');
setEmailHtml('Error: Post data missing');
return;
}
try {
const res = await fetch(`${baseUrl}/api/render/email/${post.id}`);
if (!res.ok) {
const text = await res.text();
throw new Error(text || res.statusText);
}
const html = await res.text();
setEmailHtml(html);
} catch (e) {
console.error("Failed to load email html", e);
toast.error("Failed to load email template");
setEmailHtml('Error loading template. Please try again later.');
}
};
const handleExportPdf = async () => {
if (!post?.id) return;
try {
// Get current URL and append .pdf extension
const currentUrl = new URL(window.location.href);
const pathWithExtension = currentUrl.pathname + '.pdf';
const exportUrl = `${currentUrl.origin}${pathWithExtension}${currentUrl.search}`;
// Open in new tab to trigger download
window.open(exportUrl, '_blank');
toast.success("PDF export opened");
} catch (e) {
console.error(e);
toast.error("Failed to export PDF");
}
};
const handleDownloadZip = async () => {
if (!post) return;
setIsZipping(true);
toast.info("Generating ZIP...");
try {
await generateOfflineZip(
post,
mediaItems,
authorProfile?.display_name || 'Author'
);
toast.success("ZIP downloaded!");
} catch (e) {
console.error(e);
toast.error("Failed to generate ZIP");
} finally {
setIsZipping(false);
}
};
const handleCopyLink = async () => {
if (!post) return;
const url = window.location.href;
const title = post.title || 'PolyMech Post';
const text = post.description || '';
if (navigator.share && navigator.canShare({ url, title, text })) {
try {
await navigator.share({ url, title, text });
return;
} catch (e) {
// Ignore abort errors
if ((e as Error).name !== 'AbortError') console.error('Share failed', e);
}
}
// Fallback to clipboard
try {
await navigator.clipboard.writeText(url);
toast.success("Link copied to clipboard");
} catch (e) {
console.error('Clipboard failed', e);
toast.error("Failed to copy link");
}
};
return (
<div className={className}>
<Dialog open={isEmbedDialogOpen} onOpenChange={setIsEmbedDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Embed Post</DialogTitle>
<DialogDescription>
Copy the code below to embed this post on your website.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-muted rounded-md relative group">
<code className="text-sm break-all font-mono">{embedCode}</code>
<Button
size="sm"
variant="secondary"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => {
navigator.clipboard.writeText(embedCode);
toast.success("Copied to clipboard");
}}
>
Copy
</Button>
</div>
<div className="text-xs text-muted-foreground">
Preview:
<div className="mt-2 border rounded overflow-hidden aspect-video relative">
<iframe src={embedUrl} width="100%" height="100%" frameBorder="0" className="absolute inset-0" />
</div>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={isEmailDialogOpen} onOpenChange={setIsEmailDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Export as Email</DialogTitle>
<DialogDescription>
Copy the HTML below to use in your email marketing tool.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-muted rounded-md relative group h-64 overflow-y-auto">
<code className="text-xs break-all font-mono whitespace-pre-wrap">{emailHtml || 'Loading...'}</code>
<Button
size="sm"
variant="secondary"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => {
navigator.clipboard.writeText(emailHtml);
toast.success("Copied to clipboard");
}}
>
Copy
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 gap-2 px-2 text-muted-foreground hover:text-primary">
<Download className="h-4 w-4" />
<span className="hidden lg:inline">Export</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Export & Share</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleCopyLink}>
<Link className="h-4 w-4 mr-2" />
<span>Copy Link</span>
</DropdownMenuItem>
{onExportMarkdown && (
<DropdownMenuItem onClick={onExportMarkdown}>
<FileText className="h-4 w-4 mr-2" />
<span>Export Markdown</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleExportPdf}>
<FileText className="h-4 w-4 mr-2" />
<span>Export PDF</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDownloadZip} disabled={isZipping}>
<Archive className="h-4 w-4 mr-2" />
<span>{isZipping ? 'Zipping...' : 'Download ZIP'}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsEmbedDialogOpen(true)}>
<Code className="h-4 w-4 mr-2" />
<span>Embed</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
setIsEmailDialogOpen(true);
loadEmailHtml();
}}>
<Mail className="h-4 w-4 mr-2" />
<span>Export as Email</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};