diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ea273f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,153 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Database +*.db + +# VS code +*.code-workspace +.vscode/ + +# Docker +Dockerfile +.dockerignore +docker-compose.yml + +# Git(hub) +.gitignore +.git/ +.github/ + +# Various files +*.md +LICENSE + +# Tests +tests/ diff --git a/.gitignore b/.gitignore index bb9ee72..585383e 100644 --- a/.gitignore +++ b/.gitignore @@ -133,7 +133,3 @@ dmypy.json # VS code *.code-workspace - -# Docker -Dockerfile -.dockerignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cd22d33 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.8-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY . . + +CMD [ "python3", "/app/Noted.py" ] diff --git a/backend/db.py b/backend/db.py index 54a1667..6333578 100644 --- a/backend/db.py +++ b/backend/db.py @@ -8,7 +8,7 @@ from flask import g -__DATABASE_VERSION__ = 2 +__DATABASE_VERSION__ = 3 class Singleton(type): _instances = {} @@ -79,7 +79,17 @@ def migrate_db(current_db_version: int) -> None: for reminder in reminders: new_reminders_append([round((datetime.fromtimestamp(reminder[0]) - utc_offset).timestamp()), reminder[1]]) cursor.executemany("UPDATE reminders SET time = ? WHERE id = ?;", new_reminders) - __DATABASE_VERSION__ = 2 + current_db_version = 2 + + if current_db_version == 2: + # V2 -> V3 + cursor.executescript(""" + ALTER TABLE reminders + ADD color VARCHAR(7); + ALTER TABLE templates + ADD color VARCHAR(7); + """) + current_db_version = 3 return @@ -115,6 +125,8 @@ def setup_db() -> None: repeat_interval INTEGER, original_time INTEGER, + color VARCHAR(7), + FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (notification_service) REFERENCES notification_services(id) ); @@ -125,6 +137,8 @@ def setup_db() -> None: 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) ); diff --git a/backend/reminders.py b/backend/reminders.py index 70c0692..d07167a 100644 --- a/backend/reminders.py +++ b/backend/reminders.py @@ -156,7 +156,8 @@ def get(self) -> dict: r.notification_service, ns.title AS notification_service_title, r.repeat_quantity, - r.repeat_interval + r.repeat_interval, + r.color FROM reminders r INNER JOIN notification_services ns @@ -176,7 +177,8 @@ def update( notification_service: int = None, text: str = None, repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None, - repeat_interval: int = None + repeat_interval: int = None, + color: str = None ) -> dict: """Edit the reminder @@ -185,6 +187,9 @@ def update( time (int): The new UTC epoch timestamp the the reminder should be send. Defaults to None. notification_service (int): 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. + repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The new quantity of the repeat specified for the reminder. Defaults to None. + repeat_interval (int, optional): The new amount of repeat_quantity, like "5" (hours). 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. Returns: dict: The new reminder info @@ -212,10 +217,11 @@ def update( 'notification_service': notification_service, 'text': text, 'repeat_quantity': repeat_quantity, - 'repeat_interval': repeat_interval + 'repeat_interval': repeat_interval, + 'color': color } for k, v in new_values.items(): - if k in ('repeat_quantity', 'repeat_interval') or v is not None: + if k in ('repeat_quantity', 'repeat_interval', 'color') or v is not None: data[k] = v # Update database @@ -224,7 +230,7 @@ def update( next_time = data["time"] cursor.execute(""" UPDATE reminders - SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=? + SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, color=? WHERE id = ?; """, ( data["title"], @@ -233,13 +239,14 @@ def update( data["notification_service"], data["repeat_quantity"], data["repeat_interval"], + data["color"], self.id )) else: 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=? + SET title=?, text=?, time=?, notification_service=?, repeat_quantity=?, repeat_interval=?, original_time=?, color=? WHERE id = ?; """, ( data["title"], @@ -249,6 +256,7 @@ def update( data["repeat_quantity"], data["repeat_interval"], data["time"], + data["color"], self.id )) except IntegrityError: @@ -284,7 +292,7 @@ def fetchall(self, sort_by: Literal["time", "time_reversed", "title", "title_rev sort_by (Literal["time", "time_reversed", "title", "title_reversed"], optional): How to sort the result. Defaults to "time". Returns: - List[dict]: The id, title, text, time, notification_service and notification_service_title of each reminder + List[dict]: The id, title, text, time, notification_service, notification_service_title and color of each reminder """ sort_function = self.sort_functions.get( sort_by, @@ -300,7 +308,8 @@ def fetchall(self, sort_by: Literal["time", "time_reversed", "title", "title_rev r.notification_service, ns.title AS notification_service_title, r.repeat_quantity, - r.repeat_interval + r.repeat_interval, + r.color FROM reminders r INNER JOIN notification_services ns @@ -350,7 +359,8 @@ def add( notification_service: int, text: str = '', repeat_quantity: Literal["year", "month", "week", "day", "hours", "minutes"] = None, - repeat_interval: int = None + repeat_interval: int = None, + color: str = None ) -> Reminder: """Add a reminder @@ -361,6 +371,7 @@ def add( text (str, optional): The body of the reminder. Defaults to ''. repeat_quantity (Literal["year", "month", "week", "day", "hours", "minutes"], optional): The quantity of the repeat specified for the reminder. Defaults to None. repeat_interval (int, optional): The amount of repeat_quantity, like "5" (hours). Defaults to None. + color (str, optional): The hex code of the color of the reminder, which is shown in the web-ui. Defaults to None. Returns: dict: The info about the reminder @@ -377,15 +388,15 @@ def add( try: if repeat_quantity is None and repeat_interval is None: id = get_db().execute(""" - INSERT INTO reminders(user_id, title, text, time, notification_service) - VALUES (?,?,?,?,?); - """, (self.user_id, title, text, time, notification_service) + INSERT INTO reminders(user_id, title, text, time, notification_service, color) + VALUES (?,?,?,?,?, ?); + """, (self.user_id, title, text, time, notification_service, color) ).lastrowid else: id = get_db().execute(""" - INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?); - """, (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time) + INSERT INTO reminders(user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, original_time, color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + """, (self.user_id, title, text, time, notification_service, repeat_quantity, repeat_interval, time, color) ).lastrowid except IntegrityError: raise NotificationServiceNotFound @@ -393,3 +404,26 @@ def add( # Return info return self.fetchone(id) + +def test_reminder( + title: str, + notification_service: int, + text: str = '' +) -> None: + """Test send a reminder draft + + Args: + title (str): Title 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 ''. + """ + a = Apprise() + url = get_db(dict).execute( + "SELECT url FROM notification_services WHERE id = ?", + (notification_service,) + ).fetchone() + if not url: + raise NotificationServiceNotFound + a.add(url[0]) + a.notify(title=title, body=text) + return diff --git a/backend/templates.py b/backend/templates.py index efe22ab..4f82364 100644 --- a/backend/templates.py +++ b/backend/templates.py @@ -27,7 +27,8 @@ def get(self) -> dict: SELECT id, title, text, - notification_service + notification_service, + color FROM templates WHERE id = ?; """, @@ -39,7 +40,8 @@ def get(self) -> dict: def update(self, title: str = None, notification_service: int = None, - text: str = None + text: str = None, + color: str = None ) -> dict: """Edit the template @@ -47,6 +49,7 @@ def update(self, title (str): The new title of the entry. Defaults to None. notification_service (int): The new id of the notification service to use to send the reminder. Defaults to None. text (str, optional): The new body of the template. Defaults to None. + color (str, optional): The new hex code of the color of the template, which is shown in the web-ui. Defaults to None. Returns: dict: The new template info @@ -57,21 +60,23 @@ def update(self, new_values = { 'title': title, 'notification_service': notification_service, - 'text': text + 'text': text, + 'color': color } for k, v in new_values.items(): - if v is not None: + if k in ('color',) or v is not None: data[k] = v try: cursor.execute(""" UPDATE templates - SET title=?, notification_service=?, text=? + SET title=?, notification_service=?, text=?, color=? WHERE id = ?; """, ( data['title'], data['notification_service'], data['text'], + data['color'], self.id )) except IntegrityError: @@ -95,13 +100,14 @@ def fetchall(self) -> List[dict]: """Get all templates Returns: - List[dict]: The id, title, text and notification_service + List[dict]: The id, title, text, notification_service and color """ templates: list = list(map(dict, get_db(dict).execute(""" SELECT id, title, text, - notification_service + notification_service, + color FROM templates WHERE user_id = ? ORDER BY title, id; @@ -126,7 +132,8 @@ def add( self, title: str, notification_service: int, - text: str = '' + text: str = '', + color: str = None ) -> Template: """Add a template @@ -134,16 +141,17 @@ def add( 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 template, which is shown in the web-ui. Defaults to None. Returns: Template: The info about the template """ try: id = get_db().execute(""" - INSERT INTO templates(user_id, title, text, notification_service) - VALUES (?,?,?,?); + INSERT INTO templates(user_id, title, text, notification_service, color) + VALUES (?,?,?,?,?); """, - (self.user_id, title, text, notification_service) + (self.user_id, title, text, notification_service, color) ).lastrowid except IntegrityError: raise NotificationServiceNotFound diff --git a/frontend/api.py b/frontend/api.py index 89bc6fc..ba977e3 100644 --- a/frontend/api.py +++ b/frontend/api.py @@ -3,6 +3,7 @@ from os import urandom from time import time as epoch_time from typing import Any, Tuple +from re import compile from flask import Blueprint, g, request @@ -14,12 +15,13 @@ UsernameTaken, UserNotFound) from backend.notification_service import (NotificationService, NotificationServices) -from backend.reminders import Reminders, reminder_handler +from backend.reminders import Reminders, reminder_handler, test_reminder from backend.templates import Template, Templates from backend.users import User, register_user api = Blueprint('api', __name__) api_key_map = {} +color_regex = compile(r'#[0-9a-f]{6}') """ AUTHENTICATION: @@ -104,6 +106,10 @@ def extract_key(values: dict, key: str, check_existence: bool=True) -> Any: 'text', 'query'): if not isinstance(value, str): raise InvalidKeyValue(key, value) + + elif key == 'color': + if not color_regex.search(value): + raise InvalidKeyValue(key, value) else: if key == 'sort_by': @@ -127,7 +133,7 @@ def api_login(): Requires being logged in: No Methods: POST: - Parameters (body (content-type: application/json)): + Parameters (body): username (required): the username of the user account password (required): the password of the user account Returns: @@ -216,7 +222,7 @@ def api_add_user(): Requires being logged in: No Methods: POST: - Parameters (body (content-type: application/json)): + Parameters (body): username (required): the username of the new user account password (required): the password of the new user account Returns: @@ -248,7 +254,7 @@ def api_manage_user(): Methods: PUT: Description: Change the password of the user account - Parameters (body (content-type: application/json)): + Parameters (body): new_password (required): the new password of the user account Returns: 200: @@ -297,7 +303,7 @@ def api_notification_services_list(): The id, title and url of every notification service POST: Description: Add a notification service - Parameters (body (content-type: application/json)): + Parameters (body): title (required): the title of the notification service url (required): the apprise url of the notification service Returns: @@ -340,7 +346,7 @@ def api_notification_service(n_id: int): No notification service found with the given id PUT: Description: Edit the notification service - Parameters (body (content-type: application/json)): + Parameters (body): title: The new title of the entry. url: The new apprise url of the entry. Returns: @@ -396,16 +402,17 @@ def api_reminders_list(): sort_by: how to sort the result. Allowed values are 'title', 'title_reversed', 'time' and 'time_reversed' Returns: 200: - The id, title, text, time, notification_service, notification_service_title, repeat_quantity and repeat_interval of each reminder + The id, title, text, time, notification_service, notification_service_title, repeat_quantity, repeat_interval and color of each reminder POST: Description: Add a reminder - Parameters (body (content-type: application/json)): + Parameters (body): title (required): the title of the reminder time (required): the UTC epoch timestamp that the reminder should be sent at notification_service (required): the id of the notification service to use to send the notification text: the body of the reminder repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval repeat_interval: The number of the interval + color: The hex code of the color of the reminder, which is shown in the web-ui Returns: 200: The info about the new reminder entry @@ -427,13 +434,15 @@ def api_reminders_list(): text = extract_key(data, 'text', check_existence=False) repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False) repeat_interval = extract_key(data, 'repeat_interval', check_existence=False) + color = extract_key(data, 'color', check_existence=False) result = reminders.add(title=title, time=time, notification_service=notification_service, text=text, repeat_quantity=repeat_quantity, - repeat_interval=repeat_interval) + repeat_interval=repeat_interval, + color=color) return return_api(result.get(), code=201) @api.route('/reminders/search', methods=['GET']) @@ -459,6 +468,36 @@ def api_reminders_query(): result = g.user_data.reminders.search(query) return return_api(result) +@api.route('/reminders/test', methods=['POST']) +@error_handler +@auth +def api_test_reminder(): + """ + Endpoint: /reminders/test + Description: Test send a reminder draft + Requires being logged in: Yes + Methods: + GET: + Parameters (body): + title (required): The title of the entry. + notification_service (required): The new id of the notification service to use to send the reminder. + text: The body of the reminder. + Returns: + 201: + The reminder is sent (doesn't mean it works, just that it was sent) + 400: + KeyNotFound: One of the required parameters was not given + 404: + NotificationServiceNotFound: The notification service given was not found + """ + data = request.get_json() + title = extract_key(data, 'title') + notification_service = extract_key(data, 'notification_service') + text = extract_key(data, 'text', check_existence=False) + + test_reminder(title, notification_service, text) + return return_api({}, code=201) + @api.route('/reminders/', methods=['GET','PUT','DELETE']) @error_handler @auth @@ -479,13 +518,14 @@ def api_get_reminder(r_id: int): No reminder found with the given id PUT: Description: Edit the reminder - Parameters (body (content-type: application/json)): + Parameters (body): title: The new title of the entry. time: The new UTC epoch timestamp the the reminder should be send. notification_service: The new id of the notification service to use to send the reminder. text: The new body of the reminder. - repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The quantity of the repeat_interval - repeat_interval: The number of the interval + repeat_quantity ('year', 'month', 'week', 'day', 'hours', 'minutes'): The new quantity of the repeat_interval. + repeat_interval: The new number of the interval. + color: The new hex code of the color of the reminder, which is shown in the web-ui. Returns: 200: Reminder updated successfully @@ -512,14 +552,15 @@ def api_get_reminder(r_id: int): text = extract_key(data, 'text', check_existence=False) repeat_quantity = extract_key(data, 'repeat_quantity', check_existence=False) repeat_interval = extract_key(data, 'repeat_interval', check_existence=False) - + color = extract_key(data, 'color', check_existence=False) result = reminders.fetchone(r_id).update(title=title, time=time, notification_service=notification_service, text=text, repeat_quantity=repeat_quantity, - repeat_interval=repeat_interval) + repeat_interval=repeat_interval, + color=color) return return_api(result) elif request.method == 'DELETE': @@ -543,13 +584,14 @@ def api_get_templates(): Description: Get a list of all templates Returns: 200: - The id, title, notification_service and text of every template + The id, title, notification_service, text and color of every template POST: Description: Add a template - Parameters (body (content-type: application/json)): + Parameters (body): title (required): the title of the template notification_service (required): the id of the notification service to use to send the notification text: the body of the template + color: the hex code of the color of the template, which is shown in the web-ui Returns: 200: The info about the new template entry @@ -567,10 +609,12 @@ def api_get_templates(): 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 = templates.add(title=title, notification_service=notification_service, - text=text) + text=text, + color=color) return return_api(result.get(), code=201) @api.route('/templates/', methods=['GET', 'PUT', 'DELETE']) @@ -593,10 +637,11 @@ def api_get_template(t_id: int): No template found with the given id PUT: Description: Edit the template - Parameters (body (content-type: application/json)): + Parameters (body): title: The new title of the entry. notification_service: The new id of the notification service to use to send the reminder. text: The new body of the template. + color: The new hex code of the color of the template. Returns: 200: Template updated successfully @@ -621,10 +666,12 @@ def api_get_template(t_id: int): 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 = template.update(title=title, notification_service=notification_service, - text=text) + text=text, + color=color) return return_api(result) elif request.method == 'DELETE': diff --git a/frontend/static/css/add_edit.css b/frontend/static/css/add_edit.css index d374b70..179971b 100644 --- a/frontend/static/css/add_edit.css +++ b/frontend/static/css/add_edit.css @@ -33,6 +33,7 @@ width: calc(50% - (var(--gap) / 2)); } +.form-container > form > button, .sub-inputs > button { display: flex; justify-content: center; @@ -54,6 +55,32 @@ opacity: 0; } +.color-list { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + + padding: 1rem; + border: 2px solid var(--color-gray); + border-radius: 4px; + box-shadow: var(--default-shadow); +} + +.color-list > button { + height: 1.5rem; + width: 1.5rem; + + padding: 1rem; + border: 1px solid transparent; + background-color: var(--color); +} + +.color-list > button[data-selected='true'] { + border-color: var(--color-white); +} + .repeat-bar, .repeat-edit-bar { display: flex; @@ -112,9 +139,30 @@ div.options > button { color: var(--color-error); } +#test-reminder { + display: flex; + gap: 1rem; + + overflow-x: hidden; +} + +#test-reminder > div { + width: 100%; + flex: 0 0 auto; + + font-size: inherit; + + transition: transform .1s linear; +} + +#test-reminder.show-sent > div { + transform: translateX(calc(-100% - 1rem)); +} + @media (max-width: 460px) { .sub-inputs > input, - .sub-inputs > select { + .sub-inputs > select, + .sub-inputs > button { width: 100%; } } \ No newline at end of file diff --git a/frontend/static/css/general.css b/frontend/static/css/general.css index dabaaa5..71d45c6 100644 --- a/frontend/static/css/general.css +++ b/frontend/static/css/general.css @@ -11,8 +11,8 @@ --color-gray: #3c3c3c; --color-dark: #1b1b1b; - --color-error: rgb(219, 84, 97); - --color-success: rgb(84, 219, 104); + --color-error: #db5461; + --color-success: #54db68; --header-height: 4.5rem; --nav-width: 4rem; diff --git a/frontend/static/css/reminders_templates.css b/frontend/static/css/reminders_templates.css index 5ec7de9..f518152 100644 --- a/frontend/static/css/reminders_templates.css +++ b/frontend/static/css/reminders_templates.css @@ -84,6 +84,7 @@ } .entry { + --color: var(--color-gray); width: var(--entry-width); height: 6rem; position: relative; @@ -95,7 +96,7 @@ border-radius: 4px; padding: .75rem; - background-color: var(--color-gray); + background-color: var(--color); } button.entry.fit { diff --git a/frontend/static/js/add.js b/frontend/static/js/add.js index 7fb9e14..6d1d668 100644 --- a/frontend/static/js/add.js +++ b/frontend/static/js/add.js @@ -1,8 +1,10 @@ +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') + 'text': document.getElementById('text-input'), + 'color': document.querySelector('#add .color-list') }; const type_buttons = { @@ -22,7 +24,11 @@ function addReminder() { '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 + '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; @@ -69,14 +75,43 @@ 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'; @@ -89,15 +124,53 @@ function toggleNormal() { 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(`/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 = '/'; + }; + }); +}; + // 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 index a93f52f..9b0fb30 100644 --- a/frontend/static/js/edit.js +++ b/frontend/static/js/edit.js @@ -2,7 +2,8 @@ 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') + 'text': document.getElementById('text-edit-input'), + 'color': document.querySelector('#edit .color-list') }; const edit_type_buttons = { @@ -22,7 +23,11 @@ function editReminder() { 'notification_service': edit_inputs.notification_service.value, 'text': edit_inputs.text.value, 'repeat_quantity': null, - 'repeat_interval': 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; @@ -65,6 +70,13 @@ function showEdit(id) { 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( @@ -142,6 +154,7 @@ function deleteReminder() { // 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()); diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js index 5f05675..9399fc0 100644 --- a/frontend/static/js/reminders.js +++ b/frontend/static/js/reminders.js @@ -7,6 +7,9 @@ function fillTable(result) { 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; diff --git a/frontend/static/js/templates.js b/frontend/static/js/templates.js index cb9672f..c5993d0 100644 --- a/frontend/static/js/templates.js +++ b/frontend/static/js/templates.js @@ -1,13 +1,15 @@ const template_inputs = { 'title': document.getElementById('title-template-input'), 'notification-service': document.getElementById('notification-service-template-input'), - 'text': document.getElementById('text-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') + 'text': document.getElementById('text-template-edit-input'), + 'color': document.querySelector('#edit-template .color-list') }; function loadTemplates(force=true) { @@ -68,6 +70,9 @@ function loadTemplate() { 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); + }; } else { fetch(`/api/templates/${id}?api_key=${api_key}`) .then(response => { @@ -81,6 +86,16 @@ function loadTemplate() { 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); + }; + }; }) .catch(e => { if (e === 401) { @@ -96,7 +111,11 @@ function addTemplate() { const data = { 'title': template_inputs.title.value, 'notification_service': template_inputs["notification-service"].value, - 'text': template_inputs.text.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(`/api/templates?api_key=${api_key}`, { 'method': 'POST', @@ -128,6 +147,9 @@ function closeAddTemplate() { 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); }; @@ -145,6 +167,12 @@ function showEditTemplate(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 => { @@ -161,7 +189,11 @@ function saveTemplate() { const data = { 'title': edit_template_inputs.title.value, 'notification_service': edit_template_inputs['notification-service'].value, - 'text': edit_template_inputs.text.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(`/api/templates/${id}?api_key=${api_key}`, { 'method': 'PUT', @@ -211,7 +243,9 @@ function deleteTemplate() { // 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()); diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index f3c0c2b..018ecf1 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -137,9 +137,13 @@

Noted Reminders

Add a reminder

- +
+ + +
+
@@ -178,6 +182,10 @@

Add a reminder

+
@@ -187,6 +195,8 @@

Add a reminder

Edit a reminder

+ +
@@ -298,6 +308,8 @@

Delete Account

Add a template

+ + @@ -312,6 +324,8 @@

Add a template

Edit a template

+ +