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 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 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 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 deselectHandler = (
e: KonvaEventObject<MouseEvent> | KonvaEventObject<TouchEvent>,
@@ -337,6 +347,7 @@ const Canvas = () => {
layer?.find(".guid-line").forEach((l) => l.destroy());
};
if (!stage) return null;
return (
<div className="relative flex h-full w-full flex-col items-center">
<Toolbar />
@@ -368,7 +379,7 @@ const Canvas = () => {
<Rect
width={stage.width}
height={stage.height}
fill={backgroundRect.fill}
fill={backgroundRect}
onTouchStart={deselectHandler}
onClick={deselectHandler}
id={backgroundId}
@@ -382,7 +393,7 @@ const Canvas = () => {
clipHeight={stage.height - 2 * CANVAS_PADDING_Y}
ref={layerRef}
>
{items.toReversed().map((item) => {
{items?.toReversed().map((item) => {
if (item.type === StageItemType.Image) {
return (
<TransformableImage

View File

@@ -11,7 +11,12 @@ import {
import { useAppSelector } from "@/hooks";
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 (
<Layer>
<Line

View File

@@ -1,12 +1,22 @@
import { UserNav } from "./user-nav";
import Image from "next/image";
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 = () => {
const dispatch = useAppDispatch();
const handleGoBack = () => {
dispatch(goBack());
};
const handleGoForward = () => {
dispatch(goForward());
};
return (
<div className="border-b">
<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">
<Image
src={logo}

View File

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

View File

@@ -19,8 +19,12 @@ import LayerItem from "@/components/layer-item";
export const Layers = () => {
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 { active, over } = e;
const oldIndex = items.findIndex((item) => item.id === active.id);

View File

@@ -8,10 +8,16 @@ import { Separator } from "@/components/ui/separator";
export const Toolbar = () => {
const dispatch = useAppDispatch();
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
const items = useAppSelector((state) => state.app.items);
const currentStep = useAppSelector((state) => state.app.currentStep);
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;

View File

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