diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 49a98f8..86a1866 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,7 +21,9 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], - indent: ['warn', 2], - semi: ['warn', 'always'] + indent: ['warn', 2, { SwitchCase: 1 }], + semi: ['warn', 'always'], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error' } }; diff --git a/src/App.tsx b/src/App.tsx index 5d2c9ae..97a10d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; +import './App.scss'; +import { MainMenu } from './Components/MainMenu/MainMenu'; import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel'; -import Editor from './Editor'; +import Editor, { IEditorState } from './Editor'; import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; @@ -23,40 +25,21 @@ interface IAppState { export class App extends React.Component { public state: IAppState; - public constructor(props: IAppProps) { + constructor(props: IAppProps) { super(props); - - const MainContainer = new ContainerModel( - null, - { - id: 'main', - x: 0, - y: 0, - width: 1000, - height: 1000, - fillOpacity: 0, - stroke: 'black' - } - ); this.state = { configuration: { AvailableContainers: [], AvailableSymbols: [], MainContainer: {} as AvailableContainer }, - history: [ - { - MainContainer, - SelectedContainer: MainContainer, - TypeCounters: {} - } - ], + history: [], historyCurrentStep: 0, isLoaded: false }; } - componentDidMount() { + public NewEditor() { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { // Set the main container from the given properties of the API @@ -91,6 +74,25 @@ export class App extends React.Component { }); } + public LoadEditor(files: FileList | null) { + if (files === null) { + return; + } + const file = files[0]; + const reader = new FileReader(); + reader.addEventListener('load', () => { + const result = reader.result as string; + const editorState: IEditorState = JSON.parse(result); + this.setState({ + configuration: editorState.configuration, + history: editorState.history, + historyCurrentStep: editorState.historyCurrentStep, + isLoaded: true + } as IAppState); + }); + reader.readAsText(file); + } + public render() { if (this.state.isLoaded) { return ( @@ -102,6 +104,15 @@ export class App extends React.Component { /> ); + } else { + return ( +
+ this.NewEditor()} + loadEditor={(files: FileList | null) => this.LoadEditor(files)} + /> +
+ ); } } } @@ -131,6 +142,7 @@ export async function fetchConfiguration(): Promise { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { resolve(JSON.parse(this.responseText)); } + reject(xhr.responseText); }; xhr.send(); }); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index e93f527..4e599ef 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -38,7 +38,8 @@ export class ElementsSidebar extends React.Component { const depth: number = getDepth(container); const key = container.properties.id.toString(); const text = '|\t'.repeat(depth) + key; - const selectedClass: string = this.props.SelectedContainer !== null && + const selectedClass: string = this.props.SelectedContainer !== undefined && + this.props.SelectedContainer !== null && this.props.SelectedContainer.properties.id === container.properties.id ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-400 hover:bg-slate-600'; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx new file mode 100644 index 0000000..92b19d5 --- /dev/null +++ b/src/Components/MainMenu/MainMenu.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; + +interface IMainMenuProps { + newEditor: () => void; + loadEditor: (files: FileList | null) => void +} + +enum WindowState { + MAIN, + LOAD, +} + +export const MainMenu: React.FC = (props) => { + const [windowState, setWindowState] = React.useState(WindowState.MAIN); + switch (windowState) { + case WindowState.LOAD: + return ( +
+
+ +
+ {/* */} + +
+ + ); + default: + return ( +
+ + +
+ ); + } +}; diff --git a/src/Editor.tsx b/src/Editor.tsx index 81db229..5358df5 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -2,7 +2,6 @@ import React from 'react'; import './Editor.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; -import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; import { SVG } from './Components/SVG/SVG'; import { History } from './Components/History/History'; @@ -16,12 +15,15 @@ interface IEditorProps { historyCurrentStep: number } -interface IEditorState { +export interface IEditorState { isSidebarOpen: boolean, isSVGSidebarOpen: boolean, isHistoryOpen: boolean, history: Array, historyCurrentStep: number, + // do not use it, use props.configuration + // only used for serialization purpose + configuration: Configuration } class Editor extends React.Component { @@ -33,8 +35,8 @@ class Editor extends React.Component { isSidebarOpen: true, isSVGSidebarOpen: false, isHistoryOpen: false, - configuration: props.configuration, - history: props.history, + configuration: Object.assign({}, props.configuration), + history: [...props.history], historyCurrentStep: props.historyCurrentStep } as IEditorState; } @@ -238,6 +240,17 @@ class Editor extends React.Component { } as IEditorState); } + public SaveEditor() { + const exportName = 'state'; + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(this.state, getCircularReplacer(), 4))}`; + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute('href', dataStr); + downloadAnchorNode.setAttribute('download', `${exportName}.json`); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + /** * Render the application * @returns {JSX.Element} Rendered JSX element @@ -291,9 +304,23 @@ class Editor extends React.Component { { current.MainContainer } + ); } } +const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key: any, value: object | null) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; + export default Editor; diff --git a/src/index.scss b/src/index.scss index 7327b1a..c7e0d50 100644 --- a/src/index.scss +++ b/src/index.scss @@ -12,4 +12,7 @@ .close-button { @apply transition-all w-full h-auto p-4 flex } + .mainmenu-btn { + @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg + } } \ No newline at end of file