diff --git a/app.js b/app.js index 70c9b20..9beabb8 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,7 @@ const app = express() app.use(express.json({ extended: true })) app.use('/api/auth', require('./routes/auth.routes')) -app.use('/api/server', require('./routes/phpserver.routes')) +app.use('/api/notes', require('./routes/notes.routes')) if (httpsRedirect) app.use(httpToHttps) diff --git a/client/package-lock.json b/client/package-lock.json index 47967ef..7b30a77 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3601,15 +3601,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "optional": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5218,11 +5209,18 @@ } }, "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "requires": { - "domelementtype": "1" + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } } }, "domutils": { @@ -6575,12 +6573,6 @@ } } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, "filesize": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", @@ -7219,6 +7211,19 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -7229,6 +7234,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -7362,22 +7375,40 @@ } }, "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" }, "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } } } }, @@ -10354,6 +10385,15 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, "mini-css-extract-plugin": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz", @@ -10545,12 +10585,6 @@ "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, - "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", - "optional": true - }, "nanoid": { "version": "3.1.22", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", @@ -12849,6 +12883,52 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-scripts": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz", @@ -13202,28 +13282,65 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "renderkid": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", - "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.6.tgz", + "integrity": "sha512-GIis2GBr/ho0pFNf57D4XM4+PgnQuTii0WCPjEZmZfKivzUfGuRdjN2aQYtYMiNggHmNyBve+thFnNR1iBRcKg==", "requires": { - "css-select": "^2.0.2", - "dom-converter": "^0.2", - "htmlparser2": "^3.10.1", - "lodash": "^4.17.20", - "strip-ansi": "^3.0.0" + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.0" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "requires": { - "ansi-regex": "^2.0.0" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "requires": { + "boolbase": "^1.0.0" } } } @@ -13359,6 +13476,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -15033,6 +15155,16 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -15609,6 +15741,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -15816,11 +15953,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } + "optional": true }, "glob-parent": { "version": "3.1.0", @@ -16419,11 +16552,7 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } + "optional": true }, "glob-parent": { "version": "3.1.0", diff --git a/client/package.json b/client/package.json index 15de38f..e13a192 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-markdown": "^6.0.2", + "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-stack-grid": "^0.7.1", "react-textarea-autosize": "^8.3.3", diff --git a/client/src/App.css b/client/src/App.css index cde4a48..d3cca56 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,48 +1,3 @@ -.App-logo { - height: 3em; - pointer-events: none; - user-select: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -.card:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); -} - -.card { - transition: box-shadow 0.3s; -} - -.card-text > h1 { - font-size: 1.625em; -} -.card-text > h2 { - font-size: 1.5em; -} -.card-text > h3 { - font-size: 1.375em; -} -.card-text > h4 { - font-size: 1.25em; -} -.card-text > h5 { - font-size: 1.125em; -} - .lds-dual-ring { display: inline-block; height: 1em; @@ -68,12 +23,3 @@ transform: rotate(360deg); } } - -@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=swap"); - -.brand { - font-family: "Roboto", sans-serif; - padding-top: 0.2rem !important; - pointer-events: none; - user-select: none; -} diff --git a/client/src/App.js b/client/src/App.js index d988dd3..99f523d 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,267 +1,47 @@ import React from 'react'; -import logo from './Shared/Logo/logo.svg'; -import './App.css'; -import CardList from './Cards/CardList' -import AddCard from './Cards/AddCard' -import Context from './context' -import Loader from './Shared/Loader' -import ModalCardEdit from './Cards/ModalCardEdit' -import ModalLogin from './Login/ModalLogin' -import DataService from './Services/DataService' -import Card, { checkCardsArr } from './Cards/cardType/Card' -//import useDebouncedEffect from './Shared/useDebouncedEffect.hook' - -const { loadData, postData, updDataServLogin } = DataService() - -const brandText = "Notes" - -var cardCount = 0 - -function calcCount(cards) { - let id = cardCount; - [...cards].forEach(element => { - if (Number(element.id) >= id) id = Number(element.id) - }); - return id -} -function useCardsArr(defaultValue) { - const [value, setValue] = React.useState(defaultValue) +import './App.css'; - function trySetValue(cardsArr) { - if (checkCardsArr(cardsArr) || cardsArr === null) setValue(cardsArr) - else console.error('Массив cardsArr не прошел проверку \n', cardsArr) - } +import Loader from './shared/Loader' - return [value, trySetValue] -} - -function useUpdater() { - const [updaterVal, setUpdaterVal] = React.useState(null) - const timer = React.useRef() - if (timer.current) clearTimeout(timer.current) - timer.current = setTimeout(() => { - console.log("Timed update") - setUpdaterVal(Date.now()) - }, 60 * 1000) // обновяем через минуту - return [updaterVal] -} +import { BrowserRouter as Router } from 'react-router-dom' +import { useRoutes } from './routes' +import { useAuth } from './hooks/auth.hook' +import { AuthContext } from './context/AuthContext' +import { PageContext } from './context/PageContext' +import Header from './pages/SharedComponents/Header' function App() { - const [cardsArr, setCards] = useCardsArr(null) - const [editCardId, setEditCardId] = React.useState(null) - const [loading, setLoading] = React.useState({ state: false, res: false }) + const { token, login, logout, userId, email, ready } = useAuth() + const isAuthenticated = !!token + const routes = useRoutes(isAuthenticated) - const [openLogin, setOpenLogin] = React.useState(false) - const [logged, setLogged] = React.useState(false) - const [userName, setUserName] = React.useState(undefined) + const [nav, setNav] = React.useState() - const [updaterVal] = useUpdater() - - React.useEffect(loadDataFromServer, [logged, userName, updaterVal]) // eslint-disable-line react-hooks/exhaustive-deps - //useDebouncedEffect(loadDataToServer, [cardsArr], 300) // eslint-disable-line react-hooks/exhaustive-deps - React.useEffect(loadDataToServer, [cardsArr]) // eslint-disable-line react-hooks/exhaustive-deps - React.useEffect(clearOldData, [logged, userName]) // eslint-disable-line react-hooks/exhaustive-deps - - /////////// - function onLogin(login) { - //console.log("onLogin", login) - return new Promise((res, rej) => { - onLogout() - .then(() => { - //console.log("onLogin", login, "onLogout.then") - updDataServLogin(login) - .then(r => { - //console.log("onLogin", login, "onLogout.then", "updDataServLogin.then") - setLogged(Boolean(r)) - setUserName(login) - res(r) - }) - .catch(e => { - //console.log("onLogin", login, "onLogout.then", "updDataServLogin.catch", e) - rej(e) - }) - }) - .catch(e => console.log("logout catch in onLogin", e)) - }) - } - - function onLogout() { - //console.log("onLogout") - return new Promise((res) => { - updDataServLogin(null) - .finally(() => { - if (logged) { - //console.log("onLogout - was logged, dislogin") - setUserName(undefined) - setLogged(false) - } else { - //console.log("onLogout - also not logged") - clearOldData() - } - }) - .finally(res) - .catch(e => console.log("Data service dislogin failed", e)) - }) - } - - function clearOldData() { - //console.log("clearOldData, logged:", logged) - if (!logged) setCards(null) - } - /////////// - - /////////// - function loadDataToServer() { - try { - let startUsername = userName - if (logged && userName) postData(cardsArr) - .then(res => { - if (!cardsArr) console.log("empty post!") - console.log('[onPostData]', res) - if (startUsername !== userName) { - console.log("упс, несостыковочка с userName"); - loadDataToServer() - } - }) - .catch(e => console.log(`Data post request error. Response: ${e}`)) - } - catch (e) { - console.error(e) - } - } - - function loadDataFromServer() { - try { - let startUsername = userName - if (logged && userName) { - setLoading({ state: true, res: loading.res }) - loadData() - .then(data => { - console.log('[onLoadData]', 'Данные с сервера загружены') - if (startUsername === userName) setLoadedCards(data) - else console.log("упс, несостыковочка с userName"); - setLoading({ state: false, res: true }) - }) - .catch(e => { - console.log(`Data load request error. Response:`, e) - setLoading({ state: false, res: false }) - }) - } - } - catch (e) { - setLoading({ state: false, res: false }) - console.error(e) - } - } - /////////// - - /////////// - function setLoadedCards(cards) { - setCards([...cards]) - cardCount = calcCount(cards) - } - - function removeCard(index) { - cardsArr.splice(index, 1) - setCards([...cardsArr]) - } - - function deleteAll() { - setCards([]) - } - - function addCard(cardData = {}) { - const newCard = new Card({ id: ++cardCount, name: cardData.name, color: cardData.color, text: cardData.text }) - setCards( - - (cardsArr != null) ? cardsArr.concat([newCard]) : [newCard] + if (!ready) { + return ( +
+ +
) } - function changeCardColor(index, color) { - cardsArr[index].color = color - setCards([...cardsArr]) - } - - function editCardContent(index, name, text) { - if (cardsArr[index]) { - let card = new Card(cardsArr[index]) - card.name = name - card.text = text - cardsArr[index] = card - } - setCards([...cardsArr]) - } - /////////// - - /////////// - function getCardByIndex(index) { - return index !== null ? cardsArr[index] : null - } - function setEditCard(index) { - setEditCardId(index) - } - function unsetEditCard() { - setEditCardId(null) - } - /////////// return ( - -
-
- - -
- -
- - - - {cardsArr && cardsArr.length ? ( - - ) : (loading.state || !loading.res) ? null : logged ? ( -
-

No cards. You can add a new one!

-
- ) : ( -
-

Unlogged

-
- )} - - {(logged && !loading.state && !loading.res) && ( -
-

Data not loaded

-
- )} - - {loading.state && -
- -
- } -
-
-
- ); + + + +
{nav}
+
+ {routes} +
+
+
+ +
+ ) } export default App; diff --git a/client/src/Cards/AddCard.js b/client/src/Cards/AddCard.js index d5bbf4a..98451a9 100644 --- a/client/src/Cards/AddCard.js +++ b/client/src/Cards/AddCard.js @@ -1,10 +1,11 @@ import React from 'react' -import PropTypes from 'prop-types' import TextareaAutosize from 'react-textarea-autosize' import Palette, { colors } from './palette/palette' -import useInputValue from '../Shared/useInputValue.hook' +import useInputValue from '../hooks/useInputValue.hook' +import CardsContext from '../context/CardsContext' -function AddCard({ onCreate, onDeleteAll }) { +function AddCard() { + const { addCard } = React.useContext(CardsContext) const input = useInputValue('') const defColor = colors[0] @@ -25,7 +26,7 @@ function AddCard({ onCreate, onDeleteAll }) { function submitHandler() { if (String(input.value).trim() && String(color).trim()) { - onCreate({ name: String(input.value).trim(), text: "", color: String(color) }) + addCard({ name: String(input.value).trim(), text: "", color: String(color) }) input.clear() setColor(defColor) } @@ -69,9 +70,4 @@ function AddCard({ onCreate, onDeleteAll }) { ) } -AddCard.propTypes = { - onCreate: PropTypes.func.isRequired, - onDeleteAll: PropTypes.func.isRequired -} - export default AddCard diff --git a/client/src/Cards/CardItem.js b/client/src/Cards/CardItem.js index a245adf..d61181c 100644 --- a/client/src/Cards/CardItem.js +++ b/client/src/Cards/CardItem.js @@ -1,6 +1,6 @@ import React, { useContext } from 'react' import PropTypes from 'prop-types' -import Context from '../context' +import CardsContext from '../context/CardsContext' import Card, { PropTypeCard } from './cardType/Card' import ReactMarkdown from 'react-markdown' import gfm from 'remark-gfm' @@ -10,7 +10,7 @@ function fixLineBreaks(mdStr) { } function CardItem({ card = new Card(), index }) { - const { removeCard, setEditCard } = useContext(Context) + const { removeCard, setEditCard } = useContext(CardsContext) const lineClip = 12 const bgColor = card.color diff --git a/client/src/Cards/ModalCardEdit.js b/client/src/Cards/ModalCardEdit.js index 685a91f..e5f4426 100644 --- a/client/src/Cards/ModalCardEdit.js +++ b/client/src/Cards/ModalCardEdit.js @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import Context from '../context' +import CardsContext from '../context/CardsContext' import TextareaAutosize from 'react-textarea-autosize' -import Modal, { ModalProps } from "../Shared/Modal/Modal" +import Modal, { ModalProps } from "../shared/Modal/Modal" import Card, { PropTypeCard } from './cardType/Card' import Palette from './palette/palette' @@ -20,7 +20,7 @@ function calcMaxRows() { } function ModalCardEdit({ card = new Card(), index }) { - const { removeCard, changeCardColor, unsetEditCard, editCardContent } = React.useContext(Context) + const { removeCard, changeCardColor, unsetEditCard, editCardContent } = React.useContext(CardsContext) React.useEffect(() => { if (card !== null) open() }, [card]) const [showForm, setShowForm] = React.useState(false) @@ -120,7 +120,7 @@ function ModalCardEdit({ card = new Card(), index }) {
- Id: {card && card.id} + Id {index}
diff --git a/client/src/Cards/cardType/Card.js b/client/src/Cards/cardType/Card.js index 5566b48..33e87c0 100644 --- a/client/src/Cards/cardType/Card.js +++ b/client/src/Cards/cardType/Card.js @@ -1,10 +1,7 @@ import PropTypes from 'prop-types' export const PropTypeCard = PropTypes.shape({ - id: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), + id: PropTypes.string, name: PropTypes.string, color: PropTypes.string, text: PropTypes.string, @@ -12,7 +9,7 @@ export const PropTypeCard = PropTypes.shape({ export function checkCard(card) { return ( - (typeof card.id === "number" || typeof card.id === "string") && + (typeof card.id === "string") && typeof card.name === "string" && typeof card.color === "string" && typeof card.text === "string" @@ -34,7 +31,7 @@ export function checkCardsArr(cardsArr) { export class Card { constructor({ id, name, color, text }) { - this.id = Number(id) + this.id = String(id) this.name = String(name) this.color = String(color) this.text = String(text) diff --git a/client/src/Context/cardsContext.js b/client/src/Context/cardsContext.js new file mode 100644 index 0000000..084eb5e --- /dev/null +++ b/client/src/Context/cardsContext.js @@ -0,0 +1,5 @@ +import {createContext} from 'react' + +const CardsContext = createContext() + +export default CardsContext \ No newline at end of file diff --git a/client/src/Login/ModalLogin.js b/client/src/Login/ModalLogin.js deleted file mode 100644 index da5ffd8..0000000 --- a/client/src/Login/ModalLogin.js +++ /dev/null @@ -1,133 +0,0 @@ -import React from "react" -import Modal, { ModalProps } from "../Shared/Modal/Modal" -import PropTypes from 'prop-types' -import useInputValue from '../Shared/useInputValue.hook' -import LoginService from '../Services/LoginService' -const { logIn, logOut } = LoginService() - -function checkUsername(str) { - return str && typeof str === 'string' && str.length > 3 && str.length < 20 && str === validateUsername(str) -} - -function validateUsername(str) { - return String(str).replace(/@|;|:|\.|,|\/|\\|\||\$|\?|!|#|%|\*|\^|\+|=|\[|\]| |\\ |«|<|>/gi, "").trim() -} - -function ModalLogin(props) { - const { onLogin, onLogout, logged, userName, isOpen, setOpenState } = props - - const input = useInputValue("") - - const modalProps = new ModalProps() - modalProps.isOpen = isOpen - modalProps.setOpenState = setOpenState - modalProps.sideClose = true - - const [labelAlert, setLabelAlert] = React.useState(""); - - // eslint-disable-next-line no-unused-vars - function open() { - setOpenState(true) - } - - function close() { - clearInput() - setLabelAlert("") - setOpenState(false) - } - - function clearInput() { - input.clear() - } - - function tryLogin() { - if (checkUsername(input.value)) { - logIn(input.value.trim()) - .then((name) => { - onLogin(name) - close() - console.log('Login:', name) - }) - .catch((err) => { - console.log("Не удалось залогиниться") - console.log(err) - setLabelAlert(
- Не удалось залогиниться {err && {String(err)}} -
) - }) - } else if (input.value) { - setLabelAlert(
{input.value.length > 3 ? "Исправьте логин" : "Слишком короткий логин"}
) - } else { - setLabelAlert(
Поле "Username" не должно быть пустым
) - } - } - - function tryLogout() { - logOut() - .then((name) => { - onLogout(name) - console.log('Logged out') - }) - .catch((err) => { - console.log("Не удалось разлогиниться") - console.log(err) - setLabelAlert(
- Не удалось разлогиниться {err && {String(err)}} -
) - }) - //close() - } - - return ( - -
-
-
- - {logged ? AUTORISED : 'UNAUTORISED'} - - {userName ? {userName} : ``} -
-
- -
- {labelAlert} - - e.key === 'Enter' && tryLogin()} - {...input.bind} - /> -
- -
-
- -
-
- -
-
- -
-
- -
-
- ) -} - -ModalLogin.propTypes = { - logged: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]), - userName: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - login: PropTypes.func, - logout: PropTypes.func, - - isOpen: PropTypes.bool, - setOpenState: PropTypes.func -} - -export default ModalLogin diff --git a/client/src/Services/DataService.js b/client/src/Services/DataService.js deleted file mode 100644 index 01fc03c..0000000 --- a/client/src/Services/DataService.js +++ /dev/null @@ -1,139 +0,0 @@ -import { ajax } from "jquery" -import Card, { checkCardsArr } from '../Cards/cardType/Card' - -export default function DataService() { - //////////////////////////////////////////////////////////// - var user = null - const baseUrl = '/api/server/' - - //////////////////////////////////////////////////////////// - function updDataServLogin(login) { - return new Promise((res, rej) => { - if (login && typeof login === "string") { - user = login - //console.log("data serv login set") - res(user) - } else if (login === null) { - user = null - //console.log("data serv login unset") - res() - } else { - //console.log("data serv cant set login", login) - rej(login) - } - }) - } - //////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////// - //let recuestCount = 1; - function request(target, data) { - //const rc = recuestCount++ - //console.log(` \nrequest ${rc} - "${target}" started \n params - user:"${user}" data:"${data}"`) - return new Promise((res, rej) => { - ajax({ - url: baseUrl, - type: "POST", - dataType: "html", - data: { - user: user, - target: target, - data: data, - }, - }) - .done(resdata => res(resdata)) - .fail(rejdata => rej(rejdata)) - /*.always(() => console.log( - `requested - newUser:"${user}" resolveData:"${data}" \n - request ${rc} - "${target}" ended \n ` - ))*/ - }) - } - //////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////// - function requestGetData() { - return request('getData', null) - } - - function requestPostData(data) { - return request('setData', data || []) - } - //////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////// - function tryParce(str) { - try { - return JSON.parse(str); - } catch (e) { - return str; - } - } - - function checkData(data) { - //console.log('start check data') - try { - if (data === null) console.log("null data"); - return data === null || data === [] || checkCardsArr(data) - } catch { - return false - } - - } - //////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////// - function loadData() { - return new Promise((res, rej) => { - try { - (user === null - ? Promise.reject(rej()) - : requestGetData()) - .then((d) => { - let data = tryParce(d)//here we parce json - //console.log("[DATA] from loadData(): ", data) - if (!data) console.log("empty data from server"); - if (!checkData(data)) { - console.error("[loadData] Bad data format") - console.log(data) - let checkDel = user !== null - if (checkDel && window.confirm("Bad data: " + data + ". Delete data on server?")) { - console.log('clear data') - requestPostData([new Card({ id: 0, color: "orange", name: "Error", text: "Данные были очищены из за ошибки" })]).then(() => loadData().then(res, rej), rej)//очистка данных - } else rej("Not format data" + !user ? " & unlogged" : "") - } else { - res(data || []) - } - }) - .catch(rej) - } catch (e) { - rej(e) - console.error(e) - } - }) - } - - function postData(data) { - return new Promise((res, rej) => { - try { - (user === null - ? Promise.reject(rej()) - : loadData()) - .then((d) => { - if (!data) console.log("empty data to post"); - if (!d) console.log("empty loaded to check"); - let pDat = data === null ? (d || []) : data - if (!pDat) console.log("empty will be posted"); - requestPostData(pDat).then(res, rej) - }) - .catch(rej) - } catch (e) { - rej(e) - console.error(e) - } - }) - } - //////////////////////////////////////////////////////////// - - return { loadData, postData, updDataServLogin } -} diff --git a/client/src/Services/LoginService.js b/client/src/Services/LoginService.js deleted file mode 100644 index a900754..0000000 --- a/client/src/Services/LoginService.js +++ /dev/null @@ -1,31 +0,0 @@ -export default function DataService() { - // ПОКА ЭТО ТУПО ЗАГЛУШКА - var user = null - - //////////////////////////////////////////////////////////// - - function logIn(login) { - return new Promise((res, rej) => { - if (login && typeof login === "string") { - user = login - //console.log("login serv login set") - res(user) - } else { - //console.log("login serv cant set login", login) - rej(login) - } - }) - } - - function logOut() { - return new Promise((res, rej) => { - user = null - //console.log("login serv dislogin") - res() - }) - } - - //////////////////////////////////////////////////////////// - - return { logIn, logOut } -} diff --git a/client/src/context.js b/client/src/context.js deleted file mode 100644 index f015347..0000000 --- a/client/src/context.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react' - -const Context = React.createContext() - -export default Context \ No newline at end of file diff --git a/client/src/context/AuthContext.js b/client/src/context/AuthContext.js new file mode 100644 index 0000000..b302288 --- /dev/null +++ b/client/src/context/AuthContext.js @@ -0,0 +1,12 @@ +import {createContext} from 'react' + +function noop() {} + +export const AuthContext = createContext({ + token: null, + userId: null, + email: null, + login: noop, + logout: noop, + isAuthenticated: false +}) diff --git a/client/src/context/PageContext.js b/client/src/context/PageContext.js new file mode 100644 index 0000000..0410d50 --- /dev/null +++ b/client/src/context/PageContext.js @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +function noop() { } + +export const PageContext = createContext({ setNav: noop }) diff --git a/client/src/hooks/auth.hook.js b/client/src/hooks/auth.hook.js new file mode 100644 index 0000000..588548f --- /dev/null +++ b/client/src/hooks/auth.hook.js @@ -0,0 +1,42 @@ +import {useState, useCallback, useEffect} from 'react' + +const storageName = 'userData' + +export const useAuth = () => { + const [token, setToken] = useState(null) + const [ready, setReady] = useState(false) + const [userId, setUserId] = useState(null) + const [email, setEmail] = useState(null) + + const login = useCallback((jwtToken, id, email) => { + setToken(jwtToken) + setUserId(id) + setEmail(email) + + localStorage.setItem(storageName, JSON.stringify({ + userId: id, token: jwtToken, email: email + })) + + }, []) + + + const logout = useCallback(() => { + + setToken(null) + setUserId(null) + setEmail(null) + localStorage.removeItem(storageName) + }, []) + + useEffect(() => { + const data = JSON.parse(localStorage.getItem(storageName)) + + if (data && data.token) { + login(data.token, data.userId, data.email) + } + setReady(true) + }, [login]) + + + return { login, logout, token, userId, email, ready } +} diff --git a/client/src/hooks/http.hook.js b/client/src/hooks/http.hook.js new file mode 100644 index 0000000..4c8fec0 --- /dev/null +++ b/client/src/hooks/http.hook.js @@ -0,0 +1,35 @@ +import {useState, useCallback} from 'react' + +export const useHttp = () => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const request = useCallback(async (url, method = 'GET', body = null, headers = {}) => { + setLoading(true) + try { + if (body) { + body = JSON.stringify(body) + headers['Content-Type'] = 'application/json' + } + + const response = await fetch(url, {method, body, headers}) + const data = await response.json() + + if (!response.ok) { + throw new Error(data.message || 'Что-то пошло не так') + } + + setLoading(false) + + return data + } catch (e) { + setLoading(false) + setError(e.message) + throw e + } + }, []) + + const clearError = useCallback(() => setError(null), []) + + return { loading, request, error, clearError } +} diff --git a/client/src/Shared/useDebouncedEffect.hook.js b/client/src/hooks/useDebouncedEffect.hook.js similarity index 100% rename from client/src/Shared/useDebouncedEffect.hook.js rename to client/src/hooks/useDebouncedEffect.hook.js diff --git a/client/src/Shared/useInputValue.hook.js b/client/src/hooks/useInputValue.hook.js similarity index 100% rename from client/src/Shared/useInputValue.hook.js rename to client/src/hooks/useInputValue.hook.js diff --git a/client/src/pages/AuthPage.css b/client/src/pages/AuthPage.css new file mode 100644 index 0000000..1424231 --- /dev/null +++ b/client/src/pages/AuthPage.css @@ -0,0 +1,8 @@ + +.form-body { + width: 100%; + max-width: 620px; + max-height: 100%; + overflow-y: auto; + height: fit-content; +} diff --git a/client/src/pages/AuthPage.js b/client/src/pages/AuthPage.js new file mode 100644 index 0000000..9432fb0 --- /dev/null +++ b/client/src/pages/AuthPage.js @@ -0,0 +1,144 @@ +import React, { useContext, useEffect, useState } from 'react' +import { useHttp } from '../hooks/http.hook' + +import { AuthContext } from '../context/AuthContext' +import { PageContext } from '../context/PageContext' + +import { NavLink, useHistory } from 'react-router-dom' +import './AuthPage.css' + +function AuthPage() { + const auth = useContext(AuthContext) + const page = useContext(PageContext) + const history = useHistory() + + const { loading, request, error, clearError } = useHttp() + const [form, setForm] = useState({ + email: '', password: '' + }) + const [message, setMessage] = useState(null) + + useEffect(() => { + if (error) setMessage([error, false]) + clearError() + }, [error, clearError]) + + const changeHandler = event => { + setForm({ ...form, [event.target.name]: event.target.value }) + } + + const registerHandler = async () => { + try { + const data = await request('/api/auth/register', 'POST', { ...form }) + if (data.message) setMessage([data.message, true]) + } catch (e) { } + } + + const loginHandler = async () => { + try { + const data = await request('/api/auth/login', 'POST', { ...form }) + auth.login(data.token, data.userId, data.email) + history.push("/") + } catch (e) { } + } + + const logoutHandler = event => { + event.preventDefault() + auth.logout() + } + + React.useEffect(() => { + page.setNav(auth.isAuthenticated && + + К заметкам + + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [auth.isAuthenticated, auth.token]) + + return ( +
+ + +
+
+

Авторизация

+
+ +
+
+ + {auth.isAuthenticated ? AUTORISED : 'UNAUTORISED'} + + {auth.email ? {auth.email} : ``} +
+
+ +
+ { + message && +
+ {message[1] ? "Инфо" : "Ошибка"} + {String(message[0])} +
+ } + + + e.key === 'Enter' && loginHandler()} + /> + + + e.key === 'Enter' && loginHandler()} + /> +
+ +
+ {!auth.isAuthenticated ? ( + + +
+ +
+ +
+ +
+ +
+ ) : ( + + +
+ +
+ +
+ )} +
+ +
+
+
+
+ ) +} + +export default AuthPage \ No newline at end of file diff --git a/client/src/pages/CardsPage.css b/client/src/pages/CardsPage.css new file mode 100644 index 0000000..76fa444 --- /dev/null +++ b/client/src/pages/CardsPage.css @@ -0,0 +1,23 @@ +.card:hover { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); +} + +.card { + transition: box-shadow 0.3s; +} + +.card-text > h1 { + font-size: 1.625em; +} +.card-text > h2 { + font-size: 1.5em; +} +.card-text > h3 { + font-size: 1.375em; +} +.card-text > h4 { + font-size: 1.25em; +} +.card-text > h5 { + font-size: 1.125em; +} diff --git a/client/src/pages/CardsPage.js b/client/src/pages/CardsPage.js new file mode 100644 index 0000000..62cf3e9 --- /dev/null +++ b/client/src/pages/CardsPage.js @@ -0,0 +1,206 @@ +import React from 'react'; + +import './CardsPage.css'; +import CardList from '../Cards/CardList' +import AddCard from '../Cards/AddCard' +import CardsContext from '../context/CardsContext' +import Loader from '../shared/Loader' +import ModalCardEdit from '../Cards/ModalCardEdit' + +import Card, { checkCardsArr } from '../Cards/cardType/Card' + + +import { NavLink } from 'react-router-dom' +import { AuthContext } from '../context/AuthContext' +import { PageContext } from '../context/PageContext' + +import { useHttp } from '../hooks/http.hook' + +function useCardsArr(defaultValue) { + const [value, setValue] = React.useState(defaultValue) + + function trySetValue(cardsArr) { + if (checkCardsArr(cardsArr) || cardsArr === null) setValue(cardsArr) + else console.error('Массив cardsArr не прошел проверку \n', cardsArr) + } + + return [value, trySetValue] +} + +function useUpdater() { + const [updaterVal, setUpdaterVal] = React.useState(null) + const timer = React.useRef() + React.useEffect(() => { + if (timer.current) clearTimeout(timer.current) + timer.current = setTimeout(() => { + console.log("Timed update") + setUpdaterVal(Date.now()) + }, 60 * 1000) // обновяем через минуту + return () => clearTimeout(timer.current) + }, []) // eslint-disable-line react-hooks/exhaustive-deps + return [updaterVal] +} + +function CardsPage() { + + const auth = React.useContext(AuthContext) + const page = React.useContext(PageContext) + + const { loading, request, error, clearError } = useHttp() + const fetchNotes = React.useCallback(async (url = "", method = "GET", body = null, resCallback = () => { }) => { + try { + const fetched = await request(`/api/notes${url ? ("/" + url) : ""}`, method, body, { Authorization: `Bearer ${auth.token}` }) + resCallback(tryParce(fetched)) + } catch (e) { } + function tryParce(str) { + try { + return JSON.parse(str); + } catch (e) { + return str; + } + } + }, [auth.token, request]) + + const [message, setMessage] = React.useState(null) + + React.useEffect(() => { + if (error) setMessage([error, false]) + clearError() + }, [error, clearError]) + + + + const [cardsArr, setCardsArr] = useCardsArr(null) + const [editCardId, setEditCardId] = React.useState(null) + + const [updaterVal] = useUpdater() + + const updatingEnable = React.useRef(true) + + React.useEffect(() => { + updatingEnable.current = true + loadDataFromServer() + return () => updatingEnable.current = false + }, [auth.isAuthenticated, auth.email, updaterVal]) // eslint-disable-line react-hooks/exhaustive-deps + + React.useEffect(clearOldData, [auth.isAuthenticated]) // eslint-disable-line react-hooks/exhaustive-deps + + /////////// + function clearOldData() { + //console.log("clearOldData, auth.isAuthenticated:", auth.isAuthenticated) + if (!auth.isAuthenticated) setCardsArr(null) + } + /////////// + + /////////// + function loadDataFromServer() { + fetchNotes("", "GET", null, setLoadedCards) + } + + function setLoadedCards(cards) { + if (updatingEnable.current) setCardsArr([...cards]) + } + /////////// + + /////////// + + function loadDataToServer(card = new Card(), target = 'set') { + fetchNotes(target, "POST", { card }) + } + + function removeCard(index) { + const toDelete = cardsArr.splice(index, 1)[0] + setCardsArr([...cardsArr]) + loadDataToServer(toDelete, "delete") + } + + function addCard(cardData = {}) { + const newId = String(auth.email) + String(Date.now()) + String(Math.random()) + const newCard = new Card({ id: newId, name: cardData.name, color: cardData.color, text: cardData.text }) + //console.log(newId, newCard.id); + setCardsArr( + (cardsArr != null) ? cardsArr.concat([newCard]) : [newCard] + ) + loadDataToServer(newCard, "set") + + } + + function changeCardColor(index, color) { + cardsArr[index].color = color + setCardsArr([...cardsArr]) + loadDataToServer(cardsArr[index], "set") + } + + function editCardContent(index, name, text) { + if (cardsArr[index]) { + let card = new Card(cardsArr[index]) + card.name = name + card.text = text + cardsArr[index] = card + } + setCardsArr([...cardsArr]) + loadDataToServer(cardsArr[index], "set") + } + /////////// + + /////////// + function getCardByIndex(index) { + return index !== null ? cardsArr[index] : null + } + function setEditCard(index) { + setEditCardId(index) + } + function unsetEditCard() { + setEditCardId(null) + } + /////////// + + React.useEffect(() => { + page.setNav( + + + + + {auth.email} + + + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [auth.email, auth.token]) + + return ( + +
+ +
+ + + + {cardsArr && cardsArr.length ? ( + + ) : (loading) ? null : !message ? ( +
+

No cards. You can add a new one!

+
+ ) : ( +
+

Data not loaded

+

{message}

+
+ )} + + {loading && +
+ +
+ } +
+
+
+ ); +} + +export default CardsPage diff --git a/client/src/pages/SharedComponents/Header.css b/client/src/pages/SharedComponents/Header.css new file mode 100644 index 0000000..a8a7697 --- /dev/null +++ b/client/src/pages/SharedComponents/Header.css @@ -0,0 +1,29 @@ +.App-logo { + height: 3em; + pointer-events: none; + user-select: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=swap"); + +.brand { + font-family: "Roboto", sans-serif; + padding-top: 0.2rem !important; + pointer-events: none; + user-select: none; +} diff --git a/client/src/pages/SharedComponents/Header.js b/client/src/pages/SharedComponents/Header.js new file mode 100644 index 0000000..11e3d8e --- /dev/null +++ b/client/src/pages/SharedComponents/Header.js @@ -0,0 +1,27 @@ +import React from 'react'; +import logo from '../../shared/Logo/logo.svg'; +import './Header.css'; + +const brandText = "Notes" + +function Header(props) { + + return ( + +
+ +
+ + ); +} + +export default Header; diff --git a/client/src/routes.js b/client/src/routes.js new file mode 100644 index 0000000..c38e31f --- /dev/null +++ b/client/src/routes.js @@ -0,0 +1,33 @@ +import React from 'react' +import { Switch, Route, Redirect } from 'react-router-dom' + +import CardsPage from './pages/CardsPage' + +import AuthPage from './pages/AuthPage' + +export const useRoutes = isAuthenticated => { + if (isAuthenticated) { + return ( + + + + + + + + + + + + ) + } + + return ( + + + + + + + ) +} diff --git a/models/Note.js b/models/Note.js new file mode 100644 index 0000000..4262ca8 --- /dev/null +++ b/models/Note.js @@ -0,0 +1,13 @@ +const { Schema, model, Types } = require('mongoose') + +const schema = new Schema({ + id: { type: String, required: true, unique: true /*, default: String(Date.now) + String(Math.random)*/ }, + name: { type: String }, + text: { type: String }, + color: { type: String }, + image: { type: String }, + //date: { type: Date, default: Date.now }, + owner: { type: Types.ObjectId, ref: 'User', required: true } +}) + +module.exports = model('Note', schema) diff --git a/models/User.js b/models/User.js index 170b64b..3bc1240 100644 --- a/models/User.js +++ b/models/User.js @@ -2,7 +2,8 @@ const {Schema, model, Types} = require('mongoose') const schema = new Schema({ email: {type: String, required: true, unique: true}, - password: {type: String, required: true} + password: {type: String, required: true}, + notes: [{ type: Types.ObjectId, ref: 'Note' }] }) module.exports = model('User', schema) diff --git a/package.json b/package.json index 115dca4..2f242d5 100644 --- a/package.json +++ b/package.json @@ -57,4 +57,4 @@ "cross-env": "^7.0.3", "nodemon": "^2.0.7" } -} \ No newline at end of file +} diff --git a/routes/auth.routes.js b/routes/auth.routes.js index 98c99c7..380f8c0 100644 --- a/routes/auth.routes.js +++ b/routes/auth.routes.js @@ -1,8 +1,8 @@ -const {Router} = require('express') +const { Router } = require('express') const bcrypt = require('bcryptjs') require('dotenv').config() const jwt = require('jsonwebtoken') -const {check, validationResult} = require('express-validator') +const { check, validationResult } = require('express-validator') const User = require('../models/User') const router = Router() @@ -15,35 +15,35 @@ router.post( .isLength({ min: 6 }) ], async (req, res) => { - try { - const errors = validationResult(req) - - if (!errors.isEmpty()) { - return res.status(400).json({ - errors: errors.array(), - message: 'Некорректный данные при регистрации' - }) - } + try { + const errors = validationResult(req) - const {email, password} = req.body + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array(), + message: 'Некорректный данные при регистрации' + }) + } - const candidate = await User.findOne({ email }) + const { email, password } = req.body - if (candidate) { - return res.status(400).json({ message: 'Такой пользователь уже существует' }) - } + const candidate = await User.findOne({ email }) - const hashedPassword = await bcrypt.hash(password, 12) - const user = new User({ email, password: hashedPassword }) + if (candidate) { + return res.status(400).json({ message: 'Такой пользователь уже существует' }) + } - await user.save() + const hashedPassword = await bcrypt.hash(password, 12) + const user = new User({ email, password: hashedPassword }) - res.status(201).json({ message: 'Пользователь создан' }) + await user.save() - } catch (e) { - res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) - } -}) + res.status(201).json({ message: 'Пользователь создан' }) + + } catch (e) { + res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) + } + }) // /api/auth/login router.post( @@ -53,42 +53,42 @@ router.post( check('password', 'Введите пароль').exists() ], async (req, res) => { - try { - const errors = validationResult(req) - - if (!errors.isEmpty()) { - return res.status(400).json({ - errors: errors.array(), - message: 'Некорректный данные при входе в систему' - }) - } + try { + const errors = validationResult(req) - const {email, password} = req.body + if (!errors.isEmpty()) { + return res.status(400).json({ + errors: errors.array(), + message: 'Некорректный данные при входе в систему' + }) + } - const user = await User.findOne({ email }) + const { email, password } = req.body - if (!user) { - return res.status(400).json({ message: 'Пользователь не найден' }) - } + const user = await User.findOne({ email }) - const isMatch = await bcrypt.compare(password, user.password) + if (!user) { + return res.status(400).json({ message: 'Пользователь не найден' }) + } - if (!isMatch) { - return res.status(400).json({ message: 'Неверный пароль, попробуйте снова' }) - } + const isMatch = await bcrypt.compare(password, user.password) - const token = jwt.sign( - { userId: user.id }, - process.env.jwtSecret, - { expiresIn: '1h' } - ) + if (!isMatch) { + return res.status(400).json({ message: 'Неверный пароль, попробуйте снова' }) + } - res.json({ token, userId: user.id }) + const token = jwt.sign( + { userId: user.id }, + process.env.jwtSecret, + { expiresIn: '1h' } + ) - } catch (e) { - res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) - } -}) + res.json({ token, userId: user.id, email: user.email }) + + } catch (e) { + res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) + } + }) module.exports = router diff --git a/routes/notes.routes.js b/routes/notes.routes.js new file mode 100644 index 0000000..da913ac --- /dev/null +++ b/routes/notes.routes.js @@ -0,0 +1,95 @@ +const { Router } = require('express') + +const Note = require('../models/Note') +const auth = require('../middleware/auth.middleware') +const router = Router() + +const { checkCard } = require('../validation/CardCheck') + + +router.post('/set', auth, async (req, res) => { + try { + const card = tryParce(req.body.card) + if (checkCard(card)) { + postNote(card) + res.status(201).json({ card: card }) + } else { + res.status(500).json({ message: 'Неверный формат данных заметки' }) + } + } catch (e) { + res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) + } + + async function postNote(noteToSave) { + + noteToSave.owner = req.user.userId + + const existing = await Note.findOne({ id: noteToSave.id }) + + if (existing) { + //console.log("EXITING"); + existing.overwrite(noteToSave) + existing.save() + } else { + //console.log("NON EXITING") + const note = new Note(noteToSave) + await note.save() + } + + } +}) + + + +router.post('/delete', auth, async (req, res) => { + try { + + const card = tryParce(req.body.card) + if (checkCard(card)) { + deleteNote(card) + res.status(201).json({ card: card }) + } else { + res.status(500).json({ message: 'Неверный формат данных заметки' }) + } + } catch (e) { + res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) + } + + async function deleteNote(noteToSave) { + noteToSave.owner = req.user.userId + + const existing = await Note.findOne({ id: noteToSave.id }) + + if (existing) { + //console.log("EXITING"); + + existing.remove() + + } else res.status(500).json({ message: 'уже удален' }) + } +}) + + + +router.get('/', auth, async (req, res) => { + try { + //console.log(req.user.userId); + const notes = await Note.find({ owner: req.user.userId }) + //console.log(notes); + res.json(notes) + } catch (e) { + //console.log("\n\n\n GET"); + //console.log(e); + res.status(500).json({ message: '[GET] Что-то пошло не так, попробуйте снова' }) + } +}) + +function tryParce(str) { + try { + return JSON.parse(str); + } catch (e) { + return str; + } +} + +module.exports = router \ No newline at end of file diff --git a/routes/phpserver.routes.js b/routes/phpserver.routes.js deleted file mode 100644 index f9b6b81..0000000 --- a/routes/phpserver.routes.js +++ /dev/null @@ -1,18 +0,0 @@ -const { Router } = require('express') -//const auth = require('../middleware/auth.middleware') -const router = Router() - -//temporary backend url -const phpBaseUrl = 'https://php-server-notes.herokuapp.com/' - -//auth not used -router.post('/', /*auth,*/ async (req, res) => { - try { - //console.log("backend redirect", req.url) - res.redirect(307, phpBaseUrl) - } catch (e) { - res.status(500).json({ message: 'Что-то пошло не так, попробуйте снова' }) - } -}) - -module.exports = router diff --git a/validation/CardCheck.js b/validation/CardCheck.js new file mode 100644 index 0000000..4e60bb1 --- /dev/null +++ b/validation/CardCheck.js @@ -0,0 +1,10 @@ +function checkCard(card) { + return ( + typeof card.id === "string" && + typeof card.name === "string" && + typeof card.color === "string" && + typeof card.text === "string" + ) +} + +module.exports = { checkCard } \ No newline at end of file