Merge branch 'main' of github.com:presenton/presenton
This commit is contained in:
commit
b50fa028c7
13 changed files with 377 additions and 31 deletions
|
|
@ -13,22 +13,21 @@ import {
|
|||
Code,
|
||||
} from "lucide-react";
|
||||
|
||||
|
||||
interface TiptapTextProps {
|
||||
content: string;
|
||||
|
||||
onContentChange?: (content: string) => void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
element?: HTMLElement;
|
||||
tag?: "H1" | "H2" | "H3" | "H4" | "H5" | "H6" | "P" | "SPAN" | "DIV" | any;
|
||||
|
||||
}
|
||||
|
||||
const TiptapText: React.FC<TiptapTextProps> = ({
|
||||
content,
|
||||
element,
|
||||
onContentChange,
|
||||
className = "",
|
||||
placeholder = "Enter text...",
|
||||
tag = "p",
|
||||
}) => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit, Markdown, Underline],
|
||||
|
|
@ -41,6 +40,8 @@ const TiptapText: React.FC<TiptapTextProps> = ({
|
|||
},
|
||||
},
|
||||
onBlur: ({ editor }) => {
|
||||
// const element = editor?.options.element;
|
||||
// element?.classList.add("tiptap-text-edited");
|
||||
const markdown = editor?.storage.markdown.getMarkdown();
|
||||
if (onContentChange) {
|
||||
onContentChange(markdown);
|
||||
|
|
@ -52,10 +53,15 @@ const TiptapText: React.FC<TiptapTextProps> = ({
|
|||
|
||||
// Update editor content when content prop changes
|
||||
useEffect(() => {
|
||||
if (editor && content !== editor.getText()) {
|
||||
editor.commands.setContent(content || placeholder);
|
||||
if (!editor) return;
|
||||
// Compare against current plain text to avoid unnecessary updates
|
||||
const currentText = editor?.storage.markdown.getMarkdown();
|
||||
if ((content || "") !== currentText) {
|
||||
editor.commands.setContent(content || "");
|
||||
}
|
||||
}, [content, editor, placeholder]);
|
||||
}, [content, editor]);
|
||||
|
||||
|
||||
|
||||
if (!editor) {
|
||||
return <div className={className}>{content || placeholder}</div>;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
import React, { useRef, useEffect, useState, ReactNode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import TiptapText from "./TiptapText";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
|
||||
const extensions = [StarterKit, Markdown, Underline];
|
||||
|
||||
interface TiptapTextReplacerProps {
|
||||
children: ReactNode;
|
||||
|
|
@ -21,10 +27,17 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
slideIndex,
|
||||
onContentChange = () => {},
|
||||
}) => {
|
||||
|
||||
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [processedElements, setProcessedElements] = useState(
|
||||
new Set<HTMLElement>()
|
||||
);
|
||||
// Track created React roots to update content when slideData changes
|
||||
const rootsRef = useRef<
|
||||
Map<HTMLElement, { root: any; dataPath: string; fallbackText: string }>
|
||||
>(new Map());
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
|
|
@ -38,6 +51,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
const htmlElement = element as HTMLElement;
|
||||
|
||||
// Skip if already processed
|
||||
|
||||
if (
|
||||
processedElements.has(htmlElement) ||
|
||||
htmlElement.classList.contains("tiptap-text-editor") ||
|
||||
|
|
@ -46,6 +60,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// console.log("htmlElement", htmlElement);
|
||||
// Skip if element is inside an ignored element tree
|
||||
if (isInIgnoredElementTree(htmlElement)) return;
|
||||
|
||||
|
|
@ -55,10 +70,10 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
|
||||
// Check if element has meaningful text content
|
||||
if (!trimmedText || trimmedText.length <= 2) return;
|
||||
|
||||
|
||||
// Skip elements that contain other elements with text (to avoid double processing)
|
||||
if (hasTextChildren(htmlElement)) return;
|
||||
|
||||
|
||||
// Skip certain element types that shouldn't be editable
|
||||
if (shouldSkipElement(htmlElement)) return;
|
||||
|
||||
|
|
@ -72,7 +87,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
const tiptapContainer = document.createElement("div");
|
||||
tiptapContainer.style.cssText = allStyles || "";
|
||||
tiptapContainer.className = Array.from(allClasses).join(" ");
|
||||
|
||||
|
||||
// Replace the element
|
||||
htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement);
|
||||
// Mark as processed
|
||||
|
|
@ -80,11 +95,19 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
setProcessedElements((prev) => new Set(prev).add(htmlElement));
|
||||
// Render TiptapText
|
||||
const root = ReactDOM.createRoot(tiptapContainer);
|
||||
const initialContent = dataPath.path
|
||||
? getValueByPath(slideData, dataPath.path) ?? trimmedText
|
||||
: trimmedText;
|
||||
rootsRef.current.set(tiptapContainer, {
|
||||
root,
|
||||
dataPath: dataPath.path,
|
||||
|
||||
fallbackText: trimmedText,
|
||||
});
|
||||
root.render(
|
||||
<TiptapText
|
||||
content={trimmedText}
|
||||
element={htmlElement}
|
||||
tag={htmlElement.tagName}
|
||||
content={initialContent}
|
||||
|
||||
onContentChange={(content: string) => {
|
||||
if (dataPath && onContentChange) {
|
||||
onContentChange(content, dataPath.path, slideIndex);
|
||||
|
|
@ -96,6 +119,34 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
// Replace text elements after a short delay to ensure DOM is ready
|
||||
const timer = setTimeout(replaceTextElements, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [slideData, slideIndex]);
|
||||
|
||||
// When slideData changes, update existing editors' content using the stored dataPath
|
||||
useEffect(() => {
|
||||
if (!rootsRef.current || rootsRef.current.size === 0) return;
|
||||
rootsRef.current.forEach(({ root, dataPath, fallbackText }) => {
|
||||
const newContent = dataPath ? getValueByPath(slideData, dataPath) ?? fallbackText : fallbackText;
|
||||
root.render(
|
||||
<TiptapText
|
||||
content={newContent}
|
||||
onContentChange={(content: string) => {
|
||||
if (dataPath && onContentChange) {
|
||||
onContentChange(content, dataPath, slideIndex);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [slideData, slideIndex]);
|
||||
// helper functions
|
||||
// Function to check if element is inside an ignored element tree
|
||||
const isInIgnoredElementTree = (element: HTMLElement): boolean => {
|
||||
// List of element types that should be ignored entirely with all their children
|
||||
|
|
@ -183,6 +234,21 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
return false;
|
||||
};
|
||||
|
||||
// Resolve nested values by path like "a.b[0].c"
|
||||
const getValueByPath = (obj: any, path: string): any => {
|
||||
if (!obj || !path) return undefined;
|
||||
const tokens = path
|
||||
.replace(/\[(\d+)\]/g, ".$1")
|
||||
.split(".")
|
||||
.filter(Boolean);
|
||||
let current: any = obj;
|
||||
for (const token of tokens) {
|
||||
if (current == null) return undefined;
|
||||
current = current[token as keyof typeof current];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
// Helper function to get only direct text content (not from children)
|
||||
const getDirectTextContent = (element: HTMLElement): string => {
|
||||
let text = "";
|
||||
|
|
@ -248,7 +314,8 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
if (text.length < 2) return true;
|
||||
|
||||
// Skip elements that look like numbers or single characters (might be icons/UI)
|
||||
if (/^[0-9]+$/.test(text) || text.length === 1) return true;
|
||||
// if (/^[0-9]+$/.test(text) || text.length === 1) return true;
|
||||
if (text.length <3) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
@ -289,13 +356,6 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
return { path: "", originalText: "" };
|
||||
};
|
||||
|
||||
// Replace text elements after a short delay to ensure DOM is ready
|
||||
const timer = setTimeout(replaceTextElements, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [slideData, slideIndex]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="tiptap-text-replacer">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
type KeyboardEvent = {
|
||||
key: string;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
preventDefault: () => void;
|
||||
};
|
||||
|
||||
export const useKeyboardShortcut = (
|
||||
keys: string[],
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
deps: any[] = []
|
||||
) => {
|
||||
const handleKeyPress = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const isCtrlPressed = event.ctrlKey;
|
||||
|
||||
if (keys.includes(event.key.toLowerCase()) && isCtrlPressed) {
|
||||
event.preventDefault();
|
||||
callback(event);
|
||||
}
|
||||
},
|
||||
[callback, ...deps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyPress as any);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress as any);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
};
|
||||
|
|
@ -32,6 +32,7 @@ export const useGroupLayouts = () => {
|
|||
// Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
|
||||
const renderSlideContent = useMemo(() => {
|
||||
return (slide: any, isEditMode: boolean) => {
|
||||
|
||||
const Layout = getGroupLayout(slide.layout, slide.layout_group);
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { PresentationPageProps } from "../types";
|
|||
import LoadingState from "./LoadingState";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
import { useFontLoader } from "../../hooks/useFontLoader";
|
||||
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
|
||||
const PresentationPage: React.FC<PresentationPageProps> = ({
|
||||
presentation_id,
|
||||
}) => {
|
||||
|
|
@ -74,6 +75,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
fetchUserSlides
|
||||
);
|
||||
|
||||
usePresentationUndoRedo();
|
||||
|
||||
const onSlideChange = (newSlide: number) => {
|
||||
handleSlideChange(newSlide, presentationData);
|
||||
};
|
||||
|
|
@ -141,7 +144,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
isMobilePanelOpen={isMobilePanelOpen}
|
||||
setIsMobilePanelOpen={setIsMobilePanelOpen}
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
|
||||
<div
|
||||
id="presentation-slides-wrapper"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { useGroupLayouts } from "../../hooks/useGroupLayouts";
|
|||
import { usePathname } from "next/navigation";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import NewSlide from "../../components/NewSlide";
|
||||
import { addToHistory } from "@/store/slices/undoRedoSlice";
|
||||
|
||||
interface SlideContentProps {
|
||||
slide: any;
|
||||
|
|
@ -73,7 +74,13 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
const onDeleteSlide = async () => {
|
||||
try {
|
||||
trackEvent(MixpanelEvent.Slide_Delete_API_Call);
|
||||
// Add current state to past
|
||||
dispatch(addToHistory({
|
||||
slides: presentationData?.slides,
|
||||
actionType: "DELETE_SLIDE"
|
||||
}));
|
||||
dispatch(deletePresentationSlide(slide.index));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error deleting slide:", error);
|
||||
toast.error("Error deleting slide.", {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import { finishUndoRedo, redo, undo } from "@/store/slices/undoRedoSlice";
|
||||
import { useKeyboardShortcut } from "../../hooks/use-keyboard-shortcut";
|
||||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const usePresentationUndoRedo = () => {
|
||||
const dispatch = useDispatch();
|
||||
const undoRedoState = useSelector((state: RootState) => state.undoRedo);
|
||||
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
|
||||
|
||||
// Handle undo
|
||||
useKeyboardShortcut(
|
||||
["z"],
|
||||
(e) => {
|
||||
if (e.ctrlKey && !e.shiftKey && undoRedoState.past.length > 0) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get the previous state before dispatching undo
|
||||
const previousState = undoRedoState.past[undoRedoState.past.length - 1];
|
||||
|
||||
// Perform undo
|
||||
dispatch(undo());
|
||||
|
||||
// Use the previousState directly instead of relying on the updated undoRedoState
|
||||
if (previousState) {
|
||||
// Create a deep copy to ensure no reference issues
|
||||
const newSlides = JSON.parse(JSON.stringify(previousState.slides));
|
||||
|
||||
// Update the presentation data with the properly structured slides
|
||||
dispatch(
|
||||
setPresentationData({
|
||||
...presentationData!,
|
||||
slides: newSlides,
|
||||
})
|
||||
);
|
||||
}
|
||||
// Reset the undo/redo flag
|
||||
setTimeout(() => {
|
||||
dispatch(finishUndoRedo());
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[undoRedoState.past, presentationData]
|
||||
);
|
||||
// Handle redo
|
||||
useKeyboardShortcut(
|
||||
["z"],
|
||||
(e) => {
|
||||
if (e.ctrlKey && e.shiftKey && undoRedoState.future.length > 0) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get the next state before dispatching redo
|
||||
const nextState = undoRedoState.future[0];
|
||||
|
||||
// Perform redo
|
||||
dispatch(redo());
|
||||
|
||||
// Use the nextState directly instead of relying on the updated undoRedoState
|
||||
if (nextState) {
|
||||
// Create a deep copy to ensure no reference issues
|
||||
const newSlides = JSON.parse(JSON.stringify(nextState.slides));
|
||||
|
||||
// Update the presentation data with the properly structured slides
|
||||
dispatch(
|
||||
setPresentationData({
|
||||
...presentationData!,
|
||||
slides: newSlides,
|
||||
})
|
||||
);
|
||||
}
|
||||
// Reset the undo/redo flag
|
||||
setTimeout(() => {
|
||||
dispatch(finishUndoRedo());
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[undoRedoState.future, presentationData]
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
'use client'
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store/store';
|
||||
import { PresentationGenerationApi } from '../../services/api/presentation-generation';
|
||||
import { addToHistory } from '@/store/slices/undoRedoSlice';
|
||||
|
||||
interface UseAutoSaveOptions {
|
||||
debounceMs?: number;
|
||||
|
|
@ -10,9 +11,10 @@ interface UseAutoSaveOptions {
|
|||
}
|
||||
|
||||
export const useAutoSave = ({
|
||||
debounceMs = 2000,
|
||||
debounceMs = 1000,
|
||||
enabled = true,
|
||||
}: UseAutoSaveOptions = {}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { presentationData, isStreaming, isLoading, isLayoutLoading } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
|
|
@ -66,6 +68,11 @@ export const useAutoSave = ({
|
|||
|
||||
// Trigger debounced save
|
||||
debouncedSave(presentationData);
|
||||
|
||||
dispatch(addToHistory({
|
||||
slides: presentationData.slides,
|
||||
actionType: "AUTO_SAVE"
|
||||
}));
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
return () => {
|
||||
|
|
@ -74,8 +81,10 @@ export const useAutoSave = ({
|
|||
}
|
||||
};
|
||||
}, [presentationData, enabled, debouncedSave]);
|
||||
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toast } from "sonner";
|
||||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
import { DashboardApi } from '../../services/api/dashboard';
|
||||
import { addToHistory, clearHistory } from "@/store/slices/undoRedoSlice";
|
||||
|
||||
|
||||
export const usePresentationData = (
|
||||
presentationId: string,
|
||||
|
|
@ -16,6 +18,11 @@ export const usePresentationData = (
|
|||
const data = await DashboardApi.getPresentation(presentationId);
|
||||
if (data) {
|
||||
dispatch(setPresentationData(data));
|
||||
dispatch(clearHistory());
|
||||
dispatch(addToHistory({
|
||||
slides: data.slides,
|
||||
actionType: "initial_load"
|
||||
}));
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const SlideCountSelect: React.FC<{
|
|||
<SelectContent className="font-instrument_sans">
|
||||
{/* Sticky custom input at the top */}
|
||||
<div
|
||||
className="sticky top-0 z-10 bg-white dark:bg-slate-900 p-2 border-b"
|
||||
className="sticky top-0 z-10 bg-white p-2 border-b"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
@ -97,7 +97,7 @@ const SlideCountSelect: React.FC<{
|
|||
}
|
||||
}}
|
||||
onBlur={applyCustomValue}
|
||||
placeholder="X"
|
||||
placeholder="--"
|
||||
className="h-8 w-16 px-2 text-sm"
|
||||
/>
|
||||
<span className="text-sm font-medium">slides</span>
|
||||
|
|
|
|||
|
|
@ -262,8 +262,6 @@ const presentationGenerationSlice = createSlice({
|
|||
current[Number(finalKey)] = updatedValue;
|
||||
}
|
||||
|
||||
// Add debugging
|
||||
console.log('Redux: Updated slide image at path:', path, 'with URL:', url);
|
||||
};
|
||||
|
||||
// Update the slide image
|
||||
|
|
|
|||
136
servers/nextjs/store/slices/undoRedoSlice.ts
Normal file
136
servers/nextjs/store/slices/undoRedoSlice.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Slide } from '@/app/(presentation-generator)/types/slide';
|
||||
|
||||
interface HistoryState {
|
||||
slides: Slide[];
|
||||
timestamp: number;
|
||||
actionType: string;
|
||||
}
|
||||
|
||||
interface UndoRedoState {
|
||||
past: HistoryState[];
|
||||
present: HistoryState | null;
|
||||
future: HistoryState[];
|
||||
maxHistorySize: number;
|
||||
isUndoRedoInProgress: boolean;
|
||||
}
|
||||
|
||||
// Helper function for deep copy
|
||||
const deepCopy = <T>(obj: T): T => {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
const initialState: UndoRedoState = {
|
||||
past: [],
|
||||
present: null,
|
||||
future: [],
|
||||
maxHistorySize: 30,
|
||||
isUndoRedoInProgress: false
|
||||
};
|
||||
|
||||
const undoRedoSlice = createSlice({
|
||||
name: 'undoRedo',
|
||||
initialState,
|
||||
reducers: {
|
||||
addToHistory: (state, action: PayloadAction<{slides: Slide[], actionType: string}>) => {
|
||||
// Skip if undo/redo is in progress
|
||||
if (state.isUndoRedoInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deep copy the slides to avoid reference issues
|
||||
const newSlides = deepCopy(action.payload.slides);
|
||||
|
||||
// Only add to history if the slides have actually changed
|
||||
if (!state.present) {
|
||||
state.present = {
|
||||
slides: newSlides,
|
||||
timestamp: Date.now(),
|
||||
actionType: action.payload.actionType
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if slides are identical
|
||||
if (JSON.stringify(state.present.slides) === JSON.stringify(newSlides)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add current state to past
|
||||
state.past.push(state.present);
|
||||
|
||||
// Limit history size
|
||||
if (state.past.length > state.maxHistorySize) {
|
||||
state.past.shift();
|
||||
}
|
||||
|
||||
// Clear future on new change
|
||||
state.future = [];
|
||||
|
||||
// Set new present
|
||||
state.present = {
|
||||
slides: newSlides,
|
||||
timestamp: Date.now(),
|
||||
actionType: action.payload.actionType
|
||||
};
|
||||
|
||||
|
||||
},
|
||||
|
||||
undo: (state) => {
|
||||
if (state.past.length === 0) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.isUndoRedoInProgress = true;
|
||||
|
||||
// Move present to future
|
||||
if (state.present) {
|
||||
state.future.unshift(deepCopy(state.present));
|
||||
}
|
||||
|
||||
// Get last past state
|
||||
const previous = state.past[state.past.length - 1];
|
||||
state.past = state.past.slice(0, -1);
|
||||
state.present = deepCopy(previous);
|
||||
|
||||
|
||||
},
|
||||
|
||||
redo: (state) => {
|
||||
if (state.future.length === 0) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.isUndoRedoInProgress = true;
|
||||
|
||||
// Move present to past
|
||||
if (state.present) {
|
||||
state.past.push(deepCopy(state.present));
|
||||
}
|
||||
|
||||
// Get first future state
|
||||
const next = state.future[0];
|
||||
state.future = state.future.slice(1);
|
||||
state.present = deepCopy(next);
|
||||
|
||||
|
||||
},
|
||||
|
||||
finishUndoRedo: (state) => {
|
||||
state.isUndoRedoInProgress = false;
|
||||
},
|
||||
|
||||
clearHistory: (state) => {
|
||||
state.past = [];
|
||||
state.future = [];
|
||||
// Keep present
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { addToHistory, undo, redo, finishUndoRedo, clearHistory } = undoRedoSlice.actions;
|
||||
export default undoRedoSlice.reducer;
|
||||
|
|
@ -3,11 +3,13 @@ import { configureStore } from "@reduxjs/toolkit";
|
|||
import presentationGenerationReducer from "./slices/presentationGeneration";
|
||||
import pptGenUploadReducer from "./slices/presentationGenUpload";
|
||||
import userConfigReducer from "./slices/userConfig";
|
||||
import undoRedoReducer from "./slices/undoRedoSlice";
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
presentationGeneration: presentationGenerationReducer,
|
||||
pptGenUpload: pptGenUploadReducer,
|
||||
userConfig: userConfigReducer,
|
||||
undoRedo: undoRedoReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue