ppt-tool/frontend/app/(presentation-generator)/presentation/components/Header.tsx
shubham.goyal@brandtech.plus d5bb04a837 fix download path
2026-05-17 20:35:21 +05:30

303 lines
9.3 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import {
SquareArrowOutUpRight,
Play,
Loader2,
Redo2,
Undo2,
FileCode2,
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
import { useRouter } from "next/navigation";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { apiFetch, apiUrl } from "@/lib/apiFetch";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { toast } from "sonner";
import Announcement from "@/components/Announcement";
import { PptxPresentationModel } from "@/types/pptx_models";
import HeaderNav from "../../components/HeaderNab";
import ReviewWorkflow from "../../components/ReviewWorkflow";
import PDFIMAGE from "@/public/pdf.svg";
import PPTXIMAGE from "@/public/pptx.svg";
import Image from "next/image";
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
import ToolTip from "@/components/ToolTip";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { clearHistory } from "@/store/slices/undoRedoSlice";
import Logo from "@/components/Logo";
import { TemplateCodegenExport } from "../../components/TemplateCodegenExport";
const Header = ({
presentation_id,
currentSlide,
}: {
presentation_id: string;
currentSlide?: number;
}) => {
const [open, setOpen] = useState(false);
const [showLoader, setShowLoader] = useState(false);
const [showTemplateCodegen, setShowTemplateCodegen] = useState(false);
const router = useRouter();
const dispatch = useDispatch();
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const { onUndo, onRedo, canUndo, canRedo } = usePresentationUndoRedo();
const get_presentation_pptx_model = async (id: string): Promise<PptxPresentationModel> => {
const response = await apiFetch(`/api/presentation_to_pptx_model?id=${id}`);
const pptx_model = await response.json();
return pptx_model;
};
const handleExportPptx = async () => {
if (isStreaming) return;
try {
setOpen(false);
setShowLoader(true);
// Save the presentation data before exporting
await PresentationGenerationApi.updatePresentationContent(presentationData);
const pptx_model = await get_presentation_pptx_model(presentation_id);
if (!pptx_model) {
throw new Error("Failed to get presentation PPTX model");
}
const pptx_path = await PresentationGenerationApi.exportAsPPTX(pptx_model);
if (pptx_path) {
// window.open(pptx_path, '_self');
downloadLink(pptx_path);
} else {
throw new Error("No path returned from export");
}
} catch (error) {
console.error("Export failed:", error);
setShowLoader(false);
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
});
} finally {
setShowLoader(false);
}
};
const handleExportPdf = async () => {
if (isStreaming) return;
try {
setOpen(false);
setShowLoader(true);
// Save the presentation data before exporting
await PresentationGenerationApi.updatePresentationContent(presentationData);
const response = await apiFetch('/api/export-as-pdf', {
method: 'POST',
body: JSON.stringify({
id: presentation_id,
title: presentationData?.title,
})
});
if (response.ok) {
const { path: pdfPath } = await response.json();
// window.open(pdfPath, '_blank');
downloadLink(pdfPath);
} else {
throw new Error("Failed to export PDF");
}
} catch (err) {
console.error(err);
toast.error("Having trouble exporting!", {
description:
"We are having trouble exporting your presentation. Please try again.",
});
} finally {
setShowLoader(false);
}
};
const handleReGenerate = () => {
dispatch(clearPresentationData());
dispatch(clearHistory())
router.push(`/presentation?id=${presentation_id}&stream=true`);
};
const downloadLink = (path: string) => {
const url = apiUrl(path);
// if we have popup access give direct download if not redirect to the path
if (window.opener) {
window.open(url, '_blank');
} else {
const link = document.createElement('a');
link.href = url;
link.download = url.split('/').pop() || 'download';
document.body.appendChild(link);
link.click();
}
};
const ExportOptions = ({ mobile }: { mobile: boolean }) => (
<div className={`space-y-2 max-md:mt-4 ${mobile ? "" : "bg-white"} rounded-lg`}>
<Button
onClick={() => {
handleExportPdf();
}}
variant="ghost"
className={`pb-4 border-b rounded-none border-gray-300 w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6 border-none rounded-lg" : ""}`} >
<Image src={PDFIMAGE} alt="pdf export" width={30} height={30} />
Export as PDF
</Button>
<Button
onClick={() => {
handleExportPptx();
}}
variant="ghost"
className={`pb-4 border-b rounded-none border-gray-300 w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6 border-none rounded-lg" : ""}`}
>
<Image src={PPTXIMAGE} alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
<Button
onClick={() => {
setOpen(false);
setShowTemplateCodegen(true);
}}
variant="ghost"
className={`w-full flex justify-start text-[#5146E5] ${mobile ? "bg-white py-6" : ""}`}
>
<FileCode2 className="w-7 h-7 mr-2" />
Export from Template
</Button>
</div>
);
const MenuItems = ({ mobile }: { mobile: boolean }) => (
<div className="flex flex-col lg:flex-row items-center gap-4">
{/* undo redo */}
<button onClick={handleReGenerate} disabled={isStreaming || !presentationData} className="text-white disabled:opacity-50" >
Re-Generate
</button>
<div className="flex items-center gap-2 ">
<ToolTip content="Undo">
<button disabled={!canUndo} className="text-white disabled:opacity-50" onClick={() => {
onUndo();
}}>
<Undo2 className="w-6 h-6 " />
</button>
</ToolTip>
<ToolTip content="Redo">
<button disabled={!canRedo} className="text-white disabled:opacity-50" onClick={() => {
onRedo();
}}>
<Redo2 className="w-6 h-6 " />
</button>
</ToolTip>
</div>
{/* Review Status */}
<ReviewWorkflow presentationId={presentation_id} />
{/* Present Button */}
<Button
onClick={() => {
const to = `?id=${presentation_id}&mode=present&slide=${currentSlide || 0}`;
router.push(to);
}}
variant="ghost"
className="border border-white font-bold text-white rounded-[32px] transition-all duration-300 group"
>
<Play className="w-4 h-4 mr-1 stroke-white group-hover:stroke-black" />
Present
</Button>
{/* Desktop Export Button with Popover */}
<div style={{
zIndex: 100
}} className="hidden lg:block relative ">
<Popover open={open} onOpenChange={setOpen} >
<PopoverTrigger asChild>
<Button className={`border py-5 text-[#5146E5] font-bold rounded-[32px] transition-all duration-500 hover:border hover:bg-[#5146E5] hover:text-white w-full ${mobile ? "" : "bg-white"}`}>
<SquareArrowOutUpRight className="w-4 h-4 mr-1" />
Export
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-[250px] space-y-2 py-3 px-2 ">
<ExportOptions mobile={false} />
</PopoverContent>
</Popover>
</div>
{/* Mobile Export Section */}
<div className="lg:hidden flex flex-col w-full">
<ExportOptions mobile={true} />
</div>
</div>
);
return (
<>
<OverlayLoader
show={showLoader}
text="Exporting presentation..."
showProgress={true}
duration={40}
/>
<div
className="bg-[#5146E5] w-full shadow-lg sticky top-0 ">
<Announcement />
<Wrapper className="flex items-center justify-between py-1">
<Logo variant="light" className="min-w-[162px]" />
{/* Desktop Menu */}
<div className="hidden lg:flex items-center gap-4 2xl:gap-6">
{isStreaming && (
<Loader2 className="animate-spin text-white font-bold w-6 h-6" />
)}
<MenuItems mobile={false} />
<HeaderNav />
</div>
{/* Mobile Menu */}
<div className="lg:hidden flex items-center gap-4">
<HeaderNav />
</div>
</Wrapper>
</div>
{showTemplateCodegen && (
<TemplateCodegenExport
presentationId={presentation_id}
onClose={() => setShowTemplateCodegen(false)}
/>
)}
</>
);
};
export default Header;