From 3454129dcaa93852a1addc27037d92018d530f63 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 20 Nov 2025 08:35:50 +0330 Subject: [PATCH] text component --- src/components/Select.tsx | 63 +++++++ .../components/canvas/useDrawingHandlers.ts | 11 +- .../components/sidebar/ObjectSettings.tsx | 4 +- .../sidebar/instructions/TextInstruction.tsx | 172 +++++++++++++++++- .../editor/components/tools/TextShape.tsx | 46 ++++- src/pages/editor/store/editorStore.ts | 5 + src/pages/editor/store/textDefaultsStore.ts | 33 ++++ 7 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 src/components/Select.tsx create mode 100644 src/pages/editor/store/textDefaultsStore.ts diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..8242dfe --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,63 @@ +import { type FC, type SelectHTMLAttributes } from 'react' +import { clx } from '../helpers/utils' +import { ArrowDown2 } from 'iconsax-react' +import Error from './Error' + +export type ItemsSelectType = { + value: string, + label: string, +} +type Props = { + className?: string, + items: ItemsSelectType[], + error_text?: string, + placeholder?: string, + label?: string, + value?: string | number, +} & SelectHTMLAttributes + +const Select: FC = (props: Props) => { + return ( +
+ { + props.label && + + } + + + { + props.error_text && props.error_text !== '' ? + + : null + } +
+ + ) +} + +export default Select \ No newline at end of file diff --git a/src/pages/editor/components/canvas/useDrawingHandlers.ts b/src/pages/editor/components/canvas/useDrawingHandlers.ts index 5f8d9d1..222d841 100644 --- a/src/pages/editor/components/canvas/useDrawingHandlers.ts +++ b/src/pages/editor/components/canvas/useDrawingHandlers.ts @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from "react"; import Konva from "konva"; import { useEditorStore, type EditorObject } from "@/pages/editor/store/editorStore"; +import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore"; import { createDrawingObject } from "./drawingUtils"; export const useDrawingHandlers = () => { @@ -10,6 +11,7 @@ export const useDrawingHandlers = () => { const transformerRef = useRef(null); const { tool, addObject, updateObject, setSelectedObjectId, selectedObjectId } = useEditorStore(); + const { defaults } = useTextDefaultsStore(); useEffect(() => { setTempObject(null); @@ -59,8 +61,13 @@ export const useDrawingHandlers = () => { x: pointerPos.x, y: pointerPos.y, text: "متن جدید", - fontSize: 24, - fill: "#000000", + fontSize: defaults.fontSize, + fontFamily: defaults.fontFamily, + fontWeight: defaults.fontWeight, + fill: defaults.fill, + opacity: defaults.opacity, + letterSpacing: defaults.letterSpacing, + wordSpacing: defaults.wordSpacing, }; addObject(newText); setSelectedObjectId(newText.id); diff --git a/src/pages/editor/components/sidebar/ObjectSettings.tsx b/src/pages/editor/components/sidebar/ObjectSettings.tsx index 03ad92e..4d21293 100644 --- a/src/pages/editor/components/sidebar/ObjectSettings.tsx +++ b/src/pages/editor/components/sidebar/ObjectSettings.tsx @@ -1,7 +1,6 @@ import { Trash } from "iconsax-react"; import type { EditorObject } from "../../store/editorStore"; import { - TextSettings, ShapeSettings, LineSettings, SizeSettings, @@ -10,6 +9,7 @@ import { TransformSettings, GridSettings, } from "./settings"; +import TextInstruction from "./instructions/TextInstruction"; type ObjectSettingsProps = { selectedObject: EditorObject; @@ -30,7 +30,7 @@ const ObjectSettings = ({ selectedObject, onUpdate, onDelete }: ObjectSettingsPr - {selectedObject.type === "text" && } + {selectedObject.type === "text" && } {(selectedObject.type === "rectangle" || selectedObject.type === "circle") && ( diff --git a/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx b/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx index deb2e64..a1e0219 100644 --- a/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx +++ b/src/pages/editor/components/sidebar/instructions/TextInstruction.tsx @@ -1,8 +1,170 @@ -const TextInstruction = () => ( -
-

برای افزودن متن، روی کانوس کلیک کنید

-
-); +import { type FC, useRef, useState, useEffect } from "react"; +import Input from "@/components/Input"; +import Select from "@/components/Select"; +import { useEditorStore } from "@/pages/editor/store/editorStore"; +import { useTextDefaultsStore } from "@/pages/editor/store/textDefaultsStore"; + +const fontOptions = [ + { label: "ایرانسل", value: "1" }, + { label: "فونت 2", value: "2" }, +]; + +const fontWeightOptions = [ + { label: "Bold", value: "bold" }, + { label: "Normal", value: "normal" }, + { label: "Light", value: "300" }, +]; + +const TextInstruction: FC = () => { + const { selectedObjectId, objects, updateObject } = useEditorStore(); + const { defaults, updateDefaults } = useTextDefaultsStore(); + + const selectedObject = selectedObjectId + ? objects.find((obj) => obj.id === selectedObjectId && obj.type === "text") + : null; + + const isEditing = !!selectedObject; + const currentValues = isEditing + ? { + fontFamily: selectedObject.fontFamily || defaults.fontFamily, + fontWeight: selectedObject.fontWeight || defaults.fontWeight, + fontSize: selectedObject.fontSize || defaults.fontSize, + fill: selectedObject.fill || defaults.fill, + opacity: selectedObject.opacity ?? defaults.opacity, + letterSpacing: selectedObject.letterSpacing ?? defaults.letterSpacing, + wordSpacing: selectedObject.wordSpacing ?? defaults.wordSpacing, + } + : defaults; + + const handleChange = (field: string, value: string | number) => { + if (isEditing && selectedObject) { + updateObject(selectedObject.id, { [field]: value }); + } else { + updateDefaults({ [field]: value }); + } + }; + + const hexColor = (currentValues.fill || "#000000").replace("#", "").toUpperCase(); + const opacityValue = currentValues.opacity; + const colorInputRef = useRef(null); + const [opacityInput, setOpacityInput] = useState(`${opacityValue}`); + + useEffect(() => { + setOpacityInput(`${opacityValue}`); + }, [opacityValue]); + + const handleColorClick = () => { + colorInputRef.current?.click(); + }; + + const handleOpacityChange = (e: React.ChangeEvent) => { + const value = e.target.value.replace("%", "").trim(); + setOpacityInput(value); + }; + + const handleOpacityBlur = () => { + const numValue = parseInt(opacityInput) || 0; + const clampedValue = Math.min(100, Math.max(0, numValue)); + setOpacityInput(`${clampedValue}`); + if (clampedValue !== opacityValue) { + handleChange("opacity", clampedValue); + } + }; + + return ( +
+

متن

+
+ handleChange("fontWeight", e.target.value)} + /> + + { + const value = e.target.value.replace("px", "").trim(); + const numValue = parseInt(value) || 0; + handleChange("fontSize", numValue); + }} + /> +
+ +
+
رنگ
+
+ + handleChange("fill", e.target.value)} + className="hidden" + /> +
+ { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + }} + /> +
+
+
+ +
+ { + const value = e.target.value.replace("px", "").trim(); + const numValue = parseInt(value) || 0; + handleChange("wordSpacing", numValue); + }} + /> + + { + const value = e.target.value.replace("px", "").trim(); + const numValue = parseInt(value) || 0; + handleChange("letterSpacing", numValue); + }} + /> +
+
+ ); +}; export default TextInstruction; diff --git a/src/pages/editor/components/tools/TextShape.tsx b/src/pages/editor/components/tools/TextShape.tsx index 5a6fa94..1114075 100644 --- a/src/pages/editor/components/tools/TextShape.tsx +++ b/src/pages/editor/components/tools/TextShape.tsx @@ -1,8 +1,38 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useMemo } from "react"; import { Text } from "react-konva"; import Konva from "konva"; import type { TextShapeProps } from "./types"; +const getFontFamily = (fontFamily?: string): string => { + const fontMap: Record = { + "1": "irancell", + "2": "irancell", + }; + return fontMap[fontFamily || "1"] || "irancell"; +}; + +const getFontWeight = (fontWeight?: string): string | number => { + const weightMap: Record = { + bold: "600", + normal: "300", + "300": "300", + }; + return weightMap[fontWeight || "normal"] || "300"; +}; + +const getColorWithOpacity = (fill?: string, opacity?: number): string => { + if (!fill) return "#000000"; + if (opacity === undefined || opacity === 100) return fill; + + const hex = fill.replace("#", ""); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + const alpha = opacity / 100; + + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; + const TextShape = ({ obj, isSelected, @@ -21,6 +51,10 @@ const TextShape = ({ } }, [isSelected, transformerRef]); + const fontFamily = useMemo(() => getFontFamily(obj.fontFamily), [obj.fontFamily]); + const fontWeight = useMemo(() => getFontWeight(obj.fontWeight), [obj.fontWeight]); + const fillColor = useMemo(() => getColorWithOpacity(obj.fill, obj.opacity), [obj.fill, obj.opacity]); + return ( ) => void; +}; + +export const useTextDefaultsStore = create((set) => ({ + defaults: { + fontFamily: "1", + fontWeight: "bold", + fontSize: 16, + fill: "#A27BC1", + opacity: 100, + letterSpacing: 10, + wordSpacing: 10, + }, + updateDefaults: (updates) => + set((state) => ({ + defaults: { ...state.defaults, ...updates }, + })), +})); +