feat(Nextjs): Custom Layout Font upload and apply

This commit is contained in:
shiva raj badu 2025-08-02 16:04:48 +05:45
parent b2439bba1f
commit 1d3f3d740c
No known key found for this signature in database
5 changed files with 232 additions and 418 deletions

View file

@ -67,53 +67,6 @@ const EachSlide = ({
}
}
}, [slide.processed, slide.html]);
// Load Google Fonts
useEffect(() => {
if (slide.fonts?.internally_supported_fonts) {
slide.fonts.internally_supported_fonts.forEach((font: any) => {
// Check if font link already exists
const existingFont = document.querySelector(
`link[href="${font.google_fonts_url}"]`
);
// Only add if font doesn't already exist
if (!existingFont) {
const link = document.createElement("link");
link.href = font.google_fonts_url;
link.rel = "stylesheet";
document.head.appendChild(link);
}
});
}
}, [slide.fonts]);
// Load uploaded fonts
useEffect(() => {
if (slide.uploaded_fonts && slide.uploaded_fonts.length > 0) {
slide.uploaded_fonts.forEach((fontUrl: string) => {
// Check if font style already exists
const existingStyle = document.querySelector(
`style[data-font-url="${fontUrl}"]`
);
if (!existingStyle) {
const style = document.createElement("style");
style.setAttribute("data-font-url", fontUrl);
// Extract font name from URL for font-family
const fontName =
fontUrl.split("/").pop()?.split(".")[0] || "CustomFont";
style.textContent = `
@font-face {
font-family: '${fontName}';
src: url('${fontUrl}') format('truetype');
font-display: swap;
}
`;
document.head.appendChild(style);
}
});
}
}, [slide.uploaded_fonts]);
// Set up canvas when entering edit mode
useEffect(() => {

View file

@ -1,8 +1,14 @@
import React, { useState, useEffect } from "react";
import React, { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle, AlertCircle, X } from "lucide-react";
import { toast } from "sonner";
import {
Upload,
CheckCircle,
AlertCircle,
X,
Loader2,
Type,
} from "lucide-react";
interface UploadedFont {
fontName: string;
@ -10,75 +16,99 @@ interface UploadedFont {
fontPath: string;
}
interface FontData {
internally_supported_fonts: {
name: string;
google_fonts_url: string;
}[];
not_supported_fonts: string[];
}
interface FontManagerProps {
slide: any;
onFontsUpdate: (updatedFonts: string[]) => void;
globalUploadedFonts: UploadedFont[];
fontsData: FontData;
UploadedFonts: UploadedFont[];
uploadFont: (fontName: string, file: File) => Promise<string | null>;
removeFont: (fontUrl: string) => void;
getAllUnsupportedFonts: () => string[];
}
const FontManager: React.FC<FontManagerProps> = ({
slide,
onFontsUpdate,
globalUploadedFonts,
fontsData,
UploadedFonts,
uploadFont,
removeFont,
getAllUnsupportedFonts,
}) => {
const [slideSpecificFonts, setSlideSpecificFonts] = useState<string[]>(
slide.uploaded_fonts || []
const [uploadingFonts, setUploadingFonts] = useState<Set<string>>(new Set());
const fileInputRefs = useRef<{ [key: string]: HTMLInputElement | null }>({});
const allUnsupportedFonts = getAllUnsupportedFonts();
// Filter out fonts that are already uploaded
const fontsNeedingUpload = allUnsupportedFonts.filter(
(fontName) =>
!UploadedFonts.some((uploadedFont) => uploadedFont.fontName === fontName)
);
// Update slide-specific fonts when slide changes
useEffect(() => {
setSlideSpecificFonts(slide.uploaded_fonts || []);
}, [slide.uploaded_fonts]);
const handleFontUpload = async (fontName: string, file: File) => {
if (!file) return;
const addGlobalFontToSlide = (fontUrl: string) => {
if (!slideSpecificFonts.includes(fontUrl)) {
const updatedFonts = [...slideSpecificFonts, fontUrl];
setSlideSpecificFonts(updatedFonts);
onFontsUpdate(updatedFonts);
toast.success("Font added to slide");
setUploadingFonts((prev) => new Set(prev).add(fontName));
try {
const fontUrl = await uploadFont(fontName, file);
if (fontUrl) {
// Clear the file input
if (fileInputRefs.current[fontName]) {
fileInputRefs.current[fontName]!.value = "";
}
}
} finally {
setUploadingFonts((prev) => {
const newSet = new Set(prev);
newSet.delete(fontName);
return newSet;
});
}
};
const removeSlideFont = (fontUrl: string) => {
const updatedFonts = slideSpecificFonts.filter((url) => url !== fontUrl);
setSlideSpecificFonts(updatedFonts);
onFontsUpdate(updatedFonts);
toast.info("Font removed from slide");
const handleFileInputChange = (
fontName: string,
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
handleFontUpload(fontName, file);
}
};
const getFontNameFromUrl = (url: string) => {
return url.split("/").pop()?.split(".")[0] || "Custom Font";
};
if (!slide.fonts) {
if (allUnsupportedFonts.length === 0 && UploadedFonts.length === 0) {
return null;
}
const { internally_supported_fonts = [], not_supported_fonts = [] } =
slide.fonts;
// Get fonts that are globally uploaded but not in this slide
const availableGlobalFonts = globalUploadedFonts.filter(
(font) => !slideSpecificFonts.includes(font.fontUrl)
);
return (
<Card className="mt-4">
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<span>Slide Font Management</span>
<CardTitle className="text-xl flex items-center gap-2">
<Type className="w-6 h-6" />
Global Font Management
</CardTitle>
<p className="text-sm text-gray-600">
Manage fonts across all slides. Upload fonts once and they'll be
available for all slides.
</p>
</CardHeader>
<CardContent className="space-y-4">
<CardContent className="space-y-6">
{/* Supported Fonts */}
{internally_supported_fonts.length > 0 && (
{fontsData.internally_supported_fonts.length > 0 && (
<div>
<h4 className="text-sm font-medium text-green-700 mb-2 flex items-center gap-1">
<h4 className="text-sm font-medium text-green-700 mb-3 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Supported Fonts ({internally_supported_fonts.length})
Supported Fonts ({fontsData.internally_supported_fonts.length})
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{internally_supported_fonts.map((font: any, index: number) => (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{fontsData.internally_supported_fonts.map((font, index) => (
<div
key={index}
className="p-2 bg-green-50 border border-green-200 rounded text-sm text-green-800"
@ -90,30 +120,97 @@ const FontManager: React.FC<FontManagerProps> = ({
</div>
)}
{/* Unsupported Fonts - only show if they're not globally uploaded */}
{not_supported_fonts.filter(
(fontName: string) =>
!globalUploadedFonts.some((gf) => gf.fontName === fontName)
).length > 0 && (
{/* Fonts Needing Upload */}
{fontsNeedingUpload.length > 0 && (
<div>
<h4 className="text-sm font-medium text-orange-700 mb-2 flex items-center gap-1">
<h4 className="text-sm font-medium text-orange-700 mb-3 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Missing Fonts (Upload in Global Font Manager above)
Fonts Needing Upload ({fontsNeedingUpload.length})
</h4>
<div className="space-y-3">
{fontsNeedingUpload.map((fontName: string, index: number) => (
<div
key={index}
className="p-4 bg-orange-50 border border-orange-200 rounded-lg"
>
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-orange-800">
{fontName}
</span>
<p className="text-xs text-orange-600 mt-1">
Required for presentation
</p>
</div>
<div className="flex items-center gap-2">
<input
ref={(el) => {
fileInputRefs.current[fontName] = el;
}}
type="file"
accept=".ttf,.otf,.woff,.woff2,.eot"
onChange={(e) => handleFileInputChange(fontName, e)}
className="hidden"
id={`global-font-upload-${index}`}
/>
<Button
size="sm"
variant="outline"
disabled={uploadingFonts.has(fontName)}
onClick={() => fileInputRefs.current[fontName]?.click()}
className="text-xs bg-blue-600 text-white hover:bg-blue-700 border-blue-600"
>
{uploadingFonts.has(fontName) ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="w-3 h-3 mr-1" />
Upload Font
</>
)}
</Button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Successfully Uploaded Fonts */}
{UploadedFonts.length > 0 && (
<div>
<h4 className="text-sm font-medium text-green-700 mb-3 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Uploaded Fonts ({UploadedFonts.length})
</h4>
<div className="space-y-2">
{not_supported_fonts
.filter(
(fontName: string) =>
!globalUploadedFonts.some((gf) => gf.fontName === fontName)
)
.map((fontName: string, index: number) => (
<div
key={index}
className="p-2 bg-orange-50 border border-orange-200 rounded text-sm text-orange-800"
>
{fontName} - Please upload in Global Font Manager
{UploadedFonts.map((font, index) => (
<div
key={index}
className="p-3 bg-green-50 border border-green-200 rounded-lg flex items-center justify-between"
>
<div>
<span className="text-sm font-medium text-green-800">
{font.fontName}
</span>
<p className="text-xs text-green-600 mt-1">
Available for all slides
</p>
</div>
))}
<Button
size="sm"
variant="ghost"
onClick={() => removeFont(font.fontUrl)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-1"
>
<X className="w-3 h-3" />
</Button>
</div>
))}
</div>
</div>
)}

View file

@ -1,237 +0,0 @@
import React, { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Upload,
CheckCircle,
AlertCircle,
X,
Loader2,
Type,
} from "lucide-react";
interface UploadedFont {
fontName: string;
fontUrl: string;
fontPath: string;
}
interface GlobalFontManagerProps {
slides: any[];
globalUploadedFonts: UploadedFont[];
uploadFont: (fontName: string, file: File) => Promise<string | null>;
removeFont: (fontUrl: string) => void;
getAllUnsupportedFonts: (slides: any[]) => string[];
}
const GlobalFontManager: React.FC<GlobalFontManagerProps> = ({
slides,
globalUploadedFonts,
uploadFont,
removeFont,
getAllUnsupportedFonts,
}) => {
const [uploadingFonts, setUploadingFonts] = useState<Set<string>>(new Set());
const fileInputRefs = useRef<{ [key: string]: HTMLInputElement | null }>({});
const allUnsupportedFonts = getAllUnsupportedFonts(slides);
// Filter out fonts that are already uploaded
const fontsNeedingUpload = allUnsupportedFonts.filter(
(fontName) =>
!globalUploadedFonts.some(
(uploadedFont) => uploadedFont.fontName === fontName
)
);
const handleFontUpload = async (fontName: string, file: File) => {
if (!file) return;
setUploadingFonts((prev) => new Set(prev).add(fontName));
try {
const fontUrl = await uploadFont(fontName, file);
if (fontUrl) {
// Clear the file input
if (fileInputRefs.current[fontName]) {
fileInputRefs.current[fontName]!.value = "";
}
}
} finally {
setUploadingFonts((prev) => {
const newSet = new Set(prev);
newSet.delete(fontName);
return newSet;
});
}
};
const handleFileInputChange = (
fontName: string,
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
handleFontUpload(fontName, file);
}
};
const getFontUsageCount = (fontName: string): number => {
return slides.filter((slide) =>
slide.fonts?.not_supported_fonts?.includes(fontName)
).length;
};
if (allUnsupportedFonts.length === 0 && globalUploadedFonts.length === 0) {
return null;
}
return (
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<Type className="w-6 h-6" />
Global Font Management
</CardTitle>
<p className="text-sm text-gray-600">
Manage fonts across all slides. Upload fonts once and they'll be
available for all slides.
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Fonts Needing Upload */}
{fontsNeedingUpload.length > 0 && (
<div>
<h4 className="text-sm font-medium text-orange-700 mb-3 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Fonts Needing Upload ({fontsNeedingUpload.length})
</h4>
<div className="space-y-3">
{fontsNeedingUpload.map((fontName: string, index: number) => (
<div
key={index}
className="p-4 bg-orange-50 border border-orange-200 rounded-lg"
>
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-orange-800">
{fontName}
</span>
<p className="text-xs text-orange-600 mt-1">
Used in {getFontUsageCount(fontName)} slide
{getFontUsageCount(fontName) !== 1 ? "s" : ""}
</p>
</div>
<div className="flex items-center gap-2">
<input
ref={(el) => {
fileInputRefs.current[fontName] = el;
}}
type="file"
accept=".ttf,.otf,.woff,.woff2,.eot"
onChange={(e) => handleFileInputChange(fontName, e)}
className="hidden"
id={`global-font-upload-${index}`}
/>
<Button
size="sm"
variant="outline"
disabled={uploadingFonts.has(fontName)}
onClick={() => fileInputRefs.current[fontName]?.click()}
className="text-xs bg-blue-600 text-white hover:bg-blue-700 border-blue-600"
>
{uploadingFonts.has(fontName) ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="w-3 h-3 mr-1" />
Upload Font
</>
)}
</Button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Successfully Uploaded Fonts */}
{globalUploadedFonts.length > 0 && (
<div>
<h4 className="text-sm font-medium text-green-700 mb-3 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Uploaded Fonts ({globalUploadedFonts.length})
</h4>
<div className="space-y-2">
{globalUploadedFonts.map((font, index) => {
const usageCount = getFontUsageCount(font.fontName);
return (
<div
key={index}
className="p-3 bg-green-50 border border-green-200 rounded-lg flex items-center justify-between"
>
<div>
<span className="text-sm font-medium text-green-800">
{font.fontName}
</span>
<p className="text-xs text-green-600 mt-1">
{usageCount > 0 ? (
<>
Used in {usageCount} slide
{usageCount !== 1 ? "s" : ""}
</>
) : (
<>Available for use</>
)}
</p>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeFont(font.fontUrl)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-1"
>
<X className="w-3 h-3" />
</Button>
</div>
);
})}
</div>
</div>
)}
{/* Summary */}
<div className="pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">
{allUnsupportedFonts.length}
</p>
<p className="text-xs text-gray-600">Total Unique Fonts</p>
</div>
<div className="p-3 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-700">
{globalUploadedFonts.length}
</p>
<p className="text-xs text-green-600">Fonts Uploaded</p>
</div>
<div className="p-3 bg-orange-50 rounded-lg">
<p className="text-2xl font-bold text-orange-700">
{fontsNeedingUpload.length}
</p>
<p className="text-xs text-orange-600">Fonts Needed</p>
</div>
</div>
</div>
</CardContent>
</Card>
);
};
export default GlobalFontManager;

View file

@ -16,7 +16,7 @@ import { Upload, FileText, X, Loader2 } from "lucide-react";
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
import { v4 as uuidv4 } from "uuid";
import EachSlide from "./components/EachSlide";
import GlobalFontManager from "./components/GlobalFontManager";
import FontManager from "./components/FontManager";
import Header from "@/components/Header";
// Types
@ -34,13 +34,6 @@ interface UploadedFont {
interface ProcessedSlide extends SlideData {
html?: string;
fonts: {
internally_supported_fonts: {
name: string;
google_fonts_url: string;
}[];
not_supported_fonts: string[];
};
uploaded_fonts?: string[];
processing?: boolean;
processed?: boolean;
@ -48,6 +41,14 @@ interface ProcessedSlide extends SlideData {
modified?: boolean; // Added for unsaved changes
}
interface FontData {
internally_supported_fonts: {
name: string;
google_fonts_url: string;
}[];
not_supported_fonts: string[];
}
const CustomLayoutPage = () => {
// State management
const [selectedFile, setSelectedFile] = useState<File | null>(null);
@ -55,15 +56,14 @@ const CustomLayoutPage = () => {
const [slides, setSlides] = useState<ProcessedSlide[]>([]);
const [isSavingLayout, setIsSavingLayout] = useState(false);
const [isLayoutSaved, setIsLayoutSaved] = useState(false);
const [globalUploadedFonts, setGlobalUploadedFonts] = useState<
UploadedFont[]
>([]);
const [UploadedFonts, setUploadedFonts] = useState<UploadedFont[]>([]);
const [fontsData, setFontsData] = useState<FontData | null>(null);
console.log(slides);
// Load uploaded fonts dynamically
useEffect(() => {
globalUploadedFonts.forEach((font) => {
UploadedFonts.forEach((font) => {
// Check if font style already exists
const existingStyle = document.querySelector(
`style[data-font-url="${font.fontUrl}"]`
@ -83,15 +83,43 @@ const CustomLayoutPage = () => {
document.head.appendChild(style);
}
});
}, [globalUploadedFonts]);
}, [UploadedFonts]);
// Load Google Fonts from fontsData
useEffect(() => {
if (fontsData?.internally_supported_fonts) {
fontsData.internally_supported_fonts.forEach((font) => {
// Check if font link already exists
const existingFont = document.querySelector(
`link[href="${font.google_fonts_url}"]`
);
// Only add if font doesn't already exist
if (!existingFont) {
const link = document.createElement("link");
link.href = font.google_fonts_url;
link.rel = "stylesheet";
document.head.appendChild(link);
}
});
}
}, [fontsData]);
// Warning before page unload
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
return "You have unsaved changes. Are you sure you want to leave?";
};
if (slides.length > 0 && !isLayoutSaved) {
window.addEventListener("beforeunload", handleBeforeUnload);
}
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [slides, isLayoutSaved]);
// Font management functions
const uploadFont = useCallback(
async (fontName: string, file: File): Promise<string | null> => {
// Check if font is already uploaded
const existingFont = globalUploadedFonts.find(
(f) => f.fontName === fontName
);
const existingFont = UploadedFonts.find((f) => f.fontName === fontName);
if (existingFont) {
toast.info(`Font "${fontName}" is already uploaded`);
return existingFont.fontUrl;
@ -139,7 +167,7 @@ const CustomLayoutPage = () => {
fontPath: data.font_path,
};
setGlobalUploadedFonts((prev) => [...prev, newFont]);
setUploadedFonts((prev) => [...prev, newFont]);
toast.success(`Font "${fontName}" uploaded successfully`);
return newFont.fontUrl;
} else {
@ -156,13 +184,11 @@ const CustomLayoutPage = () => {
return null;
}
},
[globalUploadedFonts]
[UploadedFonts]
);
const removeGlobalFont = useCallback((fontUrl: string) => {
setGlobalUploadedFonts((prev) =>
prev.filter((font) => font.fontUrl !== fontUrl)
);
const removeFont = useCallback((fontUrl: string) => {
setUploadedFonts((prev) => prev.filter((font) => font.fontUrl !== fontUrl));
// Remove the style element for this font
const styleElement = document.querySelector(
@ -175,34 +201,12 @@ const CustomLayoutPage = () => {
toast.info("Font removed globally");
}, []);
const getAllUnsupportedFonts = useCallback(
(slides: ProcessedSlide[]): string[] => {
const allUnsupportedFonts = new Set<string>();
slides.forEach((slide) => {
if (slide.fonts?.not_supported_fonts) {
slide.fonts.not_supported_fonts.forEach((fontName: string) => {
allUnsupportedFonts.add(fontName);
});
}
});
return Array.from(allUnsupportedFonts);
},
[]
);
// Warning before page unload
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
return "You have unsaved changes. Are you sure you want to leave?";
};
if (slides.length > 0 && !isLayoutSaved) {
window.addEventListener("beforeunload", handleBeforeUnload);
const getAllUnsupportedFonts = useCallback((): string[] => {
if (!fontsData?.not_supported_fonts) {
return [];
}
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [slides, isLayoutSaved]);
return fontsData.not_supported_fonts;
}, [fontsData]);
// Save layout functionality
const saveLayout = useCallback(async () => {
@ -219,7 +223,8 @@ const CustomLayoutPage = () => {
const presentationId = uuidv4();
// Get all uploaded font URLs
const globalFontUrls = globalUploadedFonts.map((font) => font.fontUrl);
const FontUrls = UploadedFonts.map((font) => font.fontUrl);
console.log("FontUrls", FontUrls);
for (let i = 0; i < slides.length; i++) {
const slide = slides[i];
@ -245,21 +250,12 @@ const CustomLayoutPage = () => {
`Failed to convert slide ${slide.slide_number} to React`
);
// Combine global fonts with slide-specific uploaded fonts
const slideFonts = [
...globalFontUrls,
...(slide.uploaded_fonts || []),
];
// Remove duplicates
const uniqueFonts = Array.from(new Set(slideFonts));
reactComponents.push({
presentation_id: presentationId,
layout_id: `${slide.slide_number}`,
layout_name: `Slide${slide.slide_number}`,
layout_code: data.react_component || data.component_code,
fonts: uniqueFonts,
fonts: FontUrls,
});
// Update progress
@ -327,7 +323,7 @@ const CustomLayoutPage = () => {
} finally {
setIsSavingLayout(false);
}
}, [slides, globalUploadedFonts]);
}, [slides, UploadedFonts]);
// File upload handler
const handleFileSelect = useCallback(
@ -403,7 +399,6 @@ const CustomLayoutPage = () => {
processing: false,
processed: true,
html: htmlData.html,
fonts: htmlData.fonts,
}
: s
);
@ -491,6 +486,11 @@ const CustomLayoutPage = () => {
throw new Error("No slides found in the PPTX file");
}
// Extract fonts data from the response
if (pptxData.fonts) {
setFontsData(pptxData.fonts);
}
// const pptxData = processData;
// Initialize slides with skeleton state
const initialSlides: ProcessedSlide[] = pptxData.slides.map(
@ -578,12 +578,12 @@ const CustomLayoutPage = () => {
</div>
{/* Global Font Management */}
{slides.length > 0 && (
<GlobalFontManager
slides={slides}
globalUploadedFonts={globalUploadedFonts}
{fontsData && (
<FontManager
fontsData={fontsData}
UploadedFonts={UploadedFonts}
uploadFont={uploadFont}
removeFont={removeGlobalFont}
removeFont={removeFont}
getAllUnsupportedFonts={getAllUnsupportedFonts}
/>
)}

File diff suppressed because one or more lines are too long