diff --git a/commitlint.config.js b/commitlint.config.js index e24798b..a55f3ad 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -12,10 +12,12 @@ module.exports = { "Prerelease", "Chore", "Build", + "Debug", "Perf", "Refactor", "Revert", "CI", + "Style", "Test", "Docs", "WIP" diff --git a/lefthook.yml b/lefthook.yml index 9e8191c..45abf28 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -13,4 +13,4 @@ commit-msg: commitlint: skip: - rebase - run: npx commitlint --edit --color + run: bunx --bun commitlint --edit --color diff --git a/src/components/controls/select/general.jsx b/src/components/controls/select/general.tsx similarity index 89% rename from src/components/controls/select/general.jsx rename to src/components/controls/select/general.tsx index ec18805..9e7d528 100644 --- a/src/components/controls/select/general.jsx +++ b/src/components/controls/select/general.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/core"; import { d3Extended } from "@/utils"; const GeneralSelectControls = () => { - const selectedElementIds = useSelector((state) => state.editor.selectedElementIds); + const selectedElementIds = useSelector((state: any) => state.editor.selectedElementIds); return (
diff --git a/src/components/controls/select/index.jsx b/src/components/controls/select/index.tsx similarity index 93% rename from src/components/controls/select/index.jsx rename to src/components/controls/select/index.tsx index 13f8c7b..12c3b8f 100644 --- a/src/components/controls/select/index.jsx +++ b/src/components/controls/select/index.tsx @@ -9,7 +9,7 @@ import { default as ShapeSelectControls } from "./shape"; import { default as TextSelectControls } from "./text"; const SelectControls = () => { - const selectedElementIds = useSelector((state) => state.editor.selectedElementIds); + const selectedElementIds = useSelector((state: any) => state.editor.selectedElementIds); const ControlComponent = useMemo(() => { const firstElementType = document.getElementById(selectedElementIds[0])?.getAttribute?.(dataAttributes.elementType); diff --git a/src/components/controls/select/text.jsx b/src/components/controls/select/text.tsx similarity index 96% rename from src/components/controls/select/text.jsx rename to src/components/controls/select/text.tsx index ec0c41e..4cd03a6 100644 --- a/src/components/controls/select/text.jsx +++ b/src/components/controls/select/text.tsx @@ -3,7 +3,7 @@ import { d3Extended, rgbToHex } from "@/utils"; import { default as ControlInput } from "../control-input"; const TextSelectControls = () => { - const selectedElementIds = useSelector((state) => state.editor.selectedElementIds); + const selectedElementIds = useSelector((state: any) => state.editor.selectedElementIds); const firstElement = document.getElementById(selectedElementIds[0]); diff --git a/src/components/core/button.tsx b/src/components/core/button.tsx index fe1b20a..9627456 100644 --- a/src/components/core/button.tsx +++ b/src/components/core/button.tsx @@ -48,6 +48,7 @@ interface ButtonProps extends React.HTMLProps { wrapperClassName?: string; target?: string; ariaLabel?: string; + variant?: "primary" | "secondary"; } const Button = ({ to, wrapperClassName, target, ariaLabel, ...props }: ButtonProps) => { diff --git a/src/components/core/icon-button.tsx b/src/components/core/icon-button.tsx index 508be82..f9bda17 100644 --- a/src/components/core/icon-button.tsx +++ b/src/components/core/icon-button.tsx @@ -2,7 +2,12 @@ import { twMerge } from "tailwind-merge"; import { default as Button } from "./button"; import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"; -const IconButton = ({ label, icon, className, ...props }) => { +interface IconButtonProps extends React.ComponentProps { + label?: string; + icon: React.ReactNode; +} + +const IconButton: React.FC = ({ label, icon, className, ...props }) => { const button = ( - - - ) : ( - <> - } label="Preview" /> - } label="Export JSON" onClick={onExportJson} /> - - )} - -
- - ); -}; - -export default Operations; diff --git a/src/components/operations/index.tsx b/src/components/operations/index.tsx new file mode 100644 index 0000000..faaf746 --- /dev/null +++ b/src/components/operations/index.tsx @@ -0,0 +1,122 @@ +import { Braces, Cog, Eye } from "lucide-react"; +import { useSelector } from "react-redux"; +import { twMerge } from "tailwind-merge"; +import { ids } from "@/constants"; +import { useBreakpoint } from "@/hooks"; +import { store } from "@/store"; +import { locationPlaceholder, setLocation, toggleControls } from "@/store/reducers/editor"; +import { ISTKProps } from "@/types"; +import { stateToJSON } from "@/utils"; +import { Body, Button, IconButton } from "../core"; +import { default as GridSwitch } from "./grid-switch"; + +const onCogClick = () => store.dispatch(toggleControls()); + +const Operations: React.FC = ({ + options: { showGridSwitch = true, exportButtonText = "Export JSON", operationTriggerIcon } = {}, + events, + ...props +}) => { + const { md } = useBreakpoint(); + + const location = useSelector((state: any) => state.editor.location); + + const styles = props.styles?.operations; + + const coreStyles = props.styles?.core; + + const OperationTriggerIcon = operationTriggerIcon ?? Cog; + + const onLocationChange = (e) => { + const location = e.target.innerText; + if (!location) { + document.getElementById("stk-location-name").innerText = locationPlaceholder; + return store.dispatch(setLocation(locationPlaceholder)); + } + store.dispatch(setLocation(location)); + }; + + const onExportJson = () => { + const json = stateToJSON(); + if (events?.onExport) { + events?.onExport(json); + } else { + console.log(json); + navigator.clipboard.writeText(JSON.stringify(json)); + } + }; + + return ( +
+ + {locationPlaceholder} + +
+ {showGridSwitch && } + {md ? ( + <> + + + + ) : ( + <> + } + label="Preview" + className={coreStyles?.button?.className} + style={coreStyles?.button?.properties} + /> + } + label={exportButtonText} + onClick={onExportJson} + className={coreStyles?.button?.className} + style={coreStyles?.button?.properties} + /> + + )} + +
+
+ ); +}; + +export default Operations; diff --git a/src/components/workspace/elements/booth.tsx b/src/components/workspace/elements/booth.tsx index 54fd545..07c36e4 100644 --- a/src/components/workspace/elements/booth.tsx +++ b/src/components/workspace/elements/booth.tsx @@ -1,8 +1,11 @@ import { forwardRef } from "react"; +import { IBooth } from "@/types"; export const boothSize = 39; -const Booth: React.FC = forwardRef(({ id, x, y, ...props }, ref: any) => { +export interface IBoothProps extends IBooth {} + +const Booth: React.FC = forwardRef(({ id, x, y, ...props }, ref: any) => { return ; }); diff --git a/src/components/workspace/elements/image.tsx b/src/components/workspace/elements/image.tsx index 40d2916..9ea4049 100644 --- a/src/components/workspace/elements/image.tsx +++ b/src/components/workspace/elements/image.tsx @@ -1,7 +1,12 @@ import { forwardRef } from "react"; import { twMerge } from "tailwind-merge"; +import { IImage } from "@/types"; -const Image: React.FC = forwardRef(({ x, y, id, href, width, height, ...props }, ref: any) => { +export interface IImageProps extends IImage { + className?: string; +} + +const Image: React.FC = forwardRef(({ x, y, id, href, width, height, ...props }, ref: any) => { return ( { +export const Element = ({ type = ElementType.Seat, id, x = 250, y = 250, isSelected = false, consumer, ...props }) => { const ref = useRef(); - const Element = elements[type]; + const Element = elements[type] as any; useEffect(() => { - if (!ref.current || options.mode !== STKMode.Designer) return; + if (!ref.current || consumer.mode !== STKMode.Designer) return; const node = d3.select(ref.current); if (type === ElementType.Seat) { handleSeatDrag(node); @@ -38,7 +38,7 @@ export const Element = ({ type = ElementType.Seat, id, x = 250, y = 250, isSelec } else { handleDrag(node); } - }, [ref, options.mode]); + }, [ref, consumer.mode]); const onClick = (e: any) => { const selectedTool = store.getState().toolbar.selectedTool; @@ -77,7 +77,7 @@ export const Element = ({ type = ElementType.Seat, id, x = 250, y = 250, isSelec !props.color && type !== ElementType.Text && "text-white" )} onClick={onClick} - options={options} + consumer={consumer} {...{ [dataAttributes.elementType]: type }} /> ); diff --git a/src/components/workspace/elements/polyline.tsx b/src/components/workspace/elements/polyline.tsx index 829ee9e..f4e6504 100644 --- a/src/components/workspace/elements/polyline.tsx +++ b/src/components/workspace/elements/polyline.tsx @@ -1,8 +1,13 @@ import { forwardRef } from "react"; import { twMerge } from "tailwind-merge"; import { dataAttributes } from "@/constants"; +import { IPolyline } from "@/types"; -const Polyline: React.FC = forwardRef(({ id, points, color, stroke, section, ...props }, ref: any) => { +export interface IPolylineProps extends IPolyline { + className?: string; +} + +const Polyline: React.FC = forwardRef(({ id, points, color, stroke, section, ...props }, ref: any) => { return ( = forwardRef( - ({ x, y, id, label, categories, category, status, onClick, options, element, ...props }, ref: any) => { +export interface ISeatProps extends ISeat { + className?: string; + consumer: ISTKProps; + element: ISeat; + categories: ISeatCategory[]; + onClick: (e: any) => void; +} + +const Seat: React.FC = forwardRef( + ({ x, y, id, label, categories, category, status, onClick, consumer, element, ...props }, ref: any) => { const categoryObject = useMemo(() => categories?.find?.((c) => c.id === category), [categories, category]); + const showLabel = consumer.options?.showSeatLabels ?? true; + const textX = useMemo(() => { let value = (+ref.current?.getAttribute("cx") || x) - seatLabelFontSize / 3.5; const labelLength = label?.toString()?.length ?? 0; @@ -42,7 +53,7 @@ const Seat: React.FC = forwardRef( const localOnClick = (e) => { onClick(e); - options.events?.onSeatClick?.({ + consumer.events?.onSeatClick?.({ ...element, category: categoryObject }); @@ -61,7 +72,7 @@ const Seat: React.FC = forwardRef( {...{ [dataAttributes.status]: status ?? SeatStatus.Available }} {...props} /> - {label && ( + {label && showLabel && ( = forwardRef( +export interface IShapeProps extends IShape { + className?: string; + resizable?: boolean; +} + +const Shape: React.FC = forwardRef( ({ x, y, id, name, width, height, rx, resizable, className, stroke, color, ...props }, ref: any) => { if (name === "RectangleHorizontal") { return ( diff --git a/src/components/workspace/elements/text.tsx b/src/components/workspace/elements/text.tsx index 81d37db..5d0afb4 100644 --- a/src/components/workspace/elements/text.tsx +++ b/src/components/workspace/elements/text.tsx @@ -1,8 +1,11 @@ import { forwardRef } from "react"; +import { IText } from "@/types"; export const textFontSize = 35; -const Text: React.FC = forwardRef( +export interface ITextProps extends IText {} + +const Text: React.FC = forwardRef( ({ x, y, id, label, fontSize = textFontSize, fontWeight = 200, letterSpacing = 3, color, ...props }, ref: any) => { return ( = (props) => { label: elem.label, color: elem.color, stroke: elem.stroke, - options: { + consumer: { mode: props.mode, - events: props.events + events: props.events, + options: props.options }, element: elem }), diff --git a/src/hooks/breakpoint.ts b/src/hooks/breakpoint.ts index 0c6da08..df49c88 100644 --- a/src/hooks/breakpoint.ts +++ b/src/hooks/breakpoint.ts @@ -18,7 +18,7 @@ const calculateBreakpoints = () => * * console.log(md); // true if the screen is at least 768px wide */ -const useBreakpoint = () => { +const useBreakpoint = (): Record => { const [breakpoints, setBreakpoints] = useState(calculateBreakpoints()); useEffect(() => { const resizeHandler = () => { diff --git a/src/types/elements/seat.ts b/src/types/elements/seat.ts index 9397ceb..da2370f 100644 --- a/src/types/elements/seat.ts +++ b/src/types/elements/seat.ts @@ -20,3 +20,7 @@ export interface ISeat { category?: string | null; status?: SeatStatus | string; } + +export interface IPopulatedSeat extends Omit { + category?: ISeatCategory; +} diff --git a/src/types/index.ts b/src/types/index.ts index 4aacc0c..eeac2d4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -import { IBooth, IImage, IPolyline, ISeat, ISeatCategory, ISection, IShape, IText } from "./elements"; +import { IBooth, IImage, IPolyline, IPopulatedSeat, ISeat, ISeatCategory, ISection, IShape, IText } from "./elements"; import { IStyles } from "./styles"; export * from "./elements"; @@ -9,7 +9,7 @@ export enum STKMode { } export interface IEvents { - onSeatClick?: (seat: ISeat & { category?: ISeatCategory }) => void; + onSeatClick?: (seat: IPopulatedSeat) => void; onSectionClick?: (section: ISection) => void; onExport?: (data: ISTKData) => void; } @@ -31,4 +31,11 @@ export interface ISTKProps { events?: IEvents; data?: ISTKData; styles?: IStyles; + options?: { + showGridSwitch?: boolean; + showSeatLabels?: boolean; + showFooter?: boolean; + exportButtonText?: string; + operationTriggerIcon?: React.FC; + }; } diff --git a/src/types/styles.ts b/src/types/styles.ts index f3e4f73..af9fc97 100644 --- a/src/types/styles.ts +++ b/src/types/styles.ts @@ -18,9 +18,31 @@ export interface IStyles { }; divider?: IStyle; }; + operations?: { + root?: IStyle; + input?: IStyle; + trigger?: IStyle; + }; footer: { root?: IStyle; title?: IStyle; meta?: IStyle; }; + elements?: { + all?: { + selected?: IStyle; + unselected?: IStyle; + }; + seats?: { + selected?: IStyle; + unselected?: IStyle; + }; + text?: { + selected?: IStyle; + unselected?: IStyle; + }; + }; + core?: { + button?: IStyle; + }; }