diff --git a/.vscode/settings.json b/.vscode/settings.json index 7196aa8..5aec289 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,15 @@ { "editor.insertSpaces": true, "editor.tabSize": 4, - + "editor.linkedEditing": true, + + "[html]": { + "editor.rulers": [120], + }, + "html.format.wrapLineLength": 120, + "html.format.wrapAttributes": "aligned-multiple", + "html.format.templating": true, + "[python]": { "editor.formatOnSave": true, "editor.defaultFormatter": "ms-python.autopep8", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bbe2380..ab79518 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ { - "label": "Format All", + "label": "Python: Format All", "type": "shell", "command": "python3 -m isort .; python3 -m autopep8 --in-place -r .", "windows": { diff --git a/frontend/static/css/admin.css b/frontend/static/css/admin.css index d9c5685..4cf29cf 100644 --- a/frontend/static/css/admin.css +++ b/frontend/static/css/admin.css @@ -1,160 +1,108 @@ -main { - position: relative; -} - -main a { - color: var(--color-light); -} - .action-buttons { - --spacing: .5rem; - - position: absolute; - margin: var(--spacing); - inset: 0 0 auto 0; - height: var(--nav-width); - display: flex; justify-content: center; align-items: center; - gap: calc(var(--spacing) * 3); + flex-wrap: wrap; + gap: 1rem; - padding: var(--spacing); - border-radius: 4px; - background-color: var(--color-gray); + padding: 1rem .5rem; + + & > button { + width: 10rem; + + display: flex; + justify-content: center; + align-items: center; + gap: .75rem; + + padding: .5rem; + border-radius: 4px; + background-color: var(--color-gray); + color: var(--color-light); + + transition: + background-color 150ms ease-in-out, + box-shadow 150ms ease-in-out; + + &:hover { + background-color: var(--color-light-gray); + box-shadow: var(--default-shadow); + } + + &.submit-error { + background-color: var(--color-error); + } + + & > svg { + height: 1.8rem; + width: 2rem; + } + } } -.action-buttons > button { - height: 100%; +#grid-container { + --grid-spacing: 1.25rem; - display: flex; - justify-content: center; - align-items: center; + width: min(100%, 80rem + 3 * var(--grid-spacing)); + margin-inline: auto; + + display: grid; + grid-template: auto auto auto auto / 1fr 1fr; + gap: var(--grid-spacing); + + padding: var(--grid-spacing); + padding-bottom: 3rem; +} + +#grid-container > section { + max-height: 40rem; + min-width: 0; - padding: .5rem; border-radius: 4px; + border: 3px solid var(--color-gray); background-color: var(--color-dark); - color: var(--color-light); - transition: background-color .1s ease-in-out; -} - -.action-buttons > button:hover { - background-color: var(--color-gray); -} - -.action-buttons > button > svg { - height: 1.8rem; - width: 2rem; -} - -.form-container { - height: calc(100vh - var(--header-height)); - overflow-y: auto; - - padding: .5rem; - padding-top: var(--nav-width); -} - -#settings-form, -#hosting-form { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.5rem; + box-shadow: 0px 0px 8px 3px rgba(0 0 0 / 0.8); } h2 { - width: 100%; - + padding: .4rem .75rem; border-bottom: 1px solid var(--color-gray); - padding: 1rem 1rem .25rem 1rem; - - font-size: clamp(1rem, 10vw, 2rem); } -.settings-table-container, -.user-table-container { - width: 100%; - overflow-x: auto; - +.collaps-table { + --min-width: 18rem; + --max-width: 40rem; + --header-width: 30%; + --min-data-width: 15rem; +} + +#power-section > .table-container { + padding: 1rem; + display: flex; flex-direction: column; + justify-content: center; align-items: center; + gap: 1rem; } -.settings-table { - --max-width: 55rem; - width: 100%; - max-width: var(--max-width); - min-width: 20rem; - - border-spacing: 0px; - border: none; +tr:has(:where(input, select).changed) label { + color: var(--color-changed); } -.settings-table td { - --middle-spacing: .75rem; - padding-bottom: 1rem; - vertical-align: top; +:where(input, select).changed, +div.input-style:has(input.changed) { + border-color: var(--color-changed); } -.settings-table td:first-child { - width: 50%; - padding-right: var(--middle-spacing); - padding-top: .55rem; - text-align: right; +#backup-settings-section td:has(div.checked-input-container) { + height: 6.6rem; } -.settings-table td:nth-child(2) { - min-width: calc(var(--max-width) * 0.5); - padding-left: var(--middle-spacing); -} +.add-item-container { + margin: 1rem; -.settings-table td p { - color: var(--color-light-gray); - font-size: .9rem; -} - -.settings-table td > p { - margin-top: .25rem; -} - -.number-input { - width: fit-content; - display: flex; - align-items: center; - - border: 2px solid var(--color-gray); - border-radius: 4px; - - box-shadow: var(--default-shadow); -} - -.number-input > input { - width: auto; - border: none; - box-shadow: none; - text-align: right; -} - -.number-input > * { - padding: .5rem 1rem; -} - -.number-input > p { - padding-left: 0; -} - -.settings-table select { - width: auto; -} - -.add-user-container, -.database-container { - margin-top: 2rem; - margin-bottom: 1rem; - width: 100%; display: flex; justify-content: center; align-items: center; @@ -162,140 +110,103 @@ h2 { flex-wrap: wrap; } -#download-logs-button, -#save-hosting-button, -#add-user-button, -#upload-db-button, -#restart-button, -#shutdown-button { - width: min(15rem, 100%); +.entries-table { + margin-inline: auto; + margin-bottom: 1rem; - padding: .5rem 1rem; - border-radius: 4px; - background-color: var(--color-gray); - - box-shadow: var(--default-shadow); + width: clamp(22rem, 100%, 40rem); } -.database-container:has(#download-logs-button) { - margin: 0; -} - -#save-hosting-button { - align-self: center; -} - -#add-user-button { - height: 2rem; -} - -#add-user-button > svg { - aspect-ratio: 1/1; - height: 1rem; - width: min-content; -} - -#user-table, -#backup-table { - min-width: 25rem; - border-spacing: 0px; -} - -:where(#user-table, #backup-table) :where(th, td) { - height: 2.65rem; - padding: .25rem .5rem; - text-align: left; -} - -:where(#user-table, #backup-table) tr td { - border-top: 1px solid var(--color-gray); -} - -:where(#user-table, #backup-table) :where(th, td):first-child { - width: 10rem; - padding-left: 2rem; -} - -:where(#user-table, #backup-table) :where(th, td):nth-child(2) { - width: 15rem; -} - -:where(#user-table, #backup-table) :where(th, td):last-child { - width: 5.75rem; - display: flex; - align-items: center; - gap: 1rem; - padding-right: 2rem; -} - -:where(#user-table, #backup-table) button { - height: 1.25rem; -} - -:where(#user-table, #backup-table) svg { - aspect-ratio: 1/1; - height: 100%; - width: auto; -} - -#user-table input { +#user-table td:first-child { width: 100%; - padding: .25rem; } -#upload-database-form { +#user-table td:last-child { + width: 6rem; +} + +#backup-table { + width: clamp(30rem, 100%, 40rem); +} + +#backup-table td:first-child { + width: 100%; +} + +#backup-table td:nth-child(2) { + text-wrap-mode: nowrap; +} + +#add-user-form, +#edit-user-form { + width: 16rem; + height: 8rem; + display: flex; flex-direction: column; - align-items: center; + justify-content: center; gap: 1rem; - - margin-block: 2rem 1rem; } -#hosting-form > p, -#upload-database-form > p { - max-width: 50rem; - margin-inline: auto; +#edit-user-form:has(div.hidden) { + height: unset; +} + +dialog span { + font-weight: bold; +} + +dialog:where(#upload-db-dialog, #import-db-dialog) { + --max-height: 25rem; +} + +:where(#upload-db-dialog, #import-db-dialog) .dialog-content > p { + padding-inline: 1rem; text-align: center; - word-wrap: break-word; } -#backup-table :where(th, td):first-child { - width: 20rem; +#upload-db-form tr:first-child td:last-child { + height: 6.1rem; } -@media (max-width: 40rem) { +#reset-settings-dialog .dialog-content { + height: calc(100% - 2 * 3.2rem); + justify-content: flex-start; + overflow-y: auto; +} + +#reset-settings-form { + width: 100%; +} + +#reset-table { + max-width: 30rem; +} + +@media (max-width: 24rem) { + .action-buttons button { + width: 100%; + + &:last-of-type { + flex-direction: row-reverse; + } + } +} + +@media (max-width: 29rem) { + .entries-table { + --inline-padding: 0; + } +} + +@media (max-width: 52.25rem) { + #grid-container { + grid-template: repeat(8, auto) / 1fr; + padding-bottom: var(--grid-spacing); + } + h2 { text-align: center; padding-inline: 0; } - - .settings-table-container, - .user-table-container { - align-items: flex-start; - } - - .settings-table tbody { - display: flex; - flex-direction: column; - } - - .settings-table tr { - display: inline-flex; - flex-direction: column; - padding-left: 1rem; - } - - .settings-table td { - width: 100%; - } - - .settings-table td:first-child { - width: 100%; - text-align: left; - } - - .settings-table td:nth-child(2) { - min-width: 0; - } } diff --git a/frontend/static/css/general.css b/frontend/static/css/general.css index adeacc8..4de1db9 100644 --- a/frontend/static/css/general.css +++ b/frontend/static/css/general.css @@ -2,28 +2,41 @@ box-sizing: border-box; margin: 0; padding: 0; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', + 'Helvetica Neue', sans-serif; font-size: 1rem; } :root { --color-light: #ffffff; --color-light-gray: #6b6b6b; - --color-gray: #3c3c3c; - --color-dark: #1b1b1b; + --color-mid-gray: #5c5c5c; + --color-gray: #232323; + --color-dark: #111111; + --color-changed: #b87000; --color-error: #db5461; + --color-dim-error: #bc4551; --color-success: #54db68; - + --header-height: 4.5rem; --nav-width: 4rem; + --scrollbar-width: 12px; + --rem-clamp: clamp(.5rem, 2vw, 1rem); - --default-shadow: 0 1px 2px 0 rgb(0 0 0 / 60%), 0 2px 6px 2px rgb(0 0 0 / 30%); + --default-shadow: + 0 1px 2px 0 rgb(0 0 0 / 60%), + 0 2px 6px 2px rgb(0 0 0 / 30%); } /* */ /* Default properties */ /* */ +dialog { + margin: auto; +} + img { width: 100%; } @@ -31,9 +44,6 @@ img { button, label { border: 0; - border-radius: 4px; - background-color: var(--color-dark); - color: var(--color-light); } button:hover, @@ -41,22 +51,6 @@ label:hover { cursor: pointer; } -input:not([type="checkbox"]), -select, -textarea, -.as-button { - border: 2px solid var(--color-gray); - border-radius: 4px; - padding: .6rem; - outline: 0; - background-color: var(--color-dark); - color: var(--color-light); - - box-shadow: var(--default-shadow); - - font-size: 1rem; -} - input::placeholder, textarea::placeholder { color: var(--color-gray); @@ -66,14 +60,21 @@ input[type="datetime-local"] { color-scheme: dark; } -svg path, -svg rect { +svg :where(path, rect) { fill: var(--color-light); } +a { + color: var(--color-light); +} + +table { + border-spacing: 0px; +} + ::-webkit-scrollbar { - width: 12px; - height: 12px; + width: var(--scrollbar-width); + height: var(--scrollbar-width); background-color: var(--color-dark); } @@ -120,11 +121,255 @@ svg rect { font-size: 1rem !important; } +button.spinning svg, +svg.spinning { + animation: spin-element 2.5s linear infinite forwards; +} + +@keyframes spin-element { + from { transform: rotate(0deg) } + to { transform: rotate(360deg) } +} + +:where(input, textarea, select, button, div).input-style { + height: 2.7rem; + width: 100%; + + border: 2px solid var(--color-gray); + border-radius: 4px; + outline: 0; + background-color: var(--color-dark); + color: var(--color-light); + + box-shadow: var(--default-shadow); + transition: background-color 150ms ease-in-out; + + font-size: 1rem; +} + +:where(input, textarea, select, button, div).input-style:focus-within { + border-color: var(--color-light-gray); +} + +:where(input, textarea, select, button).input-style, +div.input-style > :where(input, p) { + padding: .5rem; +} + +button:has(> svg).input-style { + display: flex; + justify-content: center; + align-items: center; +} + +button.input-style > svg { + height: 1.3rem; + width: auto; +} + +div.input-style { + width: fit-content; + display: flex; + align-items: center; + + & > input { + width: 100%; + height: 100%; + + border: 0; + outline: 0; + background-color: var(--color-dark); + color: var(--color-light); + } +} + +button.input-style:hover { + background-color: var(--color-gray); +} + +div.input-style > input[type="number"], +input[type="number"].input-style { + max-width: 6rem; +} + +div.input-style > input[type="file"], +input[type="file"].input-style { + max-width: 20rem; +} + +div.input-style > :where(input[type="text"], select), +:where(input[type="text"], select).input-style { + max-width: 16rem; +} + +button.input-style { + max-width: 16rem; +} + +:where(input, textarea, select, button).long-input-style, +div.long-input-style > input { + max-width: 25rem; +} + +.checked-input-container { + position: relative; + height: 100%; + max-height: 2.7rem; + width: 100%; + max-width: 16rem; + + transition: max-height 50ms ease-in-out, + border-color 50ms ease-in-out; +} + +.checked-input-container.error-input-container { + max-height: 4.1rem; +} + +.checked-input-container.error-input-container input { + border-color: var(--color-error); +} + +.checked-input-container :where(input, textarea) { + position: relative; + z-index: 1; +} + +.checked-input-container p { + position: absolute; + bottom: 0; + height: 1.9rem; + width: 100%; + + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border: 2px solid var(--color-error); + padding: .6rem .65rem .3rem .65rem; + background-color: var(--color-dim-error); + color: var(--color-light); + + font-size: .7rem; +} + +.table-container { + width: 100%; + overflow: auto; +} + +.entries-table { + --inline-padding: 1.5rem; + + position: relative; + + padding-inline: var(--inline-padding); +} + +.entries-table::before { + content: ""; + position: absolute; + inset: 0 auto 0 var(--inline-padding); + + width: 1.4rem; + background: linear-gradient(90deg, var(--color-dark), transparent); +} + +.entries-table::after { + content: ""; + position: absolute; + inset: 0 var(--inline-padding) 0 auto; + + width: 1.4rem; + background: linear-gradient(270deg, var(--color-dark), transparent); +} + +.entries-table > tbody > tr { + transition: background-color 200ms ease-in-out; + + &:hover { + background-color: var(--color-gray); + } +} + +.entries-table :where(th, td) { + height: 2.65rem; + padding: .25rem .5rem; + text-align: left; +} + +.entries-table td { + border-top: 1px solid var(--color-gray); +} + +.entries-table :where(th, td):first-child { + padding-left: 2rem; +} + +.entries-table :where(th, td):last-child { + padding-right: 2rem; +} + +.entries-table td:last-child:has(button) { + display: flex; + align-items: center; + gap: .75rem; +} + +.entries-table td:last-child button { + height: 1.25rem; + background-color: transparent; +} + +.entries-table svg { + aspect-ratio: 1/1; + height: 100%; + width: auto; +} + +.collaps-table { + --min-width: 20rem; + --max-width: 55rem; + --header-width: 50%; + --min-data-width: 25rem; + + width: clamp(var(--min-width), 100%, var(--max-width)); + + padding-inline: 1rem; +} + +.collaps-table tr:first-of-type :where(th, td) { + padding-top: 1rem; +} + +.collaps-table :where(th, td) { + --middle-spacing: .75rem; + padding-bottom: 1rem; + vertical-align: top; + + &:first-child { + width: var(--header-width); + padding-right: var(--middle-spacing); + text-align: right; + } + + &:last-child { + min-width: var(--min-data-width); + padding-left: var(--middle-spacing); + } + + & > p { + margin-top: .25rem; + + color: var(--color-light-gray); + + font-size: .9rem; + } +} + /* */ /* General styling */ /* */ body { - height: 100vh; + height: 100dvh; overflow-x: hidden; background-color: var(--color-dark); @@ -141,44 +386,23 @@ noscript { padding: 1rem; background-color: var(--color-error); color: var(--color-light); + + text-align: center; } -/* */ -/* Header */ -/* */ header { width: 100%; height: var(--header-height); - + display: flex; align-items: center; padding: 1rem; - box-shadow: var(--default-shadow); + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; background-color: var(--color-gray); -} -header > div { - height: 100%; - transform: translateX(-2.6rem); - - display: flex; - align-items: center; - gap: 1rem; - - transition: transform .3s ease-in-out; -} - -#toggle-nav { - --height: 1.5rem; - height: var(--height); - - background-color: transparent; -} - -#toggle-nav svg { - height: var(--height); - width: var(--height); + box-shadow: var(--default-shadow); } header img { @@ -186,152 +410,207 @@ header img { width: fit-content; } -/* */ -/* Nav */ -/* */ -.nav-divider { - position: relative; - height: calc(100% - var(--header-height)); +dialog { + --ani-duration: 100ms; + --max-height: 23rem; + + width: min(100%, 40rem); + height: min(100%, var(--max-height)); + + border-radius: 6px; + border: 4px solid var(--color-gray); + background-color: var(--color-dark); + color: var(--color-light); + + box-shadow: 0px 0px 20px 5px rgba(0 0 0 / 0.8); - display: flex; - - padding-block: var(--rem-clamp); + animation: vanish-dialog var(--ani-duration); } -body:has(#nav-switch:checked) .nav-divider > nav { - left: var(--rem-clamp); +dialog[open] { + animation: appear-dialog var(--ani-duration); } -body:has(#nav-switch:checked) .nav-divider > .window-container { - margin-left: calc(var(--nav-width) + var(--rem-clamp)); +dialog::backdrop { + backdrop-filter: blur(3px); } -nav { - --padding: .5rem; - z-index: 1; - position: absolute; - left: var(--rem-clamp); - height: calc(100% - (2 * var(--rem-clamp))); - width: var(--nav-width); - +dialog[open]::backdrop { + animation: appear-dialog-bg var(--ani-duration); +} + +@keyframes appear-dialog { + from { + opacity: 0; + translate: 0 -5%; + } + to { + opacity: 1; + translate: 0 0; + } +} + +@keyframes vanish-dialog { + from { + display: block; + opacity: 1; + translate: 0 0; + } + to { + display: none; + opacity: 0; + translate: 0 -5%; + } +} + +@keyframes appear-dialog-bg { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.dialog-container { + height: 100%; display: flex; flex-direction: column; justify-content: space-between; - align-items: center; - gap: var(--padding); - overflow-y: auto; - - padding: var(--padding); - border-radius: 4px; - background-color: var(--color-gray); - - transition: left .3s ease-in-out; } -nav > div { - width: 100%; +.dialog-header, .dialog-footer { + height: 3.2rem; + display: flex; + align-items: center; + gap: 1rem; + + padding: .4rem .75rem; + + &.dialog-header { + justify-content: flex-start; + border-bottom: 2px solid var(--color-gray); + } + + &.dialog-footer { + justify-content: flex-end; + border-top: 2px solid var(--color-gray); + } +} + +.dialog-header h2 { + padding: 0; + border-bottom: 0; +} + +.dialog-footer > button, +.confirm-container > button { + width: 5rem; + height: 2.3rem; + padding: 0; + + &:first-of-type:where(:hover, :focus-within) { + background-color: var(--color-dim-error); + } + + &:last-of-type:where(:hover, :focus-within) { + background-color: var(--color-success); + } +} + +.dialog-content { + flex-grow: 1; + display: flex; flex-direction: column; - gap: var(--padding); -} - -nav > div > button { - width: 100%; - - display: flex; justify-content: center; align-items: center; - - padding: .5rem; - border: 0; - border-radius: 4px; - background-color: var(--color-dark); - color: var(--color-light); - - transition: background-color .1s ease-in-out; + gap: 1rem; } -nav > div > button:hover { - background-color: var(--color-gray); -} - -nav > div > button svg { - height: 1.8rem; - width: 2rem; -} - -/* */ -/* Window management */ -/* */ -.window-container { - margin-left: calc(4rem + var(--rem-clamp)); - width: 100%; - +.confirm-container { display: flex; - overflow: hidden; - - transition: margin-left .3s ease-in-out; + flex-direction: row-reverse; + gap: 1rem; } -.window-container > :where(#home, .extra-window-container) { - width: 100%; - flex: 0 0 auto; - - translate: 0 0; - transition: translate .5s ease-in-out; -} - -.window-container.inter-window-ani > :where(#home, .extra-window-container) { - transition: translate .5s ease-in-out, - transform .5s ease-in-out; -} - -.extra-window-container { - --y-offset: 0%; - transform: translateY(var(--y-offset)); -} - -.extra-window-container > div { - height: 100%; - overflow-y: auto; -} - -.window-container.show-window > :where(#home, .extra-window-container) { - translate: -100% 0; -} - -.window-container.show-window > .extra-window-container { - transform: translateY(var(--y-offset)); -} - -/* */ -/* Styling extra window */ -/* */ -.extra-window-container > div { - padding: var(--rem-clamp); -} - -.extra-window-container > div > h2 { - text-align: center; - font-size: clamp(1.3rem, 5vw, 2rem); - margin-bottom: 2rem; -} - -.extra-window-container > div > h2:not(:first-of-type) { - margin-top: 1.5rem; -} - -.extra-window-container > div > p { - text-align: center; -} - -@media (max-width: 543px) { - .window-container { - margin-left: 0; +.confirm-container > button { + width: 6rem; + + &:first-of-type:where(:hover, :focus-within) { + background-color: var(--color-success); } - nav { - left: -100%; + &:last-of-type:where(:hover, :focus-within) { + background-color: var(--color-dim-error); } } + +@media (prefers-reduced-motion) { + * { + animation-duration: 0ms !important; + transition-duration: 0ms !important; + } +} + +@media (max-width: 29rem) { + .collaps-table tbody { + display: flex; + flex-direction: column; + } + + .collaps-table tr { + display: inline-flex; + flex-direction: column; + padding-left: .5rem; + } + + .collaps-table tr:first-of-type > td { + padding-top: 0rem; + } + + .collaps-table :where(th, td) { + width: 100%; + + &:first-child { + width: 100%; + text-align: left; + } + + &:last-child { + min-width: unset; + } + } + + dialog { + margin: 0; + width: 100%; + height: 100%; + max-width: 100vw; + max-height: 100vh; + + border-radius: 0; + border: 0; + } + + @keyframes appear-dialog { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes vanish-dialog { + from { + display: block; + opacity: 1; + } + to { + display: none; + opacity: 0; + } + } +} \ No newline at end of file diff --git a/frontend/static/css/library.css b/frontend/static/css/library.css index 9d10110..a7bf200 100644 --- a/frontend/static/css/library.css +++ b/frontend/static/css/library.css @@ -126,6 +126,7 @@ body:has(#wide-toggle:checked) .tab-container > div { border-radius: 4px; padding: .75rem; background-color: var(--color); + color: var(--color-light); } button.entry.fit { diff --git a/frontend/static/css/login.css b/frontend/static/css/login.css index a88256b..2b3f75b 100644 --- a/frontend/static/css/login.css +++ b/frontend/static/css/login.css @@ -1,65 +1,46 @@ main { - height: calc(100vh - var(--header-height)); - - overflow-y: hidden; -} - -main:has(#form-switch:checked) .form-container { - transform: translateY(-100%); -} - -.form-container { - height: inherit; - - display: flex; - justify-content: center; - align-items: center; - - padding: 1rem; - - transition: transform .25s ease-in-out; -} - -form { - width: 30rem; - margin-inline: auto; - + height: calc(100% - var(--header-height)); display: flex; flex-direction: column; justify-content: center; align-items: center; - gap: 1rem; +} + +.login-container { + position: relative; + width: min(40rem, 90%); + height: 22rem; + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + overflow: hidden; + border-radius: 4px; - padding: 1rem; background-color: var(--color-gray); - color: var(--color-light); + + box-shadow: 0px 0px 20px 3px rgba(0 0 0 / 0.25); +} + +form { + height: inherit; + flex: 1 0 auto; + + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 1rem; + + padding: 2rem; + + transition: translate 100ms ease-in-out; } form h2 { font-size: clamp(1.2rem, 7vw, 2rem); } -#username-error-container, -#password-error-container { - width: 100%; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: .5rem; -} - -form input { - width: min(100%, 20rem); -} - -#username-error-container p, -#password-error-container p { - width: min(100%, 19rem); -} - .switch-button { border: 0; background-color: transparent; @@ -69,12 +50,53 @@ form input { text-align: center; } -.switch-button:hover { - cursor: pointer; -} - button[type="submit"] { + width: 7rem; + padding: .5rem 1rem; + border-radius: 4px; + background-color: var(--color-mid-gray); + color: var(--color-light); font-size: 1.1rem; + + transition: + background-color 150ms ease-in-out, + box-shadow 150ms ease-in-out; +} + +button[type="submit"]:hover { + background-color: var(--color-light-gray); + box-shadow: var(--default-shadow); +} + +#form-cover { + position: absolute; + left: 0; + height: 100%; + width: 50%; + z-index: 2; + + display: flex; + + padding: 3rem; + border-radius: 4px; + background-color: var(--color-gray); + + box-shadow: 0px 0px 20px 3px rgba(0 0 0 / 0.25); + transition: left 100ms ease-in-out; +} + +main:has(#form-switch:checked) #form-cover { + left: 50%; +} + +@media (max-width: 594px) { + #form-cover { + display: none; + } + + main:has(#form-switch:checked) form { + translate: 0 -100%; + } } diff --git a/frontend/static/css/reminders.css b/frontend/static/css/reminders.css new file mode 100644 index 0000000..cb2cf42 --- /dev/null +++ b/frontend/static/css/reminders.css @@ -0,0 +1,175 @@ +/* */ +/* Nav collapse button */ +/* */ +header > div { + height: 100%; + transform: translateX(-2.6rem); + + display: flex; + align-items: center; + gap: 1rem; + + transition: transform .3s ease-in-out; +} + +#toggle-nav { + --height: 1.5rem; + height: var(--height); + + background-color: transparent; +} + +#toggle-nav svg { + height: var(--height); + width: var(--height); +} + +/* */ +/* Nav */ +/* */ +.nav-divider { + position: relative; + height: calc(100% - var(--header-height)); + + display: flex; + + padding-block: var(--rem-clamp); +} + +body:has(#nav-switch:checked) .nav-divider > nav { + left: var(--rem-clamp); +} + +body:has(#nav-switch:checked) .nav-divider > .window-container { + margin-left: calc(var(--nav-width) + var(--rem-clamp)); +} + +nav { + --padding: .5rem; + z-index: 1; + position: absolute; + left: var(--rem-clamp); + height: calc(100% - (2 * var(--rem-clamp))); + width: var(--nav-width); + + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: var(--padding); + overflow-y: auto; + + padding: var(--padding); + border-radius: 4px; + background-color: var(--color-gray); + + transition: left .3s ease-in-out; +} + +nav > div { + width: 100%; + + display: flex; + flex-direction: column; + gap: var(--padding); +} + +nav > div > button { + width: 100%; + + display: flex; + justify-content: center; + align-items: center; + + padding: .5rem; + border: 0; + border-radius: 4px; + background-color: var(--color-dark); + color: var(--color-light); + + transition: background-color .1s ease-in-out; +} + +nav > div > button:hover { + background-color: var(--color-gray); +} + +nav > div > button svg { + height: 1.8rem; + width: 2rem; +} + +/* */ +/* Window management */ +/* */ +.window-container { + margin-left: calc(4rem + var(--rem-clamp)); + width: 100%; + + display: flex; + overflow: hidden; + + transition: margin-left .3s ease-in-out; +} + +.window-container > :where(#home, .extra-window-container) { + width: 100%; + flex: 0 0 auto; + + translate: 0 0; + transition: translate .5s ease-in-out; +} + +.window-container.inter-window-ani > :where(#home, .extra-window-container) { + transition: translate .5s ease-in-out, + transform .5s ease-in-out; +} + +.extra-window-container { + --y-offset: 0%; + transform: translateY(var(--y-offset)); +} + +.extra-window-container > div { + height: 100%; + overflow-y: auto; +} + +.window-container.show-window > :where(#home, .extra-window-container) { + translate: -100% 0; +} + +.window-container.show-window > .extra-window-container { + transform: translateY(var(--y-offset)); +} + +/* */ +/* Styling extra window */ +/* */ +.extra-window-container > div { + padding: var(--rem-clamp); +} + +.extra-window-container > div > h2 { + text-align: center; + font-size: clamp(1.3rem, 5vw, 2rem); + margin-bottom: 2rem; +} + +.extra-window-container > div > h2:not(:first-of-type) { + margin-top: 1.5rem; +} + +.extra-window-container > div > p { + text-align: center; +} + +@media (max-width: 543px) { + .window-container { + margin-left: 0; + } + + nav { + left: -100%; + } +} diff --git a/frontend/static/js/admin.js b/frontend/static/js/admin.js index 1af6640..d0074f1 100644 --- a/frontend/static/js/admin.js +++ b/frontend/static/js/admin.js @@ -1,383 +1,774 @@ -const setting_inputs = { - allow_new_accounts: document.querySelector('#allow-new-accounts-input'), - login_time: document.querySelector('#login-time-input'), - login_time_reset: document.querySelector('#login-time-reset-input'), - log_level: document.querySelector('#log-level-input'), - db_backup_interval: document.querySelector('#db-backup-interval-input'), - db_backup_amount: document.querySelector('#db-backup-amount-input'), - db_backup_folder: document.querySelector('#db-backup-folder-input') -}; +// +// region Elements +// +const windows = { + resetSettings: { + dialog: document.querySelector("#reset-settings-dialog"), + form: document.querySelector("#reset-settings-form"), + close: document.querySelector("#close-reset-settings"), + submit: document.querySelector("#submit-reset-button") + }, + addUser: { + dialog: document.querySelector("#add-user-dialog"), + form: document.querySelector("#add-user-form"), + close: document.querySelector("#close-add-user"), + inputContainers: { + username: document.querySelector("#add-user-form .checked-input-container:has(input[type='text'])") + }, + inputs: { + username: document.querySelector("#add-user-username-input"), + password: document.querySelector("#add-user-password-input") + }, + errors: { + usernameInvalid: document.querySelector('#add-invalid-username-error'), + usernameTaken: document.querySelector('#add-taken-username-error') + } + }, + editUser: { + dialog: document.querySelector("#edit-user-dialog"), + username: document.querySelector("#username-edit-user"), + form: document.querySelector("#edit-user-form"), + close: document.querySelector("#close-edit-user"), + inputContainers: { + username: document.querySelector("#edit-user-form .checked-input-container:has(input[type='text'])") + }, + inputs: { + username: document.querySelector("#edit-user-username-input"), + password: document.querySelector("#edit-user-password-input") + }, + errors: { + usernameInvalid: document.querySelector('#edit-invalid-username-error'), + usernameTaken: document.querySelector('#edit-taken-username-error') + } + }, + deleteUser: { + dialog: document.querySelector("#delete-user-dialog"), + username: document.querySelector("#username-delete-user"), + close: document.querySelector("#close-delete-user"), + confirm: document.querySelector("#confirm-delete-user") + }, + uploadDb: { + dialog: document.querySelector("#upload-db-dialog"), + form: document.querySelector("#upload-db-form"), + close: document.querySelector("#close-upload-db"), + submit: document.querySelector("#upload-db-dialog button[type='submit']"), + inputContainers: { + file: document.querySelector("#upload-db-form .checked-input-container:has(input[type='file'])") + }, + inputs: { + file: document.querySelector("#upload-db-form input[type='file']"), + keepHostingSettings: document.querySelector("#upload-db-form input[type='checkbox']") + }, + errors: { + invalidFile: document.querySelector('#upload-invalid-db-error'), + } + }, + importDb: { + dialog: document.querySelector("#import-db-dialog"), + form: document.querySelector("#import-db-form"), + close: document.querySelector("#close-import-db"), + submit: document.querySelector("#import-db-dialog button[type='submit']"), + backupName: document.querySelector("#db-backup-name"), + backupCreation: document.querySelector("#db-creation-date"), + inputs: { + keepHostingSettings: document.querySelector("#import-db-form input[type='checkbox']") + } + } +} -const hosting_inputs = { - form: document.querySelector('#hosting-form'), +const els = { + logout: document.querySelector("#logout-button"), + settingsSubmit: document.querySelector('#save-button'), + changesCount: document.querySelector('#changes-count'), + settingsForm: document.querySelector('#settings-form'), + downloadLogs: document.querySelector("#download-logs-button"), + startResetSettings: document.querySelector("#open-reset-button"), + userList: document.querySelector("#user-list"), + dbBackupFolderContainer: document.querySelector('div.checked-input-container:has(#db-backup-folder-input)'), + backupList: document.querySelector("#backup-list"), + startAddUser: document.querySelector("#add-user-button"), + uploadDb: document.querySelector("#upload-db-button"), + downloadDb: document.querySelector("#download-db-button"), + about: { + mindVersion: document.querySelector("#mind-version"), + pythonVersion: document.querySelector("#python-version"), + dbVersion: document.querySelector("#db-version"), + dbLocation: document.querySelector("#db-location"), + dataFolder: document.querySelector("#data-folder") + }, + power: { + restart: document.querySelector('#restart-button'), + shutdown: document.querySelector('#shutdown-button') + } +} + +const settings = { + allowNewAccounts: document.querySelector('#allow-new-accounts-input'), + loginTime: document.querySelector('#login-time-input'), + loginTimeReset: document.querySelector('#login-time-reset-input'), host: document.querySelector('#host-input'), port: document.querySelector('#port-input'), - url_prefix: document.querySelector('#url-prefix-input'), - submit: document.querySelector('#save-hosting-button') -}; + urlPrefix: document.querySelector('#url-prefix-input'), + logLevel: document.querySelector('#log-level-input'), + dbBackupInterval: document.querySelector('#db-backup-interval-input'), + dbBackupAmount: document.querySelector('#db-backup-amount-input'), + dbBackupFolder: document.querySelector('#db-backup-folder-input') +} -const user_inputs = { - username: document.querySelector('#new-username-input'), - password: document.querySelector('#new-password-input') -}; - -const import_inputs = { - file: document.querySelector('#database-file-input'), - copy_hosting: document.querySelector('#copy-hosting-input'), - button: document.querySelector('#upload-db-button') -}; - -const power_buttons = { - restart: document.querySelector('#restart-button'), - shutdown: document.querySelector('#shutdown-button') -}; - -function checkLogin() { - fetch(`${url_prefix}/api/auth/status?api_key=${api_key}`) - .then(response => { - if (!response.ok) return Promise.reject(response.status) - return response.json(); - }) +// +// region About +// +function loadAbout() { + fetchAPI('/about') .then(json => { - if (!json.result.admin) - window.location.href = `${url_prefix}/reminders`; + els.about.mindVersion.innerText = json.result.version + els.about.pythonVersion.innerText = json.result.python_version + els.about.dbVersion.innerText = json.result.database_version + els.about.dbLocation.innerText = json.result.database_location + els.about.dataFolder.innerText = json.result.data_folder }) - .catch(e => { - if (e === 401) - window.location.href = `${url_prefix}/`; - else - console.log(e); - }); -}; - -function loadSettings() { - fetch(`${url_prefix}/api/settings`) - .then(response => response.json()) - .then(json => { - setting_inputs.allow_new_accounts.checked = json.result.allow_new_accounts; - setting_inputs.login_time.value = Math.round(json.result.login_time / 60); - setting_inputs.login_time_reset.value = json.result.login_time_reset.toString(); - setting_inputs.log_level.value = json.result.log_level; - hosting_inputs.host.value = json.result.host; - hosting_inputs.port.value = json.result.port; - hosting_inputs.url_prefix.value = json.result.url_prefix; - setting_inputs.db_backup_interval.value = json.result.db_backup_interval / 3600; - setting_inputs.db_backup_amount.value = json.result.db_backup_amount; - setting_inputs.db_backup_folder.value = json.result.db_backup_folder; - }); -}; - -function submitSettings() { - const data = { - 'allow_new_accounts': setting_inputs.allow_new_accounts.checked, - 'login_time': setting_inputs.login_time.value * 60, - 'login_time_reset': setting_inputs.login_time_reset.value === 'true', - 'log_level': parseInt(setting_inputs.log_level.value), - 'db_backup_interval': parseInt(setting_inputs.db_backup_interval.value) * 3600, - 'db_backup_amount': parseInt(setting_inputs.db_backup_amount.value), - 'db_backup_folder': setting_inputs.db_backup_folder.value - }; - fetch(`${url_prefix}/api/admin/settings?api_key=${api_key}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => response.json()) - .then(json => { - if (json.error !== null) - return Promise.reject(json) - }) - .catch(json => { - if (['ApiKeyInvalid', 'ApiKeyExpired'].includes(json.error)) - window.location.href = `${url_prefix}/`; - }); -}; - -function downloadLogFile() { - fetch(`${url_prefix}/api/admin/logs?api_key=${api_key}`) - .then(response => { - if (!response.ok) return Promise.reject(response.status) - window.location.href = `${url_prefix}/api/admin/logs?api_key=${api_key}`; - }) - .catch(e => { - if (e === 404) - alert("No debug log file to download. Enable debug logging first.") - }); -}; - -function submitHostingSettings() { - hosting_inputs.submit.innerText = 'Restarting'; - const data = { - host: hosting_inputs.host.value, - port: parseInt(hosting_inputs.port.value), - url_prefix: hosting_inputs.url_prefix.value - }; - fetch(`${url_prefix}/api/admin/settings?api_key=${api_key}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => response.json()) - .then(json => { - if (json.error !== null) - return Promise.reject(json) +} +// +// region Power +// +function restartApp() { + els.power.restart.innerHTML = icons.loading + els.power.restart.classList.add('spinning') + sendAPI("POST", "/admin/restart") + .then(response => setTimeout( () => window.location.reload(), 1000 ) - }) - .catch(json => { - if (['ApiKeyInvalid', 'ApiKeyExpired'].includes(json.error)) - window.location.href = `${url_prefix}/`; - }); -}; + ) +} -function toggleAddUser() { - const el = document.querySelector('#add-user-row'); - if (el.classList.contains('hidden')) { - // Show row - user_inputs.username.value = ''; - user_inputs.password.value = ''; - el.classList.remove('hidden'); - } else { - // Hide row - el.classList.add('hidden'); - }; -}; +function shutdownApp() { + els.power.shutdown.innerHTML = icons.loading + els.power.shutdown.classList.add('spinning') + sendAPI("POST", "/admin/shutdown") + .then(response => + setTimeout( + () => window.location.reload(), + 1000 + ) + ) +} + +// +// region Settings +// +function increaseChangeCount() { + const newCount = parseInt(els.changesCount.dataset.count) + 1 + els.changesCount.dataset.count = newCount + if (newCount === 1) + els.changesCount.innerText = `${newCount} change` + else + els.changesCount.innerText = `${newCount} changes` +} + +function decreaseChangeCount() { + const newCount = parseInt(els.changesCount.dataset.count) - 1 + els.changesCount.dataset.count = newCount + if (newCount === 1) + els.changesCount.innerText = `${newCount} change` + else + els.changesCount.innerText = `${newCount} changes` +} + +function clearChangeCount() { + els.changesCount.dataset.count = 0 + els.changesCount.innerText = '0 changes' +} + +function changesPresent() { + return parseInt(els.changesCount.dataset.count) !== 0 +} + +function initInputChangeDetection() { + clearChangeCount() + Object.values(settings).forEach(s => { + const settingValue = s.type === 'checkbox' ? s.checked : s.value + s.dataset.currentvalue = encodeURI(settingValue) + + s.classList.remove('changed') + + s.oninput = e => { + const newValue = encodeURI(s.type === 'checkbox' ? s.checked : s.value) + if ( + newValue !== s.dataset.currentvalue + && !s.classList.contains('changed') + ) { + // Setting has changed from current value to new value + increaseChangeCount() + s.classList.add('changed') + } + else if ( + newValue === s.dataset.currentvalue + && s.classList.contains('changed') + ) { + // Setting has changed from new value back to current value + decreaseChangeCount() + s.classList.remove('changed') + } + } + }) +} + +function loadSettings() { + fetchAPI('/settings') + .then(json => { + settings.allowNewAccounts.checked = json.result.allow_new_accounts + settings.loginTime.value = Math.round(json.result.login_time / 60) + settings.loginTimeReset.value = json.result.login_time_reset.toString() + settings.host.value = json.result.host + settings.port.value = json.result.port + settings.urlPrefix.value = json.result.url_prefix + settings.logLevel.value = json.result.log_level + settings.dbBackupInterval.value = json.result.db_backup_interval / 3600 + settings.dbBackupAmount.value = json.result.db_backup_amount + settings.dbBackupFolder.value = json.result.db_backup_folder + + initInputChangeDetection() + }) +} + +function submitSettings() { + els.dbBackupFolderContainer.classList.remove('error-input-container') + els.settingsSubmit.classList.remove('submit-error') + + if (!changesPresent()) + return + + let hostChanged = false, + portChanged = false, + urlPrefixChanged = false + const data = {} + + if (settings.allowNewAccounts.classList.contains('changed')) + data.allow_new_accounts = settings.allowNewAccounts.checked + + if (settings.loginTime.classList.contains('changed')) + data.login_time = settings.loginTime.value * 60 + + if (settings.loginTimeReset.classList.contains('changed')) + data.login_time_reset = settings.loginTimeReset.value === 'true' + + if (settings.host.classList.contains('changed')) { + data.host = settings.host.value + hostChanged = true + } + + if (settings.port.classList.contains('changed')) { + data.port = parseInt(settings.port.value) + portChanged = true + } + + if (settings.urlPrefix.classList.contains('changed')) { + data.url_prefix = settings.urlPrefix.value + urlPrefixChanged = true + } + + if (settings.logLevel.classList.contains('changed')) + data.log_level = parseInt(settings.logLevel.value) + + if (settings.dbBackupInterval.classList.contains('changed')) + data.db_backup_interval = parseInt(settings.dbBackupInterval.value) * 3600 + + if (settings.dbBackupAmount.classList.contains('changed')) + data.db_backup_amount = parseInt(settings.dbBackupAmount.value) + + if (settings.dbBackupFolder.classList.contains('changed')) + data.db_backup_folder = settings.dbBackupFolder.value + + if (hostChanged || portChanged || urlPrefixChanged) { + // Notify about restart and revert timer + const restartMessage = "MIND has detected changes to the hosting settings. " + + "It is required to login into MIND within 1 minute in order to keep the new hosting settings. " + + "Otherwise, MIND will go back to the old hosting settings." + if (!confirm(restartMessage)) + return + } + + sendAPI("PUT", "/admin/settings", {}, data) + .then(response => { + if (hostChanged) { + setTimeout( + () => window.location.reload(), + 1000 + ) + } + else if (portChanged || urlPrefixChanged) { + const newUrl = new URL(window.location.href) + if (portChanged) + newUrl.port = parseInt(settings.port.value) + if (urlPrefixChanged) + newUrl.pathname = settings.urlPrefix.value + newUrl.pathname.slice(settings.urlPrefix.dataset.currentvalue.length) + + setTimeout( + () => window.location.href = newUrl.toString(), + 1000 + ) + } + + initInputChangeDetection() + }) + .catch(response => { + response.json().then(json => { + if (['ApiKeyInvalid', 'ApiKeyExpired'].includes(json.error)) + window.location.href = `${urlPrefix}/` + + if ( + json.error === 'InvalidKeyValue' + && json.result.key === 'db_backup_folder' + ) { + els.dbBackupFolderContainer.classList.add('error-input-container') + els.settingsSubmit.classList.add('submit-error') + } + }) + }) +} + +function openResetSettings() { + windows.resetSettings.dialog.showModal() +} + +function closeResetSettings() { + windows.resetSettings.dialog.close() +} + +function resetSettings() { + const resetSettings = [...windows.resetSettings.form.querySelectorAll('input')] + .filter(i => i.checked) + .map(i => i.dataset.setting) + + windows.resetSettings.submit.innerHTML = icons.loading + windows.resetSettings.submit.classList.add('spinning') + + const hostChanged = resetSettings.includes("host"), + portChanged = resetSettings.includes("port"), + urlPrefixChanged = resetSettings.includes("url_prefix") + + if (hostChanged || portChanged || urlPrefixChanged) { + // Notify about restart and revert timer + const restartMessage = "MIND has detected changes to the hosting settings. " + + "It is required to login into MIND within 1 minute in order to keep the new hosting settings. " + + "Otherwise, MIND will go back to the old hosting settings." + if (!confirm(restartMessage)) { + windows.resetSettings.submit.innerText = "Reset" + windows.resetSettings.submit.classList.remove('spinning') + return + } + } + + sendAPI("DELETE", "/admin/settings", {}, { + setting_keys: resetSettings + }) + .then(response => { + if (portChanged || urlPrefixChanged) { + const newUrl = new URL(window.location.href) + if (portChanged) + newUrl.port = 8080 + if (urlPrefixChanged) + newUrl.pathname = "/" + + setTimeout( + () => window.location.href = newUrl.toString(), + 1000 + ) + + } else { + setTimeout( + () => window.location.reload(), + 1000 + ) + } + }) +} + +// +// region Add user +// +function openAddUser() { + windows.addUser.inputContainers.username.classList.remove('error-input-container') + hide([windows.addUser.errors.usernameInvalid, windows.addUser.errors.usernameTaken]) + windows.addUser.inputs.username.value = '' + windows.addUser.inputs.password.value = '' + + windows.addUser.dialog.showModal() +} + +function closeAddUser() { + windows.addUser.dialog.close() +} function addUser() { - user_inputs.username.classList.remove('error-input'); - user_inputs.username.title = ''; + windows.addUser.inputContainers.username.classList.remove('error-input-container') + hide([windows.addUser.errors.usernameInvalid, windows.addUser.errors.usernameTaken]) + const data = { - 'username': user_inputs.username.value, - 'password': user_inputs.password.value - }; - fetch(`${url_prefix}/api/admin/users?api_key=${api_key}`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => response.json()) + username: windows.addUser.inputs.username.value, + password: windows.addUser.inputs.password.value + } + + sendAPI("POST", "/admin/users", {}, data) .then(json => { - if (json.error !== null) - return Promise.reject(json.error); - toggleAddUser(); - loadUsers(); + loadUsers() + closeAddUser() }) .catch(e => { - if (e === "UsernameTaken") { - user_inputs.username.classList.add('error-input'); - user_inputs.username.title = 'Username already taken'; + e.json().then(e => { + if (e.error === 'UsernameInvalid') { + windows.addUser.errors.usernameInvalid.innerText = e.result.reason + hide([], [windows.addUser.errors.usernameInvalid]) + windows.addUser.inputContainers.username.classList.add('error-input-container') - } else if (e === "UsernameInvalid") { - user_inputs.username.classList.add('error-input'); - user_inputs.username.title = 'Username contains invalid characters'; + } else if (e.error === 'UsernameTaken') { + hide([], [windows.addUser.errors.usernameTaken]) + windows.addUser.inputContainers.username.classList.add('error-input-container') - } else - console.log(e); - }); -}; - -function editUser(id) { - const new_password = document.querySelector( - `#user-table tr[data-id="${id}"] input` - ).value; - fetch(`${url_prefix}/api/admin/users/${id}?api_key=${api_key}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify({'new_password': new_password}) + } else { + console.log(e) + } + }) }) - .then(response => loadUsers()); -}; +} -function deleteUser(id) { - document.querySelector(`#user-table tr[data-id="${id}"]`).remove(); - fetch(`${url_prefix}/api/admin/users/${id}?api_key=${api_key}`, { - 'method': 'DELETE' - }); -}; +// +// region Edit user +// +function openEditUser(id, username, isAdmin) { + windows.editUser.dialog.dataset.id = id + windows.editUser.username.innerText = username + windows.editUser.inputContainers.username.classList.remove('error-input-container') + hide([windows.editUser.errors.usernameInvalid, windows.editUser.errors.usernameTaken]) + windows.editUser.inputs.username.value = '' + windows.editUser.inputs.password.value = '' + + if (isAdmin) + hide([windows.editUser.inputContainers.username]) + else + hide([], [windows.editUser.inputContainers.username]) + + windows.editUser.dialog.showModal() +} + +function closeEditUser() { + windows.editUser.dialog.close() +} + +function editUser() { + windows.editUser.inputContainers.username.classList.remove('error-input-container') + hide([windows.editUser.errors.usernameInvalid, windows.editUser.errors.usernameTaken]) + + const data = {} + + if (windows.editUser.inputs.username.value !== '') + data.new_username = windows.editUser.inputs.username.value + + if (windows.editUser.inputs.password.value !== '') + data.new_password = windows.editUser.inputs.password.value + + if (Object.keys(data).length === 0) { + // Nothing changed + closeEditUser() + return + } + + const id = parseInt(windows.editUser.dialog.dataset.id) + sendAPI("PUT", `/admin/users/${id}`, {}, data) + .then(json => { + loadUsers() + closeEditUser() + }) + .catch(e => { + e.json().then(e => { + if (e.error === 'UsernameInvalid') { + windows.editUser.errors.usernameInvalid.innerText = e.result.reason + hide([], [windows.editUser.errors.usernameInvalid]) + windows.editUser.inputContainers.username.classList.add('error-input-container') + + } else if (e.error === 'UsernameTaken') { + hide([], [windows.editUser.errors.usernameTaken]) + windows.editUser.inputContainers.username.classList.add('error-input-container') + + } else { + console.log(e) + } + }) + }) +} + +// +// region Delete user +// +function openDeleteUser(id, username) { + windows.deleteUser.dialog.dataset.id = id + windows.deleteUser.username.innerText = username + windows.deleteUser.dialog.showModal() +} + +function closeDeleteUser() { + windows.deleteUser.dialog.close() +} + +function deleteUser() { + const id = parseInt(windows.deleteUser.dialog.dataset.id) + sendAPI("DELETE", `/admin/users/${id}`) + .then(response => { + els.userList.querySelector(`tr[data-id="${id}"]`).remove() + closeDeleteUser() + }) +} + +// +// region Load users +// function loadUsers() { - const table = document.querySelector('#user-list'); - table.innerHTML = ''; - fetch(`${url_prefix}/api/admin/users?api_key=${api_key}`) - .then(response => response.json()) + els.userList.innerHTML = '' + fetchAPI("/admin/users") .then(json => { json.result.forEach(user => { - const entry = document.createElement('tr'); - entry.dataset.id = user.id; + const entry = document.createElement('tr') + entry.dataset.id = user.id - const username = document.createElement('td'); - username.innerText = user.username; - entry.appendChild(username); + const username = document.createElement('td') + username.innerText = user.username + entry.appendChild(username) - const password = document.createElement('td'); - const new_password_form = document.createElement('form'); - new_password_form.classList.add('hidden'); - new_password_form.action = `javascript:editUser(${user.id})`; - const new_password = document.createElement('input'); - new_password.type = 'password'; - new_password.placeholder = 'New password'; - new_password_form.appendChild(new_password); - password.appendChild(new_password_form); - entry.appendChild(password); + const actions = document.createElement('td') + entry.appendChild(actions) - const actions = document.createElement('td'); - entry.appendChild(actions); - - const edit_user = document.createElement('button'); - edit_user.onclick = e => e - .currentTarget - .parentNode - .previousSibling - .querySelector('form') - .classList - .toggle('hidden'); - edit_user.innerHTML = Icons.edit; - actions.appendChild(edit_user); + const edit_user = document.createElement('button') + edit_user.onclick = e => openEditUser(user.id, user.username, user.admin) + edit_user.innerHTML = icons.edit + actions.appendChild(edit_user) if (user.username !== 'admin') { - const delete_user = document.createElement('button'); - delete_user.onclick = e => deleteUser(user.id); - delete_user.innerHTML = Icons.delete; - actions.appendChild(delete_user); - }; + const delete_user = document.createElement('button') + delete_user.onclick = e => openDeleteUser(user.id, user.username) + delete_user.innerHTML = icons.delete + actions.appendChild(delete_user) + } - table.appendChild(entry); - }); - }); -}; + els.userList.appendChild(entry) + }) + }) +} -function upload_database() { - import_inputs.button.innerText = 'Importing'; - const copy_hosting = import_inputs.copy_hosting.checked ? 'true' : 'false'; - const formData = new FormData(); - formData.append('file', import_inputs.file.files[0]); - fetch(`${url_prefix}/api/admin/database?api_key=${api_key}©_hosting_settings=${copy_hosting}`, { +// +// region Database management +// +function openUploadDb() { + windows.uploadDb.inputs.file.value = '' + windows.uploadDb.inputContainers.file.classList.remove('error-input-container') + windows.uploadDb.inputs.keepHostingSettings.checked = false + windows.uploadDb.dialog.showModal() +} + +function closeUploadDb() { + windows.uploadDb.dialog.close() +} + +function uploadDb() { + const copyHosting = windows.uploadDb.inputs.keepHostingSettings ? 'true' : 'false' + const formData = new FormData() + formData.append('file', windows.uploadDb.inputs.file.files[0]) + + windows.uploadDb.submit.innerHTML = icons.loading + windows.uploadDb.submit.classList.add('spinning') + windows.uploadDb.inputContainers.file.classList.remove('error-input-container') + fetch(`${urlPrefix}/api/admin/database?api_key=${apiKey}©_hosting_settings=${copyHosting}`, { method: 'POST', body: formData }) .then(response => { - if (!response.ok) return Promise.reject(response.status); + if (!response.ok) return Promise.reject(response.status) setTimeout( () => window.location.reload(), 1000 - ); + ) }) .catch(e => { if (e === 400) { - import_inputs.file.value = ''; - import_inputs.button.innerText = 'Import Database'; - setTimeout( - () => alert('Invalid database file'), - 10 - ); - } else - console.log(e); - }); -}; + windows.uploadDb.inputs.file.value = '' + windows.uploadDb.submit.innerText = 'Import' + windows.uploadDb.submit.classList.remove('spinning') + windows.uploadDb.inputContainers.file.classList.add('error-input-container') -function loadBackups() { - const table = document.querySelector('#backup-list'); - table.innerHTML = ''; - fetch(`${url_prefix}/api/admin/database/backups?api_key=${api_key}`) - .then(response => response.json()) - .then(json => { - json.result.forEach(backup => { - const entry = document.createElement('tr'); - entry.dataset.index = backup.index; - - const filename = document.createElement('td'); - filename.innerText = backup.filename; - entry.appendChild(filename); - - const creation = document.createElement('td'); - let formatted_datetime = new - Date(backup.creation_date * 1000) - .toLocaleString(getLocalStorage('locale')['locale']); - creation.innerText = formatted_datetime; - entry.appendChild(creation); - - const actions = document.createElement('td'); - entry.appendChild(actions); - - const download = document.createElement('button'); - download.onclick = - e => window.location.href = - `${url_prefix}/api/admin/database/backups/${backup.index}?api_key=${api_key}`; - download.innerHTML = Icons.download; - download.title = "Download database backup" - actions.appendChild(download); - - const upload = document.createElement('button'); - upload.onclick = e => { - upload.innerText = '...'; - const copy_hosting = import_inputs.copy_hosting.checked ? 'true' : 'false'; - fetch(`${url_prefix}/api/admin/database/backups/${backup.index}?api_key=${api_key}©_hosting_settings=${copy_hosting}`, { - method: 'POST', - }) - .then(response => - setTimeout( - () => window.location.reload(), - 1000 - ) - ); - }; - upload.innerHTML = Icons.upload; - upload.title = "Import database backup" - actions.appendChild(upload); - - table.appendChild(entry); - }); - }); + } else + console.log(e) + }) } -function restart_app() { - power_buttons.restart.innerText = 'Restarting...'; - fetch(`${url_prefix}/api/admin/restart?api_key=${api_key}`, { - method: "POST" +function openImportDb(index, filename, creation) { + windows.importDb.dialog.dataset.index = index + windows.importDb.backupName.innerText = filename + windows.importDb.backupCreation.innerText = new + Date(creation * 1000) + .toLocaleString(getLocalStorage('locale')['locale']) + windows.importDb.inputs.keepHostingSettings.checked = false + windows.importDb.dialog.showModal() +} + +function closeImportDb() { + windows.importDb.dialog.close() +} + +function importDb() { + const index = parseInt(windows.importDb.dialog.dataset.index) + const copyHosting = windows.importDb.inputs.keepHostingSettings ? 'true' : 'false' + windows.importDb.submit.innerHTML = icons.loading + windows.importDb.submit.classList.add('spinning') + sendAPI("POST", `/admin/database/backups/${index}`, { + copy_hosting_settings: copyHosting }) - .then(response => + .then(response => { setTimeout( () => window.location.reload(), 1000 ) - ); -}; - -function shutdown_app() { - power_buttons.shutdown.innerText = 'Shutting down...'; - fetch(`${url_prefix}/api/admin/shutdown?api_key=${api_key}`, { - method: "POST" }) - .then(response => - setTimeout( - () => window.location.reload(), - 1000 - ) - ); -}; +} -// code run on load +function loadBackups() { + els.backupList.innerHTML = '' + fetchAPI("/admin/database/backups") + .then(json => { + json.result.forEach(backup => { + const entry = document.createElement('tr') + entry.dataset.index = backup.index -checkLogin(); -loadSettings(); -loadUsers(); -loadBackups(); + const filename = document.createElement('td') + filename.innerText = backup.filename + entry.appendChild(filename) -document.querySelector('#logout-button').onclick = e => logout(); -document.querySelector('#settings-form').action = 'javascript:submitSettings();'; -document.querySelector('#download-logs-button').onclick = e => downloadLogFile(); + const creation = document.createElement('td') + let formatted_datetime = new + Date(backup.creation_date * 1000) + .toLocaleString(getLocalStorage('locale')['locale']) + creation.innerText = formatted_datetime + entry.appendChild(creation) -hosting_inputs.form.action = 'javascript:submitHostingSettings();'; + const actions = document.createElement('td') + entry.appendChild(actions) -document.querySelector('#add-user-button').onclick = e => toggleAddUser(); -document.querySelector('#add-user-form').action = 'javascript:addUser()'; + const download = document.createElement('button') + download.onclick = + e => window.location.href = + `${urlPrefix}/api/admin/database/backups/${backup.index}?api_key=${apiKey}` + download.innerHTML = icons.download + download.title = "Download database backup" + actions.appendChild(download) -document.querySelector('#upload-database-form').action = 'javascript:upload_database();'; -document.querySelector('#download-db-button').onclick = e => - window.location.href = `${url_prefix}/api/admin/database?api_key=${api_key}`; + const importDb = document.createElement('button') + importDb.onclick = e => openImportDb(backup.index, backup.filename, backup.creation_date) + importDb.innerHTML = icons.upload + importDb.title = "Import database backup" + actions.appendChild(importDb) -power_buttons.restart.onclick = e => restart_app(); -power_buttons.shutdown.onclick = e => shutdown_app(); + els.backupList.appendChild(entry) + }) + }) +} + +// +// region On load +// +checkLogin() + +loadAbout() +loadSettings() +loadUsers() +loadBackups() + +els.logout.onclick = e => { + if (changesPresent()) + if (!confirm("You have unsaved changes. Are you sure you want to leave?")) + return + window.onbeforeunload = null + + logout() +} +els.settingsForm.action = 'javascript:submitSettings();' + +window.onbeforeunload = e => { + if (!changesPresent()) + // No changes + return undefined + + // Changes detected. Returning string instead of undefined + // will make browser show confirmation message before leaving. + // Showing custom message is not allowed, so an empty string is + // good enough + e.returnValue = '' + return '' +} + +els.power.restart.onclick = e => restartApp() +els.power.shutdown.onclick = e => shutdownApp() + +els.downloadLogs.onclick = e => + window.location.href = `${urlPrefix}/api/admin/logs?api_key=${apiKey}` + +els.startResetSettings.onclick = e => openResetSettings() +windows.resetSettings.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeResetSettings() + } +} +windows.resetSettings.form.action = 'javascript:resetSettings()' +windows.resetSettings.close.onclick = e => closeResetSettings() + +els.startAddUser.onclick = e => openAddUser() +windows.addUser.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeAddUser() + } +} +windows.addUser.form.action = 'javascript:addUser()' +windows.addUser.close.onclick = e => closeAddUser() + +windows.editUser.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeEditUser() + } +} +windows.editUser.form.action = 'javascript:editUser()' +windows.editUser.close.onclick = e => closeEditUser() + +windows.deleteUser.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeDeleteUser() + } +} +windows.deleteUser.close.onclick = e => closeDeleteUser() +windows.deleteUser.confirm.onclick = e => deleteUser() + +els.uploadDb.onclick = e => openUploadDb() +windows.uploadDb.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeUploadDb() + } +} +windows.uploadDb.form.action = 'javascript:uploadDb()' +windows.uploadDb.close.onclick = e => closeUploadDb() + +els.downloadDb.onclick = e => + window.location.href = `${urlPrefix}/api/admin/database?api_key=${apiKey}` + +windows.importDb.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + closeImportDb() + } +} +windows.importDb.form.action = 'javascript:importDb()' +windows.importDb.close.onclick = e => closeImportDb() diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index bbc9fb0..492a8df 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -1,126 +1,270 @@ -// The duration of the animation set for the window translation -const window_ani_ms = 500; +// +// region Definitions +// +const constants = { + /** + * The duration of the animation set for the window translation + */ + windowAnimationDuration: 500 +} -const Types = { - 'reminder': document.getElementById('reminder-tab'), - 'static_reminder': document.getElementById('static-reminder-tab'), - 'template': document.getElementById('template-tab') -}; +const icons = { + save: '', + edit: '', + delete: '', + add: '', + download: '', + upload: '', + loading: '' +} -const Icons = { - 'save': '', - 'edit': '', - 'delete': '', - 'add': '', - 'download': '', - 'upload': '' -}; -const InfoClasses = [ - 'show-add-reminder', 'show-add-template', 'show-add-static-reminder', - 'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder' -]; +// +// region Helpers +// -function showWindow(id) { - const window_container = document.querySelector('.window-container'); - if (id === "home") { - window_container.classList.remove( - 'show-window', - 'inter-window-ani' - ); - } else { - const extra_window_container = - document.querySelector('.extra-window-container'); +/** + * Hide and show elements. + * + * @param {Array} to_hide The elements to hide, + * by adding the `hidden` class. + * + * @param {Array?} to_show The elements to show, + * by removing the `hidden` class. + */ +function hide(to_hide, to_show) { + to_hide.forEach(el => el.classList.add('hidden')) + if (to_show !== null && to_show !== undefined) + to_show.forEach(el => el.classList.remove('hidden')) +} - const offset = [ - ...extra_window_container.children - ].indexOf(document.getElementById(id)) * -100; - extra_window_container.style.setProperty( - '--y-offset', - `${offset}%` - ) - window_container.classList.add('show-window'); - setTimeout( - () => window_container.classList.add('inter-window-ani'), - window_ani_ms - ); - }; -}; - -function logout() { - fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, { - 'method': 'POST' - }) - .then(response => { - setLocalStorage({'api_key': null}); - window.location.href = `${url_prefix}/`; - }); -}; - -// -// LocalStorage -// -const default_values = { - 'api_key': null, - 'locale': 'en-GB', - 'default_service': null, - 'sorting_reminders': 'time', - 'sorting_static': 'title', - 'sorting_templates': 'title' -}; - -function setupLocalStorage() { - if (!localStorage.getItem('MIND')) - localStorage.setItem('MIND', JSON.stringify(default_values)); - - const missing_keys = [ - ...Object.keys(default_values) - ].filter(e => - ![...Object.keys(JSON.parse(localStorage.getItem('MIND')))].includes(e) - ) - - if (missing_keys.length) { - const storage = JSON.parse(localStorage.getItem('MIND')); - - missing_keys.forEach(missing_key => { - storage[missing_key] = default_values[missing_key] - }) - - localStorage.setItem('MIND', JSON.stringify(storage)); - }; - return; -}; +// +// region LocalStorage +// +const defaultValues = { + api_key: null, + locale: 'en-GB', + default_service: null, + sorting_reminders: 'time', + sorting_static: 'title', + sorting_templates: 'title', + allow_new_accounts_cache: true +} +/** + * Get the configuration stored in the local storage of the client (browser). + * + * @param {string | string[] | null | undefined} keys The keys to fetch, or all + * if null or undefined. + * + * @returns {Map} The keys and their values. + */ function getLocalStorage(keys) { - const storage = JSON.parse(localStorage.getItem('MIND')); - const result = {}; + const storage = JSON.parse(localStorage.getItem('MIND')) + + if (keys === undefined || keys === null) + return storage + + const result = {} if (typeof keys === 'string') - result[keys] = storage[keys]; - + result[keys] = storage[keys] + else if (typeof keys === 'object') for (const key in keys) - result[key] = storage[key]; + result[key] = storage[key] - return result; -}; + return result +} +/** + * Update the configuration stored in the local storage of the client (browser). + * @param {Map} keys_values The new values for the given keys. + */ function setLocalStorage(keys_values) { - const storage = JSON.parse(localStorage.getItem('MIND')); + const storage = JSON.parse(localStorage.getItem('MIND')) for (const [key, value] of Object.entries(keys_values)) - storage[key] = value; - - localStorage.setItem('MIND', JSON.stringify(storage)); - return; -}; + storage[key] = value -// code run on load + localStorage.setItem('MIND', JSON.stringify(storage)) +} -setupLocalStorage(); +/** + * Setup the configuration stored in the local storage of the client (browser) + * with default values. + */ +function setupLocalStorage() { + if (!localStorage.getItem('MIND')) + localStorage.setItem('MIND', JSON.stringify(defaultValues)) -const url_prefix = document.getElementById('url_prefix').dataset.value; -const api_key = getLocalStorage('api_key')['api_key']; -if (api_key === null) { - window.location.href = `${url_prefix}/`; -}; \ No newline at end of file + const currentValues = getLocalStorage() + const cleanedVersion = {} + Object.keys(defaultValues).forEach(k => { + if (currentValues[k] === undefined) + cleanedVersion[k] = defaultValues[k] + else + cleanedVersion[k] = currentValues[k] + }) + + localStorage.setItem('MIND', JSON.stringify(cleanedVersion)) +} + + +// +// region Fetching +// + +/** + * Make a GET request to the API. + * + * @param {string} endpoint The endpoint to make the request to, without API URL + * prefix. + * + * @param {Map} params The URL parameters to use in the request. + * API key doesn't need to be supplied. + * + * @param {bool} jsonReturn Whether to return the json response, or the raw + * response. + * + * @param {bool} checkAuth Check whether authentication failed and if so + * automatically redirect to login page. + * + * @returns The response. + */ +async function fetchAPI(endpoint, params={}, jsonReturn=true, checkAuth=true) { + let formattedParams = '' + if (apiKey !== null) + params.api_key = apiKey + if (Object.keys(params).length) { + formattedParams = '?' + Object.entries(params).map(p => p.join('=')).join('&') + } + + return fetch(`${urlPrefix}/api${endpoint}${formattedParams}`) + .then(response => { + if (!response.ok) + return Promise.reject(response) + if (jsonReturn) + return response.json() + else + return response + }) + .catch(response => { + if (checkAuth && response.status === 401) { + setLocalStorage({api_key: null}) + window.location.href = `${urlPrefix}/` + } else { + return Promise.reject(response) + } + }) +} + +/** + * Make a POST, PUT or DELETE request to the API. + * + * @param {string} method The HTTP method to use. + * + * @param {string} endpoint The endpoint to make the request to, without API URL + * prefix. + * + * @param {Map} params The URL parameters to use in the request. + * API key doesn't need to be supplied. + * + * @param {any} body The JSON to supply in the body of the request. + * + * @param {bool} jsonReturn Whether to return the json response, or the raw + * response. + * + * @param {bool} checkAuth Check whether authentication failed and if so + * automatically redirect to login page. + * + * @returns The response. + */ +async function sendAPI( + method, + endpoint, + params={}, + body={}, + jsonReturn=true, + checkAuth=true +) { + let formattedParams = '' + if (apiKey !== null) + params.api_key = apiKey + if (Object.keys(params).length) { + formattedParams = '?' + Object.entries(params).map(p => p.join('=')).join('&') + } + + return fetch(`${urlPrefix}/api${endpoint}${formattedParams}`, { + 'method': method, + 'headers': {'Content-Type': 'application/json'}, + 'body': Object.entries(body).length !== 0 ? JSON.stringify(body) : '' + }) + .then(response => { + if (!response.ok) + return Promise.reject(response) + if (jsonReturn) + return response.json() + else + return response + }) + .catch(response => { + if (checkAuth && response.status === 401) { + setLocalStorage({api_key: null}) + window.location.href = `${urlPrefix}/` + } else { + return Promise.reject(response) + } + }) +} + +/** + * Check the login status, and redirect to proper place if not already there. + */ +function checkLogin() { + if (apiKey === null) + window.location.href = `${urlPrefix}/` + + fetchAPI("/auth/status") + .then(json => { + if ( + json.result.admin + && window.location.pathname !== `${urlPrefix}/admin` + ) + window.location.href = `${urlPrefix}/admin` + + else if ( + !json.result.admin + && window.location.pathname !== `${urlPrefix}/reminders` + ) + window.location.href = `${urlPrefix}/reminders` + }) + .catch(e => console.log(e)) +} + +/** + * Log out and redirect to login page. + */ +function logout() { + sendAPI("POST", "/auth/logout") + .then(response => { + setLocalStorage({'api_key': null}) + window.location.href = `${urlPrefix}/` + }) +} + +// +// region On load +// +setupLocalStorage() + +/** + * The URL prefix that the application is running on. + */ +const urlPrefix = document.getElementById('url_prefix').dataset.value + +/** + * The API key that can be used to authenticate API requests. Not guaranteed to + * be valid. + */ +const apiKey = getLocalStorage('api_key')['api_key'] diff --git a/frontend/static/js/library.js b/frontend/static/js/library.js index 6c67d4f..62cf0c3 100644 --- a/frontend/static/js/library.js +++ b/frontend/static/js/library.js @@ -22,11 +22,11 @@ const LibEls = { // function getSorting(type, key=false) { let sorting_key; - if (type === Types.reminder) + if (type === reminderTypes.reminder) sorting_key = 'sorting_reminders'; - else if (type === Types.static_reminder) + else if (type === reminderTypes.static_reminder) sorting_key = 'sorting_static'; - else if (type === Types.template) + else if (type === reminderTypes.template) sorting_key = 'sorting_templates'; if (key) @@ -36,7 +36,7 @@ function getSorting(type, key=false) { }; function getActiveTab() { - for (let t of Object.values(Types)) { + for (let t of Object.values(reminderTypes)) { if (getComputedStyle(t).display === 'flex') return t }; @@ -63,7 +63,7 @@ function fillTable(table, results) { title.innerText = r.title; entry.appendChild(title); - if (table === Types.reminder) { + if (table === reminderTypes.reminder) { const time = document.createElement('p'); let offset = new Date(r.time * 1000).getTimezoneOffset() * -60; let d = new Date((r.time + offset) * 1000); @@ -97,21 +97,21 @@ function fillLibrary(type=null) { let tab_type = type || getActiveTab(); let url; - if (tab_type === Types.reminder) - url = `${url_prefix}/api/reminders`; - else if (tab_type === Types.static_reminder) - url = `${url_prefix}/api/staticreminders`; - else if (tab_type === Types.template) - url = `${url_prefix}/api/templates`; + if (tab_type === reminderTypes.reminder) + url = `${urlPrefix}/api/reminders`; + else if (tab_type === reminderTypes.static_reminder) + url = `${urlPrefix}/api/staticreminders`; + else if (tab_type === reminderTypes.template) + url = `${urlPrefix}/api/templates`; else return; const sorting = getSorting(tab_type); const query = LibEls.search_bar.input.value; if (query) - url = `${url}/search?api_key=${api_key}&sort_by=${sorting}&query=${query}`; + url = `${url}/search?api_key=${apiKey}&sort_by=${sorting}&query=${query}`; else - url = `${url}?api_key=${api_key}&sort_by=${sorting}`; + url = `${url}?api_key=${apiKey}&sort_by=${sorting}`; fetch(url) .then(response => { @@ -121,7 +121,7 @@ function fillLibrary(type=null) { .then(json => fillTable(tab_type, json.result)) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); @@ -148,8 +148,8 @@ function evaluateSizing() { // code run on load -Object.values(Types).forEach(t => fillLibrary(t)); -setInterval(() => fillLibrary(Types.reminder), 60000); +Object.values(reminderTypes).forEach(t => fillLibrary(t)); +setInterval(() => fillLibrary(reminderTypes.reminder), 60000); const week_days = ["Mo", "Tu", "We", "Thu", "Fr", "Sa", "Su"]; diff --git a/frontend/static/js/login.js b/frontend/static/js/login.js index 8343982..133a9d7 100644 --- a/frontend/static/js/login.js +++ b/frontend/static/js/login.js @@ -1,139 +1,112 @@ -const forms = { - 'login': { - 'form': document.querySelector('#login-form'), - 'inputs': { - 'username': document.querySelector('#login-form input[type="text"]'), - 'password': document.querySelector('#login-form input[type="password"]') - }, - 'errors': { - 'username': document.querySelector("#username-error-container"), - 'password': document.querySelector("#password-error-container") +const els = { + switchButton: document.querySelector(".switch-button"), + login: { + form: document.querySelector('#login-form'), + inputContainers: { + username: document.querySelector('#login-form .checked-input-container:has(input[type="text"])'), + password: document.querySelector('#login-form .checked-input-container:has(input[type="password"])') + }, + inputs: { + username: document.querySelector('#login-username-input'), + password: document.querySelector('#login-password-input') } }, - 'create': { - 'form': document.querySelector('#create-form'), - 'inputs': { - 'username': document.querySelector('#create-form input[type="text"]'), - 'password': document.querySelector('#create-form input[type="password"]') + create: { + form: document.querySelector('#register-form'), + inputContainers: { + username: document.querySelector('#register-form .checked-input-container:has(input[type="text"])'), + }, + inputs: { + username: document.querySelector('#register-username-input'), + password: document.querySelector('#register-password-input') }, - 'errors': { - 'username_invalid': document.querySelector('#new-username-error'), - 'username_taken': document.querySelector('#taken-username-error') + errors: { + usernameInvalid: document.querySelector('#invalid-username-error'), + usernameTaken: document.querySelector('#taken-username-error') } } -}; +} -function login(data=null) { - forms.login.errors.username.classList.remove('error-container'); - forms.login.errors.password.classList.remove('error-container'); +function login(data = null) { + els.login.inputContainers.username.classList.remove('error-input-container') + els.login.inputContainers.password.classList.remove('error-input-container') if (data === null) data = { - 'username': forms.login.inputs.username.value, - 'password': forms.login.inputs.password.value - }; + username: els.login.inputs.username.value, + password: els.login.inputs.password.value + } - fetch(`${url_prefix}/api/auth/login`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - if (!response.ok) return Promise.reject(response.status); - return response.json(); - }) + sendAPI("POST", "/auth/login", {}, data, true, false) .then(json => { - const new_stor = JSON.parse(localStorage.getItem('MIND')); - new_stor.api_key = json.result.api_key; - localStorage.setItem('MIND', JSON.stringify(new_stor)); + setLocalStorage({api_key: json.result.api_key}) if (json.result.admin) - window.location.href = `${url_prefix}/admin`; + window.location.href = `${urlPrefix}/admin` else - window.location.href = `${url_prefix}/reminders`; + window.location.href = `${urlPrefix}/reminders` }) .catch(e => { - if (e === 404) - forms.login.errors.username.classList.add('error-container'); - else if (e === 401) - forms.login.errors.password.classList.add('error-container'); + if (e.status === 404) + els.login.inputContainers.username.classList.add('error-input-container') + else if (e.status === 401) + els.login.inputContainers.password.classList.add('error-input-container') else - console.log(e); - }); -}; + console.log(e) + }) +} function create() { - forms.create.inputs.username.classList.remove('error-input'); - forms.create.errors.username_invalid.classList.add('hidden'); - forms.create.errors.username_taken.classList.add('hidden'); + els.create.inputContainers.username.classList.remove('error-input-container') + hide([els.create.errors.usernameInvalid, els.create.errors.usernameTaken]) const data = { - 'username': forms.create.inputs.username.value, - 'password': forms.create.inputs.password.value - }; - fetch(`${url_prefix}/api/user/add`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => response.json()) - .then(json => { - if (json.error !== null) return Promise.reject(json.error); - login(data); - }) - .catch(e => { - if (e === 'UsernameInvalid') { - forms.create.inputs.username.classList.add('error-input'); - forms.create.errors.username_invalid.classList.remove('hidden'); - } else if (e === 'UsernameTaken') { - forms.create.inputs.username.classList.add('error-input'); - forms.create.errors.username_taken.classList.remove('hidden'); - } else { - console.log(e); - }; - }); -}; + username: els.create.inputs.username.value, + password: els.create.inputs.password.value + } -function checkLogin() { - fetch(`${url_prefix}/api/auth/status?api_key=${api_key}`) - .then(response => { - if (!response.ok) return Promise.reject(response.status); - return response.json(); - }) - .then(json => { - if (json.result.admin) - window.location.href = `${url_prefix}/admin`; - else - window.location.href = `${url_prefix}/reminders`; - }) + sendAPI("POST", "/user/add", {}, data) + .then(json => login(data)) .catch(e => { - if (e === 401) - console.log('API key expired') - else - console.log(e); - }); -}; + e.json().then(e => { + if (e.error === 'UsernameInvalid') { + els.create.errors.usernameInvalid.innerText = e.result.reason + hide([], [els.create.errors.usernameInvalid]) + els.create.inputContainers.username.classList.add('error-input-container') + + } else if (e.error === 'UsernameTaken') { + hide([], [els.create.errors.usernameTaken]) + els.create.inputContainers.username.classList.add('error-input-container') + + } else { + console.log(e) + } + }) + }) +} function checkAllowNewAccounts() { - fetch(`${url_prefix}/api/settings`) - .then(response => response.json()) + const cachedValue = getLocalStorage("allow_new_accounts_cache").allow_new_accounts_cache + if (!cachedValue) + hide([els.switchButton]) + + fetchAPI("/settings") .then(json => { if (!json.result.allow_new_accounts) - document.querySelector('.switch-button').classList.add('hidden'); - }); -}; + hide([els.switchButton]) + else + hide([], [els.switchButton]) -// code run on load + if (cachedValue !== json.result.allow_new_accounts) + setLocalStorage({ + allow_new_accounts_cache: json.result.allow_new_accounts + }) + }) +} -if (localStorage.getItem('MIND') === null) - localStorage.setItem('MIND', JSON.stringify( - {'api_key': null, 'locale': 'en-GB', 'default_service': null} - )) +if (apiKey !== null) + checkLogin() -const url_prefix = document.getElementById('url_prefix').dataset.value; -const api_key = JSON.parse(localStorage.getItem('MIND')).api_key; +checkAllowNewAccounts() -checkLogin(); -checkAllowNewAccounts(); - -forms.login.form.action = 'javascript:login();'; -forms.create.form.action = 'javascript:create();'; +els.login.form.action = 'javascript:login();' +els.create.form.action = 'javascript:create();' diff --git a/frontend/static/js/notification.js b/frontend/static/js/notification.js index 7cd3c01..c51c275 100644 --- a/frontend/static/js/notification.js +++ b/frontend/static/js/notification.js @@ -88,11 +88,11 @@ function setNoNotificationServiceMsg(json) { LibEls.tab_container.querySelectorAll('.add-entry').forEach(ae => { ae.classList.remove('error', 'error-icon'); if (ae.id === 'add-reminder') - ae.onclick = e => showAdd(Types.reminder); + ae.onclick = e => showAdd(reminderTypes.reminder); else if (ae.id === 'add-static-reminder') - ae.onclick = e => showAdd(Types.static_reminder); + ae.onclick = e => showAdd(reminderTypes.static_reminder); else if (ae.id === 'add-template') - ae.onclick = e => showAdd(Types.template); + ae.onclick = e => showAdd(reminderTypes.template); }); } else { @@ -104,7 +104,7 @@ function setNoNotificationServiceMsg(json) { }; function fillNotificationServices() { - fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`) + fetch(`${urlPrefix}/api/notificationservices?api_key=${apiKey}`) .then(response => { if (!response.ok) return Promise.reject(response.status); return response.json(); @@ -116,7 +116,7 @@ function fillNotificationServices() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); @@ -132,7 +132,7 @@ function saveService(id) { 'title': row.querySelector(`td.title-column > input`).value, 'url': row.querySelector(`td.url-column > input`).value }; - fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, { + fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}`, { 'method': 'PUT', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) @@ -144,7 +144,7 @@ function saveService(id) { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else if (e === 400) { save_button.classList.add('error-icon'); save_button.title = 'Invalid Apprise URL'; @@ -155,7 +155,7 @@ function saveService(id) { function deleteService(id, delete_reminders_using=false) { const row = document.querySelector(`tr[data-id="${id}"]`); - fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}&delete_reminders_using=${delete_reminders_using}`, { + fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}&delete_reminders_using=${delete_reminders_using}`, { 'method': 'DELETE' }) .then(response => response.json()) @@ -165,14 +165,14 @@ function deleteService(id, delete_reminders_using=false) { row.remove(); fillNotificationServices(); if (delete_reminders_using) { - fillLibrary(Types.reminder); - fillLibrary(Types.static_reminder); - fillLibrary(Types.template); + fillLibrary(reminderTypes.reminder); + fillLibrary(reminderTypes.static_reminder); + fillLibrary(reminderTypes.template); }; }) .catch(e => { if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else if (e.error === 'NotificationServiceInUse') { const delete_reminders = confirm( @@ -200,7 +200,7 @@ function showServiceList(e) { if (notification_services !== null) return; - fetch(`${url_prefix}/api/notificationservices/available?api_key=${api_key}`) + fetch(`${urlPrefix}/api/notificationservices/available?api_key=${apiKey}`) .then(response => response.json()) .then(json => { notification_services = json.result; @@ -322,7 +322,7 @@ function createEntriesList(token) { const add_button = document.createElement('button'); add_button.type = 'button'; - add_button.innerHTML = Icons.add; + add_button.innerHTML = icons.add; add_button.onclick = e => toggleAddRow(add_row); entries_list.appendChild(add_button); @@ -568,7 +568,7 @@ function testService() { test_button.title = 'Required field missing'; return; }; - fetch(`${url_prefix}/api/notificationservices/test?api_key=${api_key}`, { + fetch(`${urlPrefix}/api/notificationservices/test?api_key=${apiKey}`, { 'method': 'POST', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) @@ -582,7 +582,7 @@ function testService() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else if (e === 400) { test_button.classList.add('error-input'); test_button.title = 'Invalid Apprise URL'; @@ -624,7 +624,7 @@ function addService() { add_button.title = 'Required field missing'; return; }; - fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`, { + fetch(`${urlPrefix}/api/notificationservices?api_key=${apiKey}`, { 'method': 'POST', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) @@ -641,7 +641,7 @@ function addService() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else if (e === 400) { add_button.classList.add('error-input'); add_button.title = 'Invalid Apprise URL'; diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js new file mode 100644 index 0000000..6455f3c --- /dev/null +++ b/frontend/static/js/reminders.js @@ -0,0 +1,38 @@ +const reminderTypes = { + reminder: document.getElementById('reminder-tab'), + static_reminder: document.getElementById('static-reminder-tab'), + template: document.getElementById('template-tab') +} + +const infoClasses = [ + 'show-add-reminder', 'show-add-template', 'show-add-static-reminder', + 'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder' +] + +function showWindow(id) { + const window_container = document.querySelector('.window-container') + if (id === "home") { + window_container.classList.remove( + 'show-window', + 'inter-window-ani' + ) + } else { + const extra_window_container = document.querySelector('.extra-window-container') + + const offset = [ + ...extra_window_container.children + ].indexOf(document.getElementById(id)) * -100 + + extra_window_container.style.setProperty( + '--y-offset', + `${offset}%` + ) + window_container.classList.add('show-window') + setTimeout( + () => window_container.classList.add('inter-window-ani'), + constants.windowAnimationDuration + ) + } +} + +checkLogin() diff --git a/frontend/static/js/settings.js b/frontend/static/js/settings.js index ba16962..de2a1ab 100644 --- a/frontend/static/js/settings.js +++ b/frontend/static/js/settings.js @@ -13,7 +13,7 @@ function loadSettings() { function updateLocale(e) { setLocalStorage({'locale': e.target.value}); - fillLibrary(Types.reminder); + fillLibrary(reminderTypes.reminder); }; function updateDefaultService(e) { @@ -25,7 +25,7 @@ function changePassword() { const data = { 'new_password': document.getElementById('password-input').value }; - fetch(`${url_prefix}/api/user?api_key=${api_key}`, { + fetch(`${urlPrefix}/api/user?api_key=${apiKey}`, { 'method': 'PUT', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) @@ -36,18 +36,18 @@ function changePassword() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); }; function deleteAccount() { - fetch(`${url_prefix}/api/user?api_key=${api_key}`, { + fetch(`${urlPrefix}/api/user?api_key=${apiKey}`, { 'method': 'DELETE' }) .then(response => { - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; }); }; diff --git a/frontend/static/js/show.js b/frontend/static/js/show.js index 2004a21..93958ab 100644 --- a/frontend/static/js/show.js +++ b/frontend/static/js/show.js @@ -19,21 +19,21 @@ function showAdd(type) { const cl = document.getElementById('info').classList; cl.forEach(c => { - if (InfoClasses.includes(c)) cl.remove(c) + if (infoClasses.includes(c)) cl.remove(c) }); document.querySelector('.options > button[type="submit"]').innerText = 'Add'; document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; const title = document.querySelector('#info h2'); - if (type === Types.reminder) { + if (type === reminderTypes.reminder) { cl.add('show-add-reminder'); title.innerText = 'Add a reminder'; inputs.time.setAttribute('required', ''); - } else if (type === Types.template) { + } else if (type === reminderTypes.template) { cl.add('show-add-template'); title.innerText = 'Add a template'; inputs.time.removeAttribute('required'); - } else if (type === Types.static_reminder) { + } else if (type === reminderTypes.static_reminder) { cl.add('show-add-static-reminder'); title.innerText = 'Add a static reminder'; inputs.time.removeAttribute('required'); @@ -44,15 +44,15 @@ function showAdd(type) { function showEdit(id, type) { let url; - if (type === Types.reminder) { - url = `${url_prefix}/api/reminders/${id}?api_key=${api_key}`; + if (type === reminderTypes.reminder) { + url = `${urlPrefix}/api/reminders/${id}?api_key=${apiKey}`; inputs.time.setAttribute('required', ''); - } else if (type === Types.template) { - url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`; + } else if (type === reminderTypes.template) { + url = `${urlPrefix}/api/templates/${id}?api_key=${apiKey}`; inputs.time.removeAttribute('required'); type_buttons.repeat_interval.removeAttribute('required'); - } else if (type === Types.static_reminder) { - url = `${url_prefix}/api/staticreminders/${id}?api_key=${api_key}`; + } else if (type === reminderTypes.static_reminder) { + url = `${urlPrefix}/api/staticreminders/${id}?api_key=${apiKey}`; document.getElementById('test-reminder').classList.remove('show-sent'); inputs.time.removeAttribute('required'); type_buttons.repeat_interval.removeAttribute('required'); @@ -69,7 +69,7 @@ function showEdit(id, type) { selectColor(json.result.color || colors[0]); inputs.title.value = json.result.title; - if (type === Types.reminder) { + if (type === reminderTypes.reminder) { inputs.enabled.checked = json.result.enabled; var trigger_date = new Date( (json.result.time @@ -86,7 +86,7 @@ function showEdit(id, type) { c => c.checked = json.result.notification_services.includes(parseInt(c.dataset.id)) ); - if (type == Types.reminder) { + if (type == reminderTypes.reminder) { if (json.result.repeat_interval !== null) { toggleRepeated(); type_buttons.repeat_interval.value = json.result.repeat_interval; @@ -107,27 +107,27 @@ function showEdit(id, type) { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); const cl = document.getElementById('info').classList; cl.forEach(c => { - if (InfoClasses.includes(c)) cl.remove(c) + if (infoClasses.includes(c)) cl.remove(c) }); document.querySelector('.options > button[type="submit"]').innerText = 'Save'; const title = document.querySelector('#info h2'); const test_text = document.querySelector('#test-reminder > div:first-child'); - if (type === Types.reminder) { + if (type === reminderTypes.reminder) { cl.add('show-edit-reminder'); title.innerText = 'Edit a reminder'; test_text.innerText = 'Test'; - } else if (type === Types.template) { + } else if (type === reminderTypes.template) { cl.add('show-edit-template'); title.innerText = 'Edit a template'; test_text.innerText = 'Test'; - } else if (type === Types.static_reminder) { + } else if (type === reminderTypes.static_reminder) { cl.add('show-edit-static-reminder'); title.innerText = 'Edit a static reminder'; test_text.innerText = 'Trigger'; diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js index 5d65b64..81b044c 100644 --- a/frontend/static/js/templates.js +++ b/frontend/static/js/templates.js @@ -1,5 +1,5 @@ function loadTemplateSelection() { - fetch(`${url_prefix}/api/templates?api_key=${api_key}`) + fetch(`${urlPrefix}/api/templates?api_key=${apiKey}`) .then(response => { if (!response.ok) return Promise.reject(response.status); return response.json(); @@ -17,7 +17,7 @@ function loadTemplateSelection() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); @@ -33,7 +33,7 @@ function applyTemplate() { selectColor(colors[0]); } else { - fetch(`${url_prefix}/api/templates/${inputs.template.value}?api_key=${api_key}`) + fetch(`${urlPrefix}/api/templates/${inputs.template.value}?api_key=${apiKey}`) .then(response => { if (!response.ok) return Promise.reject(response.status); else return response.json(); @@ -48,7 +48,7 @@ function applyTemplate() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); diff --git a/frontend/static/js/window.js b/frontend/static/js/window.js index 84d12b5..1f34fb5 100644 --- a/frontend/static/js/window.js +++ b/frontend/static/js/window.js @@ -102,7 +102,7 @@ function testReminder() { let url; if (cl.contains('show-edit-static-reminder')) { // Trigger static reminder - url = `${url_prefix}/api/staticreminders/${r_id}?api_key=${api_key}`; + url = `${urlPrefix}/api/staticreminders/${r_id}?api_key=${apiKey}`; } else { // Test reminder draft if (inputs.title.value === '') { @@ -132,7 +132,7 @@ function testReminder() { 'text': inputs.text.value }; headers.body = JSON.stringify(data); - url = `${url_prefix}/api/reminders/test?api_key=${api_key}`; + url = `${urlPrefix}/api/reminders/test?api_key=${apiKey}`; }; fetch(url, headers) .then(response => { @@ -141,7 +141,7 @@ function testReminder() { }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); @@ -153,13 +153,13 @@ function deleteInfo() { const cl = document.getElementById('info').classList; if (cl.contains('show-edit-reminder')) { // Delete reminder - url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`; + url = `${urlPrefix}/api/reminders/${e_id}?api_key=${apiKey}`; } else if (cl.contains('show-edit-template')) { // Delete template - url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; + url = `${urlPrefix}/api/templates/${e_id}?api_key=${apiKey}`; } else if (cl.contains('show-edit-static-reminder')) { // Delete static reminder - url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`; + url = `${urlPrefix}/api/staticreminders/${e_id}?api_key=${apiKey}`; } else return; fetch(url, {'method': 'DELETE'}) @@ -168,20 +168,20 @@ function deleteInfo() { if (cl.contains('show-edit-reminder')) { // Delete reminder - fillLibrary(Types.reminder); + fillLibrary(reminderTypes.reminder); } else if (cl.contains('show-edit-template')) { // Delete template - fillLibrary(Types.template); + fillLibrary(reminderTypes.template); loadTemplateSelection(); } else if (cl.contains('show-edit-static-reminder')) { // Delete static reminder - fillLibrary(Types.static_reminder); + fillLibrary(reminderTypes.static_reminder); }; showWindow("home"); }) .catch(e => { if (e === 401) - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; else console.log(e); }); @@ -244,24 +244,24 @@ function submitInfo() { }; }; - fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/reminders?api_key=${apiKey}`; fetch_data.method = 'POST'; - fetch_data.call_back = () => fillLibrary(Types.reminder); + fetch_data.call_back = () => fillLibrary(reminderTypes.reminder); } else if (cl.contains('show-add-template')) { // Add template - fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/templates?api_key=${apiKey}`; fetch_data.method = 'POST'; fetch_data.call_back = () => { loadTemplateSelection(); - fillLibrary(Types.template); + fillLibrary(reminderTypes.template); }; } else if (cl.contains('show-add-static-reminder')) { // Add static reminder - fetch_data.url = `${url_prefix}/api/staticreminders?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/staticreminders?api_key=${apiKey}`; fetch_data.method = 'POST'; - fetch_data.call_back = () => fillLibrary(Types.static_reminder); + fetch_data.call_back = () => fillLibrary(reminderTypes.static_reminder); } else if (cl.contains('show-edit-reminder')) { // Edit reminder @@ -289,24 +289,24 @@ function submitInfo() { }; }; - fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/reminders/${e_id}?api_key=${apiKey}`; fetch_data.method = 'PUT'; - fetch_data.call_back = () => fillLibrary(Types.reminder); + fetch_data.call_back = () => fillLibrary(reminderTypes.reminder); } else if (cl.contains('show-edit-template')) { // Edit template - fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/templates/${e_id}?api_key=${apiKey}`; fetch_data.method = 'PUT'; fetch_data.call_back = () => { loadTemplateSelection(); - fillLibrary(Types.template); + fillLibrary(reminderTypes.template); }; } else if (cl.contains('show-edit-static-reminder')) { // Edit a static reminder - fetch_data.url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`; + fetch_data.url = `${urlPrefix}/api/staticreminders/${e_id}?api_key=${apiKey}`; fetch_data.method = 'PUT'; - fetch_data.call_back = () => fillLibrary(Types.static_reminder); + fetch_data.call_back = () => fillLibrary(reminderTypes.static_reminder); } else return; @@ -323,7 +323,7 @@ function submitInfo() { }) .catch(e => { if (e === 401) { - window.location.href = `${url_prefix}/`; + window.location.href = `${urlPrefix}/`; } else if (e === 400) { inputs.time.classList.add('error-input'); inputs.time.title = 'Time is in the past'; diff --git a/frontend/templates/admin.html b/frontend/templates/admin.html index 63a9426..7b8ecde 100644 --- a/frontend/templates/admin.html +++ b/frontend/templates/admin.html @@ -25,40 +25,265 @@ - - + + + + Reset Settings + + + + + + + + + Allow New Accounts + + + + Login Time + + + + Login Time Trigger + + + + Host + + + + Port + + + + URL Prefix + + + + Logging Level + + + + Database Backup Interval + + + + Database Backup Retention + + + + Database Backup Folder + + + + + + + + + + + + + Add User + + + + + + Username invalid + Username already taken + + + + + + + + + + + + Edit User + + + Leave fields empty to not change for + + + + Username invalid + Username already taken + + + + + + + + + + + + Are you sure you want to delete the user ? + + Confirm + Cancel + + + + + + + + Upload Database + + + You will be uploading a database file. Login into MIND within one minute to keep the new database, or the upload will automatically be reverted. + + + + + + Database File + + + + Invalid database file + + + + + Keep Hosting Settings + + + Keep the current hosting settings instead of using the settings in the uploaded database when importing. + + + + + + + + + + + + + + Import Database + + + You will be importing database backup , created on . + Login into MIND within one minute to keep the new database, or the import will automatically be reverted. + + + + + + Keep Hosting Settings + + + Keep the current hosting settings instead of using the settings in the selected database backup when importing. + + + + + + + + + + + + + + + 0 changes - - + + + Log out - - - Authentication - - + + + About + + - Allow New Accounts + MIND Version + + + + Python Version + + + + Database Version + + + + Database Location + + + + Data Folder + + + + + + + + Power + + Restart + Shutdown + + + + Authentication + + + + + Allow New Accounts - + Allow users to register a new account. The admin can always add a new account from this panel. - Login Time + Login Time - - + + Min For how long users stay logged in before having to authenticate again. Between 1 minute and 1 month. @@ -66,9 +291,9 @@ - Login Time Trigger + Login Time Trigger - + After Last Use After Login @@ -78,195 +303,163 @@ - Logging - - + + + Hosting + + - Logging Level + Host - - Info - Debug - + + Valid IPv4 address (default is '0.0.0.0' for all available interfaces). + + + + Port + + + The port used to access the web UI (default is '8080'). + + + + URL Prefix + + + For reverse proxy support (default is empty). - - Download Debug Logs - - Database Backups - - + + + Logging and Resetting + + - Database Backup Interval + Logging Level - - + + Info + Debug + + + + + Download Logs + + Download Debug Logs + + + + Reset Setting + + Reset Setting + Opens a window to select settings for which to reset their value. + + + + + + + + Database Backup Settings + + + + + Database Backup Interval + + + Hours How often to make a backup of the database. - Database Backup Retention + Database Backup Retention - - + + Backups How many backups to keep. The oldest one will be removed if needed. - Database Backup Folder + Database Backup Folder - + + + Path doesn't exist or isn't a folder + The folder to store the backups in. - - - Hosting - - - - - Host - - - Valid IPv4 address (default is '0.0.0.0' for all available interfaces). - - - - Port - - - The port used to access the web UI (default is '8080'). - - - - URL Prefix - - - For reverse proxy support (default is empty). - - - - - Save and Restart - - IMPORTANT: Login into MIND within one minute to keep the new database, or the import will automatically be reverted. - See the documentation for more information. - - User Management - - - - + + + User Management + + + - + + + - - - - - - - - - User - - Actions - - - - - - - - - - - - - - - - - - - - - - - - - - - Database - - - - - - Keep Hosting Settings - - - Keep the current hosting settings instead of using the settings in the uploaded database when importing. - - - - Database File - - - Instead of importing a backup, import a database by uploading the file. - - + + + + + + + User + Actions + + - Import Database - IMPORTANT: Login into MIND within one minute to keep the new database, or the import will automatically be reverted. - See the documentation for more information. - - - - - File - Creation - Actions - - - - Current Database - - - - - - - - - - - - - - - - - Power - - Restart - Shutdown - + + + Database Backup Management + + Upload Database + + + + + File + Creation + Actions + + + + Current Database + + + + + + + + + + + + + + + + +
Username invalid
Username already taken
Leave fields empty to not change for
Are you sure you want to delete the user ?
You will be uploading a database file. Login into MIND within one minute to keep the new database, or the upload will automatically be reverted.
Invalid database file
Keep the current hosting settings instead of using the settings in the uploaded database when importing.
You will be importing database backup , created on . + Login into MIND within one minute to keep the new database, or the import will automatically be reverted.
Keep the current hosting settings instead of using the settings in the selected database backup when importing.
Allow users to register a new account. The admin can always add a new account from this panel.
Min
For how long users stay logged in before having to authenticate again. Between 1 minute and 1 month.
Valid IPv4 address (default is '0.0.0.0' for all available interfaces).
The port used to access the web UI (default is '8080').
For reverse proxy support (default is empty).
Opens a window to select settings for which to reset their value.
Hours
How often to make a backup of the database.
Backups
How many backups to keep. The oldest one will be removed if needed.
Path doesn't exist or isn't a folder
The folder to store the backups in.
IMPORTANT: Login into MIND within one minute to keep the new database, or the import will automatically be reverted. - See the documentation for more information.
Instead of importing a backup, import a database by uploading the file.