diff --git a/public/rich-components/input-with-stepper.svg b/public/rich-components/input-with-stepper.svg new file mode 100644 index 00000000..be747f48 --- /dev/null +++ b/public/rich-components/input-with-stepper.svg @@ -0,0 +1,15 @@ + + + + + + 0 + + + + + + + + + \ No newline at end of file diff --git a/src/common/components/front-components/input-shape.tsx b/src/common/components/front-components/input-shape.tsx index 32899dc3..df3ef440 100644 --- a/src/common/components/front-components/input-shape.tsx +++ b/src/common/components/front-components/input-shape.tsx @@ -10,10 +10,10 @@ import { useShapeProps } from '../shapes/use-shape-props.hook'; const inputShapeRestrictions: ShapeSizeRestrictions = { minWidth: 60, minHeight: 38, - maxWidth: -1, + maxWidth: 2000, maxHeight: 38, - defaultWidth: INPUT_SHAPE.DEFAULT_TEXT_WIDTH, - defaultHeight: INPUT_SHAPE.DEFAULT_TEXT_HEIGHT, + defaultWidth: 155, + defaultHeight: 38, }; export const getInputShapeSizeRestrictions = (): ShapeSizeRestrictions => diff --git a/src/common/components/front-rich-components/index.ts b/src/common/components/front-rich-components/index.ts index db76ce59..e9a5dd0e 100644 --- a/src/common/components/front-rich-components/index.ts +++ b/src/common/components/front-rich-components/index.ts @@ -13,3 +13,4 @@ export * from './modal/modal'; export * from './appBar'; export * from './buttonBar/buttonBar'; export * from './tabsbar'; +export * from './input-with-stepper/'; diff --git a/src/common/components/front-rich-components/input-with-stepper/index.ts b/src/common/components/front-rich-components/input-with-stepper/index.ts new file mode 100644 index 00000000..05cd467b --- /dev/null +++ b/src/common/components/front-rich-components/input-with-stepper/index.ts @@ -0,0 +1,2 @@ +export * from './input-with-stepper'; +export * from './input-with-stepper.business'; diff --git a/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts new file mode 100644 index 00000000..50c75d65 --- /dev/null +++ b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.business.ts @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; + +type MustBeANumberError = 'You must enter a number'; + +interface handleCounterInputWithStepperHook { + valueToString: string | MustBeANumberError; + handleIncrement: () => void; + handleDecrement: () => void; + isTextANumber: boolean; +} + +const MUST_BE_A_NUMBER: MustBeANumberError = 'You must enter a number'; + +export const useHandleCounterInputWithStepper = ( + text: string +): handleCounterInputWithStepperHook => { + const [value, setValue] = React.useState(0); + + const textToNumber = parseInt(text); + + const isTextANumber: boolean = !isNaN(textToNumber); + + useEffect(() => { + if (isTextANumber) { + setValue(textToNumber); + } else { + setValue(MUST_BE_A_NUMBER); + } + }, [text]); + + const handleIncrement = () => { + if (typeof value === 'number') { + setValue(value + 1); + } + }; + + const handleDecrement = () => { + if (typeof value === 'number') { + if (value === 0) return; + setValue(value - 1); + } + }; + + const valueToString: string = + typeof value === 'string' ? value : value.toString(); + + return { + valueToString, + handleIncrement, + handleDecrement, + isTextANumber, + }; +}; + +export const handleButtonWidth = (restrictedWidth: number): number => { + const buttonWidth = restrictedWidth * 0.3; + const minButtonWidth = 30; + const maxButtonWidth = 70; + + if (buttonWidth < minButtonWidth) return minButtonWidth; + if (buttonWidth > maxButtonWidth) return maxButtonWidth; + return buttonWidth; +}; diff --git a/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx new file mode 100644 index 00000000..2f08a18a --- /dev/null +++ b/src/common/components/front-rich-components/input-with-stepper/input-with-stepper.tsx @@ -0,0 +1,182 @@ +import { forwardRef, useMemo } from 'react'; +import { Group, Rect, Text } from 'react-konva'; +import { ShapeSizeRestrictions } from '@/core/model'; +import { ShapeType } from '../../../../core/model/index'; +import { fitSizeToShapeSizeRestrictions } from '@/common/utils/shapes'; +import { useShapeComponentSelection } from '../../shapes/use-shape-selection.hook'; +import { ShapeProps } from '../../front-components/shape.model'; +import { + handleButtonWidth, + useHandleCounterInputWithStepper, +} from './input-with-stepper.business'; +import { INPUT_SHAPE } from '../../front-components/shape.const'; +import { KonvaEventObject } from 'konva/lib/Node'; + +const inputWithStepperSizeRestrictions: ShapeSizeRestrictions = { + minWidth: 60, + minHeight: 35, + maxWidth: 500, + maxHeight: 35, + defaultWidth: 100, + defaultHeight: 35, +}; + +export const getInputWithStepperSizeRestrictions = (): ShapeSizeRestrictions => + inputWithStepperSizeRestrictions; + +const shapeType: ShapeType = 'inputWithStepper'; + +export const InputWithStepperShape = forwardRef( + (props, ref) => { + const { x, y, width, height, id, text, otherProps, ...shapeProps } = props; + + const { width: restrictedWidth, height: restrictedHeight } = + fitSizeToShapeSizeRestrictions( + inputWithStepperSizeRestrictions, + width, + height + ); + + const { handleSelection } = useShapeComponentSelection(props, shapeType); + + const handleDoubleClickInButtons = (e: KonvaEventObject) => + (e.cancelBubble = true); + + const { + valueToString: value, + handleIncrement, + handleDecrement, + isTextANumber, + } = useHandleCounterInputWithStepper(text); + + const stroke = useMemo( + () => otherProps?.stroke ?? INPUT_SHAPE.DEFAULT_STROKE_COLOR, + [otherProps?.stroke] + ); + + const fill = useMemo( + () => otherProps?.backgroundColor ?? INPUT_SHAPE.DEFAULT_FILL_BACKGROUND, + [otherProps?.backgroundColor] + ); + + const textColor = useMemo( + () => otherProps?.textColor ?? INPUT_SHAPE.DEFAULT_FILL_TEXT, + [otherProps?.textColor] + ); + + const strokeStyle = useMemo( + () => otherProps?.strokeStyle ?? [], + [otherProps?.strokeStyle] + ); + + // Reservar espacio para el stepper + const buttonWidth = handleButtonWidth(restrictedWidth); + const buttonHeight = restrictedHeight / 2; + + return ( + + {/* Caja del input */} + + + {/* Texto del input */} + + + {/* Botón de incremento (flecha arriba) */} + + + + + + {/* Botón de decremento (flecha abajo) */} + + + + + {!isTextANumber && ( + + + + )} + + ); + } +); + +export default InputWithStepperShape; diff --git a/src/core/model/index.ts b/src/core/model/index.ts index 442f9da7..87e99020 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -64,6 +64,7 @@ export type ShapeType = | 'appBar' | 'buttonBar' | 'tooltip' + | 'inputWithStepper' | 'slider'; export const ShapeDisplayName: Record = { @@ -119,6 +120,7 @@ export const ShapeDisplayName: Record = { buttonBar: 'Button Bar', tooltip: 'Tooltip', slider: 'Slider', + inputWithStepper: 'Input With Stepper', }; export type EditType = 'input' | 'textarea' | 'imageupload'; diff --git a/src/pods/canvas/canvas.model.ts b/src/pods/canvas/canvas.model.ts index 3811f8b4..3d322f43 100644 --- a/src/pods/canvas/canvas.model.ts +++ b/src/pods/canvas/canvas.model.ts @@ -61,6 +61,7 @@ import { getAppBarShapeSizeRestrictions, getButtonBarShapeSizeRestrictions, getTabsBarShapeSizeRestrictions, + getInputWithStepperSizeRestrictions, } from '@/common/components/front-rich-components'; import { getHeading1SizeRestrictions, @@ -181,6 +182,8 @@ export const getSizeRestrictionFromShape = ( return getTooltipShapeSizeRestrictions(); case 'slider': return getSliderShapeSizeRestrictions(); + case 'inputWithStepper': + return getInputWithStepperSizeRestrictions(); default: console.warn( `** Shape ${shapeType} has not defined default size, check getDefaultSizeFromShape helper function` @@ -238,6 +241,7 @@ const doesShapeAllowInlineEdition = (shapeType: ShapeType): boolean => { case 'buttonBar': case 'tabsBar': case 'tooltip': + case 'inputWithStepper': return true; default: return false; @@ -269,6 +273,7 @@ const generateTypeOfTransformer = (shapeType: ShapeType): string[] => { case 'appBar': case 'buttonBar': case 'slider': + case 'inputWithStepper': return ['middle-left', 'middle-right']; case 'verticalScrollBar': return ['top-center', 'bottom-center']; @@ -343,6 +348,8 @@ const generateDefaultTextValue = (shapeType: ShapeType): string | undefined => { return 'Button 1, Button 2, Button 3'; case 'tabsBar': return 'Tab 1, Tab 2, Tab 3'; + case 'inputWithStepper': + return '0'; default: return undefined; } @@ -376,6 +383,13 @@ export const generateDefaultOtherProps = ( shapeType: ShapeType ): OtherProps | undefined => { switch (shapeType) { + case 'inputWithStepper': + return { + stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR, + backgroundColor: INPUT_SHAPE.DEFAULT_FILL_BACKGROUND, + textColor: INPUT_SHAPE.DEFAULT_FILL_TEXT, + strokeStyle: [], + }; case 'input': return { stroke: INPUT_SHAPE.DEFAULT_STROKE_COLOR, diff --git a/src/pods/canvas/shape-renderer/index.tsx b/src/pods/canvas/shape-renderer/index.tsx index b4a9f84d..1729eb5f 100644 --- a/src/pods/canvas/shape-renderer/index.tsx +++ b/src/pods/canvas/shape-renderer/index.tsx @@ -40,6 +40,7 @@ import { renderModal, renderButtonBar, renderTabsBar, + renderInputWithStepper, } from './simple-rich-components'; import { renderDiamond, @@ -170,6 +171,8 @@ export const renderShapeComponent = ( return renderTooltip(shape, shapeRenderedProps); case 'slider': return renderSlider(shape, shapeRenderedProps); + case 'inputWithStepper': + return renderInputWithStepper(shape, shapeRenderedProps); default: return renderNotFound(shape, shapeRenderedProps); } diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts index cbee3ae9..6cd811e3 100644 --- a/src/pods/canvas/shape-renderer/simple-rich-components/index.ts +++ b/src/pods/canvas/shape-renderer/simple-rich-components/index.ts @@ -13,3 +13,4 @@ export * from './modal.renderer'; export * from './appBar.renderer'; export * from './button-bar.renderer'; export * from './tabsbar.renderer'; +export * from './input-with-stepper.renderer'; diff --git a/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx new file mode 100644 index 00000000..7767e288 --- /dev/null +++ b/src/pods/canvas/shape-renderer/simple-rich-components/input-with-stepper.renderer.tsx @@ -0,0 +1,32 @@ +import { InputWithStepperShape } from '@/common/components/front-rich-components'; +import { ShapeRendererProps } from '../model'; +import { ShapeModel } from '@/core/model'; + +export const renderInputWithStepper = ( + shape: ShapeModel, + shapeRenderedProps: ShapeRendererProps +) => { + const { handleSelected, shapeRefs, handleDragEnd, handleTransform } = + shapeRenderedProps; + + return ( + + ); +}; diff --git a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts index ca8bfa0b..537ad1bc 100644 --- a/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts +++ b/src/pods/galleries/rich-components-gallery/rich-components-gallery-data/index.ts @@ -16,6 +16,10 @@ export const mockRichComponentsCollection: ItemInfo[] = [ { thumbnailSrc: '/rich-components/breadcrumb.svg', type: 'breadcrumb' }, { thumbnailSrc: '/rich-components/modal.svg', type: 'modal' }, { thumbnailSrc: '/rich-components/tabsbar.svg', type: 'tabsBar' }, + { + thumbnailSrc: '/rich-components/input-with-stepper.svg', + type: 'inputWithStepper', + }, { thumbnailSrc: '/rich-components/calendar.svg', type: 'calendar' }, { thumbnailSrc: '/rich-components/videoPlayer.svg', type: 'videoPlayer' }, { thumbnailSrc: '/rich-components/pie.svg', type: 'pie' },