fix(nextjs): TextEditor Not applying styles issue

This commit is contained in:
shiva raj badu 2025-08-02 23:12:12 +05:45
parent d39b1aec5c
commit 04a67014eb
No known key found for this signature in database
11 changed files with 278 additions and 210 deletions

View file

@ -1,128 +1,140 @@
"use client";
import React, { useEffect } from 'react';
import React, { useEffect } from "react";
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import Underline from "@tiptap/extension-underline";
import {
Bold,
Italic,
Underline as UnderlinedIcon,
Strikethrough,
Code,
Bold,
Italic,
Underline as UnderlinedIcon,
Strikethrough,
Code,
} from "lucide-react";
interface TiptapTextProps {
content: string;
onContentChange?: (content: string) => void;
className?: string;
placeholder?: string;
tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div';
content: string;
onContentChange?: (content: string) => void;
className?: string;
placeholder?: string;
tag?: "H1" | "H2" | "H3" | "H4" | "H5" | "H6" | "P" | "SPAN" | "DIV" | any;
}
const TiptapText: React.FC<TiptapTextProps> = ({
content,
onContentChange,
className = "",
placeholder = "Enter text...",
tag = 'div',
content,
onContentChange,
className = "",
placeholder = "Enter text...",
tag = "p",
}) => {
const editor = useEditor({
extensions: [StarterKit, Markdown, Underline],
content: content || placeholder,
editorProps: {
attributes: {
class: `outline-none focus:outline-none transition-all duration-200 ${className}`,
'data-placeholder': placeholder,
},
},
onBlur: ({ editor }) => {
const markdown = editor?.storage.markdown.getMarkdown();
if (onContentChange) {
onContentChange(markdown);
}
},
editable: true,
immediatelyRender: false,
});
const editor = useEditor({
extensions: [StarterKit, Markdown, Underline],
content: content || placeholder,
// Update editor content when content prop changes
useEffect(() => {
if (editor && content !== editor.getText()) {
editor.commands.setContent(content || placeholder);
}
}, [content, editor, placeholder]);
editorProps: {
attributes: {
class: `outline-none focus:outline-none transition-all duration-200 ${className}`,
"data-placeholder": placeholder,
},
},
onBlur: ({ editor }) => {
const markdown = editor?.storage.markdown.getMarkdown();
if (onContentChange) {
onContentChange(markdown);
}
},
editable: true,
immediatelyRender: false,
});
if (!editor) {
return <div className={className}>{content || placeholder}</div>;
// Update editor content when content prop changes
useEffect(() => {
if (editor && content !== editor.getText()) {
editor.commands.setContent(content || placeholder);
}
}, [content, editor, placeholder]);
return (
<>
<BubbleMenu editor={editor} className='z-50' tippyOptions={{ duration: 100 }}>
<div style={{
zIndex: 100
}} className="flex text-black bg-white rounded-lg shadow-lg p-2 gap-1 border border-gray-200 z-50">
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("bold") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Bold"
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("italic") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Italic"
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleUnderline().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("underline") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Underline"
>
<UnderlinedIcon className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleStrike().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("strike") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleCode().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("code") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Code"
>
<Code className="h-4 w-4" />
</button>
</div>
</BubbleMenu>
if (!editor) {
return <div className={className}>{content || placeholder}</div>;
}
<EditorContent
editor={editor}
className={`tiptap-text-editor w-full`}
style={{
// Ensure the editor maintains the same visual appearance
lineHeight: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
fontFamily: 'inherit',
color: 'inherit',
textAlign: 'inherit',
return (
<>
<BubbleMenu
editor={editor}
className="z-50"
tippyOptions={{ duration: 100 }}
>
<div
style={{
zIndex: 100,
}}
className="flex text-black bg-white rounded-lg shadow-lg p-2 gap-1 border border-gray-200 z-50"
>
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
editor?.isActive("bold") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Bold"
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
editor?.isActive("italic") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Italic"
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleUnderline().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
editor?.isActive("underline") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Underline"
>
<UnderlinedIcon className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleStrike().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
editor?.isActive("strike") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleCode().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${
editor?.isActive("code") ? "bg-blue-100 text-blue-600" : ""
}`}
title="Code"
>
<Code className="h-4 w-4" />
</button>
</div>
</BubbleMenu>
}}
/>
</>
);
<EditorContent
editor={editor}
className={`tiptap-text-editor w-full`}
style={{
// Ensure the editor maintains the same visual appearance
lineHeight: "inherit",
fontSize: "inherit",
fontWeight: "inherit",
fontFamily: "inherit",
color: "inherit",
textAlign: "inherit",
}}
/>
</>
);
};
export default TiptapText;
export default TiptapText;

View file

@ -63,81 +63,16 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
if (shouldSkipElement(htmlElement)) return;
// Get all computed styles to preserve them
const computedStyles = window.getComputedStyle(htmlElement);
const preservedStyles = {
fontSize: computedStyles.fontSize,
fontWeight: computedStyles.fontWeight,
fontFamily: computedStyles.fontFamily,
color: computedStyles.color,
lineHeight: computedStyles.lineHeight,
textAlign: computedStyles.textAlign,
marginTop: computedStyles.marginTop,
marginBottom: computedStyles.marginBottom,
marginLeft: computedStyles.marginLeft,
marginRight: computedStyles.marginRight,
paddingTop: computedStyles.paddingTop,
paddingBottom: computedStyles.paddingBottom,
paddingLeft: computedStyles.paddingLeft,
paddingRight: computedStyles.paddingRight,
borderRadius: computedStyles.borderRadius,
border: computedStyles.border,
backgroundColor: computedStyles.backgroundColor,
opacity: computedStyles.opacity,
zIndex: computedStyles.zIndex,
cursor: computedStyles.cursor,
boxShadow: computedStyles.boxShadow,
textShadow: computedStyles.textShadow,
textDecoration: computedStyles.textDecoration,
textTransform: computedStyles.textTransform,
letterSpacing: computedStyles.letterSpacing,
wordSpacing: computedStyles.wordSpacing,
textOverflow: computedStyles.textOverflow,
whiteSpace: computedStyles.whiteSpace,
wordBreak: computedStyles.wordBreak,
overflow: computedStyles.overflow,
textAlignLast: computedStyles.textAlignLast,
position: computedStyles.position,
top: computedStyles.top,
left: computedStyles.left,
right: computedStyles.right,
bottom: computedStyles.bottom,
display: computedStyles.display,
flexDirection: computedStyles.flexDirection,
flexWrap: computedStyles.flexWrap,
flexGrow: computedStyles.flexGrow,
flexShrink: computedStyles.flexShrink,
flexBasis: computedStyles.flexBasis,
alignItems: computedStyles.alignItems,
justifyContent: computedStyles.justifyContent,
gap: computedStyles.gap,
gridTemplateColumns: computedStyles.gridTemplateColumns,
gridTemplateRows: computedStyles.gridTemplateRows,
gridTemplateAreas: computedStyles.gridTemplateAreas,
gridTemplate: computedStyles.gridTemplate,
gridAutoFlow: computedStyles.gridAutoFlow,
gridAutoColumns: computedStyles.gridAutoColumns,
gridAutoRows: computedStyles.gridAutoRows,
gridColumn: computedStyles.gridColumn,
gridRow: computedStyles.gridRow,
gridArea: computedStyles.gridArea,
grid: computedStyles.grid,
};
// Try to find matching data path
const allClasses = Array.from(htmlElement.classList);
const allStyles = htmlElement.getAttribute("style");
const dataPath = findDataPath(slideData, trimmedText);
// Create a container for the TiptapText
const tiptapContainer = document.createElement("div");
tiptapContainer.className = htmlElement.className;
tiptapContainer.style.cssText = allStyles || "";
tiptapContainer.className = Array.from(allClasses).join(" ");
// Apply preserved styles
Object.entries(preservedStyles).forEach(([property, value]) => {
if (value && value !== "auto") {
tiptapContainer.style.setProperty(
property.replace(/([A-Z])/g, "-$1").toLowerCase(),
value
);
}
});
// Replace the element
htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement);
// Mark as processed
@ -147,6 +82,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
root.render(
<TiptapText
content={trimmedText}
tag={htmlElement.tagName}
onContentChange={(content: string) => {
if (dataPath && onContentChange) {
onContentChange(content, dataPath.path, slideIndex);

View file

@ -112,7 +112,7 @@ const compileCustomLayout = (layoutCode: string, React: any, z: any) => {
};
`
);
// globalThis.z = z;
return factory(React, z);
};
@ -124,7 +124,6 @@ export const LayoutProvider: React.FC<{
const [error, setError] = useState<string | null>(null);
const [isPreloading, setIsPreloading] = useState(false);
const dispatch = useDispatch();
console.log("🔍 layoutData", layoutData);
const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => {
const layouts: LayoutInfo[] = [];
@ -278,7 +277,6 @@ export const LayoutProvider: React.FC<{
await layoutResponse.json();
if (!groupedLayoutsData || groupedLayoutsData.length === 0) {
console.warn("⚠️ API returned empty data");
setError("No layout groups found");
return;
}
@ -344,11 +342,10 @@ export const LayoutProvider: React.FC<{
"/api/v1/ppt/layout-management/summary"
);
const customGroupData = await customGroupResponse.json();
console.log("🔍 customGroupData", customGroupData);
const customGroup = customGroupData.presentations;
console.log("🔍 customGroup", customGroup);
for (const group of customGroup) {
console.log("🔍 group", group);
const groupName = `custom-${group.presentation_id}`;
fullDataByGroup.set(groupName, []);
if (!layoutsByGroup.has(groupName)) {

View file

@ -18,7 +18,6 @@ export const useGroupLayouts = () => {
if (layout) {
return getLayout(layoutId);
}
console.warn(`Layout ${layoutId} not found in group ${groupName}`);
return null;
};
}, [getLayoutByIdAndGroup, getLayout]);

View file

@ -107,16 +107,15 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
if (isStreaming || loading) {
return;
}
if (slide.layout_group.includes("custom")) {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, [slide, isStreaming, loading]);

View file

@ -0,0 +1,8 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
const hasKey = process.env.ANTHROPIC_API_KEY !== "";
return NextResponse.json({ hasKey });
}

View file

@ -25,14 +25,14 @@ const EachSlide = ({
retrySlide,
setSlides,
onSlideUpdate,
isProcessingPptx,
isProcessing,
}: {
slide: any;
index: number;
retrySlide: (index: number) => void;
setSlides: React.Dispatch<React.SetStateAction<any[]>>;
onSlideUpdate?: (updatedSlideData: any) => void;
isProcessingPptx: boolean;
isProcessing: boolean;
}) => {
const [isUpdating, setIsUpdating] = useState(false);
const [prompt, setPrompt] = useState("");
@ -418,14 +418,19 @@ const EachSlide = ({
)}
</div>
{!isProcessingPptx && slide.processed && (
{slide.processed && (
<div className="flex gap-6">
{slide.processed && slide.html && !isEditMode && (
<div className=" ">
<ToolTip content="Edit slide">
<button
onClick={handleEditClick}
className={`px-6 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md `}
disabled={isProcessing || !slide.processed}
className={`px-6 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md ${
isProcessing || !slide.processed
? "opacity-50 cursor-not-allowed"
: ""
}`}
>
<Edit className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
<span className="text-white">Edit Slide</span>
@ -437,19 +442,26 @@ const EachSlide = ({
<ToolTip content="Re-Design this slide">
<button
onClick={() => retrySlide(index)}
disabled={slide.processing}
className="px-6 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md"
disabled={isProcessing || !slide.processed}
className={`px-6 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md ${
isProcessing || !slide.processed
? "opacity-50 cursor-not-allowed"
: ""
}`}
>
<Repeat2 className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
<span className="text-white">Re-Design</span>
<span className="text-white">Re-Construct</span>
</button>
</ToolTip>
</div>
<div>
<ToolTip content="Delete Slide">
<button
disabled={isProcessing}
onClick={handleDeleteSlide}
className="px-4 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg hover:shadow-md transition-all duration-300 cursor-pointer shadow-md"
className={`px-4 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg hover:shadow-md transition-all duration-300 cursor-pointer shadow-md ${
isProcessing ? "opacity-50 cursor-not-allowed" : ""
}`}
>
<Trash className="w-4 sm:w-5 h-4 sm:h-5 text-red-500" />
</button>

View file

@ -10,7 +10,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { toast } from "sonner";
import { Upload, FileText, X, Loader2 } from "lucide-react";
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
@ -60,8 +59,17 @@ const CustomLayoutPage = () => {
const [isLayoutSaved, setIsLayoutSaved] = useState(false);
const [UploadedFonts, setUploadedFonts] = useState<UploadedFont[]>([]);
const [fontsData, setFontsData] = useState<FontData | null>(null);
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
const [isAnthropicKeyLoading, setIsAnthropicKeyLoading] = useState(true);
console.log(slides);
useEffect(() => {
fetch("/api/has-anthropic-key")
.then((res) => res.json())
.then((data) => {
setHasAnthropicKey(data.hasKey);
setIsAnthropicKeyLoading(false);
});
}, []);
// Load uploaded fonts dynamically
useEffect(() => {
@ -117,6 +125,9 @@ const CustomLayoutPage = () => {
}
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [slides, isLayoutSaved]);
// If User does not put Anthropic Key, Can't process the layout
// Font management functions
const uploadFont = useCallback(
async (fontName: string, file: File): Promise<string | null> => {
@ -563,6 +574,36 @@ const CustomLayoutPage = () => {
(slide) => slide.processed || slide.error
).length;
if (isAnthropicKeyLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 ">
<Header />
<div className="max-w-[1440px] min-h-screen flex items-center justify-center aspect-video mx-auto px-6 ">
<div className="text-center space-y-2 my-6 bg-white p-6 rounded-lg shadow-md">
<Loader2 className="w-6 h-6 animate-spin text-blue-600 mx-auto" />
<p>Checking Anthropic Key...</p>
</div>
</div>
</div>
);
}
if (!hasAnthropicKey) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 ">
<Header />
<div className="max-w-[1440px] min-h-screen flex items-center justify-center aspect-video mx-auto px-6 ">
<div className="text-center space-y-2 my-6 bg-white p-6 rounded-lg shadow-md">
<h1 className="text-4xl font-bold text-gray-900">
Please put Anthropic Key To Process The Layout
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
It Only works on Anthropic(Claude-4).
</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 ">
<Header />
@ -680,7 +721,7 @@ const CustomLayoutPage = () => {
key={index}
slide={slide}
index={index}
isProcessingPptx={isProcessingPptx}
isProcessing={slides.some((s) => s.processing)}
retrySlide={retrySlide}
setSlides={setSlides}
onSlideUpdate={(updatedSlideData) =>
@ -696,7 +737,7 @@ const CustomLayoutPage = () => {
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50">
<Button
onClick={saveLayout}
disabled={isSavingLayout || isProcessingPptx}
disabled={isSavingLayout || slides.some((s) => s.processing)}
className="bg-green-600 hover:bg-green-700 text-white shadow-lg hover:shadow-xl transition-all duration-200 px-10 py-3 text-lg"
size="lg"
>

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@ import LoadingStates from "./components/LoadingStates";
import { Card } from "@/components/ui/card";
import { ExternalLink } from "lucide-react";
import Header from "@/components/Header";
import CustomLayout from "./CustomLayout";
const LayoutPreview = () => {
const {
@ -127,7 +128,7 @@ const LayoutPreview = () => {
</div>
</div>
</div>
{/* <CustomLayouts /> */}
{/* <CustomLayout /> */}
</div>
</div>
);

View file

@ -39,11 +39,9 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"@types/fabric": "^5.3.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"fabric": "^6.7.1",
"html2canvas": "^1.4.1",
"jsonrepair": "^3.12.0",
"lucide-react": "^0.447.0",
@ -54,9 +52,7 @@
"puppeteer": "^24.13.0",
"react": "^18",
"react-dom": "^18",
"react-jsx-parser": "^2.4.0",
"react-redux": "^9.1.2",
"react-sketch-canvas": "^6.2.0",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"sonner": "^2.0.6",