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 @@
-
- + + + + + +
+
+

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 ?

+
+ + +
+
+
+
+ +
+
+

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.

+
+
+ + + + + + + + + + + +
+
+ +

Invalid database file

+
+
+ +

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 the current hosting settings instead of using the settings in the selected database backup when importing.

+
+
+
+
+ +
+
+ +
+
+ -
-
-
-

Authentication

-
- +
+
+

About

+
+
- + + + + + + + + + + + + + + + + + + + + +
MIND Version
Python Version
Database Version
Database Location
Data Folder
+
+
+
+

Power

+
+ + +
+
+
+

Authentication

+
+ + + + - + - +
- +

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.

@@ -66,9 +291,9 @@
- @@ -78,195 +303,163 @@
-

Logging

-
- + +
+

Hosting

+
+
- + + + + + + + + +
- + +

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).

-
- -
-

Database Backups

-
- + +
+

Logging and Resetting

+
+
- + + + + + + + + + + + +
-
- + +
+ +
+ +

Opens a window to select settings for which to reset their value.

+
+
+
+
+

Database Backup Settings

+
+ + + + + - + - +
+
+

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.

- -
-

Hosting

-
- - - - - - - - - - - - - - - -
- -

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).

-
- -
-

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

+
+ -
-
-
- - - - - - - - - - - - - - - -
UserActions
-
-

Database

-
-
- - - - - - - - - - + + + +
+
- -

Keep the current hosting settings instead of using the settings in the uploaded database when importing.

-
- -

Instead of importing a backup, import a database by uploading the file.

-
+ + + + +
UserActions
- -

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.

-
-
- - - - - - - - - - - - - - -
FileCreationActions
Current Database - -
-
-

Power

-
- - -
+
+
+

Database Backup Management

+
+ +
+
+ + + + + + + + + + + + + + +
FileCreationActions
Current Database + +
+
+
diff --git a/frontend/templates/login.html b/frontend/templates/login.html index b462b4f..35efe09 100644 --- a/frontend/templates/login.html +++ b/frontend/templates/login.html @@ -15,6 +15,7 @@ + Login - MIND @@ -26,40 +27,42 @@
-
+ +
- \ No newline at end of file + diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 8ed97e6..4f843d1 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -14,11 +14,13 @@ + + @@ -142,7 +144,7 @@