From 21386b77cf56ae95df0ad112c4845a8ce97810c1 Mon Sep 17 00:00:00 2001 From: CasVT Date: Mon, 22 May 2023 21:35:34 +0200 Subject: [PATCH 1/6] Fixes issue #47 --- frontend/static/js/add.js | 4 ++-- frontend/static/js/edit.js | 6 +++--- frontend/static/js/general.js | 4 ++-- frontend/static/js/notification.js | 8 ++++---- frontend/static/js/reminders.js | 4 ++-- frontend/static/js/settings.js | 4 ++-- frontend/static/js/templates.js | 12 ++++++------ 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/static/js/add.js b/frontend/static/js/add.js index 58d9c4b..34847a7 100644 --- a/frontend/static/js/add.js +++ b/frontend/static/js/add.js @@ -52,7 +52,7 @@ function addReminder() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e === 400) { inputs.time.classList.add('error-input'); inputs.time.title = 'Time is in the past'; @@ -158,7 +158,7 @@ function testReminder() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; }; }); }; diff --git a/frontend/static/js/edit.js b/frontend/static/js/edit.js index 42ad2e3..080e007 100644 --- a/frontend/static/js/edit.js +++ b/frontend/static/js/edit.js @@ -51,7 +51,7 @@ function editReminder() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -99,7 +99,7 @@ function showEdit(id) { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e === 404) { fillList(); } else { @@ -142,7 +142,7 @@ function deleteReminder() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e === 404) { fillList(); } else { diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index 83a7142..bb18ab8 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -4,7 +4,7 @@ function logout() { }) .then(response => { sessionStorage.removeItem('api_key'); - window.location.href = url_prefix || '/'; + window.location.href = `${url_prefix}/`; }); }; @@ -52,7 +52,7 @@ function showTab(tab_id, button_id, load_function=null) { const url_prefix = document.getElementById('url_prefix').dataset.value; const api_key = sessionStorage.getItem('api_key'); if (api_key === null) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; }; document.getElementById('toggle-nav').addEventListener('click', e => toggleNav()); diff --git a/frontend/static/js/notification.js b/frontend/static/js/notification.js index f6b6cc0..c3c8b8c 100644 --- a/frontend/static/js/notification.js +++ b/frontend/static/js/notification.js @@ -87,7 +87,7 @@ function fillNotificationSelection() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -113,7 +113,7 @@ function deleteService(id) { }) .catch(e => { if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e.error === 'NotificationServiceInUse') { const delete_button = row.querySelector('button[title="Delete"]'); delete_button.classList.add('error-icon'); @@ -151,7 +151,7 @@ function saveService(id) { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e === 400) { save_button.classList.add('error-icon'); save_button.title = 'Invalid Apprise URL'; @@ -199,7 +199,7 @@ function addService() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else if (e === 400) { inputs_buttons.save_button.classList.add('error-icon'); inputs_buttons.save_button.title = 'Invalid Apprise URL'; diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js index 54af9c7..892f7a8 100644 --- a/frontend/static/js/reminders.js +++ b/frontend/static/js/reminders.js @@ -55,7 +55,7 @@ function fillList() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -77,7 +77,7 @@ function search() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; diff --git a/frontend/static/js/settings.js b/frontend/static/js/settings.js index 9065cc5..80698e9 100644 --- a/frontend/static/js/settings.js +++ b/frontend/static/js/settings.js @@ -16,7 +16,7 @@ function changePassword() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -28,7 +28,7 @@ function deleteAccount() { 'method': 'DELETE' }) .then(response => { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; }); }; diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js index b7ff021..306a593 100644 --- a/frontend/static/js/templates.js +++ b/frontend/static/js/templates.js @@ -57,7 +57,7 @@ function loadTemplates(force=true) { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -99,7 +99,7 @@ function loadTemplate() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -134,7 +134,7 @@ function addTemplate() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -177,7 +177,7 @@ function showEditTemplate(id) { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -210,7 +210,7 @@ function saveTemplate() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; @@ -233,7 +233,7 @@ function deleteTemplate() { }) .catch(e => { if (e === 401) { - window.location.href = url_prefix; + window.location.href = `${url_prefix}/`; } else { console.log(e); }; From c60da4c0bc89e3e5b512d471d7bd8a8d0adfc477 Mon Sep 17 00:00:00 2001 From: CasVT Date: Mon, 22 May 2023 21:56:43 +0200 Subject: [PATCH 2/6] Small database command optimisations --- backend/custom_exceptions.py | 1 + backend/db.py | 20 ++++++------ backend/notification_service.py | 24 +++++++++----- backend/reminders.py | 56 ++++++++++++++++++++------------- backend/security.py | 1 + backend/templates.py | 11 +++++-- backend/users.py | 2 +- 7 files changed, 73 insertions(+), 42 deletions(-) diff --git a/backend/custom_exceptions.py b/backend/custom_exceptions.py index 9534954..6059ce2 100644 --- a/backend/custom_exceptions.py +++ b/backend/custom_exceptions.py @@ -2,6 +2,7 @@ from typing import Any, Dict + class UsernameTaken(Exception): """The username is already taken""" api_response = {'error': 'UsernameTaken', 'result': {}, 'code': 400} diff --git a/backend/db.py b/backend/db.py index 2294a47..281ce5b 100644 --- a/backend/db.py +++ b/backend/db.py @@ -37,15 +37,15 @@ def get_db(output_type: Union[dict, tuple]=tuple): Cursor: The Cursor instance to use """ try: - cursor = g.cursor + cursor = g.cursor except AttributeError: - db = DBConnection(timeout=20.0) - cursor = g.cursor = db.cursor() + db = DBConnection(timeout=20.0) + cursor = g.cursor = db.cursor() if output_type is dict: - cursor.row_factory = Row + cursor.row_factory = Row else: - cursor.row_factory = None + cursor.row_factory = None return g.cursor @@ -73,10 +73,10 @@ def migrate_db(current_db_version: int) -> None: # V1 -> V2 t = time() utc_offset = datetime.fromtimestamp(t) - datetime.utcfromtimestamp(t) - reminders = cursor.execute("SELECT time, id FROM reminders;").fetchall() + cursor.execute("SELECT time, id FROM reminders;") new_reminders = [] new_reminders_append = new_reminders.append - for reminder in reminders: + for reminder in cursor: new_reminders_append([round((datetime.fromtimestamp(reminder[0]) - utc_offset).timestamp()), reminder[1]]) cursor.executemany("UPDATE reminders SET time = ? WHERE id = ?;", new_reminders) current_db_version = 2 @@ -163,12 +163,14 @@ def setup_db() -> None: """, (__DATABASE_VERSION__,) ) - current_db_version = int(cursor.execute("SELECT value FROM config WHERE key = 'database_version' LIMIT 1;").fetchone()[0]) + current_db_version = int(cursor.execute( + "SELECT value FROM config WHERE key = 'database_version' LIMIT 1;" + ).fetchone()[0]) if current_db_version < __DATABASE_VERSION__: migrate_db(current_db_version) cursor.execute( - "UPDATE config SET value = ? WHERE key = 'database_version' LIMIT 1;", + "UPDATE config SET value = ? WHERE key = 'database_version';", (__DATABASE_VERSION__,) ) diff --git a/backend/notification_service.py b/backend/notification_service.py index f92df6b..4d6a1af 100644 --- a/backend/notification_service.py +++ b/backend/notification_service.py @@ -8,12 +8,13 @@ from backend.custom_exceptions import (InvalidURL, NotificationServiceInUse, NotificationServiceNotFound) from backend.db import get_db + class NotificationService: def __init__(self, notification_service_id: int) -> None: self.id = notification_service_id if not get_db().execute( - "SELECT 1 FROM notification_services WHERE id = ? LIMIT 1", + "SELECT 1 FROM notification_services WHERE id = ? LIMIT 1;", (self.id,) ).fetchone(): raise NotificationServiceNotFound @@ -61,13 +62,15 @@ class NotificationService: # Update database get_db().execute(""" UPDATE notification_services - SET title=?, url=? + SET title = ?, url = ? WHERE id = ?; - """, ( + """, + ( data["title"], data["url"], self.id - )) + ) + ) return self.get() @@ -103,17 +106,22 @@ class NotificationService: class NotificationServices: def __init__(self, user_id: int) -> None: self.user_id = user_id - + def fetchall(self) -> List[dict]: """Get a list of all notification services Returns: List[dict]: The list of all notification services """ - result = list(map(dict, get_db(dict).execute( - "SELECT id, title, url FROM notification_services WHERE user_id = ? ORDER BY title, id", + result = list(map(dict, get_db(dict).execute(""" + SELECT + id, title, url + FROM notification_services + WHERE user_id = ? + ORDER BY title, id; + """, (self.user_id,) - ).fetchall())) + ))) return result diff --git a/backend/reminders.py b/backend/reminders.py index 84020aa..4ff5280 100644 --- a/backend/reminders.py +++ b/backend/reminders.py @@ -94,7 +94,7 @@ class ReminderHandler(): # Send of reminder a = Apprise() url = cursor.execute( - "SELECT url FROM notification_services WHERE id = ?", + "SELECT url FROM notification_services WHERE id = ? LIMIT 1;", (reminder["notification_service"],) ).fetchone()["url"] a.add(url) @@ -102,7 +102,10 @@ class ReminderHandler(): if reminder['repeat_quantity'] is None: # Delete the reminders from the database - cursor.execute("DELETE FROM reminders WHERE id = ?", (reminder['id'],)) + cursor.execute( + "DELETE FROM reminders WHERE id = ?", + (reminder['id'],) + ) else: # Set next time new_time = _find_next_time( @@ -138,7 +141,10 @@ class Reminder: self.id = reminder_id # Check if reminder exists - if not get_db().execute("SELECT 1 FROM reminders WHERE id = ? LIMIT 1", (self.id,)).fetchone(): + if not get_db().execute( + "SELECT 1 FROM reminders WHERE id = ? LIMIT 1", + (self.id,) + ).fetchone(): raise ReminderNotFound def get(self) -> dict: @@ -147,7 +153,7 @@ class Reminder: Returns: dict: The info about the reminder """ - reminder: dict = get_db(dict).execute(""" + reminder = get_db(dict).execute(""" SELECT r.id, r.title, r.text, @@ -157,12 +163,11 @@ class Reminder: r.repeat_quantity, r.repeat_interval, r.color - FROM - reminders r - INNER JOIN notification_services ns - ON - r.notification_service = ns.id - AND r.id = ?; + FROM reminders r + INNER JOIN notification_services ns + ON r.notification_service = ns.id + WHERE r.id = ? + LIMIT 1; """, (self.id,) ).fetchone() @@ -229,7 +234,11 @@ class Reminder: next_time = data["time"] cursor.execute(""" UPDATE reminders - SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, color=? + SET + title=?, text=?, + time=?, notification_service=?, + repeat_quantity=?, repeat_interval=?, + color=? WHERE id = ?; """, ( data["title"], @@ -242,10 +251,17 @@ class Reminder: self.id )) else: - next_time = _find_next_time(data["time"], data["repeat_quantity"], data["repeat_interval"]) + next_time = _find_next_time( + data["time"], + data["repeat_quantity"], data["repeat_interval"] + ) cursor.execute(""" UPDATE reminders - SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, original_time=?, color=? + SET + title=?, text=?, + time=?, notification_service=?, + repeat_quantity=?, repeat_interval=?, original_time=?, + color=? WHERE id = ?; """, ( data["title"], @@ -309,15 +325,13 @@ class Reminders: r.repeat_quantity, r.repeat_interval, r.color - FROM - reminders r - INNER JOIN notification_services ns - ON - r.notification_service = ns.id - AND r.user_id = ?; + FROM reminders r + INNER JOIN notification_services ns + ON r.notification_service = ns.id + WHERE r.user_id = ?; """, (self.user_id,) - ).fetchall())) + ))) # Sort result reminders.sort(key=sort_function[0], reverse=sort_function[1]) @@ -418,7 +432,7 @@ def test_reminder( """ a = Apprise() url = get_db(dict).execute( - "SELECT url FROM notification_services WHERE id = ?", + "SELECT url FROM notification_services WHERE id = ? LIMIT 1;", (notification_service,) ).fetchone() if not url: diff --git a/backend/security.py b/backend/security.py index 8cb6a96..15a248d 100644 --- a/backend/security.py +++ b/backend/security.py @@ -5,6 +5,7 @@ from hashlib import pbkdf2_hmac from secrets import token_bytes from typing import Tuple + def get_hash(salt: bytes, data: str) -> bytes: """Hash a string using the supplied salt diff --git a/backend/templates.py b/backend/templates.py index 4f82364..83383b5 100644 --- a/backend/templates.py +++ b/backend/templates.py @@ -7,13 +7,17 @@ from backend.custom_exceptions import (NotificationServiceNotFound, TemplateNotFound) from backend.db import get_db + class Template: """Represents a template """ def __init__(self, template_id: int): self.id = template_id - exists = get_db().execute("SELECT 1 FROM templates WHERE id = ? LIMIT 1;", (self.id,)).fetchone() + exists = get_db().execute( + "SELECT 1 FROM templates WHERE id = ? LIMIT 1;", + (self.id,) + ).fetchone() if not exists: raise TemplateNotFound @@ -30,7 +34,8 @@ class Template: notification_service, color FROM templates - WHERE id = ?; + WHERE id = ? + LIMIT 1; """, (self.id,) ).fetchone() @@ -113,7 +118,7 @@ class Templates: ORDER BY title, id; """, (self.user_id,) - ).fetchall())) + ))) return templates diff --git a/backend/users.py b/backend/users.py index f5a313f..e267861 100644 --- a/backend/users.py +++ b/backend/users.py @@ -17,7 +17,7 @@ class User: def __init__(self, username: str, password: str): # Fetch data of user to check if user exists and to check if password is correct result = get_db(dict).execute( - "SELECT id, salt, hash FROM users WHERE username = ?", + "SELECT id, salt, hash FROM users WHERE username = ? LIMIT 1;", (username,) ).fetchone() if not result: From 645d4926f8325f3617576ace53157272b2b10808 Mon Sep 17 00:00:00 2001 From: CasVT Date: Wed, 24 May 2023 13:47:08 +0200 Subject: [PATCH 3/6] Frontend refactoring --- MIND.py | 8 +- frontend/api.py | 4 - frontend/static/css/general.css | 39 +- .../static/css/{add_edit.css => info.css} | 54 ++- .../{reminders_templates.css => library.css} | 10 +- frontend/static/css/login.css | 4 +- frontend/static/js/add.js | 176 --------- frontend/static/js/edit.js | 161 -------- frontend/static/js/general.js | 54 ++- frontend/static/js/library.js | 117 ++++++ frontend/static/js/login.js | 85 +++-- frontend/static/js/notification.js | 134 +++---- frontend/static/js/reminders.js | 100 ----- frontend/static/js/settings.js | 10 +- frontend/static/js/show.js | 102 +++++ frontend/static/js/templates.js | 360 ++++++++---------- frontend/static/js/window.js | 210 ++++++++++ frontend/templates/login.html | 18 +- frontend/templates/page_not_found.html | 2 +- frontend/templates/reminders.html | 119 +----- frontend/ui.py | 4 - 21 files changed, 838 insertions(+), 933 deletions(-) rename frontend/static/css/{add_edit.css => info.css} (70%) rename frontend/static/css/{reminders_templates.css => library.css} (92%) delete mode 100644 frontend/static/js/add.js delete mode 100644 frontend/static/js/edit.js create mode 100644 frontend/static/js/library.js delete mode 100644 frontend/static/js/reminders.js create mode 100644 frontend/static/js/show.js create mode 100644 frontend/static/js/window.js diff --git a/MIND.py b/MIND.py index 4f39517..a4e0fa7 100644 --- a/MIND.py +++ b/MIND.py @@ -7,7 +7,7 @@ from os.path import abspath, dirname, isfile, join from shutil import move from sys import version_info -from flask import Flask +from flask import Flask, render_template, request from waitress.server import create_server from werkzeug.middleware.dispatcher import DispatcherMiddleware @@ -57,6 +57,12 @@ def _create_app() -> Flask: @app.errorhandler(500) def internal_error(e): return {'error': 'Internal error', 'result': {}}, 500 + + @app.errorhandler(404) + def not_found(e): + if request.path.startswith('/api'): + return {'error': 'Not Found', 'result': {}}, 404 + return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX) app.register_blueprint(ui) app.register_blueprint(api, url_prefix="/api") diff --git a/frontend/api.py b/frontend/api.py index 8bfb57e..8119e8f 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -120,10 +120,6 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any: return value -@api.errorhandler(404) -def not_found(e): - return return_api({}, 'Not Found', 404) - #=================== # Authentication endpoints #=================== diff --git a/frontend/static/css/general.css b/frontend/static/css/general.css index 71d45c6..2b72338 100644 --- a/frontend/static/css/general.css +++ b/frontend/static/css/general.css @@ -20,7 +20,9 @@ --default-shadow: 0 1px 2px 0 rgb(0 0 0 / 60%), 0 2px 6px 2px rgb(0 0 0 / 30%); } +/* */ /* Default properties */ +/* */ img { width: 100%; } @@ -66,7 +68,9 @@ svg rect { fill: var(--color-light); } +/* */ /* Utility classes */ +/* */ .hidden { display: none !important; } @@ -90,7 +94,9 @@ svg rect { font-size: 1rem !important; } +/* */ /* General styling */ +/* */ body { height: 100vh; overflow-x: hidden; @@ -99,6 +105,21 @@ body { color: var(--color-light); } +noscript { + display: block; + max-width: 95%; + margin-inline: 1rem; + margin-bottom: 1rem; + + border-radius: 4px; + padding: 1rem; + background-color: var(--color-error); + color: var(--color-light); +} + +/* */ +/* Header */ +/* */ header { width: 100%; height: var(--header-height); @@ -138,18 +159,9 @@ h1 { font-size: clamp(1.3rem, 7vw, 2rem); } -noscript { - display: block; - max-width: 95%; - margin-inline: 1rem; - margin-bottom: 1rem; - - border-radius: 4px; - padding: 1rem; - background-color: var(--color-error); - color: var(--color-light); -} - +/* */ +/* Nav */ +/* */ .nav-divider { position: relative; height: calc(100% - var(--header-height)); @@ -222,6 +234,9 @@ nav > div > button svg { width: 2rem; } +/* */ +/* Main window */ +/* */ .window-container { margin-left: calc(4rem + var(--rem-clamp)); width: 100%; diff --git a/frontend/static/css/add_edit.css b/frontend/static/css/info.css similarity index 70% rename from frontend/static/css/add_edit.css rename to frontend/static/css/info.css index 179971b..6ca1146 100644 --- a/frontend/static/css/add_edit.css +++ b/frontend/static/css/info.css @@ -134,7 +134,7 @@ div.options > button { background-color: var(--color-gray); } -#delete-reminder { +#delete-info { border-color: var(--color-error); color: var(--color-error); } @@ -165,4 +165,54 @@ div.options > button { .sub-inputs > button { width: 100%; } -} \ No newline at end of file +} + +#info.show-add-template #template-selection, +#info.show-add-template #time-input, +#info.show-add-template #normal-button, +#info.show-add-template #repeat-button, +#info.show-add-template .repeat-bar, +#info.show-add-template #test-reminder, +#info.show-add-template #delete-info { + display: none; +} + +#info.show-add-template #text-input { + margin-top: -1rem; +} + +#info.show-add-template form > *, +#info.show-add-template .sub-inputs > * { + width: 100%; +} + +#info.show-add-reminder #delete-info { + display: none; +} + +#info.show-edit-reminder #template-selection, +#info.show-edit-reminder #test-reminder { + display: none; +} + +#info.show-edit-reminder #color-toggle { + width: 100%; +} + +#info.show-edit-template #template-selection, +#info.show-edit-template #time-input, +#info.show-edit-template #normal-button, +#info.show-edit-template #repeat-button, +#info.show-edit-template .repeat-bar, +#info.show-edit-template #test-reminder { + display: none; +} + +#info.show-edit-template #text-input { + margin-top: -1rem; +} + +#info.show-edit-template #color-toggle, +#info.show-edit-template #notification-service-input { + width: 100%; +} diff --git a/frontend/static/css/reminders_templates.css b/frontend/static/css/library.css similarity index 92% rename from frontend/static/css/reminders_templates.css rename to frontend/static/css/library.css index f518152..5d525b8 100644 --- a/frontend/static/css/reminders_templates.css +++ b/frontend/static/css/library.css @@ -61,24 +61,22 @@ padding: 1rem; } -#add-entry, -#add-template { +.entry.add-entry { justify-content: center; align-items: center; gap: .5rem; } -#add-entry svg, -#add-template svg { +.entry.add-entry svg { height: 2rem; width: 2rem; } -#add-entry:not(.error.error-icon) p { +.entry.add-entry:not(.error.error-icon) p { display: none; } -#add-entry.error.error-icon p { +.entry.add-entry.error.error-icon p { display: block; font-size: .8rem !important; } diff --git a/frontend/static/css/login.css b/frontend/static/css/login.css index 4151a8a..7456c82 100644 --- a/frontend/static/css/login.css +++ b/frontend/static/css/login.css @@ -1,6 +1,6 @@ main { height: calc(100vh - var(--header-height)); - + overflow-y: hidden; } @@ -52,7 +52,7 @@ form input { text-decoration: underline; } -form button[type="submit"] { +button[type="submit"] { padding: .5rem 1rem; font-size: 1.1rem; diff --git a/frontend/static/js/add.js b/frontend/static/js/add.js deleted file mode 100644 index 34847a7..0000000 --- a/frontend/static/js/add.js +++ /dev/null @@ -1,176 +0,0 @@ -const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"]; -const inputs = { - 'title': document.getElementById('title-input'), - 'time': document.getElementById('time-input'), - 'notification_service': document.getElementById('notification-service-input'), - 'text': document.getElementById('text-input'), - 'color': document.querySelector('#add .color-list') -}; - -const type_buttons = { - 'normal-button': document.getElementById('normal-button'), - 'repeat-button': document.getElementById('repeat-button'), - - 'repeat-bar': document.querySelector('.repeat-bar'), - 'repeat-interval': document.getElementById('repeat-interval'), - 'repeat-quantity': document.getElementById('repeat-quantity') -}; - -function addReminder() { - inputs.time.classList.remove('error-input'); - inputs.time.removeAttribute('title'); - - const data = { - 'title': inputs.title.value, - 'time': (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60), - 'notification_service': inputs.notification_service.value, - 'text': inputs.text.value, - 'color': null - }; - if (!inputs.color.classList.contains('hidden')) { - data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color; - }; - if (type_buttons['repeat-button'].dataset.selected === 'true') { - data['repeat_quantity'] = type_buttons['repeat-quantity'].value; - data['repeat_interval'] = type_buttons['repeat-interval'].value - }; - - fetch(`${url_prefix}/api/reminders?api_key=${api_key}`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - fillList(); - closeAdd(); - return - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else if (e === 400) { - inputs.time.classList.add('error-input'); - inputs.time.title = 'Time is in the past'; - } else { - console.log(e); - }; - }); -}; - -function showAdd() { - if (!document.getElementById('add-entry').classList.contains('error')) { - loadTemplates(force=false); - showWindow('add'); - } else { - showWindow('notification'); - }; -}; - -function closeAdd() { - hideWindow(); - setTimeout(() => { - document.getElementById('template-selection').value = document.querySelector('#template-selection option[selected]').value; - if (!inputs.color.classList.contains('hidden')) { - toggleColor(inputs.color); - }; - inputs.title.value = ''; - inputs.time.value = ''; - inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; - toggleNormal(); - inputs.text.value = ''; - document.getElementById('test-reminder').classList.remove('show-sent'); - }, 500); -}; - -function loadColor() { - document.querySelectorAll('.color-list').forEach(list => { - colors.forEach(color => { - const entry = document.createElement('button'); - entry.dataset.color = color; - entry.title = color; - entry.type = 'button'; - entry.style.setProperty('--color', color); - entry.addEventListener('click', e => selectColor(list, color)) - list.appendChild(entry); - }); - }); -}; - -function selectColor(list, color_code) { - list.querySelector(`button[data-color="${color_code}"]`).dataset.selected = 'true'; - list.querySelectorAll(`button:not([data-color="${color_code}"])`).forEach(b => b.dataset.selected = 'false'); - return; -} - -function toggleColor(list) { - selectColor(list, colors[0]); - list.classList.toggle('hidden'); -} - -function toggleNormal() { - type_buttons['normal-button'].dataset.selected = 'true'; - type_buttons['repeat-button'].dataset.selected = 'false'; - - type_buttons['repeat-bar'].classList.add('hidden'); - type_buttons['repeat-interval'].removeAttribute('required'); - type_buttons['repeat-interval'].value = ''; -}; - -function toggleRepeated() { - type_buttons['normal-button'].dataset.selected = 'false'; - type_buttons['repeat-button'].dataset.selected = 'true'; - - type_buttons['repeat-bar'].classList.remove('hidden'); - type_buttons['repeat-interval'].setAttribute('required', ''); -}; - -function testReminder() { - const input = document.getElementById('test-reminder'); - if (inputs.title.value === '') { - input.classList.add('error-input'); - input.title = 'No title set'; - return - } else { - input.classList.remove('error-input'); - input.removeAttribute('title'); - }; - const data = { - 'title': inputs.title.value, - 'notification_service': inputs.notification_service.value, - 'text': inputs.text.value - }; - fetch(`${url_prefix}/api/reminders/test?api_key=${api_key}`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - input.classList.add('show-sent'); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - }; - }); -}; - -// code run on load - -document.getElementById('add-form').setAttribute('action', 'javascript:addReminder();'); -document.getElementById('template-selection').addEventListener('change', e => loadTemplate()); -document.getElementById('color-toggle').addEventListener('click', e => toggleColor(inputs.color)); -document.getElementById('normal-button').addEventListener('click', e => toggleNormal()); -document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated()); -document.getElementById('close-add').addEventListener('click', e => closeAdd()); -document.getElementById('test-reminder').addEventListener('click', e => testReminder()); - -loadColor(); diff --git a/frontend/static/js/edit.js b/frontend/static/js/edit.js deleted file mode 100644 index 080e007..0000000 --- a/frontend/static/js/edit.js +++ /dev/null @@ -1,161 +0,0 @@ -const edit_inputs = { - 'title': document.getElementById('title-edit-input'), - 'time': document.getElementById('time-edit-input'), - 'notification_service': document.getElementById('notification-service-edit-input'), - 'text': document.getElementById('text-edit-input'), - 'color': document.querySelector('#edit .color-list') -}; - -const edit_type_buttons = { - 'normal-edit-button': document.getElementById('normal-edit-button'), - 'repeat-edit-button': document.getElementById('repeat-edit-button'), - - 'repeat-edit-bar': document.querySelector('.repeat-edit-bar'), - 'repeat-edit-interval': document.getElementById('repeat-edit-interval'), - 'repeat-edit-quantity': document.getElementById('repeat-edit-quantity') -}; - -function editReminder() { - const id = document.getElementById('edit-form').dataset.id; - const data = { - 'title': edit_inputs.title.value, - 'time': (new Date(edit_inputs.time.value) / 1000) + (new Date(edit_inputs.time.value).getTimezoneOffset() * 60), - 'notification_service': edit_inputs.notification_service.value, - 'text': edit_inputs.text.value, - 'repeat_quantity': null, - 'repeat_interval': null, - 'color': null - }; - if (!edit_inputs.color.classList.contains('hidden')) { - data['color'] = edit_inputs.color.querySelector('button[data-selected="true"]').dataset.color; - }; - if (edit_type_buttons['repeat-edit-button'].dataset.selected === 'true') { - data['repeat_quantity'] = edit_type_buttons['repeat-edit-quantity'].value; - data['repeat_interval'] = edit_type_buttons['repeat-edit-interval'].value; - }; - - fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - fillList(); - hideWindow(); - return - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function showEdit(id) { - document.getElementById('edit-form').dataset.id = id; - fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - return response.json(); - }) - .then(json => { - if (json.result['color'] !== null) { - if (edit_inputs.color.classList.contains('hidden')) { - toggleColor(edit_inputs.color); - }; - selectColor(edit_inputs.color, json.result['color']); - }; - - edit_inputs.title.value = json.result.title; - - var trigger_date = new Date( - (json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000 - ); - edit_inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5); - edit_inputs.notification_service.value = json.result.notification_service; - - if (json.result['repeat_interval'] === null) { - toggleEditNormal(); - } else { - toggleEditRepeated(); - edit_type_buttons['repeat-edit-interval'].value = json.result['repeat_interval']; - edit_type_buttons['repeat-edit-quantity'].value = json.result['repeat_quantity']; - }; - - edit_inputs.text.value = json.result.text !== null ? json.result.text : ''; - - showWindow('edit'); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else if (e === 404) { - fillList(); - } else { - console.log(e); - }; - }); -}; - -function toggleEditNormal() { - edit_type_buttons['normal-edit-button'].dataset.selected = 'true'; - edit_type_buttons['repeat-edit-button'].dataset.selected = 'false'; - - edit_type_buttons['repeat-edit-bar'].classList.add('hidden'); - edit_type_buttons['repeat-edit-interval'].removeAttribute('required'); - edit_type_buttons['repeat-edit-interval'].value = ''; -}; - -function toggleEditRepeated() { - edit_type_buttons['normal-edit-button'].dataset.selected = 'false'; - edit_type_buttons['repeat-edit-button'].dataset.selected = 'true'; - - edit_type_buttons['repeat-edit-bar'].classList.remove('hidden'); - edit_type_buttons['repeat-edit-interval'].setAttribute('required', ''); -}; - -function deleteReminder() { - const id = document.getElementById('edit-form').dataset.id; - fetch(`${url_prefix}/api/reminders/${id}?api_key=${api_key}`, { - 'method': 'DELETE' - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - fillList(); - hideWindow(); - return; - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else if (e === 404) { - fillList(); - } else { - console.log(e); - }; - }); -}; - -// code run on load - -document.getElementById('edit-form').setAttribute('action', 'javascript:editReminder();'); -document.getElementById('color-edit-toggle').addEventListener('click', e => toggleColor(edit_inputs.color)); -document.getElementById('normal-edit-button').addEventListener('click', e => toggleEditNormal()); -document.getElementById('repeat-edit-button').addEventListener('click', e => toggleEditRepeated()); -document.getElementById('close-edit').addEventListener('click', e => hideWindow()); -document.getElementById('delete-reminder').addEventListener('click', e => deleteReminder()); diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index bb18ab8..f083811 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -1,19 +1,22 @@ -function logout() { - fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, { - 'method': 'POST' - }) - .then(response => { - sessionStorage.removeItem('api_key'); - window.location.href = `${url_prefix}/`; - }); +const types = { + 'reminder': document.getElementById('reminder-list'), + 'template': document.getElementById('template-list') }; +const icons = { + 'save': '', + 'edit': '', + 'delete': '' +}; + +const info_classes = ['show-add-reminder', 'show-add-template', 'show-edit-reminder', 'show-edit-template']; + function toggleNav() { document.querySelector('.nav-divider').classList.toggle('show-nav'); }; function showWindow(id) { - document.querySelectorAll(`.window-container > div`).forEach(e => { + document.querySelectorAll('.window-container > div').forEach(e => { if (e.id === id || e.id === 'home') { e.classList.remove('hidden'); setTimeout(() => e.classList.add('show-window'), 0); @@ -28,23 +31,21 @@ function hideWindow() { document.querySelectorAll('.show-window').forEach(e => { e.classList.remove('show-window'); }); + setTimeout(() => { + document.querySelectorAll('.window-container > div:not(#home)').forEach( + e => e.classList.add('hidden') + ); + }, 500); }; -function showTab(tab_id, button_id, load_function=null) { - document.querySelectorAll(`.tab-selector > button`).forEach(e => { - if (e.id === button_id) { - e.dataset.selected = "true" - } else { - e.dataset.selected = "false" - }; +function logout() { + fetch(`${url_prefix}/api/auth/logout?api_key=${api_key}`, { + 'method': 'POST' + }) + .then(response => { + sessionStorage.removeItem('api_key'); + window.location.href = `${url_prefix}/`; }); - document.querySelectorAll(`#home > div:not(.tab-selector)`).forEach(e => { - e.classList.add('hidden'); - }); - document.getElementById(tab_id).classList.remove('hidden'); - if (load_function !== null) { - load_function(force=false); - }; }; // code run on load @@ -61,10 +62,3 @@ document.getElementById('home-button').addEventListener('click', e => hideWindow document.getElementById('notification-services-button').addEventListener('click', e => showWindow('notification')); document.getElementById('settings-button').addEventListener('click', e => showWindow('settings')); document.getElementById('logout-button').addEventListener('click', e => logout()); - -document.getElementById('reminders-selector').addEventListener('click', e => showTab('reminder-tab', 'reminders-selector')); -document.getElementById('templates-selector').addEventListener('click', e => showTab('template-tab', 'templates-selector', loadTemplates)); - -const edit_icon = ''; -const delete_icon = ''; -const save_icon = ''; diff --git a/frontend/static/js/library.js b/frontend/static/js/library.js new file mode 100644 index 0000000..a628fb5 --- /dev/null +++ b/frontend/static/js/library.js @@ -0,0 +1,117 @@ +function showTab(button) { + // Apply styling to selected button + document.querySelectorAll('.tab-selector > button').forEach( + b => b.dataset.selected = b === button ? 'true' : 'false' + ); + + // Show desired tab and hide all others + document.querySelectorAll('#home > div:not(.tab-selector)').forEach( + e => e.classList.add('hidden') + ); + document.getElementById(button.dataset.target).classList.remove('hidden'); +}; + +// +// Filling library +// +function fillTable(table, results) { + table.querySelectorAll('button.entry:not(.add-entry)').forEach(e => e.remove()); + + results.forEach(r => { + const entry = document.createElement('button'); + entry.classList.add('entry'); + entry.dataset.id = r.id; + entry.addEventListener('click', e => showEdit(r.id, table)); + if (r.color !== null) + entry.style.setProperty('--color', r.color); + + const title = document.createElement('h2'); + title.innerText = r.title; + entry.appendChild(title); + + if (table === types.reminder) { + const time = document.createElement('p'); + var offset = new Date(r.time * 1000).getTimezoneOffset() * -60; + var d = new Date((r.time + offset) * 1000); + var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5); + if (r.repeat_interval !== null) { + if (r.repeat_interval === 1) { + var quantity = r.repeat_quantity.endsWith('s') ? r.repeat_quantity.slice(0, -1) : r.repeat_quantity; + var interval_text = ` (each ${quantity})`; + } else { + var quantity = r.repeat_quantity.endsWith('s') ? r.repeat_quantity : r.repeat_quantity + 's'; + var interval_text = ` (every ${r.repeat_interval} ${quantity})`; + }; + formatted_date += interval_text; + }; + time.innerText = formatted_date; + entry.appendChild(time); + }; + + table.appendChild(entry); + + if (title.clientHeight < title.scrollHeight) + entry.classList.add('expand'); + }); + table.querySelectorAll('button.entry:not(.add-entry)').forEach(r => r.classList.add('fit')); +}; + +function fillLibrary(url, type) { + fetch(url) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + return response.json(); + }) + .then(json => fillTable(type, json.result)) + .catch(e => { + if (e === 401) + window.location.href = `${url_prefix}/`; + else + console.log(e); + }); +}; + +function fillReminders() { + fillLibrary(`/api/reminders?api_key=${api_key}`, types.reminder); +}; + +function fillTemplates() { + fillLibrary(`/api/templates?api_key=${api_key}`, types.template); +}; + +// +// Library search +// +function searchLibrary() { + const query = document.querySelector('#search-input').value; + fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + return response.json(); + }) + .then(json => fillTable(types.reminder, json.result)) + .catch(e => { + if (e === 401) + window.location.href = `${url_prefix}/`; + else + console.log(e); + }); +}; + +function clearSearchLibrary() { + document.querySelector('#search-input').value = ''; + fillReminders(); +}; + +// code run on load + +document.querySelectorAll('.tab-selector > button').forEach(b => { + b.addEventListener('click', e => showTab(b)); +}); + +fillReminders(); +fillTemplates(); +setInterval(fillReminders, 60000); + +document.querySelector('#search-form').setAttribute('action', 'javascript:searchLibrary();'); +document.querySelector('#clear-button').addEventListener('click', e => clearSearchLibrary()); diff --git a/frontend/static/js/login.js b/frontend/static/js/login.js index 8992c33..9e1bb09 100644 --- a/frontend/static/js/login.js +++ b/frontend/static/js/login.js @@ -1,26 +1,46 @@ -function login(data=null) { - document.getElementById('username-error').classList.add('hidden'); - document.getElementById('username-input').classList.remove('error-input'); - document.getElementById('password-error').classList.add('hidden'); - document.getElementById('password-input').classList.remove('error-input'); +const login_inputs = { + 'username': document.querySelector('#login-form > input[type="text"]'), + 'password': document.querySelector('#login-form > input[type="password"]') +}; - if (data === null) { +const login_errors = { + 'username': document.getElementById('username-error'), + 'password': document.getElementById('password-error') +}; + +const create_inputs = { + 'username': document.querySelector('#create-form > input[type="text"]'), + 'password': document.querySelector('#create-form > input[type="password"]') +} + +const create_errors = { + 'username_invalid': document.getElementById('new-username-error'), + 'username_taken': document.getElementById('taken-username-error'), +}; + +function toggleWindow() { + document.querySelector('main').classList.toggle('show-create'); +}; + +function login(data=null) { + login_inputs.username.classList.remove('error-input'); + login_errors.username.classList.add('hidden'); + login_inputs.password.classList.remove('error-input'); + login_errors.password.classList.add('hidden'); + + if (data === null) data = { - 'username': document.getElementById('username-input').value, - 'password': document.getElementById('password-input').value + 'username': login_inputs.username.value, + 'password': login_inputs.password.value }; - }; + fetch(`${url_prefix}/api/auth/login`, { 'method': 'POST', 'headers': {'Content-Type': 'application/json'}, 'body': JSON.stringify(data) }) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - + if (!response.ok) return Promise.reject(response.status); return response.json(); }) .then(json => { @@ -29,11 +49,11 @@ function login(data=null) { }) .catch(e => { if (e === 401) { - document.getElementById('password-error').classList.remove('hidden'); - document.getElementById('password-input').classList.add('error-input'); + login_inputs.password.classList.add('error-input'); + login_errors.password.classList.remove('hidden'); } else if (e === 404) { - document.getElementById('username-error').classList.remove('hidden'); - document.getElementById('username-input').classList.add('error-input'); + login_inputs.username.classList.add('error-input'); + login_errors.username.classList.remove('hidden'); } else { console.log(e); }; @@ -41,42 +61,35 @@ function login(data=null) { }; function create() { - document.getElementById('new-username-error').classList.add('hidden'); - document.getElementById('new-username-input').classList.remove('error-input'); - document.getElementById('taken-username-error').classList.add('hidden'); + create_inputs.username.classList.remove('error-input'); + create_errors.username_invalid.classList.add('hidden'); + create_errors.username_taken.classList.add('hidden'); const data = { - 'username': document.getElementById('new-username-input').value, - 'password': document.getElementById('new-password-input').value + 'username': create_inputs.username.value, + 'password': 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(response => response.json()) .then(json => { - // catch errors - if (json.error !== null) { - return Promise.reject(json.error); - }; + if (json.error !== null) return Promise.reject(json.error); login(data); }) .catch(e => { if (e === 'UsernameInvalid') { - document.getElementById('new-username-error').classList.remove('hidden'); - document.getElementById('new-username-input').classList.add('error-input'); + create_inputs.username.classList.add('error-input'); + create_errors.username_invalid.classList.remove('hidden'); } else if (e === 'UsernameTaken') { - document.getElementById('taken-username-error').classList.remove('hidden'); - document.getElementById('new-username-input').classList.add('error-input'); + create_inputs.username.classList.add('error-input'); + create_errors.username_taken.classList.remove('hidden'); } else { console.log(e); }; }); -} - -function toggleWindow() { - document.querySelector('main').classList.toggle('show-create'); }; // code run on load diff --git a/frontend/static/js/notification.js b/frontend/static/js/notification.js index c3c8b8c..ccefbb6 100644 --- a/frontend/static/js/notification.js +++ b/frontend/static/js/notification.js @@ -1,31 +1,28 @@ +const inputs_buttons = { + 'save_button': document.querySelector('#add-row button[data-type="save"]'), + 'title': document.querySelector('#add-row td.title-column input'), + 'url': document.querySelector('#add-row td.url-column input') +}; + function fillNotificationSelection() { fetch(`${url_prefix}/api/notificationservices?api_key=${api_key}`) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - + if (!response.ok) return Promise.reject(response.status); return response.json(); }) .then(json => { if (json.result.length) { - document.getElementById('add-entry').classList.remove('error', 'error-icon'); - [ - document.getElementById('notification-service-input'), - document.getElementById('notification-service-edit-input'), - document.getElementById('notification-service-template-input'), - document.getElementById('notification-service-template-edit-input') - ].forEach(options => { - options.innerHTML = ''; - json.result.forEach(service => { - const entry = document.createElement('option'); - entry.value = service.id; - entry.innerText = service.title; - options.appendChild(entry); - }); - options.querySelector(':nth-child(1)').setAttribute('selected', ''); + document.getElementById('add-reminder').classList.remove('error', 'error-icon'); + + const options = document.getElementById('notification-service-input'); + options.innerHTML = ''; + json.result.forEach(service => { + const entry = document.createElement('option'); + entry.value = service.id; + entry.innerText = service.title; + options.appendChild(entry); }); + options.querySelector(':nth-child(1)').setAttribute('selected', ''); const table = document.getElementById('services-list'); table.querySelectorAll('tr:not(#add-row)').forEach(e => e.remove()); @@ -60,7 +57,7 @@ function fillNotificationSelection() { edit_button.addEventListener('click', e => editService(service.id)); edit_button.title = 'Edit'; edit_button.setAttribute('aria-label', 'Edit'); - edit_button.innerHTML = edit_icon; + edit_button.innerHTML = icons.edit; actions.appendChild(edit_button); const save_button = document.createElement('button'); @@ -68,7 +65,7 @@ function fillNotificationSelection() { save_button.addEventListener('click', e => saveService(service.id)); save_button.title = 'Save Edits'; save_button.setAttribute('aria-label', 'Save Edits'); - save_button.innerHTML = save_icon; + save_button.innerHTML = icons.save; actions.appendChild(save_button); const delete_button = document.createElement('button'); @@ -76,51 +73,20 @@ function fillNotificationSelection() { delete_button.addEventListener('click', e => deleteService(service.id)); delete_button.title = 'Delete'; delete_button.setAttribute('aria-label', 'Delete'); - delete_button.innerHTML = delete_icon; + delete_button.innerHTML = icons.delete; actions.appendChild(delete_button); table.appendChild(entry); }); } else { - document.getElementById('add-entry').classList.add('error', 'error-icon'); + document.getElementById('add-reminder').classList.add('error', 'error-icon'); }; }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else { + else console.log(e); - }; - }); -}; - -function deleteService(id) { - const row = document.querySelector(`tr[data-id="${id}"]`); - fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, { - 'method': 'DELETE' - }) - .then(response => response.json()) - .then(json => { - // catch errors - if (json.error !== null) { - return Promise.reject(json); - }; - - row.remove(); - if (document.querySelectorAll('#services-list > tr:not(#add-row)').length === 0) { - document.getElementById('add-entry').classList.add('error', 'error-icon'); - }; - }) - .catch(e => { - if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') { - window.location.href = `${url_prefix}/`; - } else if (e.error === 'NotificationServiceInUse') { - const delete_button = row.querySelector('button[title="Delete"]'); - delete_button.classList.add('error-icon'); - delete_button.title = `The notification service is still in use by a ${e.result.type}`; - } else { - console.log(e); - }; }); }; @@ -142,23 +108,44 @@ function saveService(id) { 'body': JSON.stringify(data) }) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; + if (!response.ok) return Promise.reject(response.status); fillNotificationSelection(); }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else if (e === 400) { + else if (e === 400) { save_button.classList.add('error-icon'); save_button.title = 'Invalid Apprise URL'; save_button.setAttribute('aria-label', 'Invalid Apprise URL'); - } else { + } else + console.log(e); + }); +}; + +function deleteService(id) { + const row = document.querySelector(`tr[data-id="${id}"]`); + fetch(`${url_prefix}/api/notificationservices/${id}?api_key=${api_key}`, { + 'method': 'DELETE' + }) + .then(response => response.json()) + .then(json => { + if (json.error !== null) return Promise.reject(json); + + row.remove(); + if (document.querySelectorAll('#services-list > tr:not(#add-row)').length === 0) + document.getElementById('add-entry').classList.add('error', 'error-icon'); + }) + .catch(e => { + if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') + window.location.href = `${url_prefix}/`; + else if (e.error === 'NotificationServiceInUse') { + const delete_button = row.querySelector('button[title="Delete"]'); + delete_button.classList.add('error-icon'); + delete_button.title = `The notification service is still in use by a ${e.result.type}`; + } else console.log(e); - }; }); }; @@ -167,11 +154,6 @@ function toggleAddService() { }; function addService() { - const inputs_buttons = { - 'save_button': document.querySelector('#add-row button[data-type="save"]'), - 'title': document.querySelector('#add-row td.title-column input'), - 'url': document.querySelector('#add-row td.url-column input') - }; const data = { 'title': inputs_buttons.title.value, 'url': inputs_buttons.url.value @@ -182,10 +164,7 @@ function addService() { 'body': JSON.stringify(data) }) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; + if (!response.ok) return Promise.reject(response.status); inputs_buttons.title.value = ''; inputs_buttons.url.value = ''; @@ -198,15 +177,14 @@ function addService() { fillNotificationSelection(); }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else if (e === 400) { + else if (e === 400) { inputs_buttons.save_button.classList.add('error-icon'); inputs_buttons.save_button.title = 'Invalid Apprise URL'; inputs_buttons.save_button.setAttribute('aria-label', 'Invalid Apprise URL'); - } else { + } else console.log(e); - }; }); }; diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js deleted file mode 100644 index 892f7a8..0000000 --- a/frontend/static/js/reminders.js +++ /dev/null @@ -1,100 +0,0 @@ -function fillTable(result) { - const table = document.getElementById('reminder-list'); - table.querySelectorAll('button.entry:not(#add-entry)').forEach(e => e.remove()); - - result.forEach(reminder => { - const entry = document.createElement('button'); - entry.classList.add('entry'); - entry.dataset.id = reminder.id; - entry.addEventListener('click', e => showEdit(reminder.id)); - if (reminder.color !== null) { - entry.style.setProperty('--color', reminder.color); - }; - - const title = document.createElement('h2'); - title.innerText = reminder.title; - entry.appendChild(title); - - const time = document.createElement('p'); - var offset = new Date(reminder.time * 1000).getTimezoneOffset() * -60; - var d = new Date((reminder.time + offset) * 1000); - var formatted_date = d.toLocaleString('en-CA').slice(0,10) + ' ' + d.toTimeString().slice(0,5); - if (reminder.repeat_interval !== null) { - if (reminder.repeat_interval === 1) { - var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity.slice(0, -1) : reminder.repeat_quantity; - var interval_text = ` (each ${quantity})`; - } else { - var quantity = reminder.repeat_quantity.endsWith('s') ? reminder.repeat_quantity : reminder.repeat_quantity + 's'; - var interval_text = ` (every ${reminder.repeat_interval} ${quantity})`; - }; - formatted_date += interval_text; - }; - time.innerText = formatted_date; - entry.appendChild(time); - - table.appendChild(entry); - - if (title.clientHeight < title.scrollHeight) { - entry.classList.add('expand'); - }; - }); - table.querySelectorAll('button.entry:not(#add-entry)').forEach(reminder => reminder.classList.add('fit')); -}; - -function fillList() { - fetch(`${url_prefix}/api/reminders?api_key=${api_key}`) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - return response.json(); - }) - .then(json => { - fillTable(json.result); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function search() { - const query = document.getElementById('search-input').value; - fetch(`${url_prefix}/api/reminders/search?api_key=${api_key}&query=${query}`) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - return response.json(); - }) - .then(json => { - fillTable(json.result); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function clearSearch() { - document.getElementById('search-input').value = ''; - fillList(); -} - -// code run on load - -fillList(); -setInterval(fillList, 60000); - -document.getElementById('search-form').setAttribute('action', 'javascript:search();'); -document.getElementById('clear-button').addEventListener('click', e => clearSearch()); -document.getElementById('add-entry').addEventListener('click', e => showAdd()); -document.getElementById('add-template').addEventListener('click', e => showWindow('add-template')); diff --git a/frontend/static/js/settings.js b/frontend/static/js/settings.js index 80698e9..ef21adc 100644 --- a/frontend/static/js/settings.js +++ b/frontend/static/js/settings.js @@ -8,18 +8,14 @@ function changePassword() { 'body': JSON.stringify(data) }) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; + if (!response.ok) return Promise.reject(response.status); window.location.reload(); }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else { + else console.log(e); - }; }); }; diff --git a/frontend/static/js/show.js b/frontend/static/js/show.js new file mode 100644 index 0000000..d8a5952 --- /dev/null +++ b/frontend/static/js/show.js @@ -0,0 +1,102 @@ +function showAdd(type) { + inputs.template.value = '0'; + inputs.title.value = ''; + inputs.text.value = ''; + inputs.time.value = ''; + inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; + toggleNormal(); + toggleColor(true); + + const cl = document.getElementById('info').classList; + cl.forEach(c => { + if (info_classes.includes(c)) cl.remove(c) + }); + document.querySelector('.options > button[type="submit"]').innerText = 'Add'; + if (type === types.reminder) { + cl.add('show-add-reminder'); + document.querySelector('#info h2').innerText = 'Add a reminder'; + inputs.time.setAttribute('required', ''); + } else if (type === types.template) { + cl.add('show-add-template'); + document.querySelector('#info h2').innerText = 'Add a template'; + inputs.time.removeAttribute('required'); + } else + return; + showWindow('info'); +}; + +function showEdit(id, type) { + let url; + if (type === types.reminder) { + url = `${url_prefix}/api/reminders/${id}?api_key=${api_key}`; + inputs.time.setAttribute('required', ''); + } else if (type === types.template) { + url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`; + inputs.time.removeAttribute('required'); + type_buttons.repeat_interval.removeAttribute('required'); + } else return; + + fetch(url) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + return response.json(); + }) + .then(json => { + document.getElementById('info').dataset.id = id; + if (json.result.color !== null) { + if (inputs.color.classList.contains('hidden')) { + toggleColor(); + }; + selectColor(json.result['color']); + }; + + inputs.title.value = json.result.title; + + if (type === types.reminder) { + var trigger_date = new Date( + (json.result.time + new Date(json.result.time * 1000).getTimezoneOffset() * -60) * 1000 + ); + inputs.time.value = trigger_date.toLocaleString('en-CA').slice(0,10) + 'T' + trigger_date.toTimeString().slice(0,5); + }; + inputs.notification_service.value = json.result.notification_service; + + if (type == types.reminder) { + if (json.result.repeat_interval === null) { + toggleNormal(); + } else { + toggleRepeated(); + type_buttons.repeat_interval.value = json.result.repeat_interval; + type_buttons.repeat_quantity.value = json.result.repeat_quantity; + }; + }; + + inputs.text.value = json.result.text; + + showWindow('info'); + }) + .catch(e => { + if (e === 401) + window.location.href = `${url_prefix}/`; + else + console.log(e); + }); + + const cl = document.getElementById('info').classList; + cl.forEach(c => { + if (info_classes.includes(c)) cl.remove(c) + }); + document.querySelector('.options > button[type="submit"]').innerText = 'Save'; + if (type === types.reminder) { + cl.add('show-edit-reminder'); + document.querySelector('#info h2').innerText = 'Edit a reminder'; + } else if (type === types.template) { + cl.add('show-edit-template'); + document.querySelector('#info h2').innerText = 'Edit a template'; + } else + return; +}; + +// code run on load + +document.getElementById('add-reminder').addEventListener('click', e => showAdd(types.reminder)); +document.getElementById('add-template').addEventListener('click', e => showAdd(types.template)); diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js index 306a593..e75e90a 100644 --- a/frontend/static/js/templates.js +++ b/frontend/static/js/templates.js @@ -1,29 +1,7 @@ -const template_inputs = { - 'title': document.getElementById('title-template-input'), - 'notification-service': document.getElementById('notification-service-template-input'), - 'text': document.getElementById('text-template-input'), - 'color': document.querySelector('#add-template .color-list') -}; - -const edit_template_inputs = { - 'title': document.getElementById('title-template-edit-input'), - 'notification-service': document.getElementById('notification-service-template-edit-input'), - 'text': document.getElementById('text-template-edit-input'), - 'color': document.querySelector('#edit-template .color-list') -}; - -function loadTemplates(force=true) { - const table = document.getElementById('template-list'); - if (!force && !!table.querySelector('button:not(#add-template)')) { - return - }; - +function loadTemplateSelection() { fetch(`${url_prefix}/api/templates?api_key=${api_key}`) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; + if (!response.ok) return Promise.reject(response.status); return response.json(); }) .then(json => { @@ -36,216 +14,180 @@ function loadTemplates(force=true) { select_list.appendChild(entry); }); - - table.querySelectorAll('button:not(#add-template)').forEach(e => e.remove()); - json.result.forEach(template => { - const entry = document.createElement('button'); - entry.classList.add('entry'); - entry.addEventListener('click', e => showEditTemplate(template.id)); - - const title = document.createElement('h2'); - title.innerText = template.title; - entry.appendChild(title); - - table.appendChild(entry); - - if (title.clientHeight < title.scrollHeight) { - entry.classList.add('expand'); - }; - }); - table.querySelectorAll('button:not(#add-template)').forEach(template => template.classList.add('fit')); }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else { + else console.log(e); - }; }); }; -function loadTemplate() { - const id = document.getElementById('template-selection').value; - if (id === "0") { +function applyTemplate() { + if (inputs.template.value === '0') { inputs.title.value = ''; inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; inputs.text.value = ''; - if (!inputs.color.classList.contains('hidden')) { - toggleColor(inputs.color); - }; + toggleColor(true); } else { - fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`) + fetch(`${url_prefix}/api/templates/${inputs.template.value}?api_key=${api_key}`) .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - return response.json(); + if (!response.ok) return Promise.reject(response.status); + else return response.json(); }) .then(json => { inputs.title.value = json.result.title; inputs.notification_service.value = json.result.notification_service; inputs.text.value = json.result.text; if (json.result.color !== null) { - if (inputs.color.classList.contains('hidden')) { - toggleColor(inputs.color); - }; - selectColor(inputs.color, json.result.color); - } else { - if (!inputs.color.classList.contains('hidden')) { - toggleColor(inputs.color); - }; - }; + if (inputs.color.classList.contains('hidden')) + toggleColor(); + selectColor(json.result.color); + } else + toggleColor(true); }) .catch(e => { - if (e === 401) { + if (e === 401) window.location.href = `${url_prefix}/`; - } else { + else console.log(e); - }; }); }; }; -function addTemplate() { - const data = { - 'title': template_inputs.title.value, - 'notification_service': template_inputs["notification-service"].value, - 'text': template_inputs.text.value, - 'color': null - }; - if (!template_inputs.color.classList.contains('hidden')) { - data['color'] = template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; - }; - fetch(`${url_prefix}/api/templates?api_key=${api_key}`, { - 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - loadTemplates(); - closeAddTemplate(); - return - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function closeAddTemplate() { - hideWindow(); - setTimeout(() => { - template_inputs.title.value = ''; - template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value; - template_inputs.text.value = ''; - if (!template_inputs.color.classList.contains('hidden')) { - toggleColor(inputs.color); - }; - }, 500); -}; - -function showEditTemplate(id) { - fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - return response.json(); - }) - .then(json => { - document.getElementById('template-edit-form').dataset.id = id; - edit_template_inputs.title.value = json.result.title; - edit_template_inputs['notification-service'].value = json.result.notification_service; - edit_template_inputs.text.value = json.result.text; - if (json.result.color !== null) { - if (edit_template_inputs.color.classList.contains('hidden')) { - toggleColor(edit_template_inputs.color); - }; - selectColor(edit_template_inputs.color, json.result.color); - }; - showWindow('edit-template'); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function saveTemplate() { - const id = document.getElementById('template-edit-form').dataset.id; - const data = { - 'title': edit_template_inputs.title.value, - 'notification_service': edit_template_inputs['notification-service'].value, - 'text': edit_template_inputs.text.value, - 'color': null - }; - if (!edit_template_inputs.color.classList.contains('hidden')) { - data['color'] = edit_template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; - }; - fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - loadTemplates(); - hideWindow(); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - -function deleteTemplate() { - const id = document.getElementById('template-edit-form').dataset.id; - fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { - 'method': 'DELETE' - }) - .then(response => { - // catch errors - if (!response.ok) { - return Promise.reject(response.status); - }; - - loadTemplates(); - hideWindow(); - }) - .catch(e => { - if (e === 401) { - window.location.href = `${url_prefix}/`; - } else { - console.log(e); - }; - }); -}; - // code run on load -document.getElementById('template-form').setAttribute('action', 'javascript:addTemplate();'); -document.getElementById('color-template-toggle').addEventListener('click', e => toggleColor(template_inputs.color)); -document.getElementById('close-template').addEventListener('click', e => closeAddTemplate()); -document.getElementById('template-edit-form').setAttribute('action', 'javascript:saveTemplate()'); -document.getElementById('color-template-edit-toggle').addEventListener('click', e => toggleColor(edit_template_inputs.color)); -document.getElementById('close-edit-template').addEventListener('click', e => hideWindow()); -document.getElementById('delete-template').addEventListener('click', e => deleteTemplate()); +loadTemplateSelection(); + +// function addTemplate() { +// const data = { +// 'title': template_inputs.title.value, +// 'notification_service': template_inputs["notification-service"].value, +// 'text': template_inputs.text.value, +// 'color': null +// }; +// if (!template_inputs.color.classList.contains('hidden')) { +// data['color'] = template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; +// }; +// fetch(`${url_prefix}/api/templates?api_key=${api_key}`, { +// 'method': 'POST', +// 'headers': {'Content-Type': 'application/json'}, +// 'body': JSON.stringify(data) +// }) +// .then(response => { +// // catch errors +// if (!response.ok) { +// return Promise.reject(response.status); +// }; + +// loadTemplateSelection(); +// closeAddTemplate(); +// return +// }) +// .catch(e => { +// if (e === 401) { +// window.location.href = `${url_prefix}/`; +// } else { +// console.log(e); +// }; +// }); +// }; + +// function closeAddTemplate() { +// hideWindow(); +// setTimeout(() => { +// template_inputs.title.value = ''; +// template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value; +// template_inputs.text.value = ''; +// if (!template_inputs.color.classList.contains('hidden')) { +// toggleColor(inputs.color); +// }; +// }, 500); +// }; + +// function showEditTemplate(id) { +// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`) +// .then(response => { +// // catch errors +// if (!response.ok) { +// return Promise.reject(response.status); +// }; +// return response.json(); +// }) +// .then(json => { +// document.getElementById('template-edit-form').dataset.id = id; +// edit_template_inputs.title.value = json.result.title; +// edit_template_inputs['notification-service'].value = json.result.notification_service; +// edit_template_inputs.text.value = json.result.text; +// if (json.result.color !== null) { +// if (edit_template_inputs.color.classList.contains('hidden')) { +// toggleColor(edit_template_inputs.color); +// }; +// selectColor(edit_template_inputs.color, json.result.color); +// }; +// showWindow('edit-template'); +// }) +// .catch(e => { +// if (e === 401) { +// window.location.href = `${url_prefix}/`; +// } else { +// console.log(e); +// }; +// }); +// }; + +// function saveTemplate() { +// const id = document.getElementById('template-edit-form').dataset.id; +// const data = { +// 'title': edit_template_inputs.title.value, +// 'notification_service': edit_template_inputs['notification-service'].value, +// 'text': edit_template_inputs.text.value, +// 'color': null +// }; +// if (!edit_template_inputs.color.classList.contains('hidden')) { +// data['color'] = edit_template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; +// }; +// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { +// 'method': 'PUT', +// 'headers': {'Content-Type': 'application/json'}, +// 'body': JSON.stringify(data) +// }) +// .then(response => { +// // catch errors +// if (!response.ok) { +// return Promise.reject(response.status); +// }; +// loadTemplateSelection(); +// hideWindow(); +// }) +// .catch(e => { +// if (e === 401) { +// window.location.href = `${url_prefix}/`; +// } else { +// console.log(e); +// }; +// }); +// }; + +// function deleteTemplate() { +// const id = document.getElementById('template-edit-form').dataset.id; +// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { +// 'method': 'DELETE' +// }) +// .then(response => { +// // catch errors +// if (!response.ok) { +// return Promise.reject(response.status); +// }; + +// loadTemplateSelection(); +// hideWindow(); +// }) +// .catch(e => { +// if (e === 401) { +// window.location.href = `${url_prefix}/`; +// } else { +// console.log(e); +// }; +// }); +// }; diff --git a/frontend/static/js/window.js b/frontend/static/js/window.js new file mode 100644 index 0000000..74c1f56 --- /dev/null +++ b/frontend/static/js/window.js @@ -0,0 +1,210 @@ +const colors = ["#3c3c3c", "#49191e", "#171a42", "#083b06", "#3b3506", "#300e40"]; + +const inputs = { + 'template': document.getElementById('template-selection'), + 'title': document.getElementById('title-input'), + 'time': document.getElementById('time-input'), + 'notification_service': document.getElementById('notification-service-input'), + 'text': document.getElementById('text-input'), + 'color': document.querySelector('.color-list') +}; + +const type_buttons = { + 'normal_button': document.getElementById('normal-button'), + 'repeat_button': document.getElementById('repeat-button'), + + 'repeat_bar': document.querySelector('.repeat-bar'), + 'repeat_interval': document.getElementById('repeat-interval'), + 'repeat_quantity': document.getElementById('repeat-quantity') +}; + +function loadColor() { + colors.forEach(color => { + const entry = document.createElement('button'); + entry.dataset.color = color; + entry.title = color; + entry.type = 'button'; + entry.style.setProperty('--color', color); + entry.addEventListener('click', e => selectColor(color)) + inputs.color.appendChild(entry); + }); +}; + +function selectColor(color_code) { + inputs.color.querySelector(`button[data-color="${color_code}"]`).dataset.selected = 'true'; + inputs.color.querySelectorAll(`button:not([data-color="${color_code}"])`).forEach(b => b.dataset.selected = 'false'); + return; +}; + +function toggleColor(hide=false) { + selectColor(colors[0]) + if (!hide) + inputs.color.classList.toggle('hidden'); + else + inputs.color.classList.add('hidden'); +}; + +function toggleNormal() { + type_buttons.normal_button.dataset.selected = 'true'; + type_buttons.repeat_button.dataset.selected = 'false'; + + type_buttons.repeat_bar.classList.add('hidden'); + type_buttons.repeat_interval.removeAttribute('required'); + type_buttons.repeat_interval.value = ''; +}; + +function toggleRepeated() { + type_buttons.normal_button.dataset.selected = 'false'; + type_buttons.repeat_button.dataset.selected = 'true'; + + type_buttons.repeat_bar.classList.remove('hidden'); + type_buttons.repeat_interval.setAttribute('required', ''); +}; + +function testReminder() { + const input = document.getElementById('test-reminder'); + if (inputs.title.value === '') { + input.classList.add('error-input'); + input.title = 'No title set'; + return + } else { + input.classList.remove('error-input'); + input.removeAttribute('title'); + }; + const data = { + 'title': inputs.title.value, + 'notification_service': inputs.notification_service.value, + 'text': inputs.text.value + }; + fetch(`${url_prefix}/api/reminders/test?api_key=${api_key}`, { + 'method': 'POST', + 'headers': {'Content-Type': 'application/json'}, + 'body': JSON.stringify(data) + }) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + input.classList.add('show-sent'); + }) + .catch(e => { + if (e === 401) + window.location.href = `${url_prefix}/`; + else + console.log(e); + }); +}; + +function deleteInfo() { + let url; + const e_id = document.getElementById('info').dataset.id; + 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}`; + } else if (cl.contains('show-edit-template')) { + // Delete template + url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; + } else return; + + fetch(url, {'method': 'DELETE'}) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + + fillNotificationSelection(); + fillReminders(); + fillTemplates(); + hideWindow(); + }) + .catch(e => { + if (e === 401) + window.location.href = `${url_prefix}/`; + else + console.log(e); + }); +}; + +function submitInfo() { + inputs.time.classList.remove('error-input'); + inputs.time.removeAttribute('title'); + let fetch_data = { + url: null, + method: null + }; + const data = { + 'title': inputs.title.value, + 'notification_service': inputs.notification_service.value, + 'text': inputs.text.value, + 'color': null + }; + if (!inputs.color.classList.contains('hidden')) { + data['color'] = inputs.color.querySelector('button[data-selected="true"]').dataset.color; + }; + + const e_id = document.getElementById('info').dataset.id; + const cl = document.getElementById('info').classList; + if (cl.contains('show-add-reminder')) { + // Add reminder + data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60) + if (type_buttons.repeat_button.dataset.selected === 'true') { + data['repeat_quantity'] = type_buttons.repeat_quantity.value; + data['repeat_interval'] = type_buttons.repeat_interval.value + }; + fetch_data.url = `${url_prefix}/api/reminders?api_key=${api_key}`; + fetch_data.method = 'POST'; + + } else if (cl.contains('show-add-template')) { + // Add template + fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`; + fetch_data.method = 'POST'; + + } else if (cl.contains('show-edit-reminder')) { + // Edit reminder + data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60) + if (type_buttons.repeat_button.dataset.selected === 'true') { + data['repeat_quantity'] = type_buttons.repeat_quantity.value; + data['repeat_interval'] = type_buttons.repeat_interval.value + }; + fetch_data.url = `${url_prefix}/api/reminders/${e_id}?api_key=${api_key}`; + fetch_data.method = 'PUT'; + + } else if (cl.contains('show-edit-template')) { + // Edit template + fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; + fetch_data.method = 'PUT'; + + } else return; + + fetch(fetch_data.url, { + 'method': fetch_data.method, + 'headers': {'Content-Type': 'application/json'}, + 'body': JSON.stringify(data) + }) + .then(response => { + if (!response.ok) return Promise.reject(response.status); + + fillReminders(); + fillTemplates(); + hideWindow(); + }) + .catch(e => { + if (e === 401) { + window.location.href = `${url_prefix}/`; + } else if (e === 400) { + inputs.time.classList.add('error-input'); + inputs.time.title = 'Time is in the past'; + } else + console.log(e); + }); +}; + +// code run on load + +loadColor(); + +document.getElementById('template-selection').addEventListener('change', e => applyTemplate()); +document.getElementById('color-toggle').addEventListener('click', e => toggleColor()); +document.getElementById('normal-button').addEventListener('click', e => toggleNormal()); +document.getElementById('repeat-button').addEventListener('click', e => toggleRepeated()); +document.getElementById('close-info').addEventListener('click', e => hideWindow()); +document.getElementById('delete-info').addEventListener('click', e => deleteInfo()); +document.getElementById('test-reminder').addEventListener('click', e => testReminder()); +document.getElementById('info-form').setAttribute('action', 'javascript:submitInfo();'); diff --git a/frontend/templates/login.html b/frontend/templates/login.html index ca6385a..02e93f0 100644 --- a/frontend/templates/login.html +++ b/frontend/templates/login.html @@ -21,22 +21,28 @@

Login

- + + - + + - + +

Create

- + + - - + + + +
diff --git a/frontend/templates/page_not_found.html b/frontend/templates/page_not_found.html index 202755a..1d7b929 100644 --- a/frontend/templates/page_not_found.html +++ b/frontend/templates/page_not_found.html @@ -17,7 +17,7 @@

MIND

404 - Page not found :(

- Go to home page + Go to home page
\ No newline at end of file diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 1f6badd..6609615 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -7,17 +7,17 @@ - - + + - - - + + + + - Reminders - MIND @@ -64,7 +64,7 @@
- - + +
@@ -108,7 +108,7 @@
-
-
-

Add a reminder

+ +
+

-
+
- + +
-
-

Edit a reminder

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

Repeat every

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

Notification Services

Setup your notification providers here. See the Apprise URL documentation to learn how to make a valid Apprise URL.

@@ -293,6 +247,8 @@
+ +

Settings

@@ -305,39 +261,6 @@
-
-

Add a template

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

Edit a template

-
-
- - - - - -
- - - -
-
-
-
diff --git a/frontend/ui.py b/frontend/ui.py index 113ae20..50aa990 100644 --- a/frontend/ui.py +++ b/frontend/ui.py @@ -7,10 +7,6 @@ ui = Blueprint('ui', __name__) methods = ['GET'] -@ui.errorhandler(404) -def not_found(e): - return render_template('page_not_found.html', url_prefix=logging.URL_PREFIX) - @ui.route('/', methods=methods) def ui_login(): return render_template('login.html', url_prefix=logging.URL_PREFIX) From d657c5c0d30647155f62f895360bb41207cf70b3 Mon Sep 17 00:00:00 2001 From: CasVT Date: Wed, 24 May 2023 15:11:51 +0200 Subject: [PATCH 4/6] Fixes issue #9 --- backend/db.py | 12 ++ backend/notification_service.py | 10 +- backend/static_reminders.py | 209 ++++++++++++++++++++++++++++++ backend/users.py | 12 ++ frontend/api.py | 130 ++++++++++++++++++- frontend/static/css/info.css | 53 +++++++- frontend/static/css/library.css | 6 +- frontend/static/js/general.js | 6 +- frontend/static/js/library.js | 7 +- frontend/static/js/show.js | 27 +++- frontend/static/js/templates.js | 133 ------------------- frontend/static/js/window.js | 60 ++++++--- frontend/templates/reminders.html | 14 ++ 13 files changed, 509 insertions(+), 170 deletions(-) create mode 100644 backend/static_reminders.py diff --git a/backend/db.py b/backend/db.py index 281ce5b..e48f3b9 100644 --- a/backend/db.py +++ b/backend/db.py @@ -151,6 +151,18 @@ def setup_db() -> None: FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (notification_service) REFERENCES notification_services(id) ); + CREATE TABLE IF NOT EXISTS static_reminders( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + text TEXT, + notification_service INTEGER NOT NULL, + + color VARCHAR(7), + + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (notification_service) REFERENCES notification_services(id) + ); CREATE TABLE IF NOT EXISTS config( key VARCHAR(255) PRIMARY KEY, value TEXT NOT NULL diff --git a/backend/notification_service.py b/backend/notification_service.py index 4d6a1af..130d3f6 100644 --- a/backend/notification_service.py +++ b/backend/notification_service.py @@ -96,7 +96,15 @@ class NotificationService: ) if cursor.fetchone(): raise NotificationServiceInUse('template') - + + # Check if no static reminders exist with this service + cursor.execute( + "SELECT 1 FROM static_reminders WHERE notification_service = ? LIMIT 1;", + (self.id,) + ) + if cursor.fetchone(): + raise NotificationServiceInUse('static reminder') + cursor.execute( "DELETE FROM notification_services WHERE id = ?", (self.id,) diff --git a/backend/static_reminders.py b/backend/static_reminders.py new file mode 100644 index 0000000..c390578 --- /dev/null +++ b/backend/static_reminders.py @@ -0,0 +1,209 @@ +#-*- coding: utf-8 -*- + +from sqlite3 import IntegrityError +from typing import List + +from apprise import Apprise + +from backend.custom_exceptions import NotificationServiceNotFound, ReminderNotFound +from backend.db import get_db + + +class StaticReminder: + """Represents a static reminder + """ + def __init__(self, reminder_id: int) -> None: + self.id = reminder_id + + # Check if reminder exists + if not get_db().execute( + "SELECT 1 FROM static_reminders WHERE id = ? LIMIT 1;", + (self.id,) + ).fetchone(): + raise ReminderNotFound + + def get(self) -> dict: + """Get info about the static reminder + + Returns: + dict: The info about the reminder + """ + reminder = get_db(dict).execute(""" + SELECT + r.id, + r.title, r.text, + r.notification_service, + ns.title AS notification_service_title, + r.color + FROM static_reminders r + INNER JOIN notification_services ns + ON r.notification_service = ns.id + WHERE r.id = ? + LIMIT 1; + """, + (self.id,) + ).fetchone() + + return dict(reminder) + + def update( + self, + title: str = None, + notification_service: int = None, + text: str = None, + color: str = None + ) -> dict: + """Edit the static reminder + + Args: + title (str, optional): The new title of the entry. Defaults to None. + notification_service (int, optional): The new id of the notification service to use to send the reminder. Defaults to None. + text (str, optional): The new body of the reminder. Defaults to None. + color (str, optional): The new hex code of the color of the reminder, which is shown in the web-ui. Defaults to None. + + Raises: + NotificationServiceNotFound: The notification service with the given id was not found + + Returns: + dict: The new static reminder info + """ + # Get current data and update it with new values + data = self.get() + new_values = { + 'title': title, + 'notification_service': notification_service, + 'text': text, + 'color': color + } + for k, v in new_values.items(): + if k == 'color' or v is not None: + data[k] = v + + # Update database + try: + get_db().execute(""" + UPDATE static_reminders + SET + title = ?, text = ?, + notification_service = ?, + color = ? + WHERE id = ?; + """, + (data['title'], data['text'], + data['notification_service'], + data['color'], + self.id) + ) + except IntegrityError: + raise NotificationServiceNotFound + + return self.get() + + def delete(self) -> None: + """Delete the static reminder + """ + get_db().execute("DELETE FROM static_reminders WHERE id = ?", (self.id,)) + return + +class StaticReminders: + """Represents the static reminder library of the user account + """ + + def __init__(self, user_id: int) -> None: + self.user_id = user_id + + def fetchall(self) -> List[dict]: + """Get all static reminders + + Returns: + List[dict]: The id, title, text, notification_service, notification_service_title and color of each static reminder + """ + reminders: list = list(map( + dict, + get_db(dict).execute(""" + SELECT + r.id, + r.title, r.text, + r.notification_service, + ns.title AS notification_service_title, + r.color + FROM static_reminders r + INNER JOIN notification_services ns + ON r.notification_service = ns.id + WHERE r.user_id = ? + ORDER BY r.title, r.id; + """, + (self.user_id,) + ) + )) + + return reminders + + def fetchone(self, id: int) -> StaticReminder: + """Get one static reminder + + Args: + id (int): The id of the static reminder to fetch + + Returns: + StaticReminder: A StaticReminder instance + """ + return StaticReminder(id) + + def add( + self, + title: str, + notification_service: int, + text: str = '', + color: str = None + ) -> StaticReminder: + """Add a static reminder + + Args: + title (str): The title of the entry + notification_service (int): The id of the notification service to use to send the reminder. + text (str, optional): The body of the reminder. Defaults to ''. + color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None. + + Raises: + NotificationServiceNotFound: The notification service with the given id was not found + + Returns: + StaticReminder: A StaticReminder instance representing the newly created static reminder + """ + try: + id = get_db().execute(""" + INSERT INTO static_reminders(user_id, title, text, notification_service, color) + VALUES (?,?,?,?,?); + """, + (self.user_id, title, text, notification_service, color) + ).lastrowid + except IntegrityError: + raise NotificationServiceNotFound + + return self.fetchone(id) + + def trigger_reminder(self, id: int) -> None: + """Trigger a static reminder to send it's reminder + + Args: + id (int): The id of the static reminder to trigger + + Raises: + ReminderNotFound: The static reminder with the given id was not found + """ + reminder = get_db(dict).execute(""" + SELECT r.title, r.text, ns.url + FROM static_reminders r + INNER JOIN notification_services ns + ON r.notification_service = ns.id + WHERE r.id = ?; + """, (id,)).fetchone() + if not reminder: + raise ReminderNotFound + reminder = dict(reminder) + + a = Apprise() + a.add(reminder['url']) + a.notify(title=reminder['title'], body=reminder['text']) + return diff --git a/backend/users.py b/backend/users.py index e267861..e107fbd 100644 --- a/backend/users.py +++ b/backend/users.py @@ -6,6 +6,7 @@ from backend.db import get_db from backend.notification_service import NotificationServices from backend.reminders import Reminders from backend.security import generate_salt_hash, get_hash +from backend.static_reminders import StaticReminders from backend.templates import Templates ONEPASS_USERNAME_CHARACTERS = 'abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.!@$' @@ -64,6 +65,17 @@ class User: self.templates_instance = Templates(self.user_id) return self.templates_instance + @property + def static_reminders(self) -> StaticReminders: + """Get access to the static reminders of the user account + + Returns: + StaticReminders: StaticReminders instance that can be used to access the static reminders of the user account + """ + if not hasattr(self, 'static_reminders_instance'): + self.static_reminders_instance = StaticReminders(self.user_id) + return self.static_reminders_instance + def edit_password(self, new_password: str) -> None: """Change the password of the account diff --git a/frontend/api.py b/frontend/api.py index 8119e8f..b22b18b 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -1,9 +1,9 @@ #-*- coding: utf-8 -*- from os import urandom +from re import compile from time import time as epoch_time from typing import Any, Tuple -from re import compile from flask import Blueprint, g, request @@ -16,6 +16,7 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue, from backend.notification_service import (NotificationService, NotificationServices) from backend.reminders import Reminders, reminder_handler, test_reminder +from backend.static_reminders import StaticReminders from backend.templates import Template, Templates from backend.users import User, register_user @@ -243,7 +244,7 @@ def api_add_user(): user_id = register_user(username, password) return return_api({'user_id': user_id}, code=201) -@api.route('/user', methods=['PUT','DELETE']) +@api.route('/user', methods=['PUT', 'DELETE']) @error_handler @auth def api_manage_user(): @@ -287,7 +288,7 @@ def api_manage_user(): # Notification service endpoints #=================== -@api.route('/notificationservices', methods=['GET','POST']) +@api.route('/notificationservices', methods=['GET', 'POST']) @error_handler @auth def api_notification_services_list(): @@ -387,7 +388,7 @@ def api_notification_service(n_id: int): # Library endpoints #=================== -@api.route('/reminders', methods=['GET','POST']) +@api.route('/reminders', methods=['GET', 'POST']) @error_handler @auth def api_reminders_list(): @@ -498,7 +499,7 @@ def api_test_reminder(): test_reminder(title, notification_service, text) return return_api({}, code=201) -@api.route('/reminders/', methods=['GET','PUT','DELETE']) +@api.route('/reminders/', methods=['GET', 'PUT', 'DELETE']) @error_handler @auth def api_get_reminder(r_id: int): @@ -677,3 +678,122 @@ def api_get_template(t_id: int): elif request.method == 'DELETE': template.delete() return return_api({}) + +#=================== +# Static reminder endpoints +#=================== + +@api.route('/staticreminders', methods=['GET', 'POST']) +@error_handler +@auth +def api_static_reminders_list(): + """ + Endpoint: /staticreminders + Description: Manage the static reminders + Requires being logged in: Yes + Methods: + GET: + Description: Get a list of all static reminders + Returns: + 200: + The id, title, text, notification_service, notification_service_title and color of each static reminder + POST: + Description: Add a static reminder + Parameters (body): + title (required): the title of the static reminder + notification_service (required): the id of the notification service to use to send the notification + text: the body of the static reminder + color: The hex code of the color of the static reminder, which is shown in the web-ui + Returns: + 200: + The info about the new static reminder entry + 400: + KeyNotFound: One of the required parameters was not given + """ + reminders: StaticReminders = g.user_data.static_reminders + + if request.method == 'GET': + result = reminders.fetchall() + return return_api(result) + + elif request.method == 'POST': + data = request.get_json() + title = extract_key(data, 'title') + notification_service = extract_key(data, 'notification_service') + text = extract_key(data, 'text', check_existence=False) + color = extract_key(data, 'color', check_existence=False) + + result = reminders.add(title=title, + notification_service=notification_service, + text=text, + color=color) + return return_api(result.get(), code=201) + +@api.route('/staticreminders/', methods=['GET', 'POST', 'PUT', 'DELETE']) +@error_handler +@auth +def api_get_static_reminder(r_id: int): + """ + Endpoint: /staticreminders/ + Description: Manage a specific static reminder + Requires being logged in: Yes + URL Parameters: + : + The id of the static reminder + Methods: + GET: + Returns: + 200: + All info about the static reminder + 404: + No static reminder found with the given id + POST: + Description: Trigger the static reminder + Returns: + 200: + Static reminder triggered successfully + PUT: + Description: Edit the static reminder + Parameters (body): + title: The new title of the static reminder. + notification_service: The new id of the notification service to use to send the reminder. + text: The new body of the static reminder. + color: The new hex code of the color of the static reminder, which is shown in the web-ui. + Returns: + 200: + Static reminder updated successfully + 404: + No static reminder found with the given id + DELETE: + Description: Delete the static reminder + Returns: + 200: + Static reminder deleted successfully + 404: + No static reminder found with the given id + """ + reminders: StaticReminders = g.user_data.static_reminders + if request.method == 'GET': + result = reminders.fetchone(r_id).get() + return return_api(result) + + elif request.method == 'POST': + reminders.trigger_reminder(r_id) + return return_api({}) + + elif request.method == 'PUT': + data = request.get_json() + title = extract_key(data, 'title', check_existence=False) + notification_service = extract_key(data, 'notification_service', check_existence=False) + text = extract_key(data, 'text', check_existence=False) + color = extract_key(data, 'color', check_existence=False) + + result = reminders.fetchone(r_id).update(title=title, + notification_service=notification_service, + text=text, + color=color) + return return_api(result) + + elif request.method == 'DELETE': + reminders.fetchone(r_id).delete() + return return_api({}) diff --git a/frontend/static/css/info.css b/frontend/static/css/info.css index 6ca1146..456aa54 100644 --- a/frontend/static/css/info.css +++ b/frontend/static/css/info.css @@ -167,6 +167,31 @@ div.options > button { } } +/* */ +/* Adding */ +/* */ +#info.show-add-reminder #delete-info { + display: none; +} + +#info.show-add-static-reminder #time-input, +#info.show-add-static-reminder #normal-button, +#info.show-add-static-reminder #repeat-button, +#info.show-add-static-reminder .repeat-bar, +#info.show-add-static-reminder #delete-info { + display: none; +} + +#info.show-add-static-reminder #text-input { + margin-top: -1rem; +} + +#info.show-add-static-reminder #template-selection, +#info.show-add-static-reminder #color-toggle, +#info.show-add-static-reminder #notification-service-input { + width: 100%; +} + #info.show-add-template #template-selection, #info.show-add-template #time-input, #info.show-add-template #normal-button, @@ -181,15 +206,14 @@ div.options > button { margin-top: -1rem; } -#info.show-add-template form > *, -#info.show-add-template .sub-inputs > * { +#info.show-add-template #color-toggle, +#info.show-add-template #notification-service-input { width: 100%; } -#info.show-add-reminder #delete-info { - display: none; -} - +/* */ +/* Editing */ +/* */ #info.show-edit-reminder #template-selection, #info.show-edit-reminder #test-reminder { display: none; @@ -199,6 +223,23 @@ div.options > button { width: 100%; } +#info.show-edit-static-reminder #template-selection, +#info.show-edit-static-reminder #time-input, +#info.show-edit-static-reminder #normal-button, +#info.show-edit-static-reminder #repeat-button, +#info.show-edit-static-reminder .repeat-bar { + display: none; +} + +#info.show-edit-static-reminder #text-input { + margin-top: -1rem; +} + +#info.show-edit-static-reminder #color-toggle, +#info.show-edit-static-reminder #notification-service-input { + width: 100%; +} + #info.show-edit-template #template-selection, #info.show-edit-template #time-input, #info.show-edit-template #normal-button, diff --git a/frontend/static/css/library.css b/frontend/static/css/library.css index 5d525b8..80562e9 100644 --- a/frontend/static/css/library.css +++ b/frontend/static/css/library.css @@ -47,6 +47,7 @@ /* REMINDER LIST */ #reminder-list, +#static-reminder-list, #template-list { --gap: 1rem; --entry-width: 13rem; @@ -115,11 +116,6 @@ button.entry.fit { font-weight: 500; } -#delete-template { - border-color: var(--color-error); - color: var(--color-error); -} - @media (max-width: 543px) { header > div { transform: translateX(0); diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index f083811..2481954 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -1,5 +1,6 @@ const types = { 'reminder': document.getElementById('reminder-list'), + 'static_reminder': document.getElementById('static-reminder-list'), 'template': document.getElementById('template-list') }; @@ -9,7 +10,10 @@ const icons = { 'delete': '' }; -const info_classes = ['show-add-reminder', 'show-add-template', 'show-edit-reminder', 'show-edit-template']; +const info_classes = [ + 'show-add-reminder', 'show-add-template', 'show-add-static-reminder', + 'show-edit-reminder', 'show-edit-template', 'show-edit-static-reminder' +]; function toggleNav() { document.querySelector('.nav-divider').classList.toggle('show-nav'); diff --git a/frontend/static/js/library.js b/frontend/static/js/library.js index a628fb5..3f9d990 100644 --- a/frontend/static/js/library.js +++ b/frontend/static/js/library.js @@ -16,7 +16,7 @@ function showTab(button) { // function fillTable(table, results) { table.querySelectorAll('button.entry:not(.add-entry)').forEach(e => e.remove()); - + results.forEach(r => { const entry = document.createElement('button'); entry.classList.add('entry'); @@ -75,6 +75,10 @@ function fillReminders() { fillLibrary(`/api/reminders?api_key=${api_key}`, types.reminder); }; +function fillStaticReminders() { + fillLibrary(`/api/staticreminders?api_key=${api_key}`, types.static_reminder); +} + function fillTemplates() { fillLibrary(`/api/templates?api_key=${api_key}`, types.template); }; @@ -110,6 +114,7 @@ document.querySelectorAll('.tab-selector > button').forEach(b => { }); fillReminders(); +fillStaticReminders(); fillTemplates(); setInterval(fillReminders, 60000); diff --git a/frontend/static/js/show.js b/frontend/static/js/show.js index d8a5952..25ccf7e 100644 --- a/frontend/static/js/show.js +++ b/frontend/static/js/show.js @@ -6,6 +6,7 @@ function showAdd(type) { inputs.notification_service.value = document.querySelector('#notification-service-input option[selected]').value; toggleNormal(); toggleColor(true); + document.getElementById('test-reminder').classList.remove('show-sent'); const cl = document.getElementById('info').classList; cl.forEach(c => { @@ -15,10 +16,17 @@ function showAdd(type) { if (type === types.reminder) { cl.add('show-add-reminder'); document.querySelector('#info h2').innerText = 'Add a reminder'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; inputs.time.setAttribute('required', ''); } else if (type === types.template) { cl.add('show-add-template'); document.querySelector('#info h2').innerText = 'Add a template'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; + inputs.time.removeAttribute('required'); + } else if (type === types.static_reminder) { + cl.add('show-add-static-reminder'); + document.querySelector('#info h2').innerText = 'Add a static reminder'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; inputs.time.removeAttribute('required'); } else return; @@ -34,6 +42,11 @@ function showEdit(id, type) { url = `${url_prefix}/api/templates/${id}?api_key=${api_key}`; 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}`; + document.getElementById('test-reminder').classList.remove('show-sent'); + inputs.time.removeAttribute('required'); + type_buttons.repeat_interval.removeAttribute('required'); } else return; fetch(url) @@ -89,14 +102,26 @@ function showEdit(id, type) { if (type === types.reminder) { cl.add('show-edit-reminder'); document.querySelector('#info h2').innerText = 'Edit a reminder'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; } else if (type === types.template) { cl.add('show-edit-template'); document.querySelector('#info h2').innerText = 'Edit a template'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Test'; + } else if (type === types.static_reminder) { + cl.add('show-edit-static-reminder'); + document.querySelector('#info h2').innerText = 'Edit a static reminder'; + document.querySelector('#test-reminder > div:first-child').innerText = 'Trigger'; } else return; }; // code run on load -document.getElementById('add-reminder').addEventListener('click', e => showAdd(types.reminder)); +document.getElementById('add-reminder').addEventListener('click', e => { + if (document.getElementById('add-reminder').classList.contains('error')) + showWindow('notification'); + else + showAdd(types.reminder); +}); +document.getElementById('add-static-reminder').addEventListener('click', e => showAdd(types.static_reminder)); document.getElementById('add-template').addEventListener('click', e => showAdd(types.template)); diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js index e75e90a..ee9c7a1 100644 --- a/frontend/static/js/templates.js +++ b/frontend/static/js/templates.js @@ -58,136 +58,3 @@ function applyTemplate() { // code run on load loadTemplateSelection(); - -// function addTemplate() { -// const data = { -// 'title': template_inputs.title.value, -// 'notification_service': template_inputs["notification-service"].value, -// 'text': template_inputs.text.value, -// 'color': null -// }; -// if (!template_inputs.color.classList.contains('hidden')) { -// data['color'] = template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; -// }; -// fetch(`${url_prefix}/api/templates?api_key=${api_key}`, { -// 'method': 'POST', -// 'headers': {'Content-Type': 'application/json'}, -// 'body': JSON.stringify(data) -// }) -// .then(response => { -// // catch errors -// if (!response.ok) { -// return Promise.reject(response.status); -// }; - -// loadTemplateSelection(); -// closeAddTemplate(); -// return -// }) -// .catch(e => { -// if (e === 401) { -// window.location.href = `${url_prefix}/`; -// } else { -// console.log(e); -// }; -// }); -// }; - -// function closeAddTemplate() { -// hideWindow(); -// setTimeout(() => { -// template_inputs.title.value = ''; -// template_inputs['notification-service'].value = document.querySelector('#notification-service-template-input option[selected]').value; -// template_inputs.text.value = ''; -// if (!template_inputs.color.classList.contains('hidden')) { -// toggleColor(inputs.color); -// }; -// }, 500); -// }; - -// function showEditTemplate(id) { -// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`) -// .then(response => { -// // catch errors -// if (!response.ok) { -// return Promise.reject(response.status); -// }; -// return response.json(); -// }) -// .then(json => { -// document.getElementById('template-edit-form').dataset.id = id; -// edit_template_inputs.title.value = json.result.title; -// edit_template_inputs['notification-service'].value = json.result.notification_service; -// edit_template_inputs.text.value = json.result.text; -// if (json.result.color !== null) { -// if (edit_template_inputs.color.classList.contains('hidden')) { -// toggleColor(edit_template_inputs.color); -// }; -// selectColor(edit_template_inputs.color, json.result.color); -// }; -// showWindow('edit-template'); -// }) -// .catch(e => { -// if (e === 401) { -// window.location.href = `${url_prefix}/`; -// } else { -// console.log(e); -// }; -// }); -// }; - -// function saveTemplate() { -// const id = document.getElementById('template-edit-form').dataset.id; -// const data = { -// 'title': edit_template_inputs.title.value, -// 'notification_service': edit_template_inputs['notification-service'].value, -// 'text': edit_template_inputs.text.value, -// 'color': null -// }; -// if (!edit_template_inputs.color.classList.contains('hidden')) { -// data['color'] = edit_template_inputs.color.querySelector('button[data-selected="true"]').dataset.color; -// }; -// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { -// 'method': 'PUT', -// 'headers': {'Content-Type': 'application/json'}, -// 'body': JSON.stringify(data) -// }) -// .then(response => { -// // catch errors -// if (!response.ok) { -// return Promise.reject(response.status); -// }; -// loadTemplateSelection(); -// hideWindow(); -// }) -// .catch(e => { -// if (e === 401) { -// window.location.href = `${url_prefix}/`; -// } else { -// console.log(e); -// }; -// }); -// }; - -// function deleteTemplate() { -// const id = document.getElementById('template-edit-form').dataset.id; -// fetch(`${url_prefix}/api/templates/${id}?api_key=${api_key}`, { -// 'method': 'DELETE' -// }) -// .then(response => { -// // catch errors -// if (!response.ok) { -// return Promise.reject(response.status); -// }; - -// loadTemplateSelection(); -// hideWindow(); -// }) -// .catch(e => { -// if (e === 401) { -// window.location.href = `${url_prefix}/`; -// } else { -// console.log(e); -// }; -// }); -// }; diff --git a/frontend/static/js/window.js b/frontend/static/js/window.js index 74c1f56..5751efd 100644 --- a/frontend/static/js/window.js +++ b/frontend/static/js/window.js @@ -63,24 +63,35 @@ function toggleRepeated() { function testReminder() { const input = document.getElementById('test-reminder'); - if (inputs.title.value === '') { - input.classList.add('error-input'); - input.title = 'No title set'; - return - } else { - input.classList.remove('error-input'); - input.removeAttribute('title'); - }; - const data = { - 'title': inputs.title.value, - 'notification_service': inputs.notification_service.value, - 'text': inputs.text.value - }; - fetch(`${url_prefix}/api/reminders/test?api_key=${api_key}`, { + const cl = document.getElementById('info').classList; + const r_id = document.getElementById('info').dataset.id; + const headers = { 'method': 'POST', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) + 'headers': {'Content-Type': 'application/json'} + }; + let url; + if (cl.contains('show-edit-static-reminder')) { + // Trigger static reminder + url = `${url_prefix}/api/staticreminders/${r_id}?api_key=${api_key}`; + } else { + // Test reminder draft + if (inputs.title.value === '') { + input.classList.add('error-input'); + input.title = 'No title set'; + return + } else { + input.classList.remove('error-input'); + input.removeAttribute('title'); + }; + const data = { + 'title': inputs.title.value, + 'notification_service': inputs.notification_service.value, + 'text': inputs.text.value + }; + headers.body = JSON.stringify(data); + url = `${url_prefix}/api/reminders/test?api_key=${api_key}`; + }; + fetch(url, headers) .then(response => { if (!response.ok) return Promise.reject(response.status); input.classList.add('show-sent'); @@ -103,6 +114,9 @@ function deleteInfo() { } else if (cl.contains('show-edit-template')) { // Delete template url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; + } else if (cl.contains('show-edit-static-reminder')) { + // Delete static reminder + url = `${url_prefix}/api/staticreminders/${e_id}?api_key=${api_key}`; } else return; fetch(url, {'method': 'DELETE'}) @@ -111,6 +125,7 @@ function deleteInfo() { fillNotificationSelection(); fillReminders(); + fillStaticReminders(); fillTemplates(); hideWindow(); }) @@ -156,6 +171,11 @@ function submitInfo() { fetch_data.url = `${url_prefix}/api/templates?api_key=${api_key}`; fetch_data.method = 'POST'; + } else if (cl.contains('show-add-static-reminder')) { + // Add static reminder + fetch_data.url = `${url_prefix}/api/staticreminders?api_key=${api_key}`; + fetch_data.method = 'POST'; + } else if (cl.contains('show-edit-reminder')) { // Edit reminder data['time'] = (new Date(inputs.time.value) / 1000) + (new Date(inputs.time.value).getTimezoneOffset() * 60) @@ -171,6 +191,11 @@ function submitInfo() { fetch_data.url = `${url_prefix}/api/templates/${e_id}?api_key=${api_key}`; fetch_data.method = 'PUT'; + } 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.method = 'PUT'; + } else return; fetch(fetch_data.url, { @@ -182,6 +207,7 @@ function submitInfo() { if (!response.ok) return Promise.reject(response.status); fillReminders(); + fillStaticReminders(); fillTemplates(); hideWindow(); }) diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 6609615..4c47245 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -78,6 +78,7 @@
+
@@ -120,6 +121,19 @@
+