diff --git a/client/package-lock.json b/client/package-lock.json index 661fba1..322ff61 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -23,6 +23,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.30.0", "prettier": "^2.5.1", + "sass": "^1.54.5", "ts-node": "^8.10.1", "typescript": "^4.3.2", "vite": "^3.0.9", @@ -1352,6 +1353,19 @@ "node": ">=4" } }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1476,6 +1490,15 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/binary-search-bounds": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", @@ -1641,6 +1664,45 @@ "node": ">=4" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/clamp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", @@ -3869,6 +3931,12 @@ "quantize": "^1.0.2" } }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3945,6 +4013,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-blob": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", @@ -4616,6 +4696,15 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-svg-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", @@ -5225,6 +5314,18 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", @@ -5461,6 +5562,23 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sass": { + "version": "1.54.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.5.tgz", + "integrity": "sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7271,6 +7389,16 @@ "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -7369,6 +7497,12 @@ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "binary-search-bounds": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", @@ -7490,6 +7624,33 @@ "supports-color": "^5.3.0" } }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "clamp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", @@ -9199,6 +9360,12 @@ "quantize": "^1.0.2" } }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9260,6 +9427,15 @@ "has-bigints": "^1.0.1" } }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-blob": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", @@ -9767,6 +9943,12 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "normalize-svg-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", @@ -10227,6 +10409,15 @@ } } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", @@ -10411,6 +10602,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass": { + "version": "1.54.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.5.tgz", + "integrity": "sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", diff --git a/client/package.json b/client/package.json index 2d39b73..f14447b 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.30.0", "prettier": "^2.5.1", + "sass": "^1.54.5", "ts-node": "^8.10.1", "typescript": "^4.3.2", "vite": "^3.0.9", diff --git a/client/src/assets/components/_dashboard-header.scss b/client/src/assets/components/_dashboard-header.scss new file mode 100644 index 0000000..738d3db --- /dev/null +++ b/client/src/assets/components/_dashboard-header.scss @@ -0,0 +1,20 @@ +.dashboard-head { + display: flex; + align-items: center; + margin-left: auto; + + .spacer { + margin: 0 1rem; + width: 1px; + background: var(--header-spacer-color); + height: 20px; + } + + .filter-button { + font-size: 125%; + cursor: pointer; + display: flex; + align-items: center; + margin-left: 0.5rem; + } +} diff --git a/client/src/assets/components/_filters-panel.scss b/client/src/assets/components/_filters-panel.scss new file mode 100644 index 0000000..6973526 --- /dev/null +++ b/client/src/assets/components/_filters-panel.scss @@ -0,0 +1,27 @@ +.filters-panel { + position: fixed; + right: 0; + top: 0; + bottom: 0; + z-index: 2; + width: 20rem; + display: flex; + + .inner { + background-color: var(--box-bg-color); + padding: 0.25rem 0.5rem; + flex: 1; + } + + .shadow { + background: var(--filters-menu-shadow); + width: 8px; + flex-shrink: 0; + flex-grow: 0; + } + + .filter-close { + cursor: pointer; + font-size: 150%; + } +} diff --git a/client/src/assets/icons/cancel.svg b/client/src/assets/icons/cancel.svg new file mode 100644 index 0000000..cb00cfe --- /dev/null +++ b/client/src/assets/icons/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/icons/filters.svg b/client/src/assets/icons/filters.svg new file mode 100644 index 0000000..b4120a8 --- /dev/null +++ b/client/src/assets/icons/filters.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/icons/menu.svg b/client/src/assets/icons/menu.svg new file mode 100644 index 0000000..7daca0c --- /dev/null +++ b/client/src/assets/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/icons/plus.svg b/client/src/assets/icons/plus.svg new file mode 100644 index 0000000..4fa0e34 --- /dev/null +++ b/client/src/assets/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/style.css b/client/src/assets/style.scss similarity index 72% rename from client/src/assets/style.css rename to client/src/assets/style.scss index e0f42d3..942d823 100644 --- a/client/src/assets/style.css +++ b/client/src/assets/style.scss @@ -1,9 +1,9 @@ :root { - --main-bg-color: #eee; + --main-bg-color: #eee; --main-fg-color: #000; - --button-bg-color: #3988FF; + --button-bg-color: #3988ff; --button-fg-color: #fff; - --button-hover-bg-color: #0F6FFF; + --button-hover-bg-color: #0f6fff; --button-hover-fg-color: #fff; --button-cancel-bg-color: transparent; --button-cancel-fg-color: #000; @@ -11,24 +11,27 @@ --button-remove-fg-color: #f00; --header-bg-color: #fff; --header-spacer-color: #ddd; - --header-shadow: linear-gradient(0deg, rgba(255,255,255,0) 5%, rgba(190,190,190,0.6) 100%); + --header-shadow: linear-gradient(0deg, rgba(255, 255, 255, 0) 5%, rgba(190, 190, 190, 0.6) 100%); --box-bg-color: #fff; --box-fg-color: #111; --box-loader-bg-color: rgba(128, 128, 128, 0.3); --box-loader-fg-color: #fff; --box-action-fg-color: #666; - --box-preview-bg-color: #3988FF; - --box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1); - --modal-overlay-bg-color: rgba(0,0,0,0.2); + --box-preview-bg-color: #3988ff; + --box-shadow: 0px 10px 15px -3px rgba(0, 0, 0, 0.1); + --modal-overlay-bg-color: rgba(0, 0, 0, 0.2); --graph-axis-fg-color: #777; --graph-grid-color: rgb(238, 238, 238); + --link-fg-color: #3988ff; + --menu-shadow: linear-gradient(270deg, rgba(255, 255, 255, 0) 0%, rgba(90, 90, 90, 0.2) 100%); + --filters-menu-shadow: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(90, 90, 90, 0.2) 100%); } @media (prefers-color-scheme: dark) { :root { --main-bg-color: #222; - --main-fg-color: #b8b8b8; - --header-bg-color: #000; + --main-fg-color: #ccc; + --header-bg-color: #111; --header-spacer-color: #333; --header-shadow: none; --box-bg-color: #111; @@ -36,10 +39,24 @@ --box-shadow: none; --graph-axis-fg-color: #666; --graph-grid-color: rgb(25, 25, 25); + --button-bg-color: #0b3c9f; + --button-fg-color: #eee; + --button-cancel-fg-color: #ccc; + } + + select, + input { + border: 1px solid #333; + background: #222; + color: #ccc; + border-radius: 0.25rem; + box-sizing: border-box; + padding: 0.1rem 0.25rem; } } -body, html { +body, +html { padding: 0; margin: 0; width: 100%; @@ -49,11 +66,13 @@ body, html { body { background: var(--main-bg-color); color: var(--main-fg-color); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } -button, input, select { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +button, +input, +select { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } button { @@ -72,11 +91,16 @@ button:hover { color: var(--button-hover-fg-color); } -input, select { +input, +select { padding: 0.15rem 0.4rem; box-sizing: border-box; } +a { + color: var(--link-fg-color); +} + .login { width: 300px; margin: 2rem auto; @@ -93,6 +117,95 @@ input, select { flex-direction: column; } +main.layout { + height: 100%; + overflow: auto; + display: flex; + flex-direction: column; +} + +.menu { + position: fixed; + left: 0; + top: 0; + bottom: 0; + color: var(--box-fg-color); + width: 20rem; + transition: left 0.1s; + z-index: 2; + display: flex; +} + +.menu .inner { + background-color: var(--box-bg-color); + flex: 1; + padding: 0.25em 0.5rem; +} + +.menu .shadow { + width: 8px; + background: var(--menu-shadow); + flex-shrink: 0; + flex-grow: 0; +} + +.menu .menu-close { + font-size: 150%; + cursor: pointer; +} + +.menu nav { + display: flex; + flex-direction: column; +} + +.menu nav a { + text-decoration: none; +} + +.menu-overlay { + background-color: var(--modal-overlay-bg-color); + z-index: 1; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +header.header { + flex-grow: 0; + flex-shrink: 0; +} + +header.header > .inner { + display: flex; + padding: 0.5rem 0.5rem; + background-color: var(--header-bg-color); +} + +header.header > .shadow { + position: absolute; + background: var(--header-shadow); + width: 100%; + height: 8px; + z-index: 1; +} + +header.header > .inner > .menu-button { + display: flex; + align-items: center; + font-size: 125%; + cursor: pointer; +} + +section.content { + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; +} + .sensors { display: grid; grid-template-columns: 1fr 1fr; @@ -103,9 +216,9 @@ input, select { } @media only screen and (max-width: 1200px) { - .sensors { - grid-template-columns: 1fr; - } + .sensors { + grid-template-columns: 1fr; + } } .box { @@ -168,6 +281,10 @@ form .input { margin-bottom: 0.5rem; } +form .input label { + margin-bottom: 0.2rem; +} + form .actions { text-align: right; margin-top: 1rem; @@ -198,35 +315,12 @@ form.horizontal .input label { margin-right: 0.25rem; } -.dashboard-head .inner { - display: flex; - align-items: center; - justify-content: flex-end; - padding: 0.5rem 0.5rem; - background-color: var(--header-bg-color); -} - -.dashboard-head .shadow { - position: absolute; - background: var(--header-shadow); - width: 100%; - height: 8px; - z-index: 1; -} - -.dashboard-head .spacer { - margin: 0 1rem; - width: 1px; - background: var(--header-spacer-color); - height: 20px; -} - .checkbox-label { display: inline-flex; align-items: center; } -.checkbox-label input[type=checkbox] { +.checkbox-label input[type="checkbox"] { margin-top: 6px; } @@ -235,7 +329,7 @@ form.horizontal .input label { padding: 0.25rem; flex: 1; overflow: auto; - min-height: 0; + min-height: 0; } .grid-sensors { @@ -308,7 +402,7 @@ form.horizontal .input label { .grid-sensors .grid-box .box .resize { position: absolute; - right: 0;; + right: 0; bottom: 0; width: 10px; height: 10px; @@ -394,6 +488,13 @@ form.horizontal .input label { } @keyframes rotate { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(-360deg); } -} \ No newline at end of file + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } +} + +@import "components/dashboard-header"; +@import "components/filters-panel"; diff --git a/client/src/icons.ts b/client/src/icons.ts index 1db3e9a..6cd8254 100644 --- a/client/src/icons.ts +++ b/client/src/icons.ts @@ -1,2 +1,6 @@ export { ReactComponent as SettingsIcon } from '@/assets/icons/settings.svg' export { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg' +export { ReactComponent as MenuIcon } from '@/assets/icons/menu.svg' +export { ReactComponent as CancelIcon } from '@/assets/icons/cancel.svg' +export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg' +export { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg' diff --git a/client/src/index.tsx b/client/src/index.tsx index 3ca6435..d9df779 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -3,7 +3,7 @@ import { Root } from './Root' import { render } from 'preact' -import './assets/style.css' +import './assets/style.scss' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion render(, document.getElementById('application')!) diff --git a/client/src/layouts/UserLayout/UserLayout.tsx b/client/src/layouts/UserLayout/UserLayout.tsx new file mode 100644 index 0000000..d79a75d --- /dev/null +++ b/client/src/layouts/UserLayout/UserLayout.tsx @@ -0,0 +1,31 @@ +import { MenuIcon } from '@/icons' +import { cn } from '@/utils/cn' +import { ComponentChild } from 'preact' +import { useState } from 'preact/hooks' +import { UserMenu } from './components/UserMenu' + +type Props = { + children: ComponentChild + header?: ComponentChild + className?: string +} + +export const UserLayout = ({ children, header, className }: Props) => { + const [menuShown, setMenuShown] = useState(false) + + return ( +
+
+
+
setMenuShown(true)}> + +
+ {header} +
+
+
+
{children}
+ setMenuShown(false)} /> +
+ ) +} diff --git a/client/src/layouts/UserLayout/components/UserMenu.tsx b/client/src/layouts/UserLayout/components/UserMenu.tsx new file mode 100644 index 0000000..f0618a6 --- /dev/null +++ b/client/src/layouts/UserLayout/components/UserMenu.tsx @@ -0,0 +1,28 @@ +import { CancelIcon } from '@/icons' + +type Props = { + shown: boolean + onHide: () => void +} + +export const UserMenu = ({ shown, onHide }: Props) => { + return ( + <> +
+
+
+ +
+ + +
+
+
+ {shown &&
} + + ) +} diff --git a/client/src/pages/dashboard/NewDashboardPage.tsx b/client/src/pages/dashboard/NewDashboardPage.tsx index 6f7aeaa..da1c9ef 100644 --- a/client/src/pages/dashboard/NewDashboardPage.tsx +++ b/client/src/pages/dashboard/NewDashboardPage.tsx @@ -3,12 +3,13 @@ import { getDashboards, updateDashboard, } from '@/api/dashboards' +import { UserLayout } from '@/layouts/UserLayout/UserLayout' import { createDashboardContent } from '@/utils/createDashboardContent' import { parseDashboard } from '@/utils/parseDashboard' import { useEffect, useMemo, useState } from 'preact/hooks' import { useQuery, useQueryClient } from 'react-query' -import { DashboardGrid } from './components/DashboardGrid' -import { DashboardHeader } from './components/DashboardHeader' +import { DashboardGrid } from './components/DashboardGrid/DashboardGrid' +import { DashboardHeader } from './components/DashboardHeader/DashboardHeader' import { GRID_H_SNAP, GRID_WIDTH } from './constants' import { DashboardContextProvider } from './contexts/DashboardContext' import { BoxDefinition } from './types' @@ -82,10 +83,14 @@ export const NewDashboardPage = () => { return ( -
- + + } + > -
+
) } diff --git a/client/src/pages/dashboard/components/DashboardGrid.tsx b/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx similarity index 87% rename from client/src/pages/dashboard/components/DashboardGrid.tsx rename to client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx index b1dd6d0..da56b35 100644 --- a/client/src/pages/dashboard/components/DashboardGrid.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx @@ -1,6 +1,6 @@ -import { BoxDefinition } from '../types' -import { normalizeBoxes } from '../utils/normalizeBoxes' -import { EditableBox } from './EditableBox' +import { BoxDefinition } from '../../types' +import { normalizeBoxes } from '../../utils/normalizeBoxes' +import { EditableBox } from './components/EditableBox' type Props = { boxes: BoxDefinition[] diff --git a/client/src/pages/dashboard/components/BoxDialContent.tsx b/client/src/pages/dashboard/components/DashboardGrid/components/BoxDialContent.tsx similarity index 91% rename from client/src/pages/dashboard/components/BoxDialContent.tsx rename to client/src/pages/dashboard/components/DashboardGrid/components/BoxDialContent.tsx index 1be0c0b..723c962 100644 --- a/client/src/pages/dashboard/components/BoxDialContent.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid/components/BoxDialContent.tsx @@ -3,8 +3,8 @@ import { DashboardDialData } from '@/utils/parseDashboard' import { RefObject } from 'preact' import { useMemo } from 'preact/hooks' import { useQuery } from 'react-query' -import { useDashboardContext } from '../contexts/DashboardContext' -import { BoxDefinition } from '../types' +import { useDashboardContext } from '../../../contexts/DashboardContext' +import { BoxDefinition } from '../../../types' import { BoxLoader } from './BoxLoader' type Props = { diff --git a/client/src/pages/dashboard/components/BoxGraphContent.tsx b/client/src/pages/dashboard/components/DashboardGrid/components/BoxGraphContent.tsx similarity index 95% rename from client/src/pages/dashboard/components/BoxGraphContent.tsx rename to client/src/pages/dashboard/components/DashboardGrid/components/BoxGraphContent.tsx index 7449356..2b69b94 100644 --- a/client/src/pages/dashboard/components/BoxGraphContent.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid/components/BoxGraphContent.tsx @@ -1,12 +1,12 @@ import { getSensorValues } from '@/api/sensorValues' +import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext' +import { BoxDefinition } from '@/pages/dashboard/types' import { max } from '@/utils/max' import { min } from '@/utils/min' import { DashboardGraphData } from '@/utils/parseDashboard' import { RefObject } from 'preact' import { useEffect, useRef } from 'preact/hooks' import { useQuery } from 'react-query' -import { useDashboardContext } from '../contexts/DashboardContext' -import { BoxDefinition } from '../types' import { BoxLoader } from './BoxLoader' type Props = { diff --git a/client/src/pages/dashboard/components/BoxLoader.tsx b/client/src/pages/dashboard/components/DashboardGrid/components/BoxLoader.tsx similarity index 100% rename from client/src/pages/dashboard/components/BoxLoader.tsx rename to client/src/pages/dashboard/components/DashboardGrid/components/BoxLoader.tsx diff --git a/client/src/pages/dashboard/components/EditableBox.tsx b/client/src/pages/dashboard/components/DashboardGrid/components/EditableBox.tsx similarity index 90% rename from client/src/pages/dashboard/components/EditableBox.tsx rename to client/src/pages/dashboard/components/DashboardGrid/components/EditableBox.tsx index 97745df..6f4b2c4 100644 --- a/client/src/pages/dashboard/components/EditableBox.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid/components/EditableBox.tsx @@ -1,15 +1,15 @@ import { useWindowEvent } from '@/utils/hooks/useWindowEvent' import { useRef, useState } from 'preact/hooks' -import { GRID_WIDTH } from '../constants' -import { useDashboardContext } from '../contexts/DashboardContext' -import { useDragging } from '../hooks/useDragging' -import { ResizingMode, useResize } from '../hooks/useResize' -import { BoxDefinition } from '../types' import { BoxDialContent } from './BoxDialContent' import { BoxGraphContent } from './BoxGraphContent' -import { BoxSettings } from './BoxSettings/BoxSettings' import { useElementOffsets } from '@/utils/hooks/useElementOffsets' import { RefreshIcon, SettingsIcon } from '@/icons' +import { BoxDefinition } from '@/pages/dashboard/types' +import { GRID_WIDTH } from '@/pages/dashboard/constants' +import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext' +import { useDragging } from '@/pages/dashboard/hooks/useDragging' +import { useResize, ResizingMode } from '@/pages/dashboard/hooks/useResize' +import { BoxSettings } from '../../BoxSettings/BoxSettings' type Props = { box: BoxDefinition @@ -54,6 +54,10 @@ export const EditableBox = ({ const handleMouseDown = (e: MouseEvent) => { e.preventDefault() + if (verticalMode) { + return + } + if (!dragging.active && boxRef) { const pos = { top: boxRef.offsetTop, @@ -74,6 +78,10 @@ export const EditableBox = ({ e.preventDefault() e.stopPropagation() + if (verticalMode) { + return + } + if (resizing.mode === ResizingMode.NONE) { setResizing({ mode: target, diff --git a/client/src/pages/dashboard/components/DashboardHeader.tsx b/client/src/pages/dashboard/components/DashboardHeader.tsx deleted file mode 100644 index 57b07f6..0000000 --- a/client/src/pages/dashboard/components/DashboardHeader.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { RefreshIcon } from '@/icons' -import { Filters } from './Filters' - -type Props = { - onNewBox: () => void - onRefresh: () => void -} - -export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => { - return ( -
-
- -
- -
- -
-
-
- ) -} diff --git a/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx b/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx new file mode 100644 index 0000000..2171c19 --- /dev/null +++ b/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -0,0 +1,59 @@ +import { CancelIcon, FiltersIcon, PlusIcon, RefreshIcon } from '@/icons' +import { useState } from 'preact/hooks' +import { useDashboardContext } from '../../contexts/DashboardContext' +import { DashboardFilters } from './components/DashboardFilters' + +type Props = { + onNewBox: () => void + onRefresh: () => void +} + +export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => { + const { verticalMode } = useDashboardContext() + const [filtersShown, setFiltersShown] = useState(false) + + return ( +
+ {verticalMode && ( + <> + + +
setFiltersShown(true)}> + +
+ + {filtersShown && ( +
+
+
+
setFiltersShown(false)} + > + +
+ + +
+
+ )} + + )} + {!verticalMode && ( + <> + +
+ +
+ + + )} +
+ ) +} diff --git a/client/src/pages/dashboard/components/Filters.tsx b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardFilters.tsx similarity index 87% rename from client/src/pages/dashboard/components/Filters.tsx rename to client/src/pages/dashboard/components/DashboardHeader/components/DashboardFilters.tsx index 166f4fb..c361ca9 100644 --- a/client/src/pages/dashboard/components/Filters.tsx +++ b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardFilters.tsx @@ -1,7 +1,7 @@ import { DateTimeInput } from '@/components/DateTimeInput' import { intervalToRange } from '@/utils/intervalToRange' import { useEffect, useState } from 'preact/hooks' -import { useDashboardContext } from '../contexts/DashboardContext' +import { useDashboardContext } from '../../../contexts/DashboardContext' export type FilterInterval = | 'hour' @@ -17,8 +17,8 @@ export type FilterValue = { customTo: Date } -export const Filters = () => { - const { filter: preset, setFilter } = useDashboardContext() +export const DashboardFilters = () => { + const { filter: preset, setFilter, verticalMode } = useDashboardContext() const [value, setValue] = useState(preset) @@ -50,7 +50,10 @@ export const Filters = () => { return (
-
+