add history

This commit is contained in:
Artur AGH
2023-12-17 16:04:56 +01:00
parent 493c787831
commit af8e61ff51
7 changed files with 304 additions and 161 deletions

View File

@@ -36,14 +36,24 @@ const GUIDELINE_OFFSET = 5;
const Canvas = () => { const Canvas = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const stage = useAppSelector((state) => state.app.stage); const currentStep = useAppSelector((state) => state.app.currentStep);
const stage = useAppSelector(
(state) => state.app.history[currentStep]?.stage,
);
const stageRef = useRef<Konva.Stage>(null); const stageRef = useRef<Konva.Stage>(null);
const layerRef = useRef<Konva.Layer>(null); const layerRef = useRef<Konva.Layer>(null);
const selectedItemId = useAppSelector((state) => state.app.selectedItemId); const selectedItemId = useAppSelector(
(state) => state.app.history[currentStep]?.selectedItemId,
);
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id)); const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
const items = useAppSelector((state) => state.app.items); const items = useAppSelector(
(state) => state.app.history[currentStep]?.items,
);
const backgroundRect = useAppSelector((state) => state.app.background); const backgroundRect = useAppSelector(
(state) => state.app.history[currentStep]?.backgroundColor,
);
const backgroundId = "background"; const backgroundId = "background";
const deselectHandler = ( const deselectHandler = (
e: KonvaEventObject<MouseEvent> | KonvaEventObject<TouchEvent>, e: KonvaEventObject<MouseEvent> | KonvaEventObject<TouchEvent>,
@@ -337,6 +347,7 @@ const Canvas = () => {
layer?.find(".guid-line").forEach((l) => l.destroy()); layer?.find(".guid-line").forEach((l) => l.destroy());
}; };
if (!stage) return null;
return ( return (
<div className="relative flex h-full w-full flex-col items-center"> <div className="relative flex h-full w-full flex-col items-center">
<Toolbar /> <Toolbar />
@@ -368,7 +379,7 @@ const Canvas = () => {
<Rect <Rect
width={stage.width} width={stage.width}
height={stage.height} height={stage.height}
fill={backgroundRect.fill} fill={backgroundRect}
onTouchStart={deselectHandler} onTouchStart={deselectHandler}
onClick={deselectHandler} onClick={deselectHandler}
id={backgroundId} id={backgroundId}
@@ -382,7 +393,7 @@ const Canvas = () => {
clipHeight={stage.height - 2 * CANVAS_PADDING_Y} clipHeight={stage.height - 2 * CANVAS_PADDING_Y}
ref={layerRef} ref={layerRef}
> >
{items.toReversed().map((item) => { {items?.toReversed().map((item) => {
if (item.type === StageItemType.Image) { if (item.type === StageItemType.Image) {
return ( return (
<TransformableImage <TransformableImage

View File

@@ -11,7 +11,12 @@ import {
import { useAppSelector } from "@/hooks"; import { useAppSelector } from "@/hooks";
export default function LayerBorder() { export default function LayerBorder() {
const stage = useAppSelector((state) => state.app.stage); const currentStep = useAppSelector((state) => state.app.currentStep);
const stage = useAppSelector(
(state) => state.app.history[currentStep]?.stage,
);
if (!stage) return null;
return ( return (
<Layer> <Layer>
<Line <Line

View File

@@ -1,12 +1,22 @@
import { UserNav } from "./user-nav"; import { UserNav } from "./user-nav";
import Image from "next/image"; import Image from "next/image";
import logo from "@/assets/logo.png"; import logo from "@/assets/logo.png";
import { Switch } from "@/components/ui/switch"; import { useAppDispatch } from "@/hooks";
import { goBack, goForward } from "@/store/app.slice";
export const Navbar = () => { export const Navbar = () => {
const dispatch = useAppDispatch();
const handleGoBack = () => {
dispatch(goBack());
};
const handleGoForward = () => {
dispatch(goForward());
};
return ( return (
<div className="border-b"> <div className="border-b">
<div className="flex h-16 items-center px-4"> <div className="flex h-16 items-center px-4">
<button onClick={handleGoBack}>Back</button>
<button onClick={handleGoForward}>Forward</button>
<div className="relative h-12 w-12"> <div className="relative h-12 w-12">
<Image <Image
src={logo} src={logo}

View File

@@ -14,15 +14,12 @@ import { selectBackground } from "@/store/app.slice";
export const BackgroundSelect = () => { export const BackgroundSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const bgColor = useAppSelector((state) => state.app.background); const bgColor = useAppSelector((state) => state.app.backgroundColor);
const handleBackgroundSelect = (color: string) => { const handleBackgroundSelect = (color: string) => {
dispatch(selectBackground(color)); dispatch(selectBackground(color));
}; };
const handleSizeSelect = () => {
setOpen(false);
};
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -49,7 +46,7 @@ export const BackgroundSelect = () => {
<input <input
className="m-2 " className="m-2 "
type="color" type="color"
value={bgColor} value={bgColor ?? undefined}
onChange={(e) => handleBackgroundSelect(e.target.value)} onChange={(e) => handleBackgroundSelect(e.target.value)}
/> />
</Label> </Label>

View File

@@ -19,8 +19,12 @@ import LayerItem from "@/components/layer-item";
export const Layers = () => { export const Layers = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const items = useAppSelector((state) => state.app.items);
const currentStep = useAppSelector((state) => state.app.currentStep);
const items = useAppSelector(
(state) => state.app.history[currentStep]?.items,
);
if (!items) return;
const handleDragEnd = (e: DragEndEvent) => { const handleDragEnd = (e: DragEndEvent) => {
const { active, over } = e; const { active, over } = e;
const oldIndex = items.findIndex((item) => item.id === active.id); const oldIndex = items.findIndex((item) => item.id === active.id);

View File

@@ -8,10 +8,16 @@ import { Separator } from "@/components/ui/separator";
export const Toolbar = () => { export const Toolbar = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selectedItemId = useAppSelector((state) => state.app.selectedItemId); const currentStep = useAppSelector((state) => state.app.currentStep);
const items = useAppSelector((state) => state.app.items);
const currentItem = items.find((t) => t.id === selectedItemId); const selectedItemId = useAppSelector(
(state) => state.app.history[currentStep]?.selectedItemId,
);
const items = useAppSelector(
(state) => state.app.history[currentStep]?.items,
);
const currentItem = items?.find((t) => t.id === selectedItemId);
if (!currentItem || !selectedItemId) return null; if (!currentItem || !selectedItemId) return null;

View File

@@ -1,9 +1,8 @@
import type { TransformableImageProps } from "@/components/transformable-image"; import type { TransformableImageProps } from "@/components/transformable-image";
import type { TransformableTextProps } from "@/components/transformable-text"; import type { TransformableTextProps } from "@/components/transformable-text";
import type { PayloadAction } from "@reduxjs/toolkit"; import type { AnyAction, PayloadAction } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit"; import { createSlice, current } from "@reduxjs/toolkit";
import { v1 } from "uuid"; import { v1 } from "uuid";
import { type RectConfig } from "konva/lib/shapes/Rect";
import { type WritableDraft } from "immer/src/types/types-external"; import { type WritableDraft } from "immer/src/types/types-external";
export enum StageItemType { export enum StageItemType {
@@ -31,10 +30,15 @@ type StageItemSpecific = StageTextItem | StageImageItem;
export type StageItem = StageItemCommon & StageItemSpecific; export type StageItem = StageItemCommon & StageItemSpecific;
const initialState = { const initialState = {
history: [
{
stage: { width: 500, height: 500, scale: 1, x: 0, y: 0 }, stage: { width: 500, height: 500, scale: 1, x: 0, y: 0 },
items: [] as StageItem[], items: [] as StageItem[],
selectedItemId: null as string | null, selectedItemId: null as string | null,
background: null as RectConfig | null, backgroundColor: null as string | null,
},
],
currentStep: 0,
}; };
const defaultTextConfig = { const defaultTextConfig = {
@@ -57,25 +61,65 @@ const defaultImageConfig = {
scaleY: 1, scaleY: 1,
}; };
const addNewItemToHistory =
(
callback: (
state: WritableDraft<typeof initialState>,
action: any,
currentHistoryEntry: WritableDraft<
(typeof initialState)["history"][number]
>,
) => void,
) =>
(state: WritableDraft<typeof initialState>, action: any) => {
state.history.slice(0, state.currentStep + 1);
const currentHistoryEntry = state.history[state.currentStep];
if (!currentHistoryEntry) {
return;
}
state.currentStep = state.history.length;
callback(state, action, currentHistoryEntry);
};
export const appSlice = createSlice({ export const appSlice = createSlice({
name: "app", name: "app",
initialState, initialState,
reducers: { reducers: {
setStageItems: (state, action: PayloadAction<StageItem[]>) => { goBack: (state) => {
state.items = action.payload; if (state.currentStep === 0) {
return;
}
state.currentStep = state.currentStep - 1;
}, },
setStageScale: ( goForward: (state) => {
state, console.log(current(state));
action: PayloadAction<{ x: number; y: number; scale: number }>, if (state.history.length - 1 === state.currentStep) {
) => { return;
state.stage = { }
...state.stage, state.currentStep = state.currentStep + 1;
...action.payload, },
//+
setStageItems: addNewItemToHistory(
(state, action: PayloadAction<StageItem[]>, currentHistoryEntry) => {
const newHistoryEntry = {
...currentHistoryEntry,
items: action.payload,
}; };
state.history.push(newHistoryEntry);
}, },
addText: (state, action: PayloadAction<{ initialValue: string }>) => { ),
// +
addText: addNewItemToHistory(
(
state,
action: PayloadAction<{ initialValue: string }>,
currentHistoryEntry,
) => {
const newEntry = JSON.parse(
JSON.stringify(currentHistoryEntry),
) as typeof currentHistoryEntry;
const textId = v1(); const textId = v1();
state.items.push({ const newText = {
type: StageItemType.Text, type: StageItemType.Text,
id: textId, id: textId,
isBlocked: false, isBlocked: false,
@@ -83,20 +127,26 @@ export const appSlice = createSlice({
text: action.payload.initialValue, text: action.payload.initialValue,
...defaultTextConfig, ...defaultTextConfig,
}, },
}); } as const;
newEntry.items = [...newEntry.items, newText];
state.history.push(newEntry);
}, },
addImage: ( ),
// +
addImage: addNewItemToHistory(
(
state, state,
action: PayloadAction<{ action: PayloadAction<{
imageUrl: string; imageUrl: string;
width: number; width: number;
height: number; height: number;
}>, }>,
currentHistoryEntry,
) => { ) => {
const file = action.payload; const file = action.payload;
if (!file) return; if (!file) return;
const imageId = v1(); const imageId = v1();
state.items.push({ const newImage = {
type: StageItemType.Image, type: StageItemType.Image,
id: imageId, id: imageId,
isBlocked: false, isBlocked: false,
@@ -106,60 +156,96 @@ export const appSlice = createSlice({
height: action.payload.height, height: action.payload.height,
...defaultImageConfig, ...defaultImageConfig,
}, },
}); } as const;
const newHistoryEntry = {
...currentHistoryEntry,
items: [...currentHistoryEntry.items, newImage],
};
state.history.push(newHistoryEntry);
}, },
setBlockedItem: ( ),
// +
setBlockedItem: addNewItemToHistory(
(
state, state,
action: PayloadAction<{ id: string; blocked: boolean }>, action: PayloadAction<{ id: string; blocked: boolean }>,
currentHistoryEntry,
) => { ) => {
const itemToUpdate = state.items.find( const newEntry = JSON.parse(
JSON.stringify(currentHistoryEntry),
) as typeof currentHistoryEntry;
const itemToUpdate = newEntry.items.find(
(item) => item.id === action.payload.id, (item) => item.id === action.payload.id,
); );
if (!itemToUpdate) return; if (!itemToUpdate) return;
if (newEntry.selectedItemId === itemToUpdate.id) {
if (state.selectedItemId === itemToUpdate.id) { newEntry.selectedItemId = null;
state.selectedItemId = null;
} }
itemToUpdate.isBlocked = action.payload.blocked; itemToUpdate.isBlocked = action.payload.blocked;
state.history.push(newEntry);
}, },
),
selectBackground: (state, action: PayloadAction<string>) => { // +
state.background = { selectBackground: addNewItemToHistory(
fill: action.payload, (state, action: PayloadAction<string>, currentHistoryEntry) => {
}; state.history.push({
...currentHistoryEntry,
backgroundColor: action.payload,
});
}, },
),
selectItem: (state, action: PayloadAction<string>) => { // -
const itemToSelect = state.items.find( selectItem: addNewItemToHistory(
(state, action: PayloadAction<string>, currentHistoryEntry) => {
const itemToSelect = currentHistoryEntry.items.find(
(item) => item.id === action.payload, (item) => item.id === action.payload,
); );
if (!itemToSelect || itemToSelect.isBlocked) { if (!itemToSelect || itemToSelect.isBlocked) {
return; return;
} }
state.history.push({
state.selectedItemId = action.payload; ...currentHistoryEntry,
selectedItemId: action.payload,
});
}, },
),
deselectItem: (state) => { // -
state.selectedItemId = null; deselectItem: addNewItemToHistory((state, _action, currentHistoryEntry) => {
}, state.history.push({
...currentHistoryEntry,
updateStage: ( selectedItemId: null,
});
}),
// -
updateStage: addNewItemToHistory(
(
state, state,
action: PayloadAction<{ width?: number; height?: number }>, action: PayloadAction<{ width?: number; height?: number }>,
currentHistoryEntry,
) => { ) => {
state.stage = { ...state.stage, ...action.payload }; currentHistoryEntry.stage = {
...currentHistoryEntry.stage,
...action.payload,
};
}, },
),
updateText: ( // +
updateText: addNewItemToHistory(
(
state, state,
action: PayloadAction<{ id: string } & Partial<StageTextItem["params"]>>, action: PayloadAction<
{ id: string } & Partial<StageTextItem["params"]>
>,
currentHistoryEntry,
) => { ) => {
const { id, ...params } = action.payload; const { id, ...params } = action.payload;
const textToUpdateIndex = state.items.findIndex((t) => t.id === id); const newEntry = JSON.parse(
const textToUpdate = state.items[textToUpdateIndex]; JSON.stringify(currentHistoryEntry),
) as typeof currentHistoryEntry;
const textToUpdateIndex = newEntry.items.findIndex((t) => t.id === id);
const textToUpdate = newEntry.items[textToUpdateIndex];
if ( if (
!textToUpdate || !textToUpdate ||
@@ -168,22 +254,33 @@ export const appSlice = createSlice({
) )
return; return;
state.items[textToUpdateIndex] = { newEntry.items[textToUpdateIndex] = {
...textToUpdate, ...textToUpdate,
params: { params: {
...textToUpdate.params, ...textToUpdate.params,
...params, ...params,
}, },
}; };
state.history.push(newEntry);
}, },
),
updateImage: ( // +
updateImage: addNewItemToHistory(
(
state, state,
action: PayloadAction<{ id: string } & Partial<StageImageItem["params"]>>, action: PayloadAction<
{ id: string } & Partial<StageImageItem["params"]>
>,
currentHistoryEntry,
) => { ) => {
const { id, ...params } = action.payload; const { id, ...params } = action.payload;
const imageToUpdateIndex = state.items.findIndex((img) => img.id === id); const newEntry = JSON.parse(
const imageToUpdate = state.items[imageToUpdateIndex]; JSON.stringify(currentHistoryEntry),
) as typeof currentHistoryEntry;
const imageToUpdateIndex = newEntry.items.findIndex(
(img) => img.id === id,
);
const imageToUpdate = newEntry.items[imageToUpdateIndex];
if ( if (
!imageToUpdate || !imageToUpdate ||
@@ -192,21 +289,32 @@ export const appSlice = createSlice({
) )
return; return;
(state.items[imageToUpdateIndex] as WritableDraft<StageImageItem>) = { newEntry.items = newEntry.items.map((item, index) => {
if (index !== imageToUpdateIndex) return item;
return {
...imageToUpdate, ...imageToUpdate,
params: { params: {
...imageToUpdate.params, ...imageToUpdate.params,
...params, ...params,
}, },
}; };
}, });
deleteStageItem: (state, action: PayloadAction<string>) => { state.history.push(newEntry);
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
}, },
),
// +
deleteStageItem: addNewItemToHistory(
(state, action: PayloadAction<string>, currentHistoryEntry) => {
const newEntry = {
...currentHistoryEntry,
items: currentHistoryEntry.items.filter(
(item) => item.id !== action.payload,
),
};
state.history.push(newEntry);
},
),
}, },
}); });
@@ -222,4 +330,6 @@ export const {
setStageItems, setStageItems,
selectItem, selectItem,
setBlockedItem, setBlockedItem,
goBack,
goForward,
} = appSlice.actions; } = appSlice.actions;