diff --git a/src/hooks/useComponent.ts b/src/hooks/useComponent.ts new file mode 100644 index 0000000..2cb531e --- /dev/null +++ b/src/hooks/useComponent.ts @@ -0,0 +1,265 @@ +import type { ButtonGeneralData, Component, GeneralData, TableGeneralData, TextFieldGeneralData, TextGeneralData, UIData } from "../types/component"; +import { ComponentType } from "../types/component"; +import type { Screen } from "../types/screen"; +import { useCallback, useMemo, useRef, useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; + +export interface useComponentData { + selectingComponent: Component | null, + selectingComponentIndex: number, + curScreenComponents: Component[] +} + +export type SelectComponentListenerType = (selected: Component | null) => void + +export interface useComponentActions { + addComponent: (type: ComponentType) => void, + addComponents: (components: Component[], screenId: string) => void + moveComponent: (index: number, uiData: UIData) => void, + updateComponent: (index: number, newComponent: Component) => void, + deleteComponent: (index: number, id: string) => void, + setSelectComponentListener: (listner: SelectComponentListenerType) => void + selectComponent: (index: number) => void, + resetSelectingComponent: () => void, + activateSelectMode: () => void +} + +export interface useComponentDispatch { + setSelectingComponentIndex: Dispatch>, +} + +export interface ComponentDataAndActions { + componentData: useComponentData, + componentActions: useComponentActions, + componentDispatch: useComponentDispatch +} + +export function useComponent( + curScreenIndex: number, + curScreen: Screen, + setScreens: Dispatch> +): ComponentDataAndActions { + + const [selectingComponentIndex, setSelectingComponentIndex] = useState(-1); + const [selectMode, setSelectMode] = useState(false); + // const [selectComponentCallback, setSelectComponentCallback] = useState(null); + const selectComponentCallbackRef = useRef(null); + const curScreenComponents: Component[] = useMemo(() => curScreen.components, [curScreen]) + const selectingComponent: Component | null = useMemo(() => { + if (selectingComponentIndex === -1) { + return null; + } + return curScreenComponents[selectingComponentIndex] + }, [curScreenComponents, selectingComponentIndex]); + + function getInitialGeneralData(type: ComponentType): GeneralData { + switch (type) { + case ComponentType.Text: + return { text: "text" } + case ComponentType.Button: + return { text: "button" } + case ComponentType.TextField: + return { text: "text field" } + case ComponentType.Table: + return { referenceTableName: "", columns: [] } + } + } + + + const addComponent = async (type: ComponentType) => { + let nextIndex = 0; + if (curScreenComponents.length > 0) { + nextIndex = curScreenComponents[curScreenComponents.length - 1].index + 1; + } + + const uiData: UIData = { + posX: 100, + posY: 100, + width: 75, + height: 25 + } + const newId = "w" + self.crypto.randomUUID().replace(/-/g, "") + + let newComponent: Component; + + switch (type) { + case ComponentType.Button: + newComponent = { + id: newId, + name: newId, + type: type, + index: nextIndex, + screenId: curScreen.id, + generalData: getInitialGeneralData(type) as ButtonGeneralData, + style: {}, + uiData: uiData, + } + break; + case ComponentType.Text: + newComponent = { + id: newId, + name: newId, + type: type, + index: nextIndex, + screenId: curScreen.id, + generalData: getInitialGeneralData(type) as TextGeneralData, + style: {}, + uiData: uiData, + } + break; + case ComponentType.TextField: + newComponent = { + id: newId, + name: newId, + type: type, + index: nextIndex, + screenId: curScreen.id, + generalData: getInitialGeneralData(type) as TextFieldGeneralData, + style: {}, + uiData: uiData, + } + break; + case ComponentType.Table: + newComponent = { + id: newId, + name: newId, + type: type, + index: nextIndex, + screenId: curScreen.id, + generalData: getInitialGeneralData(type) as TableGeneralData, + style: {}, + uiData: uiData, + } + break; + } + + setScreens(screens => screens.map((screen, i) => + i === curScreenIndex ? { + ...screen, + components: [...screen.components, newComponent] + } : screen + )); + + // await fetch(`/api/projects/${curScreen.projectId}/screens/${curScreen.id}/components`, { + // method: "POST", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // newComponents: [newComponent], + // }), + // }); + } + + const addComponents = async (components: Component[], screenId: string) => { + setScreens(screens => screens.map((screen) => + screen.id === screenId ? { + ...screen, + components: [...screen.components, ...components] + } : screen + )); + + // await fetch(`/api/projects/${curScreen.projectId}/screens/${screenId}/components`, { + // method: "POST", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // newComponents: components, + // }), + // }); + } + + const moveComponent = async (index: number, uiData: UIData) => { + if (!selectingComponent) return; + setScreens(screens => screens.map((screen, i) => + i === curScreenIndex ? { + ...screen, + components: screens[i].components.map((comp, j) => j === index ? { ...comp, uiData: uiData } : comp) + } : screen + )); + // await fetch(`/api/projects/${curScreen.projectId}/screens/${curScreen.id}/components/${selectingComponent.id}/uiData`, { + // method: "PUT", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ uiData: uiData }), + // }); + } + + const deleteComponent = async (index: number) => { + setScreens((prev) => (prev.map((screen, scrIdx) => scrIdx === curScreenIndex ? { + ...screen, + components: screen.components.filter((_, compIdx) => compIdx !== index) + } : screen))); + + // await fetch(`/api/projects/${curScreen.projectId}/screens/${curScreen.id}/components/${id}`, { + // method: "DELETE", + // }); + } + + const updateComponent = (index: number, newComponent: Component) => { + if (index == -1) { //-1の時は現在選択中のコンポーネント + index = selectingComponentIndex + } + console.log(newComponent); + setScreens(screens => screens.map((screen, i) => + i === curScreenIndex ? { + ...screen, + components: screens[i].components.map((comp, j) => j === index ? newComponent : comp) + } : screen + )); + } + + const setSelectComponentListener = useCallback((listener: SelectComponentListenerType) => { + selectComponentCallbackRef.current = listener; + }, []) + + const selectComponent = (index: number) => { + if (selectMode) { + selectComponentCallbackRef.current?.(curScreenComponents[index]); + selectComponentCallbackRef.current = null; + setSelectMode(false); + } else { + setSelectingComponentIndex(index); + } + } + + + const resetSelectingComponent = () => { + if (selectMode) { + selectComponentCallbackRef.current?.(null); + selectComponentCallbackRef.current = null; + setSelectMode(false); + } else { + setSelectingComponentIndex(-1); + } + } + + const activateSelectMode = () => { + setSelectMode(true); + } + + return { + componentData: { + selectingComponent, + selectingComponentIndex, + curScreenComponents + }, + componentActions: { + addComponent, + addComponents, + deleteComponent, + moveComponent, + updateComponent, + setSelectComponentListener, + selectComponent, + resetSelectingComponent, + activateSelectMode + }, + componentDispatch: { + setSelectingComponentIndex + } + } + +} \ No newline at end of file diff --git a/src/hooks/useDragComponent.ts b/src/hooks/useDragComponent.ts new file mode 100644 index 0000000..f123cc8 --- /dev/null +++ b/src/hooks/useDragComponent.ts @@ -0,0 +1,130 @@ +import type { Component, UIData } from "../types/component"; +import { useEffect, useMemo, useState } from "react"; + +export function useDragComponent( + selectingComponent: Component | null, + selectingComponentIndex: number, + moveComponent: (index: number, uiData: UIData) => void, + scale: number, + widgetScale: number +) { + + const [componentX, setComponentX] = useState(0); + const [componentY, setComponentY] = useState(0); + const [componentW, setComponentW] = useState(0); + const [componentH, setComponentH] = useState(0); + const baseX = useMemo(() => selectingComponent ? selectingComponent.uiData.posX : 0, [selectingComponent]) + const baseY = useMemo(() => selectingComponent ? selectingComponent.uiData.posY : 0, [selectingComponent]) + const baseW = useMemo(() => selectingComponent ? selectingComponent.uiData.width : 0, [selectingComponent]) + const baseH = useMemo(() => selectingComponent ? selectingComponent.uiData.height : 0, [selectingComponent]) + + const [prevX, setPrevX] = useState(0); + const [prevY, setPrevY] = useState(0); + + const MIN_WIDTH = 50; + const MIN_HEIGHT = 20; + + useEffect(() => { + setComponentX(baseX); + }, [baseX]) + + useEffect(() => { + setComponentY(baseY) + }, [baseY]) + + useEffect(() => { + setComponentW(baseW); + }, [baseW]); + + useEffect(() => { + setComponentH(baseH); + }, [baseH]) + + const onDragStart = (e: React.DragEvent) => { + e.stopPropagation(); + setPrevX(e.clientX); + setPrevY(e.clientY); + const img = new Image(); + img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/VEvKukAAAAASUVORK5CYII="; // 1x1透明PNG + e.dataTransfer.setDragImage(img, 0, 0); + } + + const draggingComponent = (e: React.DragEvent) => { + e.stopPropagation(); + if (e.clientX == 0 || e.clientY == 0) { + return; + } + setComponentX((prev) => prev + (e.clientX - prevX) / (scale * widgetScale)) + setComponentY((prev) => prev + (e.clientY - prevY) / (scale * widgetScale)) + setPrevX(e.clientX); + setPrevY(e.clientY); + } + + const draggingSquare = (e: React.DragEvent, index: number) => { + e.stopPropagation() + if (e.clientX == 0 || e.clientY == 0) { + return; + } + const dx = (e.clientX - prevX) / (scale * widgetScale); + const dy = (e.clientY - prevY) / (scale * widgetScale); + if (index == 0) { + if (componentW - dx > MIN_WIDTH) { + setComponentX((prev) => prev + dx) + setComponentW((prev) => prev - dx); + } + if (componentH - dy > MIN_HEIGHT) { + setComponentY((prev) => prev + dy) + setComponentH((prev) => prev - dy); + } + } else if (index == 1) { + if (componentW + dx > MIN_WIDTH) { + setComponentW((prev) => prev + dx); + } + if (componentH - dy > MIN_HEIGHT) { + setComponentY((prev) => prev + dy) + setComponentH((prev) => prev - dy); + } + } else if (index == 2) { + if (componentW + dx > MIN_WIDTH) { + setComponentW((prev) => prev + dx); + } + if (componentH + dy > MIN_HEIGHT) { + setComponentH((prev) => prev + dy); + } + } else { + if (componentW - dx > MIN_WIDTH) { + setComponentX((prev) => prev + dx) + setComponentW((prev) => prev - dx); + } + if (componentH + dy > MIN_HEIGHT) { + setComponentH((prev) => prev + dy); + } + } + setPrevX(e.clientX); + setPrevY(e.clientY); + } + + const onDragEnd = (e: React.DragEvent) => { + e.stopPropagation(); + if (!selectingComponent) return; + const nextUiData: UIData = { + posX: componentX, + posY: componentY, + width: componentW, + height: componentH + } + moveComponent(selectingComponentIndex, nextUiData); + } + + return { + componentX, + componentY, + componentW, + componentH, + onDragStart, + draggingComponent, + draggingSquare, + onDragEnd + }; + +} \ No newline at end of file diff --git a/src/hooks/useScreen.ts b/src/hooks/useScreen.ts new file mode 100644 index 0000000..2ad0814 --- /dev/null +++ b/src/hooks/useScreen.ts @@ -0,0 +1,99 @@ +import type { Project } from "../types/project"; +import type { Screen } from "../types/screen"; +import { useMemo, useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; + +export interface useScreenData { + screens: Screen[], + curScreenIndex: number, + curScreen: Screen +} + +export interface useScreenActions { + setCurScreenIndex: (index: number) => void + addScreen: (name: string) => Promise, + deleteScreen: (index: number) => void, + updateScreen: (index: number, name: string) => void +} + +export interface ScreenDataAndActions { + screenData: useScreenData, + screenActions: useScreenActions, + screenDispatch: { + setScreens: Dispatch> + } +} + +export function useScreen(project: Project): ScreenDataAndActions { + + const [screens, setScreens] = useState(project.screens); + const [curScreenIndex, setCurScreenIndex] = useState(0); + + const curScreen: Screen = useMemo(() => screens[curScreenIndex], [screens, curScreenIndex]); + + + const addScreen = async (name: string): Promise => { + const nextIndex = screens[screens.length - 1].index + 1; + const newScreenId = "s" + self.crypto.randomUUID().replace(/-/g, ""); + const newScreen: Screen = { + id: newScreenId, + components: [], + index: nextIndex, + name: name, + projectId: project.id + } + + setScreens((curScreens) => [...curScreens, newScreen]); + setCurScreenIndex(screens.length - 1); + + // await fetch(`/api/projects/${project.id}/screens`, { + // method: "POST", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // newScreen: newScreen + // }), + // }); + return newScreenId; + }; + + const deleteScreen = async (index: number) => { + setCurScreenIndex(0); + // const screenId = screens[index].id; + // await fetch(`/api/projects/${project.id}/screens/${screenId}`, { method: "DELETE" }) + setScreens((prev) => prev.filter((_, i) => i !== index)); + } + + const updateScreen = async (index: number, name: string) => { + // const screenId = screens[index].id; + // await fetch(`/api/projects/${project.id}/screens/${screenId}`, { + // method: "PUT", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // name: name, + // }), + // }) + setScreens((prev) => prev.map((item, i) => i === index ? { ...item, name: name } : item)); + } + + return { + screenData: { + screens, + curScreenIndex, + curScreen + }, + screenActions: { + setCurScreenIndex, + addScreen, + updateScreen, + deleteScreen + }, + screenDispatch: { + setScreens + } + } + +} \ No newline at end of file diff --git a/src/hooks/useTables.ts b/src/hooks/useTables.ts new file mode 100644 index 0000000..059b210 --- /dev/null +++ b/src/hooks/useTables.ts @@ -0,0 +1,66 @@ +import type { Project } from "../types/project"; +import type { Table } from "../types/table"; +import { useState } from "react"; + +export interface useTablesData { + tables: Table[] +} + +export interface useTablesActions { + addTable: (table: Table) => void, + deleteTable: (id: string) => void, + updateTable: (updatedTable: Table) => void +} + +export interface TableDataAndActions { + tableData: useTablesData, + tableActions: useTablesActions +} + +export function useTables(project: Project): TableDataAndActions { + + const [tables, setTables] = useState(project.tables); + + const addTable = async (table: Table) => { + setTables(cur => [...cur, table]) + // await fetch(`/api/projects/${project.id}/tables`, { + // method: "POST", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // newTable: table, + // }), + // }); + } + + const deleteTable = async (id: string) => { + setTables(cur => cur.filter((cur) => cur.id !== id)) + // await fetch(`/api/projects/${project.id}/tables/${id}`, { + // method: "DELETE", + // }); + } + + const updateTable = async (updatedTable: Table) => { + setTables(cur => cur.map((table) => table.id === updatedTable.id ? updatedTable : table)); + // await fetch(`/api/projects/${project.id}/tables/${updatedTable.id}`, { + // method: "PUT", + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // table: updatedTable, + // }), + // }); + } + + return { + tableData: { tables }, + tableActions: { + addTable, + updateTable, + deleteTable + } + } + +} \ No newline at end of file diff --git a/src/types/project.ts b/src/types/project.ts index e69de29..c4d7a9c 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -0,0 +1,20 @@ +import type { Screen } from "./screen"; +import type { Table } from "./table"; + +export interface ProjectData { + _id: string; + title: string; + detail: string; + createdTime: Date; + updatedTime: Date; +} + +export interface Project { + id: string; + title: string; + detail: string; + createdTime: string; + updatedTime: string; + screens: Screen[], + tables: Table[] +} \ No newline at end of file