Browse Source

add prettier to Editors component

yankevych0210 1 year ago
parent
commit
8c6ac47a36

+ 88 - 69
package-lock.json

@@ -16,7 +16,6 @@
         "graphql": "^16.6.0",
         "graphql-request": "^5.1.0",
         "react": "^18.2.0",
-        "react-ace": "^10.1.0",
         "react-codemirror2": "^7.2.1",
         "react-dom": "^18.2.0",
         "react-redux": "^8.0.5",
@@ -28,6 +27,10 @@
         "redux-thunk": "^2.4.2",
         "sass": "^1.58.3",
         "web-vitals": "^2.1.4"
+      },
+      "devDependencies": {
+        "eslint-plugin-prettier": "^4.2.1",
+        "prettier": "^2.8.4"
       }
     },
     "node_modules/@adobe/css-tools": {
@@ -4764,11 +4767,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/ace-builds": {
-      "version": "1.15.3",
-      "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.15.3.tgz",
-      "integrity": "sha512-hq8+4DfQcUYcUyAF3vF7UoGFXwNxXST5A2IdarUOp9/Xg1thWTfxusPI2HAlTvXRTVjLDQOj9O34uPoTehEs0A=="
-    },
     "node_modules/acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@@ -7086,11 +7084,6 @@
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
     },
-    "node_modules/diff-match-patch": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
-      "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
-    },
     "node_modules/diff-sequences": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
@@ -7848,6 +7841,27 @@
         "semver": "bin/semver.js"
       }
     },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
+      "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+      "dev": true,
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.28.0",
+        "prettier": ">=2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/eslint-plugin-react": {
       "version": "7.32.2",
       "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
@@ -8433,6 +8447,12 @@
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
+    "node_modules/fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "dev": true
+    },
     "node_modules/fast-glob": {
       "version": "3.2.12",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@@ -12672,16 +12692,6 @@
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
     },
-    "node_modules/lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
-    },
-    "node_modules/lodash.isequal": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
-      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
-    },
     "node_modules/lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -15356,6 +15366,33 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/prettier": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
+      "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/pretty-bytes": {
       "version": "5.6.0",
       "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -15630,22 +15667,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/react-ace": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-10.1.0.tgz",
-      "integrity": "sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==",
-      "dependencies": {
-        "ace-builds": "^1.4.14",
-        "diff-match-patch": "^1.0.5",
-        "lodash.get": "^4.4.2",
-        "lodash.isequal": "^4.5.0",
-        "prop-types": "^15.7.2"
-      },
-      "peerDependencies": {
-        "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0",
-        "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
-      }
-    },
     "node_modules/react-app-polyfill": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz",
@@ -22793,11 +22814,6 @@
         "negotiator": "0.6.3"
       }
     },
-    "ace-builds": {
-      "version": "1.15.3",
-      "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.15.3.tgz",
-      "integrity": "sha512-hq8+4DfQcUYcUyAF3vF7UoGFXwNxXST5A2IdarUOp9/Xg1thWTfxusPI2HAlTvXRTVjLDQOj9O34uPoTehEs0A=="
-    },
     "acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
@@ -24508,11 +24524,6 @@
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
     },
-    "diff-match-patch": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
-      "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
-    },
     "diff-sequences": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
@@ -25177,6 +25188,15 @@
         }
       }
     },
+    "eslint-plugin-prettier": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
+      "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
+      "dev": true,
+      "requires": {
+        "prettier-linter-helpers": "^1.0.0"
+      }
+    },
     "eslint-plugin-react": {
       "version": "7.32.2",
       "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
@@ -25514,6 +25534,12 @@
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
+    "fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "dev": true
+    },
     "fast-glob": {
       "version": "3.2.12",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@@ -28601,16 +28627,6 @@
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
     },
-    "lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
-    },
-    "lodash.isequal": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
-      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
-    },
     "lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -30390,6 +30406,21 @@
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
       "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
     },
+    "prettier": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
+      "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
+      "dev": true
+    },
+    "prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "requires": {
+        "fast-diff": "^1.1.2"
+      }
+    },
     "pretty-bytes": {
       "version": "5.6.0",
       "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@@ -30596,18 +30627,6 @@
         "loose-envify": "^1.1.0"
       }
     },
-    "react-ace": {
-      "version": "10.1.0",
-      "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-10.1.0.tgz",
-      "integrity": "sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==",
-      "requires": {
-        "ace-builds": "^1.4.14",
-        "diff-match-patch": "^1.0.5",
-        "lodash.get": "^4.4.2",
-        "lodash.isequal": "^4.5.0",
-        "prop-types": "^15.7.2"
-      }
-    },
     "react-app-polyfill": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz",

+ 4 - 1
package.json

@@ -12,7 +12,6 @@
     "graphql": "^16.6.0",
     "graphql-request": "^5.1.0",
     "react": "^18.2.0",
-    "react-ace": "^10.1.0",
     "react-codemirror2": "^7.2.1",
     "react-dom": "^18.2.0",
     "react-redux": "^8.0.5",
@@ -48,5 +47,9 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
+  },
+  "devDependencies": {
+    "eslint-plugin-prettier": "^4.2.1",
+    "prettier": "^2.8.4"
   }
 }

+ 17 - 7
src/App.js

@@ -1,10 +1,20 @@
-import { Routes, Route } from "react-router-dom";
-import { LoginPage } from "./components/pages/LoginPage/LoginPage";
-import { ErrorPage } from "./components/pages/ErrorPage/ErrorPage";
-import { HomePage } from "./components/pages/HomePage/HomePage";
-import { PenPage } from "./components/pages/PenPage/PenPage";
-import { SignUpPage } from "./components/pages/SignUpPage/SignUpPage";
-import { YourWorks } from "./components/pages/YourWorksPage/YourWorksPage";
+import { Routes, Route } from 'react-router-dom';
+import { LoginPage } from './components/pages/LoginPage/LoginPage';
+import { ErrorPage } from './components/pages/ErrorPage/ErrorPage';
+import { HomePage } from './components/pages/HomePage/HomePage';
+import { PenPage } from './components/pages/PenPage/PenPage';
+import { SignUpPage } from './components/pages/SignUpPage/SignUpPage';
+import { YourWorks } from './components/pages/YourWorksPage/YourWorksPage';
+
+//  TODO:
+//  1. icons
+//  2. console
+//  3. search
+//  4. sort
+//  5. deleted
+//  6. aliases
+//  7. extraKeys for Editor
+//  8. dropFile
 
 function App() {
   return (

+ 105 - 0
src/components/common/Console/Console.jsx

@@ -0,0 +1,105 @@
+import { useState, useRef, useEffect } from 'react';
+import styles from './Console.module.scss';
+
+export const Console = () => {
+  const [isConsoleOpen, setIsConsoleOpen] = useState(false);
+  const [consoleInput, setConsoleInput] = useState('');
+  const [consoleOutput, setConsoleOutput] = useState([]);
+
+  const inputRef = useRef(null);
+
+  const handleConsoleButtonClick = () => {
+    setIsConsoleOpen(true);
+    inputRef.current.focus();
+  };
+
+  const handleConsoleInputSubmit = (event) => {
+    event.preventDefault();
+    const userInput = consoleInput.trim();
+    if (!userInput) {
+      return;
+    }
+
+    try {
+      const result = eval(userInput);
+      setConsoleOutput((prevState) => [...prevState, { type: 'output', data: result }]);
+    } catch (error) {
+      setConsoleOutput((prevState) => [
+        ...prevState,
+        { type: 'error', data: `${error.name}: ${error.message}` },
+      ]);
+    }
+    setConsoleInput('');
+  };
+
+  const handleClearButtonClick = () => {
+    setConsoleOutput([]);
+    inputRef.current.focus();
+  };
+
+  const handleCloseButtonClick = () => {
+    setIsConsoleOpen(false);
+    setConsoleOutput([]);
+  };
+
+  useEffect(() => {
+    const handleKeyDown = (event) => {
+      if (event.key === 'Escape') {
+        setIsConsoleOpen(false);
+      }
+    };
+
+    window.addEventListener('keydown', handleKeyDown);
+
+    return () => {
+      window.removeEventListener('keydown', handleKeyDown);
+    };
+  }, []);
+
+  return (
+    <footer className={styles.console}>
+      {!isConsoleOpen && (
+        <button className={styles.consoleButton} onClick={handleConsoleButtonClick}>
+          Console.log
+        </button>
+      )}
+      {isConsoleOpen && (
+        <div className={styles.consoleWrapper}>
+          <div className={styles.consoleHeader}>
+            <button className={styles.closeButton} onClick={handleCloseButtonClick}>
+              X
+            </button>
+            <button className={styles.minimizeButton} onClick={() => setIsConsoleOpen(false)}>
+              &minus;
+            </button>
+            <button className={styles.maximizeButton} onClick={() => setIsConsoleOpen(true)}>
+              &#x25A1;
+            </button>
+          </div>
+          <div className={styles.consoleBody}>
+            {consoleOutput.map((output, index) => (
+              <div key={index} className={output.type === 'error' ? styles.error : styles.output}>
+                {output.data}
+              </div>
+            ))}
+            <form onSubmit={handleConsoleInputSubmit}>
+              <input
+                type="text"
+                name="consoleInput"
+                className={styles.input}
+                value={consoleInput}
+                onChange={(event) => setConsoleInput(event.target.value)}
+                ref={inputRef}
+              />
+            </form>
+          </div>
+          <div className={styles.consoleFooter}>
+            <button className={styles.clearButton} onClick={handleClearButtonClick}>
+              Clear
+            </button>
+          </div>
+        </div>
+      )}
+    </footer>
+  );
+};

+ 115 - 0
src/components/common/Console/Console.module.scss

@@ -0,0 +1,115 @@
+$console-color: #2b2b2b;
+$console-bg: #f2f2f2;
+$console-border: 1px solid #ccc;
+
+.console {
+  display: flex;
+  justify-content: center;
+  margin-top: 2rem;
+
+  &Button {
+    background-color: $console-color;
+    color: #fff;
+    border: none;
+    padding: 0.5rem 1rem;
+    cursor: pointer;
+
+    &:hover {
+      background-color: darken($console-color, 5%);
+    }
+  }
+
+  &Wrapper {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    padding: 1rem;
+    background-color: $console-bg;
+    border-top: $console-border;
+    border-bottom: $console-border;
+    z-index: 999;
+  }
+
+  &Header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 1rem;
+  }
+
+  &CloseButton {
+    background-color: transparent;
+    border: none;
+    font-size: 1.5rem;
+    cursor: pointer;
+
+    &:hover {
+      color: $console-color;
+    }
+  }
+
+  &ClearButton {
+    background-color: $console-color;
+    color: #fff;
+    border: none;
+    padding: 0.5rem 1rem;
+    margin-left: 1rem;
+    cursor: pointer;
+
+    &:hover {
+      background-color: darken($console-color, 5%);
+    }
+  }
+
+  &Body {
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 1rem;
+    font-family: monospace;
+
+    &Input {
+      display: flex;
+
+      &Text {
+        flex: 1;
+        padding: 0.5rem;
+        border: $console-border;
+        border-radius: 0.25rem;
+        margin-right: 1rem;
+
+        &:focus {
+          outline: none;
+          border-color: $console-color;
+        }
+      }
+
+      &Button {
+        background-color: $console-color;
+        color: #fff;
+        border: none;
+        padding: 0.5rem 1rem;
+        cursor: pointer;
+
+        &:hover {
+          background-color: darken($console-color, 5%);
+        }
+      }
+    }
+
+    &Output {
+      padding: 0.5rem;
+      margin-bottom: 0.5rem;
+      border-radius: 0.25rem;
+
+      &.Error {
+        background-color: #ffcccc;
+        color: #ff0000;
+      }
+
+      &.Output {
+        background-color: #e6ffe6;
+        color: #008000;
+      }
+    }
+  }
+}

+ 57 - 22
src/components/common/Editor/Editor.jsx

@@ -1,25 +1,52 @@
-import React from "react";
-import "codemirror/lib/codemirror.css";
-import "codemirror/mode/xml/xml";
-import "codemirror/mode/javascript/javascript";
-import "codemirror/mode/css/css";
-import { Controlled as ControlledEditor } from "react-codemirror2";
-import { useDispatch } from "react-redux";
-import style from "./Editor.module.scss";
-import "./themes/twilight.css";
-import { ReactComponent as HtmlLogo } from "../../../assets/img/htmlLogo.svg";
-import { ReactComponent as CssLogo } from "../../../assets/img/cssLogo.svg";
-import { ReactComponent as JsLogo } from "../../../assets/img/jsLogo.svg";
-import { Section } from "react-simple-resizer";
+import { useRef } from 'react';
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/mode/xml/xml';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/css/css';
+import { Controlled as ControlledEditor } from 'react-codemirror2';
+import { useDispatch, useSelector } from 'react-redux';
+import style from './Editor.module.scss';
+import './themes/twilight.css';
+import { ReactComponent as HtmlLogo } from '../../../assets/img/htmlLogo.svg';
+import { ReactComponent as CssLogo } from '../../../assets/img/cssLogo.svg';
+import { ReactComponent as JsLogo } from '../../../assets/img/jsLogo.svg';
+import { Section } from 'react-simple-resizer';
+import { saveFiles } from '../../../store/currentWork/actions/saveFiles';
+import { setFormatCode } from '../../../store/currentWork/currentWorkSlice';
+import { askToLogin } from '../../../utils/askToLogin.js';
+import { useNavigate } from 'react-router-dom';
 
-export default function Editor(props) {
-  const { language, displayName, value, onChange } = props;
+const logos = {
+  xml: <HtmlLogo />,
+  css: <CssLogo />,
+  javascript: <JsLogo />,
+};
+
+export default function Editor({ language, displayName, value, onChange }) {
   const dispatch = useDispatch();
+  const navigate = useNavigate();
+  const editorRef = useRef(null);
+  const { id, files } = useSelector((state) => state.currentWork);
+  const { isAuth } = useSelector((state) => state.auth);
+  const handleSave = () => {
+    dispatch(setFormatCode());
+
+    if (!isAuth) {
+      if (askToLogin()) navigate('/login');
+    } else {
+      dispatch(
+        saveFiles({
+          id,
+          html: files.html.text,
+          css: files.css.text,
+          js: files.js.text,
+        })
+      );
+    }
+  };
 
-  const logos = {
-    xml: <HtmlLogo />,
-    css: <CssLogo />,
-    javascript: <JsLogo />,
+  const handleFormat = () => {
+    dispatch(setFormatCode());
   };
 
   function handleChange(editor, data, value) {
@@ -41,12 +68,20 @@ export default function Editor(props) {
         value={value}
         className={style.codeMirrorWrapper}
         options={{
-          lineWrapping: true,
           lint: true,
           mode: language,
-          theme: "twilight",
+          theme: 'twilight',
           lineNumbers: true,
-          autoCorrect: true,
+          keyMap: 'default',
+          extraKeys: {
+            'Cmd-S': (editor) => handleSave(),
+            'Ctrl-S': (editor) => handleSave(),
+            'Cmd-F': (editor) => handleFormat(),
+            'Ctrl-F': (editor) => handleFormat(),
+          },
+        }}
+        editorDidMount={(editor) => {
+          editorRef.current = editor;
         }}
       />
     </Section>

+ 1 - 0
src/components/common/Editor/Editor.module.scss

@@ -28,6 +28,7 @@
   }
 
   .codeMirrorWrapper {
+    font-size: 15px;
     width: 100%;
     flex-grow: 1;
     overflow: hidden;

+ 15 - 18
src/components/common/HeaderPen/HeaderPen.jsx

@@ -1,9 +1,11 @@
-import React from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { ReactComponent as Logo } from "../../../assets/img/logo.svg";
-import { NavLink, useNavigate } from "react-router-dom";
-import { saveFiles } from "../../../store/currentWork/actions/saveFiles";
-import style from "./HeaderPen.module.scss";
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { ReactComponent as Logo } from '../../../assets/img/logo.svg';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { saveFiles } from '../../../store/currentWork/actions/saveFiles';
+import style from './HeaderPen.module.scss';
+import { setFormatCode } from '../../../store/currentWork/currentWorkSlice';
+import { askToLogin } from '../../../utils/askToLogin';
 
 export const HeaderPen = () => {
   const dispatch = useDispatch();
@@ -11,20 +13,17 @@ export const HeaderPen = () => {
   const { isAuth } = useSelector((state) => state.auth);
   const { id, title, owner, files } = useSelector((state) => state.currentWork);
 
-  const askToLogin = () => {
-    const unAuthMessage = `You’ll have to Log In or Sign Up  to save your Pen.
-    Don’t worry! All your work will be saved to your account.`;
-    // eslint-disable-next-line no-restricted-globals
-    if (confirm(unAuthMessage)) navigate("/login");
-  };
-
   const logout = () => {
-    localStorage.removeItem("authToken");
+    localStorage.removeItem('authToken');
     dispatch(logout());
   };
 
   const handleSaveFiles = () => {
-    if (isAuth) {
+    dispatch(setFormatCode());
+
+    if (!isAuth) {
+      if (askToLogin()) navigate('/login');
+    } else {
       dispatch(
         saveFiles({
           id,
@@ -33,8 +32,6 @@ export const HeaderPen = () => {
           js: files.js.text,
         })
       );
-    } else {
-      askToLogin();
     }
   };
 
@@ -57,7 +54,7 @@ export const HeaderPen = () => {
           <>
             <NavLink to="/your-works">Your works</NavLink>
             <NavLink
-              style={{ backgroundColor: "#dc143c" }}
+              style={{ backgroundColor: '#dc143c' }}
               onClick={logout}
               to="/"
             >

+ 22 - 0
src/components/common/Works/Works.jsx

@@ -0,0 +1,22 @@
+import { useSelector } from "react-redux";
+import { WorkCard } from "../WorkCard/WorkCard";
+import style from "./Works.module.scss";
+
+export const Works = ({ openPopup }) => {
+  const { works } = useSelector((state) => state.works);
+
+  if (!works.length) {
+    return (
+      <div className={style.empty}>
+        <h3>You haven't created any Pens yet.</h3>
+        <button onClick={openPopup}>Go make one!</button>
+      </div>
+    );
+  }
+
+  return (
+    <div className={style.works}>
+      {works && works.map((work) => <WorkCard key={work._id} work={work} />)}
+    </div>
+  );
+};

+ 41 - 0
src/components/common/Works/Works.module.scss

@@ -0,0 +1,41 @@
+@import "../../../scss/index.scss";
+
+.works {
+  display: grid;
+  justify-items: center;
+  align-items: center;
+  grid-template-columns: repeat(3, 1fr);
+  grid-auto-rows: minmax(380px, auto);
+  grid-gap: 10px;
+}
+
+.empty {
+  margin: 50px auto 0;
+  padding: 25px;
+  width: 100%;
+  background-color: #2c3039;
+  border-radius: 6px;
+  color: white;
+
+  h3 {
+    font-size: 1.8rem;
+    margin-bottom: 15px;
+  }
+
+  button {
+    @extend %buttonGreen;
+    font-size: 1rem;
+  }
+}
+
+@media screen and (max-width: 1150px) {
+  .works {
+    grid-template-columns: 50% 50%;
+  }
+}
+
+@media screen and (max-width: 700px) {
+  .works {
+    grid-template-columns: 100%;
+  }
+}

+ 15 - 14
src/components/pages/PenPage/PenPage.jsx

@@ -1,14 +1,15 @@
-import React, { useEffect, useState } from "react";
-import { Preview } from "../../common/Preview/Preview";
-import { useDispatch, useSelector } from "react-redux";
-import { fetchCurrentWork } from "../../../store/currentWork/actions/fetchCurrentWork";
-import { useParams } from "react-router-dom";
-import { LoadingPage } from "../LoadingPage/LoadingPage";
-import { HeaderPen } from "../../common/HeaderPen/HeaderPen";
-import style from "./PenPage.module.scss";
-import { Bar, Container, Section } from "react-simple-resizer";
-import { Editors } from "../../common/Editors/Editors";
-import { ErrorPage } from "../ErrorPage/ErrorPage";
+import React, { useEffect, useState } from 'react';
+import { Preview } from '../../common/Preview/Preview';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchCurrentWork } from '../../../store/currentWork/actions/fetchCurrentWork';
+import { useParams } from 'react-router-dom';
+import { LoadingPage } from '../LoadingPage/LoadingPage';
+import { HeaderPen } from '../../common/HeaderPen/HeaderPen';
+import style from './PenPage.module.scss';
+import { Bar, Container, Section } from 'react-simple-resizer';
+import { Editors } from '../../common/Editors/Editors';
+import { ErrorPage } from '../ErrorPage/ErrorPage';
+import { Console } from '../../common/Console/Console';
 
 export const PenPage = () => {
   const dispatch = useDispatch();
@@ -28,8 +29,7 @@ export const PenPage = () => {
       vertical={true}
       className={style.container}
       onActivate={() => setIsSizeChanged(true)}
-      afterResizing={() => setIsSizeChanged(false)}
-    >
+      afterResizing={() => setIsSizeChanged(false)}>
       <HeaderPen />
 
       <Section children={<Editors />} />
@@ -37,9 +37,10 @@ export const PenPage = () => {
       <Bar className={style.barVertical} />
 
       <Section
-        style={isSizeChanged ? { pointerEvents: "none" } : null}
+        style={isSizeChanged ? { pointerEvents: 'none' } : null}
         children={<Preview html={html.text} css={css.text} js={js.text} />}
       />
+      <Console />
     </Container>
   );
 };

+ 1 - 0
src/components/pages/SignUpPage/SignUpPage.jsx

@@ -41,6 +41,7 @@ export const SignUpPage = () => {
 
   useEffect(() => {
     if (isAuth) navigate("/your-works");
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [isAuth]);
 
   if (loading) return <LoadingPage />;

+ 14 - 16
src/components/pages/YourWorksPage/YourWorksPage.jsx

@@ -1,19 +1,19 @@
-import { Header } from "../../common/Header/Header";
-import style from "./YourWorksPage.module.scss";
-import { useDispatch, useSelector } from "react-redux";
-import { WorkCard } from "../../common/WorkCard/WorkCard";
-import { Footer } from "../../common/Footer/Footer";
-import { useEffect } from "react";
-import { getUserIdFromJwt } from "../../../utils/getUserIdFromJwt";
-import { usePopup } from "../../../hooks/usePopup";
-import { CreateWorkPopup } from "./CreateWorkPopup/CreateWorkPopup";
-import { LoadingPage } from "../LoadingPage/LoadingPage";
-import { fetchWorks } from "../../../store/works/actions/fetchWorks";
+import { Header } from '../../common/Header/Header';
+import style from './YourWorksPage.module.scss';
+import { useDispatch, useSelector } from 'react-redux';
+import { Footer } from '../../common/Footer/Footer';
+import { useEffect } from 'react';
+import { getUserIdFromJwt } from '../../../utils/getUserIdFromJwt';
+import { usePopup } from '../../../hooks/usePopup';
+import { CreateWorkPopup } from './CreateWorkPopup/CreateWorkPopup';
+import { LoadingPage } from '../LoadingPage/LoadingPage';
+import { fetchWorks } from '../../../store/works/actions/fetchWorks';
+import { Works } from '../../common/Works/Works';
 
 export const YourWorks = () => {
   const dispatch = useDispatch();
-  const { works, isLoading } = useSelector((state) => state.works);
   const { isAuth } = useSelector((state) => state.auth);
+  const { isLoading } = useSelector((state) => state.works);
   const { isPopupVisible, ref, open, close } = usePopup();
 
   useEffect(() => {
@@ -21,6 +21,7 @@ export const YourWorks = () => {
       const userId = getUserIdFromJwt(localStorage.authToken);
       dispatch(fetchWorks(userId));
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
   if (isLoading) return <LoadingPage />;
@@ -36,10 +37,7 @@ export const YourWorks = () => {
           <button onClick={open}>+</button>
         </div>
 
-        <div className={style.works}>
-          {works &&
-            works.map((work) => <WorkCard key={work._id} work={work} />)}
-        </div>
+        <Works openPopup={open} />
       </div>
 
       {isPopupVisible && <CreateWorkPopup popupRef={ref} close={close} />}

+ 0 - 37
src/components/pages/YourWorksPage/YourWorksPage.module.scss

@@ -36,42 +36,5 @@
         float: right;
       }
     }
-
-    .loading {
-      width: 100%;
-      height: 500px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-    }
-
-    .works {
-      display: grid;
-      justify-items: center;
-      align-items: center;
-      grid-template-columns: repeat(3, 1fr);
-      grid-auto-rows: minmax(380px, auto);
-      grid-gap: 10px;
-    }
-  }
-}
-
-@media screen and (max-width: 1150px) {
-  .yourWorks {
-    .container {
-      .works {
-        grid-template-columns: 50% 50%;
-      }
-    }
-  }
-}
-
-@media screen and (max-width: 700px) {
-  .yourWorks {
-    .container {
-      .works {
-        grid-template-columns: 100%;
-      }
-    }
   }
 }

+ 1 - 0
src/scss/templates/_buttons.scss

@@ -10,6 +10,7 @@
   border-radius: 4px;
   transition: all 0.1s ease-in;
   border: none;
+  cursor: pointer;
 }
 
 %buttonGrey {

+ 11 - 11
src/store/currentWork/actions/fetchCurrentWork.js

@@ -1,20 +1,20 @@
-import { createAsyncThunk } from "@reduxjs/toolkit";
-import { getGql } from "../../../services/api";
-import { setEmptyFilesInLS } from "../../../utils/setEmptyFilesInLS";
+import { createAsyncThunk } from '@reduxjs/toolkit';
+import { getGql } from '../../../services/api';
+import { setEmptyFilesInLS } from '../../../utils/setEmptyFilesInLS';
 
 export const fetchCurrentWork = createAsyncThunk(
-  "currentWork/fetch",
+  'currentWork/fetch',
 
   async (id) => {
     if (!id) {
-      if (!localStorage.getItem("localFiles")) {
+      if (!localStorage.getItem('localFiles')) {
         setEmptyFilesInLS();
       }
       return {
         _id: null,
-        title: "Untitled",
-        owner: { login: "Captain anonymous" },
-        files: JSON.parse(localStorage.getItem("localFiles")),
+        title: 'Untitled',
+        owner: { login: 'Captain anonymous' },
+        files: JSON.parse(localStorage.getItem('localFiles')),
       };
     }
 
@@ -49,9 +49,9 @@ export const fetchCurrentWork = createAsyncThunk(
       };
 
       SnippetFindOne.files.forEach((file) => {
-        if (file.type === "HTML") files.html = file;
-        if (file.type === "CSS") files.css = file;
-        if (file.type === "JS") files.js = file;
+        if (file.type === 'HTML') files.html = file;
+        if (file.type === 'CSS') files.css = file;
+        if (file.type === 'JS') files.js = file;
       });
 
       return { ...SnippetFindOne, files: files };

+ 41 - 18
src/store/currentWork/currentWorkSlice.js

@@ -1,22 +1,23 @@
-import { createSlice } from "@reduxjs/toolkit";
-import { fetchCurrentWork } from "./actions/fetchCurrentWork";
-import { saveFiles } from "./actions/saveFiles";
+import { createSlice } from '@reduxjs/toolkit';
+import { formatCode } from '../../utils/formatCode';
+import { fetchCurrentWork } from './actions/fetchCurrentWork';
+import { saveFiles } from './actions/saveFiles';
 
 const initialState = {
-  id: "",
-  title: "",
-  owner: { id: "", login: "" },
+  id: '',
+  title: '',
+  owner: { id: '', login: '' },
   files: {
-    html: { text: "", type: "HTML" },
-    css: { text: "", type: "CSS" },
-    js: { text: "", type: "JS" },
+    html: { text: '', type: 'HTML' },
+    css: { text: '', type: 'CSS' },
+    js: { text: '', type: 'JS' },
   },
   isLoading: false,
   error: null,
 };
 
 export const currentWorkSlice = createSlice({
-  name: "currentWork",
+  name: 'currentWork',
   initialState,
   reducers: {
     setHtml(state, action) {
@@ -32,33 +33,48 @@ export const currentWorkSlice = createSlice({
     setLocalHtml(state, action) {
       state.files.html.text = action.payload;
       localStorage.setItem(
-        "localFiles",
+        'localFiles',
         JSON.stringify({
           ...state.files,
-          html: { type: "HTML", text: action.payload },
+          html: { type: 'HTML', text: action.payload },
         })
       );
     },
     setLocalCss(state, action) {
       state.files.css.text = action.payload;
       localStorage.setItem(
-        "localFiles",
+        'localFiles',
         JSON.stringify({
           ...state.files,
-          css: { type: "CSS", text: action.payload },
+          css: { type: 'CSS', text: action.payload },
         })
       );
     },
     setLocalJs(state, action) {
       state.files.js.text = action.payload;
       localStorage.setItem(
-        "localFiles",
+        'localFiles',
         JSON.stringify({
           ...state.files,
-          js: { type: "JS", text: action.payload },
+          js: { type: 'JS', text: action.payload },
         })
       );
     },
+
+    setFormatCode(state, action) {
+      state.files.html.text = formatCode(
+        state.files.html.text,
+        state.files.html.type
+      );
+      state.files.css.text = formatCode(
+        state.files.css.text,
+        state.files.css.type
+      );
+      state.files.js.text = formatCode(
+        state.files.js.text,
+        state.files.js.type
+      );
+    },
   },
   extraReducers: (builder) => {
     // fetchWorks
@@ -89,5 +105,12 @@ export const currentWorkSlice = createSlice({
   },
 });
 
-export const { setHtml, setCss, setJs, setLocalHtml, setLocalCss, setLocalJs } =
-  currentWorkSlice.actions;
+export const {
+  setHtml,
+  setCss,
+  setJs,
+  setLocalHtml,
+  setLocalCss,
+  setLocalJs,
+  setFormatCode,
+} = currentWorkSlice.actions;

+ 0 - 9
src/store/works/worksSlice.js

@@ -2,7 +2,6 @@ import { createSlice } from "@reduxjs/toolkit";
 import { fetchWorks } from "./actions/fetchWorks";
 import { createWork } from "./actions/createWork";
 import { deleteWork } from "./actions/deleteWork";
-import { updateWorkInfo } from "./actions/updateWorkInfo";
 
 const initialState = {
   works: [],
@@ -45,14 +44,6 @@ export const worksSlice = createSlice({
     builder.addCase(deleteWork.rejected, (state, action) => {
       state.error = action.payload;
     });
-
-    // // updateWorkInfo (title | description)
-    // builder.addCase(updateWorkInfo.fulfilled, (state, action) => {
-    //   state.work
-    // });
-    // builder.addCase(updateWorkInfo.rejected, (state, action) => {
-    //   state.error = action.payload;
-    // });
   },
 });
 

+ 8 - 0
src/utils/askToLogin.js

@@ -0,0 +1,8 @@
+export const askToLogin = () => {
+  const unAuthMessage = `
+    You’ll have to Log In or Sign Up  to save your Pen.
+    Don’t worry! All your work will be saved to your account.`;
+
+  // eslint-disable-next-line no-restricted-globals
+  return confirm(unAuthMessage);
+};

+ 24 - 0
src/utils/formatCode.js

@@ -0,0 +1,24 @@
+import prettier from 'prettier/standalone';
+import parserHtml from 'prettier/parser-html';
+import parserCss from 'prettier/parser-postcss';
+import parserJs from 'prettier/parser-babel';
+
+export const formatCode = (value, language) => {
+  const parser = {
+    HTML: 'html',
+    xml: 'html',
+    CSS: 'css',
+    css: 'css',
+    JS: 'babel',
+    javascript: 'babel',
+  };
+
+  const formattedCode = prettier.format(value, {
+    parser: parser[language],
+    plugins: [parserHtml, parserCss, parserJs],
+    tabWidth: 2,
+    useTabs: false,
+  });
+
+  return formattedCode;
+};