add konva js to project + create some components with ai

This commit is contained in:
hamid zarghami
2025-11-16 16:33:54 +03:30
parent c1c6b0ebf7
commit 1a0c869101
38 changed files with 1855 additions and 42 deletions
@@ -0,0 +1,69 @@
import { Trash } from "iconsax-react";
import type { EditorObject } from "../../store/editorStore";
import {
TextSettings,
ShapeSettings,
LineSettings,
SizeSettings,
LinkSettings,
VideoSettings,
TransformSettings,
} from "./settings";
type ObjectSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
onDelete: (id: string) => void;
};
const ObjectSettings = ({ selectedObject, onUpdate, onDelete }: ObjectSettingsProps) => {
return (
<div className="space-y-4">
<div className="flex items-center justify-between pb-4 border-b border-border">
<h3 className="text-sm font-medium">تنظیمات شیء</h3>
<button
onClick={() => onDelete(selectedObject.id)}
className="p-2 hover:bg-red-50 rounded-lg transition-colors text-red-500"
>
<Trash size={20} />
</button>
</div>
{selectedObject.type === "text" && <TextSettings selectedObject={selectedObject} onUpdate={onUpdate} />}
{(selectedObject.type === "rectangle" || selectedObject.type === "circle") && (
<ShapeSettings selectedObject={selectedObject} onUpdate={onUpdate} />
)}
{(selectedObject.type === "line" || selectedObject.type === "arrow") && (
<LineSettings selectedObject={selectedObject} onUpdate={onUpdate} />
)}
{selectedObject.type === "image" && (
<SizeSettings selectedObject={selectedObject} onUpdate={onUpdate} />
)}
{selectedObject.type === "link" && (
<LinkSettings selectedObject={selectedObject} onUpdate={onUpdate} />
)}
{selectedObject.type === "video" && (
<VideoSettings selectedObject={selectedObject} onUpdate={onUpdate} />
)}
{(selectedObject.type === "document" || selectedObject.type === "sticker") && (
<SizeSettings
selectedObject={selectedObject}
onUpdate={onUpdate}
defaultWidth={selectedObject.type === "document" ? 200 : 100}
defaultHeight={selectedObject.type === "document" ? 250 : 100}
/>
)}
<TransformSettings selectedObject={selectedObject} onUpdate={onUpdate} />
</div>
);
};
export default ObjectSettings;
@@ -0,0 +1,30 @@
import type { ToolType } from "../../store/editorStore";
import { ImageUpload, DocumentUpload, VideoInput, LinkInput, SimpleInstruction } from "./instructions";
type ToolInstructionsProps = {
tool: ToolType;
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const ToolInstructions = ({ tool, onImageUpload }: ToolInstructionsProps) => {
if (tool === "image") {
return <ImageUpload onImageUpload={onImageUpload} />;
}
if (tool === "document") {
return <DocumentUpload />;
}
if (tool === "video") {
return <VideoInput />;
}
if (tool === "link") {
return <LinkInput />;
}
return <SimpleInstruction tool={tool} />;
};
export default ToolInstructions;
@@ -0,0 +1,65 @@
import { clx } from "@/helpers/utils";
import {
DocumentUpload,
Element,
Gallery,
Grid8,
Link,
MouseSquare,
Shapes,
Sticker,
Text,
VideoSquare,
Minus,
} from "iconsax-react";
import type { ToolType } from "../../store/editorStore";
const tools: Array<{ icon: (color: string, variant?: "Bold" | "Outline" | "Broken" | "Bulk" | "Linear" | "TwoTone") => React.ReactNode; tool: ToolType; label: string }> = [
{ icon: (color, variant) => <MouseSquare size={20} color={color} variant={variant} />, tool: "select", label: "انتخاب" },
{ icon: (color, variant) => <Shapes size={20} color={color} variant={variant} />, tool: "rectangle", label: "مستطیل" },
{ icon: (color, variant) => <Shapes size={20} color={color} variant={variant} />, tool: "circle", label: "دایره" },
{ icon: (color, variant) => <Text size={20} color={color} variant={variant} />, tool: "text", label: "متن" },
{ icon: (color, variant) => <Minus size={20} color={color} variant={variant} />, tool: "line", label: "خط" },
{ icon: (color, variant) => <Grid8 size={20} color={color} variant={variant} />, tool: "grid", label: "گرید" },
{ icon: (color, variant) => <Sticker size={20} color={color} variant={variant} />, tool: "sticker", label: "استیکر" },
{ icon: (color, variant) => <DocumentUpload size={20} color={color} variant={variant} />, tool: "document", label: "سند" },
{ icon: (color, variant) => <Element size={20} color={color} variant={variant} />, tool: "arrow", label: "پیکان" },
{ icon: (color, variant) => <Gallery size={20} color={color} variant={variant} />, tool: "image", label: "تصویر" },
{ icon: (color, variant) => <VideoSquare size={20} color={color} variant={variant} />, tool: "video", label: "ویدیو" },
{ icon: (color, variant) => <Link size={20} color={color} variant={variant} />, tool: "link", label: "لینک" },
];
type ToolsBarProps = {
tool: ToolType;
onToolClick: (tool: ToolType) => void;
};
const ToolsBar = ({ tool, onToolClick }: ToolsBarProps) => {
return (
<div className="flex h-full max-w-[72px] flex-1 flex-col items-center gap-4 overflow-y-auto border-l border-border py-2">
{tools.map((item, index) => {
const isActive = tool === item.tool;
const iconColor = "black";
const iconVariant = isActive ? "Bold" : undefined;
return (
<button
key={index}
onClick={() => onToolClick(item.tool)}
className={clx(
"min-h-10 w-10 flex items-center justify-center rounded-lg transition-colors text-black",
// isActive
// ? "bg-black text-white"
// : "hover:bg-gray-100 text-black",
)}
title={item.label}
>
{item.icon(iconColor, iconVariant)}
</button>
);
})}
</div>
);
};
export default ToolsBar;
@@ -0,0 +1,4 @@
export { default as ToolsBar } from "./ToolsBar";
export { default as ObjectSettings } from "./ObjectSettings";
export { default as ToolInstructions } from "./ToolInstructions";
@@ -0,0 +1,51 @@
import { DocumentUpload } from "iconsax-react";
import { useEditorStore, type ToolType } from "../../../store/editorStore";
import Button from "@/components/Button";
const DocumentUploadComponent = () => {
return (
<div className="space-y-4">
<div>
<label className="block text-sm mb-2">آپلود سند</label>
<input
type="file"
accept=".pdf,.doc,.docx"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const fileUrl = event.target?.result as string;
const newDocument = {
id: `document-${Date.now()}`,
type: "document" as ToolType,
x: 100,
y: 100,
width: 200,
height: 250,
imageUrl: fileUrl,
};
const { addObject } = useEditorStore.getState();
addObject(newDocument);
};
reader.readAsDataURL(file);
}
}}
className="hidden"
id="document-upload"
/>
<Button
variant="outline"
onClick={() => document.getElementById("document-upload")?.click()}
className="w-full"
>
<DocumentUpload size={20} />
انتخاب سند
</Button>
</div>
</div>
);
};
export default DocumentUploadComponent;
@@ -0,0 +1,32 @@
import { Gallery } from "iconsax-react";
import Button from "@/components/Button";
type ImageUploadProps = {
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const ImageUpload = ({ onImageUpload }: ImageUploadProps) => {
return (
<div>
<label className="block text-sm mb-2">آپلود تصویر</label>
<input
type="file"
accept="image/*"
onChange={onImageUpload}
className="hidden"
id="image-upload"
/>
<Button
variant="outline"
onClick={() => document.getElementById("image-upload")?.click()}
className="w-full"
>
<Gallery size={20} />
انتخاب تصویر
</Button>
</div>
);
};
export default ImageUpload;
@@ -0,0 +1,39 @@
import { useEditorStore, type ToolType } from "../../../store/editorStore";
import Input from "@/components/Input";
const LinkInput = () => {
return (
<div className="space-y-4">
<div>
<Input
label="آدرس لینک"
placeholder="https://example.com"
onEnter={() => {
const input = document.querySelector('input[placeholder="https://example.com"]') as HTMLInputElement;
if (input?.value) {
const newLink = {
id: `link-${Date.now()}`,
type: "link" as ToolType,
x: 100,
y: 100,
text: input.value,
linkUrl: input.value,
fontSize: 24,
fill: "#0000ff",
};
const { addObject } = useEditorStore.getState();
addObject(newLink);
input.value = "";
}
}}
/>
</div>
<div className="text-sm text-gray-600">
<p>آدرس لینک را وارد کنید و Enter بزنید</p>
</div>
</div>
);
};
export default LinkInput;
@@ -0,0 +1,31 @@
import type { ToolType } from "../../../store/editorStore";
type SimpleInstructionProps = {
tool: ToolType;
};
const SimpleInstruction = ({ tool }: SimpleInstructionProps) => {
const instructions: Record<ToolType, string> = {
select: "برای انتخاب و جابجایی اشیاء، روی آن‌ها کلیک کنید",
rectangle: "برای رسم مستطیل، روی کانوس کلیک کرده و بکشید",
circle: "برای رسم دایره، روی کانوس کلیک کرده و بکشید",
text: "برای افزودن متن، روی کانوس کلیک کنید",
line: "برای رسم خط، روی کانوس کلیک کرده و بکشید",
arrow: "برای رسم پیکان، روی کانوس کلیک کرده و بکشید",
sticker: "برای افزودن استیکر، روی کانوس کلیک کنید",
grid: "برای افزودن گرید، روی کانوس کلیک کنید",
image: "",
document: "",
video: "",
link: "",
};
return (
<div className="text-sm text-gray-600">
<p>{instructions[tool]}</p>
</div>
);
};
export default SimpleInstruction;
@@ -0,0 +1,38 @@
import { useEditorStore, type ToolType } from "../../../store/editorStore";
import Input from "@/components/Input";
const VideoInput = () => {
return (
<div className="space-y-4">
<div>
<Input
label="آدرس ویدیو"
placeholder="https://example.com/video.mp4"
onEnter={() => {
const input = document.querySelector('input[placeholder="https://example.com/video.mp4"]') as HTMLInputElement;
if (input?.value) {
const newVideo = {
id: `video-${Date.now()}`,
type: "video" as ToolType,
x: 100,
y: 100,
width: 400,
height: 300,
videoUrl: input.value,
};
const { addObject } = useEditorStore.getState();
addObject(newVideo);
input.value = "";
}
}}
/>
</div>
<div className="text-sm text-gray-600">
<p>آدرس ویدیو را وارد کنید</p>
</div>
</div>
);
};
export default VideoInput;
@@ -0,0 +1,6 @@
export { default as ImageUpload } from "./ImageUpload";
export { default as DocumentUpload } from "./DocumentUpload";
export { default as VideoInput } from "./VideoInput";
export { default as LinkInput } from "./LinkInput";
export { default as SimpleInstruction } from "./SimpleInstruction";
@@ -0,0 +1,33 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type LineSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const LineSettings = ({ selectedObject, onUpdate }: LineSettingsProps) => {
return (
<>
<Input
label="رنگ خط"
type="color"
value={selectedObject.stroke || "#000000"}
onChange={(e) => onUpdate(selectedObject.id, { stroke: e.target.value })}
/>
<Input
label="ضخامت خط"
type="number"
value={selectedObject.strokeWidth || 2}
onChange={(e) =>
onUpdate(selectedObject.id, {
strokeWidth: parseInt(e.target.value) || 2,
})
}
/>
</>
);
};
export default LineSettings;
@@ -0,0 +1,44 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type LinkSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const LinkSettings = ({ selectedObject, onUpdate }: LinkSettingsProps) => {
return (
<>
<Input
label="آدرس لینک"
value={selectedObject.linkUrl || ""}
onChange={(e) => onUpdate(selectedObject.id, { linkUrl: e.target.value })}
placeholder="https://example.com"
/>
<Input
label="متن لینک"
value={selectedObject.text || ""}
onChange={(e) => onUpdate(selectedObject.id, { text: e.target.value })}
/>
<Input
label="اندازه فونت"
type="number"
value={selectedObject.fontSize || 24}
onChange={(e) =>
onUpdate(selectedObject.id, {
fontSize: parseInt(e.target.value) || 24,
})
}
/>
<Input
label="رنگ متن"
type="color"
value={selectedObject.fill || "#0000ff"}
onChange={(e) => onUpdate(selectedObject.id, { fill: e.target.value })}
/>
</>
);
};
export default LinkSettings;
@@ -0,0 +1,59 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type ShapeSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
return (
<>
<Input
label="عرض"
type="number"
value={selectedObject.width || 100}
onChange={(e) =>
onUpdate(selectedObject.id, {
width: parseInt(e.target.value) || 100,
})
}
/>
<Input
label="ارتفاع"
type="number"
value={selectedObject.height || 100}
onChange={(e) =>
onUpdate(selectedObject.id, {
height: parseInt(e.target.value) || 100,
})
}
/>
<Input
label="رنگ پس‌زمینه"
type="color"
value={selectedObject.fill || "#3b82f6"}
onChange={(e) => onUpdate(selectedObject.id, { fill: e.target.value })}
/>
<Input
label="رنگ خط"
type="color"
value={selectedObject.stroke || "#1e40af"}
onChange={(e) => onUpdate(selectedObject.id, { stroke: e.target.value })}
/>
<Input
label="ضخامت خط"
type="number"
value={selectedObject.strokeWidth || 2}
onChange={(e) =>
onUpdate(selectedObject.id, {
strokeWidth: parseInt(e.target.value) || 2,
})
}
/>
</>
);
};
export default ShapeSettings;
@@ -0,0 +1,39 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type SizeSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
defaultWidth?: number;
defaultHeight?: number;
};
const SizeSettings = ({ selectedObject, onUpdate, defaultWidth = 100, defaultHeight = 100 }: SizeSettingsProps) => {
return (
<>
<Input
label="عرض"
type="number"
value={selectedObject.width || defaultWidth}
onChange={(e) =>
onUpdate(selectedObject.id, {
width: parseInt(e.target.value) || defaultWidth,
})
}
/>
<Input
label="ارتفاع"
type="number"
value={selectedObject.height || defaultHeight}
onChange={(e) =>
onUpdate(selectedObject.id, {
height: parseInt(e.target.value) || defaultHeight,
})
}
/>
</>
);
};
export default SizeSettings;
@@ -0,0 +1,38 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type TextSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const TextSettings = ({ selectedObject, onUpdate }: TextSettingsProps) => {
return (
<>
<Input
label="متن"
value={selectedObject.text || ""}
onChange={(e) => onUpdate(selectedObject.id, { text: e.target.value })}
/>
<Input
label="اندازه فونت"
type="number"
value={selectedObject.fontSize || 24}
onChange={(e) =>
onUpdate(selectedObject.id, {
fontSize: parseInt(e.target.value) || 24,
})
}
/>
<Input
label="رنگ متن"
type="color"
value={selectedObject.fill || "#000000"}
onChange={(e) => onUpdate(selectedObject.id, { fill: e.target.value })}
/>
</>
);
};
export default TextSettings;
@@ -0,0 +1,47 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
type TransformSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const TransformSettings = ({ selectedObject, onUpdate }: TransformSettingsProps) => {
return (
<div className="pt-4 border-t border-border">
<Input
label="موقعیت X"
type="number"
value={Math.round(selectedObject.x)}
onChange={(e) =>
onUpdate(selectedObject.id, {
x: parseInt(e.target.value) || 0,
})
}
/>
<Input
label="موقعیت Y"
type="number"
value={Math.round(selectedObject.y)}
onChange={(e) =>
onUpdate(selectedObject.id, {
y: parseInt(e.target.value) || 0,
})
}
/>
<Input
label="چرخش (درجه)"
type="number"
value={selectedObject.rotation || 0}
onChange={(e) =>
onUpdate(selectedObject.id, {
rotation: parseInt(e.target.value) || 0,
})
}
/>
</div>
);
};
export default TransformSettings;
@@ -0,0 +1,25 @@
import type { EditorObject } from "../../../store/editorStore";
import Input from "@/components/Input";
import SizeSettings from "./SizeSettings";
type VideoSettingsProps = {
selectedObject: EditorObject;
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
};
const VideoSettings = ({ selectedObject, onUpdate }: VideoSettingsProps) => {
return (
<>
<Input
label="آدرس ویدیو"
value={selectedObject.videoUrl || ""}
onChange={(e) => onUpdate(selectedObject.id, { videoUrl: e.target.value })}
placeholder="https://example.com/video.mp4"
/>
<SizeSettings selectedObject={selectedObject} onUpdate={onUpdate} defaultWidth={400} defaultHeight={300} />
</>
);
};
export default VideoSettings;
@@ -0,0 +1,8 @@
export { default as TextSettings } from "./TextSettings";
export { default as ShapeSettings } from "./ShapeSettings";
export { default as LineSettings } from "./LineSettings";
export { default as SizeSettings } from "./SizeSettings";
export { default as LinkSettings } from "./LinkSettings";
export { default as VideoSettings } from "./VideoSettings";
export { default as TransformSettings } from "./TransformSettings";