diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..c190ae0 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,3 @@ +{type}: {component} + +{summary} \ No newline at end of file diff --git a/000-default.conf b/000-default.conf index 82e75d3..f857085 100644 --- a/000-default.conf +++ b/000-default.conf @@ -9,7 +9,7 @@ #ServerName www.example.com ServerAdmin webmaster@localhost - DocumentRoot /var/www/html/public + DocumentRoot /var/www/html/public/index.php # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, # error, crit, alert, emerg. diff --git a/Dockerfile b/Dockerfile index 6be4c33..2f83576 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-apache +FROM php:8.4-apache RUN apt update && \ apt upgrade -y && \ @@ -19,7 +19,6 @@ RUN docker-php-ext-configure gd --with-jpeg RUN docker-php-ext-configure zip RUN docker-php-ext-install \ - #pdo_sqlite \ zip \ mbstring \ exif \ @@ -45,10 +44,14 @@ RUN rm -rf /var/www/html/tests RUN rm -rf /var/www/html/translations RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --no-scripts --no-dev --optimize-autoloader -RUN mv /var/www/html/data/data.db /var/www/html/var/ +RUN mkdir /data +COPY data/data.db /data/data.db RUN mkdir /var/www/html/var/cache RUN mkdir /var/www/html/var/log +#RUN symfony console asset-map:compile -RUN chown -R www-data:www-data /var/www/html -RUN chmod -R 755 /var/www/html \ No newline at end of file +RUN chown -R 33:33 /var/www/html /data +RUN chmod -R 755 /var/www/html /data + +EXPOSE 80 diff --git a/README.md b/README.md index b6d1954..7b501b0 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,26 @@ A program to take notes during a sermon. The web app was built with PHP and Symfony. +## ATTN: !!!!BREAKING CHANGE!!!! v1.0 -> v1.1 + +This was my first publicly available docker container so I did not realize what some decisions would do. If you are upgrading from v1 you first need to save your database OR you will lose all your current notes!! Follow the steps below to do that + +1. You need to make sure that you have a running SSH server on your host computer +2. On your host computer, `docker exec -it sermon-notes bash` +3. `cd var/` +4. `scp data.db {user}@{host computer IP}:{path}` +5. Authenticate with the password +6. This will copy the file over SFTP to the host computer +7. After this then you run the `docker run...` command in Step 1 of the `Installation` instructions below, once the container is running you need to copy the `data.db` file into the working directory of the docker container. + - For example, if you have `~/docker/sermon-notes` as the path for the container on the host computer, you'll copy the `data.db` to `~/docker/sermon-notes/data` + ## Installation -1. Run `docker run -d --name sermon-notes -p 80:80 ryanprather/sermon-notes:latest`, this will download and start the container and keep it running in the background. If you already have something on port 80 change the first `80` to whatever open port you'd like. -2. Run `docker exec -it sermon-notes bash install.sh` This will run an install script to create an .env file specific to your install, populate with the beginning factors, and then run a `composer` command to download the necessary package dependancies. -3. Once complete you have a running system that you can navigate to in your browser with `http://{ip}:{port}|{hostname}:{port}`. Then you just need to register for an account. The first account that is created is made an admin so that you can access the `Reference Editor` and update any reference material if necessary. +1. Make a directory in your desired docker storage folder (e.g. `~/docker/sermon-notes`), then `cd` into it. +2. Create a file called `.env` in that folder, no need to add anything to it right now. +3. Run `docker run -d --name sermon-notes -p 80:80 -v $PWD/data:/data -v $PWD/.env:/var/www/html/.env gitea.rkprather.com/ryan/sermon-notes:latest`, this will download and start the container and keep it running in the background. If you already have something on port 80 change the first `80` to whatever open port you'd like. +4. Run `docker exec -it sermon-notes bash install.sh` This will run an install script to create an .env file specific to your install, populate with the beginning factors, and then run a `composer` command to download the necessary package dependancies. +5. Once complete you have a running system that you can navigate to in your browser with `http://{ip}:{port}`|`http://{hostname}:{port}`. Then you just need to register for an account. The first account that is created is made an admin so that you can access the `Reference Editor` and update any reference material if necessary. ## Operation diff --git a/assets/app.js b/assets/app.js index 8725cc5..c31a289 100644 --- a/assets/app.js +++ b/assets/app.js @@ -6,5 +6,6 @@ import './bootstrap.js'; * which should already be in your base.html.twig. */ import './styles/app.css'; +const $ = require('jquery'); -console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉'); +// console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉'); diff --git a/public/theme/assets/css/fontawesome-all.min.css b/assets/css/fontawesome-all.min.css similarity index 100% rename from public/theme/assets/css/fontawesome-all.min.css rename to assets/css/fontawesome-all.min.css diff --git a/public/theme/assets/css/images/ui-bg_glass_100_f5f0e5_1x400.png b/assets/css/images/ui-bg_glass_100_f5f0e5_1x400.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_glass_100_f5f0e5_1x400.png rename to assets/css/images/ui-bg_glass_100_f5f0e5_1x400.png diff --git a/public/theme/assets/css/images/ui-bg_glass_25_cb842e_1x400.png b/assets/css/images/ui-bg_glass_25_cb842e_1x400.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_glass_25_cb842e_1x400.png rename to assets/css/images/ui-bg_glass_25_cb842e_1x400.png diff --git a/public/theme/assets/css/images/ui-bg_glass_70_ede4d4_1x400.png b/assets/css/images/ui-bg_glass_70_ede4d4_1x400.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_glass_70_ede4d4_1x400.png rename to assets/css/images/ui-bg_glass_70_ede4d4_1x400.png diff --git a/public/theme/assets/css/images/ui-bg_highlight-hard_100_f4f0ec_1x100.png b/assets/css/images/ui-bg_highlight-hard_100_f4f0ec_1x100.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_highlight-hard_100_f4f0ec_1x100.png rename to assets/css/images/ui-bg_highlight-hard_100_f4f0ec_1x100.png diff --git a/public/theme/assets/css/images/ui-bg_highlight-hard_65_fee4bd_1x100.png b/assets/css/images/ui-bg_highlight-hard_65_fee4bd_1x100.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_highlight-hard_65_fee4bd_1x100.png rename to assets/css/images/ui-bg_highlight-hard_65_fee4bd_1x100.png diff --git a/public/theme/assets/css/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png b/assets/css/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png rename to assets/css/images/ui-bg_highlight-hard_75_f5f5b5_1x100.png diff --git a/public/theme/assets/css/images/ui-bg_inset-soft_100_f4f0ec_1x100.png b/assets/css/images/ui-bg_inset-soft_100_f4f0ec_1x100.png similarity index 100% rename from public/theme/assets/css/images/ui-bg_inset-soft_100_f4f0ec_1x100.png rename to assets/css/images/ui-bg_inset-soft_100_f4f0ec_1x100.png diff --git a/public/theme/assets/css/images/ui-icons_c47a23_256x240.png b/assets/css/images/ui-icons_c47a23_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_c47a23_256x240.png rename to assets/css/images/ui-icons_c47a23_256x240.png diff --git a/public/theme/assets/css/images/ui-icons_cb672b_256x240.png b/assets/css/images/ui-icons_cb672b_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_cb672b_256x240.png rename to assets/css/images/ui-icons_cb672b_256x240.png diff --git a/public/theme/assets/css/images/ui-icons_f08000_256x240.png b/assets/css/images/ui-icons_f08000_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_f08000_256x240.png rename to assets/css/images/ui-icons_f08000_256x240.png diff --git a/public/theme/assets/css/images/ui-icons_f35f07_256x240.png b/assets/css/images/ui-icons_f35f07_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_f35f07_256x240.png rename to assets/css/images/ui-icons_f35f07_256x240.png diff --git a/public/theme/assets/css/images/ui-icons_ff7519_256x240.png b/assets/css/images/ui-icons_ff7519_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_ff7519_256x240.png rename to assets/css/images/ui-icons_ff7519_256x240.png diff --git a/public/theme/assets/css/images/ui-icons_ffffff_256x240.png b/assets/css/images/ui-icons_ffffff_256x240.png similarity index 100% rename from public/theme/assets/css/images/ui-icons_ffffff_256x240.png rename to assets/css/images/ui-icons_ffffff_256x240.png diff --git a/public/theme/assets/css/jquery-ui.structure.css b/assets/css/jquery-ui.structure.css similarity index 100% rename from public/theme/assets/css/jquery-ui.structure.css rename to assets/css/jquery-ui.structure.css diff --git a/public/theme/assets/css/jquery-ui.theme.css b/assets/css/jquery-ui.theme.css similarity index 100% rename from public/theme/assets/css/jquery-ui.theme.css rename to assets/css/jquery-ui.theme.css diff --git a/public/theme/assets/css/login.css b/assets/css/login.css similarity index 100% rename from public/theme/assets/css/login.css rename to assets/css/login.css diff --git a/public/theme/assets/css/main.css b/assets/css/main.css similarity index 98% rename from public/theme/assets/css/main.css rename to assets/css/main.css index 01c068b..07d07ba 100644 --- a/public/theme/assets/css/main.css +++ b/assets/css/main.css @@ -106,6 +106,7 @@ menu, nav { display: block; width: 250px; + margin-bottom: 1em; } body { @@ -306,7 +307,7 @@ h6 a { } h1 { - font-size: 4em; + font-size: 2em; margin: 0 0 0.5em 0; line-height: 1.3; } @@ -1891,10 +1892,11 @@ header p { text-transform: uppercase; } -header.major> :last-child { +header.major { + /*> :last-child {*/ border-bottom: solid 3px #f56a6a; display: inline-block; - margin: 0 0 2em 0; + margin: 0 0 10px 0; padding: 0 0.75em 0.5em 0; } @@ -1921,6 +1923,7 @@ input[type="email"], input[type="tel"], input[type="search"], input[type="url"], +input[type="number"], select, textarea { -moz-appearance: none; @@ -1967,7 +1970,7 @@ select { background-size: 1.25em; background-repeat: no-repeat; background-position: calc(100% - 1em) center; - height: 2.75em; + height: 2em; padding-right: 2.75em; text-overflow: ellipsis; } @@ -1992,8 +1995,9 @@ input[type="tel"], input[type="search"], input[type="url"], input[type="date"], +input[type="number"], select { - height: 2.75em; + height: 1.5em; } textarea { @@ -2233,7 +2237,6 @@ a.image:hover img { ol { list-style: decimal; margin: 0 0 2em 0; - padding-left: 1.5em; } ol li { @@ -2243,7 +2246,6 @@ ol li { ul { list-style: disc; margin: 0 0 2em 0; - padding-left: 1em; } ul.alt { @@ -2679,8 +2681,7 @@ button:disabled, /* Mini Posts */ .mini-posts article { border-top: solid 1px rgba(210, 215, 217, 0.75); - margin-top: 2em; - padding-top: 2em; + padding-top: 1em; } .mini-posts article .image { @@ -2695,7 +2696,7 @@ button:disabled, .mini-posts article:first-child { border-top: 0; - margin-top: 0; + margin-top: 1em; padding-top: 0; } @@ -3088,13 +3089,13 @@ button:disabled, } #main>.inner { - padding: 0 0.5em 0.1em 0.5em; + padding: 0 0.25em 0.1em 0.25em; margin: 0 auto; max-width: 110em; } #main>.inner>section { - padding: 0.5em 0 0.5em 0; + padding: 0.25em 0 0.25em 0; border-top: solid 2px rgba(210, 215, 217, 0.75); } @@ -3104,13 +3105,13 @@ button:disabled, @media screen and (max-width: 1680px) { #main>.inner { - padding: 0 0 0 2em; + padding: 0 0 0 0; } } @media screen and (max-width: 1280px) { #main>.inner { - padding: 0 0 0 2em; + padding: 0 0 0 0; } } @@ -3199,7 +3200,7 @@ button:disabled, #sidebar>.inner>* { border-bottom: solid 2px rgba(210, 215, 217, 0.75); margin: 0 0 3.5em 0; - padding: 0 0 3.5em 0; + /*padding: 0 0 3.5em 0;*/ } #sidebar>.inner>*> :last-child { @@ -3229,9 +3230,9 @@ button:disabled, -webkit-tap-highlight-color: rgba(255, 255, 255, 0); border: 0; display: block; - height: 7.5em; + height: 4.5em; left: 26em; - line-height: 7.5em; + line-height: 4.5em; outline: 0; overflow: hidden; position: absolute; @@ -3283,17 +3284,19 @@ button:disabled, } #sidebar>.inner>.alt { - margin: -1.66667em 0 3.33333em -1.66667em; + margin: 0; padding: 1.66667em; width: calc(100% + 3.33333em); } #sidebar .toggle { - height: 6.25em; - left: 24em; - line-height: 6.25em; + height: 4.5em; + left: 23em; + line-height: 4.5em; text-indent: 5em; width: 5em; + background-color: #fff; + margin-left: 1em; } #sidebar .toggle:before { @@ -3338,7 +3341,7 @@ button:disabled, #sidebar .toggle { text-indent: 6em; - width: 6em; + width: 4.5em; } #sidebar .toggle:before { @@ -3408,7 +3411,18 @@ button:disabled, } @media screen and (max-width: 1680px) { - #header {} + #header .logo { + font-size: 1.25em; + margin: 0; + } + + #header .icons { + height: 5em; + line-height: 5em; + position: absolute; + right: -0.5em; + top: 0; + } } @media screen and (max-width: 736px) { diff --git a/public/theme/assets/css/register.css b/assets/css/register.css similarity index 100% rename from public/theme/assets/css/register.css rename to assets/css/register.css diff --git a/public/theme/images/lined-paper-template-01.png b/assets/images/lined-paper-template-01.png similarity index 100% rename from public/theme/images/lined-paper-template-01.png rename to assets/images/lined-paper-template-01.png diff --git a/logo/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg b/assets/images/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg similarity index 100% rename from logo/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg rename to assets/images/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg diff --git a/public/theme/assets/js/breakpoints.min.js b/assets/js/breakpoints.min.js similarity index 100% rename from public/theme/assets/js/breakpoints.min.js rename to assets/js/breakpoints.min.js diff --git a/public/theme/assets/js/browser.min.js b/assets/js/browser.min.js similarity index 100% rename from public/theme/assets/js/browser.min.js rename to assets/js/browser.min.js diff --git a/public/theme/assets/js/jquery-ui.js b/assets/js/jquery-ui.js similarity index 100% rename from public/theme/assets/js/jquery-ui.js rename to assets/js/jquery-ui.js diff --git a/public/theme/assets/js/jquery.min.js b/assets/js/jquery.min.js similarity index 100% rename from public/theme/assets/js/jquery.min.js rename to assets/js/jquery.min.js diff --git a/public/theme/assets/js/login.js b/assets/js/login.js similarity index 100% rename from public/theme/assets/js/login.js rename to assets/js/login.js diff --git a/public/theme/assets/js/main.js b/assets/js/main.js similarity index 100% rename from public/theme/assets/js/main.js rename to assets/js/main.js diff --git a/assets/js/reference.js b/assets/js/reference.js new file mode 100644 index 0000000..f778dd5 --- /dev/null +++ b/assets/js/reference.js @@ -0,0 +1,85 @@ +/** + * Retrieves the reference type from the server and populates the reference series dropdown. + * + * @param {HTMLElement} el - The element that triggered the function. + * @return {Promise} A promise that resolves with the response from the server. + */ +export function retrieveReferenceType(el) { + fetch('/reference/' + el.value, { + method: 'GET', + header: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(results => { + document.querySelector('#referenceSeries').innerHTML = ''; + var none = document.createElement('option'); + none.value = ''; + none.text = '-- Select --'; + document.querySelector('#referenceSeries').appendChild(none); + + for (var x in results) { + var newSeries = document.createElement('option'); + newSeries.value = results[x].id; + newSeries.text = results[x].label; + document.querySelector('#referenceSeries').appendChild(newSeries); + } + }); +} + +/** + * Retrieves a reference based on the provided element value. + * + * @param {Element} el - The element triggering the reference retrieval + * @return {void} No return value + */ +export function retrieveReference(el) { + if (el.value == 'new') { + document.querySelector('#refName').style.display = 'inline-block'; + return; + } + fetch('/get-reference', { + method: "POST", + header: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + id: el.value + }) + }) + .then(response => response.json()) + .then(results => { + document.querySelector('#reference').value = results.text; + }); +} + +/** + * Saves a reference by sending a POST request to the server with the selected type, + * file, and text values. Displays an alert with the response message, and clears + * the reference and file input fields. + * + * @return {Promise} A Promise that resolves with the response message from the server. + */ +export function saveReference() { + let ref = document.querySelector('#referenceSeries'); + let cont = document.querySelector('#reference'); + fetch('/save-reference', { + method: 'POST', + header: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + refId: ref.value, + text: cont.value + }) + }) + .then(response => response.json()) + .then(results => { + //alert(results.msg); + + document.querySelector('#reference').value = ''; + document.querySelector('#referenceType').value = ''; + document.querySelector('#referenceSeries').value = ''; + }); +} diff --git a/public/theme/assets/js/register.js b/assets/js/register.js similarity index 66% rename from public/theme/assets/js/register.js rename to assets/js/register.js index 64c16e7..90240c2 100644 --- a/public/theme/assets/js/register.js +++ b/assets/js/register.js @@ -1,9 +1,8 @@ // Get references to the form elements -const nameInput = document.getElementById("name"); -const emailInput = document.getElementById("emailAddress"); -const passwordInput = document.getElementById("password"); -const confirmPasswordInput = document.getElementById("confirmPassword"); -const csrfToken = document.getElementById("csrfToken").value; +const nameInput = document.getElementById("registration_form_name"); +const emailInput = document.getElementById("registration_form_email"); +const passwordInput = document.getElementById("registration_form_plainPassword"); +const csrfToken = document.getElementById("registration_form__token").value; // Add event listeners to the form const registerBtn = document.querySelector("#register-btn"); @@ -18,28 +17,21 @@ function handleSubmit(event) { const name = nameInput.value; const email = emailInput.value; const password = passwordInput.value; - const confirmPassword = confirmPasswordInput.value; if (name === "" || email === "" || password === "") { alert("Please fill in all fields."); return; } - if (password !== confirmPassword) { - alert("Passwords do not match."); - return; - } - // Send data to server for processing const data = { "name": name, "email": email, - "password": password, "plainPassword": password, - "csrf_token": csrfToken + "_token": csrfToken }; - fetch("/index.php/register", { + fetch("/register", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/public/js/script.js b/assets/js/script.js similarity index 63% rename from public/js/script.js rename to assets/js/script.js index 473798e..1c2c165 100644 --- a/public/js/script.js +++ b/assets/js/script.js @@ -4,87 +4,21 @@ var references = {}; var tabs = []; let saved = false; let textDirty = false; -let saveTimeout = 10000; var to = null; let controller; var BOOKS = {}; -fetch('/js/data.json') - .then((res) => { - if (!res.ok) { - throw new Error('HTTP Error: Status: ${res.status}'); - } - return res.json(); - }) - .then((data) => { - BOOKS = data; - }) - .catch((error) => { - console.log(error); - }) - -document.addEventListener('keyup', function (event) { - if (event.key == "F3") { - openRef(false); - } -}); - -document.querySelector('#notes').addEventListener('keyup', function (event) { - let key = event.keyCode; - - if (key >= 48 && key <= 90 || key >= 96 && key <= 111 || key >= 186 && key <= 222) { - textDirty = true; - document.querySelector('#note-header-left h2').classList.add('dirty'); - } -}); - -function setHeight() { - md = new markdownit({ - html: true, - linkify: true, - breaks: true - }); - - body = document.querySelector('body'); - body.style.height = window.innerHeight + 'px'; - - cont = document.querySelector('#main'); - cont.style.height = (window.innerHeight) + 'px'; - - tabs = document.querySelector('.ref-tab'); - tabs.style.height = (window.innerHeight - 13) + 'px'; - - ref = document.querySelector('.ref'); - ref.style.height = (window.innerHeight - 50) + 'px'; - - noteList = document.querySelector('#note-list'); - noteList.style.height = (window.innerHeight - 50) + 'px'; - - notes = document.querySelector('.notes'); - notes.style.height = (window.innerHeight - 50) + 'px'; - - notePreview = document.querySelector('#notePreview'); - notePreview.style.height = (window.innerHeight - 50) + 'px'; - - if ($('#noteDate')) { - $('#noteDate').datepicker(); - } - - if ($('#query')) { - document.querySelector('#query').addEventListener('keyup', function (event) { - if (event.key == "Enter") { - search(); - } - }); - } - if (!to) { - to = setTimeout(saveNote, saveTimeout); - } -} +/** + * Searches for notes based on the query entered in the search field. + * Sends a POST request to the '/search' endpoint with the query as a JSON payload. + * Updates the '#old-notes' element with the search results. + * + * @return {Promise} A Promise that resolves with the search results. + */ function search() { query = document.querySelector('#query').value; - fetch('/index.php/search', { + fetch('/search', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -115,6 +49,15 @@ function search() { }); } +/** + * Resets the state of the note editor by clearing the text and form fields, + * resetting the references, and removing any dirty classes. It also sets the + * date to the current date, clears the speaker, series, template, passage, + * recording, and note ID fields. Finally, it clears the reference list and + * reference display. + * + * @return {void} This function does not return anything. + */ function newNote() { notes = document.querySelector('#notes'); notes.text = ''; @@ -133,7 +76,8 @@ function newNote() { document.querySelector('#series').value = 0; document.querySelector('#template').value = 0; document.querySelector('#passage').value = ''; - document.querySelector('#noteId').value = uuidv4(); + document.querySelector('#recording').value = ''; + document.querySelector('#noteId').value = ''; document.querySelector('#ref-list').innerHTML = ''; document.querySelector('#ref').innerHTML = ''; @@ -151,13 +95,21 @@ function saveNote(event) { event.preventDefault(); } + document.querySelector('#noteTitle').classList.remove('input-error'); + document.querySelector('#noteDate').classList.remove('input-error'); + document.querySelector('#speaker').classList.remove('input-error'); + document.querySelector('#series').classList.remove('input-error'); + document.querySelector('#passage').classList.remove('input-error'); + document.querySelector('#notes').classList.remove('input-error'); + if (!textDirty || !validateNote()) { clearTimeout(to); - to = setTimeout(saveNote, saveTimeout); + to = setTimeout(saveNote, saveInterval); return; } let saveCheck = document.querySelector('#save-check'); + var noteText = document.querySelector('#notes').value; startSave(); @@ -169,44 +121,58 @@ function saveNote(event) { series: document.querySelector('#series').value, passage: document.querySelector('#passage').value, note: document.querySelector('#notes').value, + recording: document.querySelector('#recording').value, refs: references }; $.ajax({ - url: '/index.php/save-note', + url: '/save-note', method: 'POST', contentType: 'application/json', data: JSON.stringify(note), dataType: 'json', - timeout: 5000 + timeout: saveTimeout }) .done(function (data) { if (data.msg == 'saved' && !saved) { - saveCheck.classList.remove('saving'); + saveFailureCount = SAVE_FAILURE_LIMIT; + saveCheck.classList.remove('saving', 'error', 'fa-times-circle', 'fa-save'); showSave(); - saved = true; - textDirty = false; - document.querySelector('#note-header-left h2').classList.remove('dirty'); + if (noteText == document.querySelector('#notes').value) { + saved = true; + textDirty = false; + document.querySelector('#note-header-left h2').classList.remove('dirty'); + } if (data.new) { document.querySelector('#noteId').value = data.id; } } }) - .fail(function (data) { - saveCheck.classList.remove('saving'); - saveCheck.classList.add('error'); - console.error(data); + .fail(function (xhr, status, error) { + saveFailureCount--; + saveCheck.classList.remove('saving', 'fa-save'); + saveCheck.classList.add('fa-times-circle', 'error'); + console.error(error); }) .always(function (xhr, status) { if (status == 'timeout') { - saveCheck.classList.remove('saving'); - saveCheck.classList.add('error'); + saveCheck.classList.remove('saving', 'fa-save'); + saveCheck.classList.add('error', 'fa-times-circle'); } clearTimeout(to); - to = setTimeout(saveNote, saveTimeout); + if (saveFailureCount > 0) { + to = setTimeout(saveNote, saveInterval); + } else { + saveFailureCount = SAVE_FAILURE_LIMIT; + } }); } +/** + * Validates a note by checking if all required fields are filled. + * + * @return {boolean} Returns true if all required fields are filled, false otherwise. + */ function validateNote() { const note = document.querySelector('#notes'); const date = document.querySelector('#noteDate'); @@ -215,23 +181,42 @@ function validateNote() { const title = document.querySelector('#noteTitle'); const psg = document.querySelector('#passage'); - if (!title.value.length) { return false; } - if (!date.value) { return false; } - if (!parseInt(speaker.value)) { return false; } - if (!parseInt(series.value)) { return false; } - if (!psg.value) { return false; } - if (!note.value.length) { return false; } + let ret = true; - return true; + if (!title.value.length) { title.classList.add('input-error'); ret = false; } + if (!date.value) { date.classList.add('input-error'); ret = false; } + if (!parseInt(speaker.value)) { speaker.classList.add('input-error'); ret = false; } + if (!parseInt(series.value)) { series.classList.add('input-error'); ret = false; } + if (!psg.value) { psg.classList.add('input-error'); ret = false; } + if (!note.value.length) { note.classList.add('input-error'); ret = false; } + + if (!ret) { + toggleFields(true); + } + + return ret; } +/** + * Checks if a given UUID is valid. + * + * @param {string} uuid - The UUID to be validated. + * @return {boolean} Returns true if the UUID is valid, false otherwise. + */ function isUuidValid(uuid) { const regex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[8|9|a|b][a-f0-9]{3}-[a-f0-9]{12}$/i; return regex.test(uuid); } +/** + * Starts the save process by updating the save-check element's classList, removing error, fa-times-circle, and fa-save classes, + * adding the 'saving' and 'fa-save' classes, and setting the opacity to 1. + * + * @return {void} This function does not return anything. + */ function startSave() { - document.querySelector('#save-check').classList.add('saving'); + document.querySelector('#save-check').classList.remove('error', 'fa-times-circle', 'fa-save'); + document.querySelector('#save-check').classList.add('saving', 'fa-save'); document.querySelector('#save-check').style.opacity = 1; } @@ -245,6 +230,7 @@ function showSave() { if (saved) { return; } var checkmark = document.getElementById("save-check"); + checkmark.classList.add('fa-save'); // Schedule the animation to run every 1 second (which is equivalent to a 1-second delay between each iteration) var si = setInterval(function () { @@ -264,33 +250,44 @@ function showSave() { /** * Function to discard the note by clearing all input fields and closing the menu. */ -function discardNote() { +function deleteNote(noteId, link) { document.querySelector('#noteTitle').value = ''; document.querySelector('#speaker').value = 0; document.querySelector('#series').value = 0; document.querySelector('#template').value = 0; document.querySelector('#passage').value = ''; document.querySelector('#notes').value = ''; + document.querySelector('#recording').value = ''; + document.querySelector('#noteDate').value = ''; + document.querySelector('#noteId').value = ''; - fetch('/index.php/discard-note', { + var row = link.parentElement.parentElement; + + fetch('/delete-note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - 'id': document.querySelector('#noteId').value + 'id': noteId }) - .then(response => response.json()) - .then(data => { - if (data.msg == 'deleted') { - alert('Note deleted.'); - } - }) - }); + }) + .then(response => response.json()) + .then(data => { + if (data.msg != 'deleted') { + return; + } - openRef(); + alert('Note deleted.'); + row.remove(); + }); } +/** + * Toggles the display of the new speaker input field and hides the speaker select field. + * + * @return {void} This function does not return anything. + */ function newSpeaker() { if (document.querySelector('#speaker').value == 'new') { document.querySelector('#newSpeaker').style.display = 'inline-block'; @@ -301,9 +298,20 @@ function newSpeaker() { textDirty = true; } +/** + * Saves a new speaker to the database and updates the UI with the new speaker option. + * + * @param {Event} event - The keydown event triggered by the user. + * @return {Promise} A Promise that resolves with the results of the fetch request. + */ function saveSpeaker(event) { - if (event.keyCode == 13) { - fetch('/index.php/save-speaker', { + if (event.keyCode == 27) { + document.querySelector('#newSpeaker').style.display = 'none'; + document.querySelector('#speaker').style.display = 'inline-block'; + document.querySelector('#speaker').value = 0; + } + if (event.keyCode == 13 && document.querySelector('#newSpeaker').value != '') { + fetch('/save-speaker', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -329,6 +337,9 @@ function saveSpeaker(event) { } } +/** + * A description of the entire function. + */ function newSeries() { if (document.querySelector('#series').value == 'new') { document.querySelector('#newSeries').style.display = 'inline-block'; @@ -339,9 +350,20 @@ function newSeries() { textDirty = true; } +/** + * Saves a series by making a POST request to '/save-series' with the series name as the request body. + * + * @param {Event} event - The keydown event. + * @return {Promise} A Promise that resolves with the response from the server. + */ function saveSeries(event) { - if (event.keyCode == 13) { - fetch('/index.php/save-series', { + if (event.keyCode == 27) { + document.querySelector('#newSeries').style.display = 'none'; + document.querySelector('#series').style.display = 'inline-block'; + document.querySelector('#series').value = 0; + } + if (event.keyCode == 13 && document.querySelector('#newSeries').value != '') { + fetch('/save-series', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -367,7 +389,14 @@ function saveSeries(event) { } } +/** + * Opens the reference with the option to close the sidebar. + * + * @param {boolean} closeSidebar - Indicates whether to close the sidebar when opening the reference. + */ function openRef(closeSidebar = true) { + document.querySelector('#openRefBtn').classList.add('active'); + refQuery = document.querySelector('#refQuery'); refQuery.style.display = 'block'; @@ -379,6 +408,11 @@ function openRef(closeSidebar = true) { } } +/** + * Closes the reference query and resets the reference search form. + * + * @return {void} This function does not return anything. + */ function closeRef() { document.querySelector('#referenceSearch').value = ''; document.querySelector('#referenceSearch').style.display = ''; @@ -389,8 +423,17 @@ function closeRef() { document.querySelector('#verse-range').innerText = ''; document.querySelector('#refQuery').style.display = 'none'; + document.querySelector('#openRefBtn').classList.remove('active'); } +/** + * Fetches a reference based on the provided type, book, and input. + * + * @param {string} type - The type of reference. + * @param {string} book - The book of the reference. + * @param {string} input - The input for the reference. + * @return {void} This function does not return anything directly, but processes the fetched reference data. + */ function queryRef(type = null, book = null, input = null) { if (!input) { var input = document.querySelector('#refQuery #referenceSearch').value; @@ -401,7 +444,7 @@ function queryRef(type = null, book = null, input = null) { if (!book) { var book = document.querySelector('#referenceBook').value; } - fetch('/index.php/retrieve-reference', { + fetch('/retrieve-reference', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -435,13 +478,21 @@ function queryRef(type = null, book = null, input = null) { }); } +/** + * A function to create a button element with the specified title and event listeners for click and double click actions. + * + * @param {string} title - The title to be displayed on the button. + * @return {Element} The created button element. + */ function makeButton(title) { var btn = document.createElement('button'); btn.innerText = title; + btn.class = 'button'; + btn.style = 'line-height:normal;' btn.addEventListener('click', function () { removeActiveRef(); document.querySelector('#ref').innerHTML = md.render(references[title]); - this.classList.add('activeRef'); + this.classList.add('active'); findRefLinks(); }); @@ -456,80 +507,53 @@ function makeButton(title) { }); removeActiveRef(); - btn.classList.add('activeRef'); + btn.classList.add('active'); return btn; } +/** + * Removes the 'active' class from all elements with the class 'active'. + * + * @return {void} This function does not return a value. + */ function removeActiveRef() { - tabs = document.querySelectorAll('.activeRef'); + tabs = document.querySelectorAll('.active'); for (var t in tabs) { if (isFinite(parseInt(t))) { - tabs[t].classList.remove('activeRef'); + tabs[t].classList.remove('active'); } } } -function retrieveTemplate(orig, dest) { - const temp = document.querySelector('#' + orig); - if (temp.value == '0') { - document.querySelector('#' + dest).value = ''; - return; - } - fetch('/index.php/retrieve-template', { - method: 'POST', - headers: { - 'Content-Type': 'plain/text' - }, - body: JSON.stringify({ - 'template': temp.value - }) - }) - .then(response => response.text()) - .then(results => { - const div = document.querySelector('#' + dest); - div.value = results; - }); -} - /** - * Saves the template by sending a POST request to the server with template data. + * Toggles the visibility of the fields container and updates the active state of the show/hide button. + * + * @param boolean show + * @return {void} */ -function saveTemplate() { - fetch('/index.php/save-template', { - method: 'POST', - headers: { - 'Content-Type': 'plain/text' - }, - body: JSON.stringify({ - 'template_id': document.querySelector('#template_id').value, - 'template_name': document.querySelector('#template_name').value, - 'template_value': document.querySelector('#template_value').value, - }) - }) - .then(response => response.text()) - .then(results => { - alert(results); - }); -} - -function toggleFields() { +function toggleFields(show = false) { const fieldsContainer = document.getElementById('fields-container'); const showHideBtn = document.getElementById('show-hide-btn'); - if (fieldsContainer.classList.contains('show')) { - fieldsContainer.classList.remove('show'); - fieldsContainer.style.display = 'none'; - showHideBtn.classList.remove('active'); - } else { + if (show || !fieldsContainer.classList.contains('show')) { fieldsContainer.classList.add('show'); fieldsContainer.style.display = 'block'; showHideBtn.classList.add('active'); + } else { + fieldsContainer.classList.remove('show'); + fieldsContainer.style.display = 'none'; + showHideBtn.classList.remove('active'); } setHeight(); } +/** + * Retrieves the list of books based on the selected reference type. + * + * @return {void} + */ function retrieveBooks() { document.querySelector('#chapter-range').innerText = ''; document.querySelector('#verse-range').innerText = ''; @@ -573,6 +597,51 @@ function retrieveBooks() { newBook.text = BOOKS.cd[x]; bookList.appendChild(newBook); } + } else if (selectedType == 'hc') { + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + for (var x in BOOKS[selectedType]) { + var newBook = document.createElement("optgroup"); + newBook.label = "Lord's Day " + (parseInt(x) + 1) + var ld = document.createElement("option"); + ld.value = 'ld' + (parseInt(x) + 1); + ld.text = "LD " + (parseInt(x) + 1) + " All"; + newBook.appendChild(ld); + + for (var y in BOOKS[selectedType][x]) { + var question = document.createElement("option"); + question.value = 'hc' + BOOKS[selectedType][x][y]; + question.text = "HC" + BOOKS[selectedType][x][y]; + newBook.appendChild(question); + } + bookList.appendChild(newBook); + } + } else if (selectedType == 'note') { + var none = document.createElement("option"); + none.value = ''; + none.text = '-- Select --'; + bookList.appendChild(none); + + fetch('/retrieve-reference', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'type': 'note' + }) + }) + .then(response => response.json()) + .then(results => { + for (var x in results) { + var newBook = document.createElement("option"); + newBook.value = results[x].id; + newBook.text = results[x].title; + bookList.appendChild(newBook); + } + }); } else { var min = BOOKS[selectedType][0]; var max = BOOKS[selectedType][1]; @@ -589,6 +658,11 @@ function retrieveBooks() { } } +/** + * Filters the books based on the selected reference type and updates the chapter range. + * + * @return {void} This function does not return anything. + */ function filterBooks() { document.querySelector('#chapter-range').innerText = ''; document.querySelector('#verse-range').innerText = ''; @@ -604,6 +678,11 @@ function filterBooks() { chapterRange.innerText = 'Chapters: ' + max; } +/** + * Filters the verse based on the selected book and chapter. + * + * @return {void} This function does not return anything. + */ function filterVerse() { if (document.querySelector('#referenceType').value != 'bible') { return; @@ -622,45 +701,12 @@ function filterVerse() { verseRange.innerText = 'Verse: ' + verse; } -function retrieveReference(el) { - fetch('/index.php/get-reference', { - method: "POST", - header: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - file: el.value, - type: el.options[el.selectedIndex].getAttribute('type') - }) - }) - .then(response => response.json()) - .then(results => { - document.querySelector('#reference').value = results.text; - }); -} - -function saveReference() { - var select = document.querySelector('#references'); - fetch('/index.php/save-reference', { - method: 'POST', - header: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - type: select.options[select.selectedIndex].getAttribute('type'), - file: select.value, - text: document.querySelector('#reference').value - }) - }) - .then(response => response.json()) - .then(results => { - alert(results.msg); - - document.querySelector('#reference').value = ''; - document.querySelector('#references').value = ''; - }); -} - +/** + * Previews a note by rendering the markdown content of the note in a preview section. + * Toggles between the note text and preview sections. + * + * @return {void} This function does not return anything. + */ function previewNote() { var noteText = document.querySelector('#notes'); var notePreview = document.querySelector('#notePreview'); @@ -688,6 +734,11 @@ function previewNote() { findLinks(); } +/** + * Finds all links in the note preview and adds event listeners to them. + * + * @return {void} + */ function findLinks() { var links = document.querySelector('#notePreview').querySelectorAll('a'); @@ -730,6 +781,9 @@ function findLinks() { } } +/** + * Function that finds reference links and fetches passage data when clicked. + */ function findRefLinks() { var links = document.querySelector('#ref').querySelectorAll('a'); @@ -772,6 +826,13 @@ function findRefLinks() { } } +/** + * Shows a passage in a popup element relative to the cursor position. + * + * @param {Event} event - The event that triggered the function. + * @param {string} text - The text to be displayed in the popup. + * @return {void} This function does not return a value. + */ function showPassage(event, text) { // Create a new div element for the popup const popup = document.querySelector('#passage-popup'); @@ -787,12 +848,23 @@ function showPassage(event, text) { popup.style.display = 'block'; } +/** + * Closes the passage popup by clearing its content and hiding it. + * + * @return {void} This function does not return anything. + */ function closePopup() { const popup = document.querySelector('#passage-popup'); popup.innerHTML = ''; popup.style.display = 'none'; } +/** + * Toggles the visibility of the note list and reference elements. + * + * @param {boolean} [openSidebar=true] - Whether to open the sidebar after toggling the visibility. + * @return {void} + */ function openNote(openSidebar = true) { const noteList = document.querySelector('#note-list'); const refs = document.querySelector('#ref'); @@ -810,8 +882,15 @@ function openNote(openSidebar = true) { } } +/** + * Retrieves a note from the server based on the provided ID. + * + * @param {string} id - The ID of the note to retrieve. + * @param {boolean} [runOpen=true] - Whether to open the note sidebar after retrieving the note. + * @return {Promise} A promise that resolves when the note is successfully retrieved and the UI is updated. + */ function retrieveNote(id, runOpen = true) { - fetch('/index.php/get-note', { + fetch('/get-note', { method: 'POST', header: { 'Content-Type': 'application/json' @@ -857,14 +936,91 @@ function retrieveNote(id, runOpen = true) { }); } +/** + * Opens the share note functionality. + */ +function openShareNote() { + var id = document.querySelector('#noteId').value; + if (!id) { + alert('No Open Note Found'); + return; + } + + bd = document.querySelector('#modal-backdrop'); + bd.style.display = 'block'; + cont = document.querySelector('#modal-container'); + cont.style.display = bd.style.display; + + emailCont = document.querySelector('#modal-container'); + emailCont.style.left = ((window.innerWidth / 2) - (emailCont.clientWidth / 2)) + 'px'; + emailCont.style.top = ((window.innerHeight / 2) - (emailCont.clientHeight / 2)) + 'px'; +} + +/** + * Closes the share note modal by hiding the backdrop and container, + * and clears the email input value. + */ +function closeShareNote() { + var bd = document.querySelector('#modal-backdrop'); + var cont = document.querySelector('#modal-container'); + bd.style.display = 'none'; + cont.style.display = 'none'; + document.querySelector('#shareEmail').value = ''; +} + +/** + * Function to share a note by sending the note ID and email to the server. + */ +function shareNote(event) { + var id = document.querySelector('#noteId').value; + var email = document.querySelector('#shareEmail').value; + if (!id || !email) { + alert('Invalid Input'); + return; + } + + fetch('/share-note', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + 'id': id, + 'email': email + }) + }) + .then(response => response.json()) + .then(result => { + if (result) { + alert(result.msg); + } + }); + + closeShareNote(); +} + +/** + * Increases the font size of the element with the id 'ref' by 1 point. + * + * @return {void} This function does not return a value. + */ function increaseFont() { var currentSize = document.querySelector('#ref').style.fontSize; document.querySelector('#ref').style.fontSize = (parseInt(currentSize) + 1) + 'pt'; + document.querySelector('#notes').style.fontSize = (parseInt(currentSize) + 1) + 'pt'; + document.querySelector('#notePreview').style.fontSize = (parseInt(currentSize) + 1) + 'pt'; } +/** + * Decreases the font size of the element with the id 'ref' by 1 point. + * + * @return {void} This function does not return a value. + */ function decreaseFont() { var currentSize = document.querySelector('#ref').style.fontSize; document.querySelector('#ref').style.fontSize = (parseInt(currentSize) - 1) + 'pt'; + document.querySelector('#notes').style.fontSize = (parseInt(currentSize) - 1) + 'pt'; + document.querySelector('#notePreview').style.fontSize = (parseInt(currentSize) - 1) + 'pt'; } /** diff --git a/assets/js/script.min.js b/assets/js/script.min.js new file mode 100644 index 0000000..84500ff --- /dev/null +++ b/assets/js/script.min.js @@ -0,0 +1 @@ +function setBooks(){fetch("/js/data.json").then(res=>{if(!res.ok)throw new Error("HTTP Error: Status: ${res.status}");return res.json()}).then(data=>{BOOKS=data}).catch(error=>{console.log(error)})}function setEventListeners(){document.addEventListener("keyup",function(event){"F3"==event.key&&openRef(!1)}),document.querySelector("#notes").addEventListener("keyup",function(event){let key=event.keyCode;(key>=48&&key<=90||key>=96&&key<=111||key>=186&&key<=222)&&(textDirty=!0,document.querySelector("#note-header-left h2").classList.add("dirty"))})}function setHeight(){md=new markdownit({html:!0,linkify:!0,breaks:!0}),body=document.querySelector("body"),body.style.height=window.innerHeight+"px",cont=document.querySelector("#main"),cont.style.height=window.innerHeight+"px",tabs=document.querySelector(".ref-tab"),tabs.style.height=window.innerHeight-13+"px",ref=document.querySelector(".ref"),ref.style.height=window.innerHeight-60+"px",noteList=document.querySelector("#note-list"),noteList.style.height=window.innerHeight-60+"px",notes=document.querySelector(".notes"),notes.style.height=window.innerHeight-60+"px",notePreview=document.querySelector("#notePreview"),notePreview.style.height=window.innerHeight-50+"px",$("#noteDate")&&$("#noteDate").datepicker(),$("#query")&&document.querySelector("#query").addEventListener("keyup",function(event){"Enter"==event.key&&search()}),to||(to=setTimeout(saveNote,saveInterval))}function search(){query=document.querySelector("#query").value,fetch("/search",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:query})}).then(response=>response.json()).then(results=>{var oldNotes=document.querySelector("#old-notes");for(var n in oldNotes.innerHTML="",results){var link=document.createElement("a");link.href="#",link.setAttribute("onclick","retrieveNote('"+results[n].id+"');openNote();"),link.innerHTML=results[n].title;var p=document.createElement("p");p.innerHTML=results[n].passage;var article=document.createElement("article");article.appendChild(link),article.appendChild(p),oldNotes.append(article)}})}function newNote(){notes=document.querySelector("#notes"),notes.text="",notes.value="",references={},saved=!0,textDirty=!1,document.querySelector("#note-header-left h2").classList.remove("dirty"),dt=new Date,document.querySelector("#noteDate").value=dt.getFullYear()+"-"+(dt.getMonth()<9?"0"+(dt.getMonth()+1):dt.getMonth()+1)+"-"+(dt.getDate()<10?"0"+dt.getDate():dt.getDate()),document.querySelector("#noteTitle").value="",document.querySelector("#speaker").value=0,document.querySelector("#series").value=0,document.querySelector("#template").value=0,document.querySelector("#passage").value="",document.querySelector("#recording").value="",document.querySelector("#noteId").value="",document.querySelector("#ref-list").innerHTML="",document.querySelector("#ref").innerHTML="",document.querySelector(".toggle").click()}function saveNote(event){if(event&&event.preventDefault(),!textDirty||!validateNote())return clearTimeout(to),void(to=setTimeout(saveNote,saveInterval));let saveCheck=document.querySelector("#save-check");var noteText=document.querySelector("#notes").value;startSave();var note={id:document.querySelector("#noteId").value,date:document.querySelector("#noteDate").value,title:document.querySelector("#noteTitle").value,speaker:document.querySelector("#speaker").value,series:document.querySelector("#series").value,passage:document.querySelector("#passage").value,note:document.querySelector("#notes").value,recording:document.querySelector("#recording").value,refs:references};$.ajax({url:"/save-note",method:"POST",contentType:"application/json",data:JSON.stringify(note),dataType:"json",timeout:saveTimeout}).done(function(data){"saved"!=data.msg||saved||(saveFailureCount=SAVE_FAILURE_LIMIT,saveCheck.classList.remove("saving","error","fa-times-circle","fa-save"),showSave(),noteText==document.querySelector("#notes").value&&(saved=!0,textDirty=!1,document.querySelector("#note-header-left h2").classList.remove("dirty")),data.new&&(document.querySelector("#noteId").value=data.id))}).fail(function(xhr,status,error){saveFailureCount--,saveCheck.classList.remove("saving","fa-save"),saveCheck.classList.add("fa-times-circle","error"),console.error(error)}).always(function(xhr,status){"timeout"==status&&(saveCheck.classList.remove("saving","fa-save"),saveCheck.classList.add("error","fa-times-circle")),clearTimeout(to),saveFailureCount>0?to=setTimeout(saveNote,saveInterval):saveFailureCount=SAVE_FAILURE_LIMIT})}function validateNote(){const note=document.querySelector("#notes"),date=document.querySelector("#noteDate"),speaker=document.querySelector("#speaker"),series=document.querySelector("#series"),title=document.querySelector("#noteTitle"),psg=document.querySelector("#passage");return!!title.value.length&&(!!date.value&&(!!parseInt(speaker.value)&&(!!parseInt(series.value)&&(!!psg.value&&!!note.value.length))))}function isUuidValid(uuid){const regex=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[8|9|a|b][a-f0-9]{3}-[a-f0-9]{12}$/i;return regex.test(uuid)}function startSave(){document.querySelector("#save-check").classList.remove("error","fa-times-circle","fa-save"),document.querySelector("#save-check").classList.add("saving","fa-save"),document.querySelector("#save-check").style.opacity=1}function showSave(){if(!saved){var checkmark=document.getElementById("save-check");checkmark.classList.add("fa-save");var si=setInterval(function(){op=parseFloat(checkmark.style.opacity),checkmark.style.opacity=op-.1,.1==checkmark.style.opacity&&(checkmark.style.opacity=0,clearInterval(si),saved=!1)},100)}}function deleteNote(noteId,link){document.querySelector("#noteTitle").value="",document.querySelector("#speaker").value=0,document.querySelector("#series").value=0,document.querySelector("#template").value=0,document.querySelector("#passage").value="",document.querySelector("#notes").value="",document.querySelector("#recording").value="",document.querySelector("#noteDate").value="",document.querySelector("#noteId").value="";var row=link.parentElement.parentElement;fetch("/delete-note",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:noteId})}).then(response=>response.json()).then(data=>{"deleted"==data.msg&&(alert("Note deleted."),row.remove())})}function newSpeaker(){"new"==document.querySelector("#speaker").value&&(document.querySelector("#newSpeaker").style.display="inline-block",document.querySelector("#speaker").style.display="none"),saved=!1,textDirty=!0}function saveSpeaker(event){13==event.keyCode&&fetch("/save-speaker",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({speakerName:document.querySelector("#newSpeaker").value})}).then(response=>response.json()).then(results=>{var newSpeaker=document.createElement("option");newSpeaker.text=document.querySelector("#newSpeaker").value,newSpeaker.value=results.id,document.querySelector("#speaker").add(newSpeaker),alert(results.msg),document.querySelector("#newSpeaker").style.display="none",document.querySelector("#speaker").style.display="inline-block",document.querySelector("#newSpeaker").value="",document.querySelector("#speaker").value=results.id})}function newSeries(){"new"==document.querySelector("#series").value&&(document.querySelector("#newSeries").style.display="inline-block",document.querySelector("#series").style.display="none"),saved=!1,textDirty=!0}function saveSeries(event){13==event.keyCode&&fetch("/save-series",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({seriesName:document.querySelector("#newSeries").value})}).then(response=>response.json()).then(results=>{var newSeries=document.createElement("option");newSeries.text=document.querySelector("#newSeries").value,newSeries.value=results.id,document.querySelector("#series").add(newSeries),alert(results.msg),document.querySelector("#newSeries").style.display="none",document.querySelector("#series").style.display="inline-block",document.querySelector("#newSeries").value="",document.querySelector("#series").value=results.id})}function openRef(closeSidebar=!0){document.querySelector("#openRefBtn").classList.add("active"),refQuery=document.querySelector("#refQuery"),refQuery.style.display="block",ref=document.querySelector("#ref"),refQuery.style.left=ref.offsetLeft+"px",refQuery.style.top=ref.offsetTop+"px",closeSidebar&&document.querySelector(".toggle").click()}function closeRef(){document.querySelector("#referenceSearch").value="",document.querySelector("#referenceSearch").style.display="",document.querySelector("#referenceType").value="",document.querySelector("#referenceBook").value="",document.querySelector("#referenceBook").style.display="none",document.querySelector("#chapter-range").innerText="",document.querySelector("#verse-range").innerText="",document.querySelector("#refQuery").style.display="none",document.querySelector("#openRefBtn").classList.remove("active")}function queryRef(type=null,book=null,input=null){if(!input)var input=document.querySelector("#refQuery #referenceSearch").value;if(!type)var type=document.querySelector("#referenceType").value;if(!book)var book=document.querySelector("#referenceBook").value;fetch("/retrieve-reference",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:type,book:book,reference:input})}).then(response=>response.json()).then(results=>{const list=document.querySelector("#ref-list");var newList=document.createElement("li");newList.className="tab",button=makeButton(results.title),newList.appendChild(button),list.appendChild(newList);const ref=document.querySelector("#ref");ref.innerHTML=md.render(results.text),references[results.title]=results.text,closeRef(),saved=!1,textDirty=!0,saveNote(),findRefLinks()})}function makeButton(title){var btn=document.createElement("button");return btn.innerText=title,btn.class="button",btn.addEventListener("click",function(){removeActiveRef(),document.querySelector("#ref").innerHTML=md.render(references[title]),this.classList.add("active"),findRefLinks()}),btn.addEventListener("dblclick",function(){document.querySelector("#ref").innerHTML="",delete references[title];var list=this.parentElement;list.remove(),saved=!1,textDirty=!0,saveNote()}),removeActiveRef(),btn.classList.add("active"),btn}function removeActiveRef(){for(var t in tabs=document.querySelectorAll(".active"),tabs)isFinite(parseInt(t))&&tabs[t].classList.remove("active")}function retrieveTemplate(orig,dest){const temp=document.querySelector("#"+orig);"0"!=temp.value?fetch("/retrieve-template",{method:"POST",headers:{"Content-Type":"plain/text"},body:JSON.stringify({template:temp.value})}).then(response=>response.text()).then(results=>{const div=document.querySelector("#"+dest);div.value=results}):document.querySelector("#"+dest).value=""}function saveTemplate(){fetch("/save-template",{method:"POST",headers:{"Content-Type":"plain/text"},body:JSON.stringify({template_id:document.querySelector("#template_id").value,template_name:document.querySelector("#template_name").value,template_value:document.querySelector("#template_value").value})}).then(response=>response.text()).then(results=>{alert(results)})}function toggleFields(){const fieldsContainer=document.getElementById("fields-container"),showHideBtn=document.getElementById("show-hide-btn");fieldsContainer.classList.contains("show")?(fieldsContainer.classList.remove("show"),fieldsContainer.style.display="none",showHideBtn.classList.remove("active")):(fieldsContainer.classList.add("show"),fieldsContainer.style.display="block",showHideBtn.classList.add("active")),setHeight()}function retrieveBooks(){document.querySelector("#chapter-range").innerText="",document.querySelector("#verse-range").innerText="",document.querySelector("#referenceSearch").value="",document.querySelector("#referenceSearch").style.display="none";const selectedType=document.querySelector("#referenceType").value;if(selectedType){var bookList=document.querySelector("#referenceBook");if(bookList.style.display="block",bookList.innerHTML="","bible"==selectedType){document.querySelector("#referenceSearch").style.display="block";var none=document.createElement("option");for(var x in none.value="",none.text="-- Select --",bookList.appendChild(none),BOOKS.bible){var newBook=document.createElement("option");newBook.text=x,bookList.appendChild(newBook)}}else if("creed"==selectedType){var none=document.createElement("option");for(var x in none.value="",none.text="-- Select --",bookList.appendChild(none),BOOKS.creed){var newBook=document.createElement("option");newBook.value=x,newBook.text=BOOKS.creed[x],bookList.appendChild(newBook)}}else if("cd"==selectedType){var none=document.createElement("option");for(var x in none.value="",none.text="-- Select --",bookList.appendChild(none),BOOKS.cd){var newBook=document.createElement("option");newBook.text=BOOKS.cd[x],bookList.appendChild(newBook)}}else if("hc"==selectedType){var none=document.createElement("option");for(var x in none.value="",none.text="-- Select --",bookList.appendChild(none),BOOKS[selectedType]){var newBook=document.createElement("optgroup");newBook.label="Lord's Day "+(parseInt(x)+1);var ld=document.createElement("option");for(var y in ld.value=parseInt(x)+1,ld.text="LD "+(parseInt(x)+1)+" All",newBook.appendChild(ld),BOOKS[selectedType][x]){var question=document.createElement("option");question.value=y,question.text="HC"+BOOKS[selectedType][x][y],newBook.appendChild(question)}bookList.appendChild(newBook)}}else if("note"==selectedType){var none=document.createElement("option");none.value="",none.text="-- Select --",bookList.appendChild(none),fetch("/retrieve-reference",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"note"})}).then(response=>response.json()).then(results=>{for(var x in results){var newBook=document.createElement("option");newBook.value=results[x].id,newBook.text=results[x].title,bookList.appendChild(newBook)}})}else{var min=BOOKS[selectedType][0],max=BOOKS[selectedType][1],none=document.createElement("option");none.value="",none.text="-- Select --",bookList.appendChild(none);for(var x=min;x<=max;x++){var newBook=document.createElement("option");newBook.value=x,newBook.text=x,bookList.appendChild(newBook)}}}}function filterBooks(){if(document.querySelector("#chapter-range").innerText="",document.querySelector("#verse-range").innerText="","bible"==document.querySelector("#referenceType").value){var bookList=document.querySelector("#referenceBook"),book=BOOKS.bible[bookList.value],max=Object.keys(book).length,chapterRange=document.querySelector("#chapter-range");chapterRange.innerText="Chapters: "+max}}function filterVerse(){if("bible"==document.querySelector("#referenceType").value){var bookList=document.querySelector("#referenceBook").value,search=document.querySelector("#referenceSearch").value,chapter=search.split(":")[0],verseRange=document.querySelector("#verse-range");if(BOOKS.bible[bookList]&&BOOKS.bible[bookList][chapter]){var verse=BOOKS.bible[bookList][chapter];verseRange.innerText="Verse: "+verse}else verseRange.innerText="Unknown Chapter"}}function retrieveReferenceType(el){fetch("/reference/"+el.value,{method:"GET",header:{"Content-Type":"application/json"}}).then(response=>response.json()).then(results=>{document.querySelector("#referenceSeries").innerHTML="";var none=document.createElement("option");for(var x in none.value="",none.text="-- Select --",document.querySelector("#referenceSeries").appendChild(none),results){var newSeries=document.createElement("option");newSeries.value=results[x].id,newSeries.text=results[x].label,document.querySelector("#referenceSeries").appendChild(newSeries)}})}function retrieveReference(el){"new"!=el.value?fetch("/get-reference",{method:"POST",header:{"Content-Type":"application/json"},body:JSON.stringify({id:el.value})}).then(response=>response.json()).then(results=>{document.querySelector("#reference").value=results.text}):document.querySelector("#refName").style.display="inline-block"}function saveReference(){var select=document.querySelector("#references");fetch("/save-reference",{method:"POST",header:{"Content-Type":"application/json"},body:JSON.stringify({type:select.options[select.selectedIndex].getAttribute("type"),file:select.value,text:document.querySelector("#reference").value})}).then(response=>response.json()).then(results=>{alert(results.msg),document.querySelector("#reference").value="",document.querySelector("#references").value=""})}function previewNote(){var noteText=document.querySelector("#notes"),notePreview=document.querySelector("#notePreview"),previewButton=document.querySelector("#previewBtn");const title=document.querySelector("#noteTitle"),speaker=document.querySelector("#speaker"),passage=document.querySelector("#passage"),markdownPreview="# "+title.value+" - "+speaker.options[speaker.selectedIndex].text+" - "+passage.value+"\n\n"+noteText.value;notePreview.innerHTML=md.render(markdownPreview),previewButton.classList.contains("active")?(noteText.style.display="block",notePreview.style.display="none",previewButton.classList.remove("active")):(noteText.style.display="none",notePreview.style.display="block",previewButton.classList.add("active")),findLinks()}function findLinks(){for(var links=document.querySelector("#notePreview").querySelectorAll("a"),i=0;iresponse.text()).then(result=>{passage=passage.replace(/\+/g," "),psg=passage.split(" "),psg.length>2?(book=psg[0]+" "+psg[1],cv=psg[2]):(book=psg[0],cv=psg[1]),showPassage(e,"  
"+result)})}})}function findRefLinks(){for(var links=document.querySelector("#ref").querySelectorAll("a"),i=0;iresponse.text()).then(result=>{passage=passage.replace(/\+/g," "),psg=passage.split(" "),psg.length>2?(book=psg[0]+" "+psg[1],cv=psg[2]):(book=psg[0],cv=psg[1]),showPassage(e,"  
"+result)})}})}function showPassage(event,text){const popup=document.querySelector("#passage-popup");popup.innerHTML=md.render(text);let x=event.clientX+window.scrollX,y=event.clientY+window.scrollY;popup.style.top=`${y}px`,popup.style.left=`${x}px`,popup.style.display="block"}function closePopup(){const popup=document.querySelector("#passage-popup");popup.innerHTML="",popup.style.display="none"}function openNote(openSidebar=!0){const noteList=document.querySelector("#note-list"),refs=document.querySelector("#ref");"block"==noteList.style.display?(noteList.style.display="none",refs.style.display="block"):(noteList.style.display="block",refs.style.display="none"),openSidebar&&document.querySelector(".toggle").click()}function retrieveNote(id,runOpen=!0){fetch("/get-note",{method:"POST",header:{"Content-Type":"application/json"},body:JSON.stringify({id:id})}).then(response=>response.json()).then(result=>{var dt=new Date(result.date.date);document.querySelector("#notes").value=result.text,document.querySelector("#passage").value=result.passage,document.querySelector("#series").value=result.series.id,document.querySelector("#speaker").value=result.speaker.id,document.querySelector("#noteTitle").value=result.title,document.querySelector("#noteDate").value="",document.querySelector("#noteDate").value=(dt.getMonth()<9?"0"+(dt.getMonth()+1):dt.getMonth()+1)+"/"+(dt.getDate()<10?"0"+dt.getDate():dt.getDate())+"/"+dt.getFullYear(),document.querySelector("#noteId").value=result.id,result.refs&&(references=result.refs);const list=document.querySelector("#ref-list");list.innerHTML="";var newList=null;for(var x in references){var newList=document.createElement("li");newList.className="tab";var button=makeButton(x);newList.appendChild(button),list.appendChild(newList)}runOpen&&openNote(!1)})}function openShareNote(){var id=document.querySelector("#noteId").value;id?(bd=document.querySelector("#modal-backdrop"),bd.style.display="block",cont=document.querySelector("#modal-container"),cont.style.display=bd.style.display,emailCont=document.querySelector("#modal-container"),emailCont.style.left=window.innerWidth/2-emailCont.clientWidth/2+"px",emailCont.style.top=window.innerHeight/2-emailCont.clientHeight/2+"px"):alert("No Open Note Found")}function closeShareNote(){var bd=document.querySelector("#modal-backdrop"),cont=document.querySelector("#modal-container");bd.style.display="none",cont.style.display="none",document.querySelector("#shareEmail").value=""}function shareNote(event){var id=document.querySelector("#noteId").value,email=document.querySelector("#shareEmail").value;id&&email?(fetch("/share-note",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:id,email:email})}).then(response=>response.json()).then(result=>{result&&alert(result.msg)}),closeShareNote()):alert("Invalid Input")}function increaseFont(){var currentSize=document.querySelector("#ref").style.fontSize;document.querySelector("#ref").style.fontSize=parseInt(currentSize)+1+"pt"}function decreaseFont(){var currentSize=document.querySelector("#ref").style.fontSize;document.querySelector("#ref").style.fontSize=parseInt(currentSize)-1+"pt"}function uuidv4(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(c){const r=16*Math.random()|0,v="x"==c?r:3&r|8;return v.toString(16)})}var md=null,references={},tabs=[];let saved=!1,textDirty=!1;var to=null;let controller;var BOOKS={};$(function(){setHeight(),setBooks(),setEventListeners(),$("#note-table").DataTable({paging:!1,ajax:{url:"/get-notes",type:"POST"},columns:[{data:"link"},{data:"speaker.name"},{data:"passage"},{data:"date.date",render:DataTable.render.date("L")}]}),$("#shareBtn").on("click",openShareNote),$("#modal-backdrop").on("click",closeShareNote)}); \ No newline at end of file diff --git a/assets/js/site.js b/assets/js/site.js new file mode 100644 index 0000000..ba1d05b --- /dev/null +++ b/assets/js/site.js @@ -0,0 +1,116 @@ +export function initHome() { + setHeight(); + setBooks(); + setEventListeners(); + $('#note-table').DataTable({ + paging: false, + ajax: { + url: '/get-notes', + type: 'POST' + }, + columns: [ + { data: 'link' }, + { data: 'speaker.name' }, + { data: 'passage' }, + { + data: 'date.date', + render: DataTable.render.date("L") + }, + ] + }); + $('#shareBtn').on('click', openShareNote); + $('#modal-backdrop').on('click', closeShareNote); +} + +/** + * Fetches data from '/js/data.json', assigns it to BOOKS, and handles errors. + * + * @return {void} + */ +export function setBooks() { + fetch('js/data.json') + .then((res) => { + if (!res.ok) { + throw new Error('HTTP Error: Status: ${res.status}'); + } + return res.json(); + }) + .then((data) => { + BOOKS = data; + }) + .catch((error) => { + console.log(error); + }) +} + +/** + * Sets event listeners for keyup events on the document and the '#notes' element. + * + * @return {void} + */ +export function setEventListeners() { + document.addEventListener('keyup', function (event) { + if (event.key == "F3") { + openRef(false); + } + }); + + document.querySelector('#notes').addEventListener('keyup', function (event) { + let key = event.keyCode; + + if (key == 8 || key >= 48 && key <= 90 || key >= 96 && key <= 111 || key >= 186 && key <= 222) { + textDirty = true; + document.querySelector('#note-header-left h2').classList.add('dirty'); + } + }); +} + +/** + * Sets the height of various elements on the page based on the window's inner height. + * Also initializes a datepicker and event listener for the search input field. + * + * @return {void} + */ +export function setHeight() { + md = new markdownit({ + html: true, + linkify: true, + breaks: true + }); + + var body = document.querySelector('body'); + body.style.height = window.innerHeight + 'px'; + + var cont = document.querySelector('#main'); + cont.style.height = (window.innerHeight) + 'px'; + + var tabs = document.querySelector('.ref-tab'); + tabs.style.height = (window.innerHeight - 13) + 'px'; + + var ref = document.querySelector('.ref'); + ref.style.height = (window.innerHeight - 60) + 'px'; + + var noteList = document.querySelector('#note-list'); + noteList.style.height = (window.innerHeight - 60) + 'px'; + + var notes = document.querySelector('.notes'); + notes.style.height = (window.innerHeight - 60) + 'px'; + + var notePreview = document.querySelector('#notePreview'); + notePreview.style.height = (window.innerHeight - 50) + 'px'; + + if ($('#noteDate')) { + $('#noteDate').datepicker(); + } + + if ($('#query')) { + document.querySelector('#query').addEventListener('keyup', function (event) { + if (event.key == "Enter") { + search(); + } + }); + } + if (!to) { + to = setTimeout(saveNote, saveInterval); + } +} diff --git a/assets/js/template.js b/assets/js/template.js new file mode 100644 index 0000000..ae91649 --- /dev/null +++ b/assets/js/template.js @@ -0,0 +1,49 @@ +/** + * Retrieves a template from the server and sets it as the value of a specified destination element. + * + * @param {string} orig - The ID of the element containing the original template value. + * @param {string} dest - The ID of the destination element where the retrieved template will be set. + * @return {Promise} A Promise that resolves when the template is successfully retrieved and set as the value of the destination element. + */ +export function retrieveTemplate(orig, dest) { + const temp = document.querySelector('#' + orig); + if (temp.value == '0') { + document.querySelector('#' + dest).value = ''; + return; + } + fetch('/retrieve-template', { + method: 'POST', + headers: { + 'Content-Type': 'plain/text' + }, + body: JSON.stringify({ + 'template': temp.value + }) + }) + .then(response => response.text()) + .then(results => { + const div = document.querySelector('#' + dest); + div.value = results; + }); +} + +/** + * Saves the template by sending a POST request to the server with template data. + */ +export function saveTemplate() { + fetch('/save-template', { + method: 'POST', + headers: { + 'Content-Type': 'plain/text' + }, + body: JSON.stringify({ + 'template_id': document.querySelector('#template_id').value, + 'template_name': document.querySelector('#template_name').value, + 'template_value': document.querySelector('#template_value').value, + }) + }) + .then(response => response.text()) + .then(results => { + alert(results); + }); +} diff --git a/public/theme/assets/js/util.js b/assets/js/util.js similarity index 100% rename from public/theme/assets/js/util.js rename to assets/js/util.js diff --git a/public/theme/assets/sass/base/_page.scss b/assets/sass/base/_page.scss similarity index 100% rename from public/theme/assets/sass/base/_page.scss rename to assets/sass/base/_page.scss diff --git a/public/theme/assets/sass/base/_reset.scss b/assets/sass/base/_reset.scss similarity index 100% rename from public/theme/assets/sass/base/_reset.scss rename to assets/sass/base/_reset.scss diff --git a/public/theme/assets/sass/base/_typography.scss b/assets/sass/base/_typography.scss similarity index 100% rename from public/theme/assets/sass/base/_typography.scss rename to assets/sass/base/_typography.scss diff --git a/public/theme/assets/sass/components/_actions.scss b/assets/sass/components/_actions.scss similarity index 100% rename from public/theme/assets/sass/components/_actions.scss rename to assets/sass/components/_actions.scss diff --git a/public/theme/assets/sass/components/_box.scss b/assets/sass/components/_box.scss similarity index 100% rename from public/theme/assets/sass/components/_box.scss rename to assets/sass/components/_box.scss diff --git a/public/theme/assets/sass/components/_button.scss b/assets/sass/components/_button.scss similarity index 100% rename from public/theme/assets/sass/components/_button.scss rename to assets/sass/components/_button.scss diff --git a/public/theme/assets/sass/components/_contact.scss b/assets/sass/components/_contact.scss similarity index 100% rename from public/theme/assets/sass/components/_contact.scss rename to assets/sass/components/_contact.scss diff --git a/public/theme/assets/sass/components/_features.scss b/assets/sass/components/_features.scss similarity index 100% rename from public/theme/assets/sass/components/_features.scss rename to assets/sass/components/_features.scss diff --git a/public/theme/assets/sass/components/_form.scss b/assets/sass/components/_form.scss similarity index 100% rename from public/theme/assets/sass/components/_form.scss rename to assets/sass/components/_form.scss diff --git a/public/theme/assets/sass/components/_icon.scss b/assets/sass/components/_icon.scss similarity index 100% rename from public/theme/assets/sass/components/_icon.scss rename to assets/sass/components/_icon.scss diff --git a/public/theme/assets/sass/components/_icons.scss b/assets/sass/components/_icons.scss similarity index 100% rename from public/theme/assets/sass/components/_icons.scss rename to assets/sass/components/_icons.scss diff --git a/public/theme/assets/sass/components/_image.scss b/assets/sass/components/_image.scss similarity index 100% rename from public/theme/assets/sass/components/_image.scss rename to assets/sass/components/_image.scss diff --git a/public/theme/assets/sass/components/_list.scss b/assets/sass/components/_list.scss similarity index 100% rename from public/theme/assets/sass/components/_list.scss rename to assets/sass/components/_list.scss diff --git a/public/theme/assets/sass/components/_mini-posts.scss b/assets/sass/components/_mini-posts.scss similarity index 100% rename from public/theme/assets/sass/components/_mini-posts.scss rename to assets/sass/components/_mini-posts.scss diff --git a/public/theme/assets/sass/components/_pagination.scss b/assets/sass/components/_pagination.scss similarity index 100% rename from public/theme/assets/sass/components/_pagination.scss rename to assets/sass/components/_pagination.scss diff --git a/public/theme/assets/sass/components/_posts.scss b/assets/sass/components/_posts.scss similarity index 100% rename from public/theme/assets/sass/components/_posts.scss rename to assets/sass/components/_posts.scss diff --git a/public/theme/assets/sass/components/_row.scss b/assets/sass/components/_row.scss similarity index 100% rename from public/theme/assets/sass/components/_row.scss rename to assets/sass/components/_row.scss diff --git a/public/theme/assets/sass/components/_section.scss b/assets/sass/components/_section.scss similarity index 100% rename from public/theme/assets/sass/components/_section.scss rename to assets/sass/components/_section.scss diff --git a/public/theme/assets/sass/components/_table.scss b/assets/sass/components/_table.scss similarity index 100% rename from public/theme/assets/sass/components/_table.scss rename to assets/sass/components/_table.scss diff --git a/public/theme/assets/sass/layout/_banner.scss b/assets/sass/layout/_banner.scss similarity index 100% rename from public/theme/assets/sass/layout/_banner.scss rename to assets/sass/layout/_banner.scss diff --git a/public/theme/assets/sass/layout/_footer.scss b/assets/sass/layout/_footer.scss similarity index 100% rename from public/theme/assets/sass/layout/_footer.scss rename to assets/sass/layout/_footer.scss diff --git a/public/theme/assets/sass/layout/_header.scss b/assets/sass/layout/_header.scss similarity index 100% rename from public/theme/assets/sass/layout/_header.scss rename to assets/sass/layout/_header.scss diff --git a/public/theme/assets/sass/layout/_main.scss b/assets/sass/layout/_main.scss similarity index 100% rename from public/theme/assets/sass/layout/_main.scss rename to assets/sass/layout/_main.scss diff --git a/public/theme/assets/sass/layout/_menu.scss b/assets/sass/layout/_menu.scss similarity index 100% rename from public/theme/assets/sass/layout/_menu.scss rename to assets/sass/layout/_menu.scss diff --git a/public/theme/assets/sass/layout/_sidebar.scss b/assets/sass/layout/_sidebar.scss similarity index 100% rename from public/theme/assets/sass/layout/_sidebar.scss rename to assets/sass/layout/_sidebar.scss diff --git a/public/theme/assets/sass/layout/_wrapper.scss b/assets/sass/layout/_wrapper.scss similarity index 100% rename from public/theme/assets/sass/layout/_wrapper.scss rename to assets/sass/layout/_wrapper.scss diff --git a/public/theme/assets/sass/libs/_breakpoints.scss b/assets/sass/libs/_breakpoints.scss similarity index 100% rename from public/theme/assets/sass/libs/_breakpoints.scss rename to assets/sass/libs/_breakpoints.scss diff --git a/public/theme/assets/sass/libs/_functions.scss b/assets/sass/libs/_functions.scss similarity index 100% rename from public/theme/assets/sass/libs/_functions.scss rename to assets/sass/libs/_functions.scss diff --git a/public/theme/assets/sass/libs/_html-grid.scss b/assets/sass/libs/_html-grid.scss similarity index 100% rename from public/theme/assets/sass/libs/_html-grid.scss rename to assets/sass/libs/_html-grid.scss diff --git a/public/theme/assets/sass/libs/_mixins.scss b/assets/sass/libs/_mixins.scss similarity index 100% rename from public/theme/assets/sass/libs/_mixins.scss rename to assets/sass/libs/_mixins.scss diff --git a/public/theme/assets/sass/libs/_vars.scss b/assets/sass/libs/_vars.scss similarity index 100% rename from public/theme/assets/sass/libs/_vars.scss rename to assets/sass/libs/_vars.scss diff --git a/public/theme/assets/sass/libs/_vendor.scss b/assets/sass/libs/_vendor.scss similarity index 100% rename from public/theme/assets/sass/libs/_vendor.scss rename to assets/sass/libs/_vendor.scss diff --git a/public/theme/assets/sass/main.css b/assets/sass/main.css similarity index 100% rename from public/theme/assets/sass/main.css rename to assets/sass/main.css diff --git a/public/theme/assets/sass/main.css.map b/assets/sass/main.css.map similarity index 100% rename from public/theme/assets/sass/main.css.map rename to assets/sass/main.css.map diff --git a/public/theme/assets/sass/main.min.css b/assets/sass/main.min.css similarity index 100% rename from public/theme/assets/sass/main.min.css rename to assets/sass/main.min.css diff --git a/public/theme/assets/sass/main.min.css.map b/assets/sass/main.min.css.map similarity index 100% rename from public/theme/assets/sass/main.min.css.map rename to assets/sass/main.min.css.map diff --git a/public/theme/assets/sass/main.scss b/assets/sass/main.scss similarity index 100% rename from public/theme/assets/sass/main.scss rename to assets/sass/main.scss diff --git a/public/css/style.css b/assets/styles/style.css similarity index 72% rename from public/css/style.css rename to assets/styles/style.css index 2119088..782d913 100644 --- a/public/css/style.css +++ b/assets/styles/style.css @@ -18,7 +18,7 @@ body { .ref-tab { width: 60px; - padding-top: 75px !important; + padding-top: 60px !important; } .ref-tab ul { @@ -43,24 +43,12 @@ body { height: 80px; width: 100%; text-align: center; - background-color: #f56a6a; - color: #fff !important; - border: none; - border-radius: 3px; - box-shadow: 0 4px 5px rgba(0, 0, 0, 0.8); font-size: 14px; margin-bottom: 3px; } -.tab button:active { - background-color: #7a0016; -} - -.activeRef { - background-color: #3e8e41 !important; -} .ref { - width: 35%; + width: 36%; } .ref > div#ref { @@ -76,7 +64,7 @@ body { } #ref { - padding: 3px 3px 3px 10px; + padding: 3px 3px 3px 3px; } #passage { @@ -102,7 +90,7 @@ body { } .notes { - width: 55%; + width: 57%; } textarea#notes { @@ -114,10 +102,18 @@ textarea#notes { #notePreview { display: none; overflow-x: scroll; + padding-left: 10px; +} + +#notePreview ul, +#notePreview ol { + list-style-position: inside; } #previewBtn.active, -#show-hide-btn.active { +#show-hide-btn.active, +.tab button.active, +#openRefBtn.active { background-color: #f56a6a !important; color: white !important; } @@ -140,6 +136,8 @@ textarea#notes { #note-list { display: none; + height: 100%; + overflow-y: scroll; } #note-list ul { @@ -205,7 +203,7 @@ div#refQuery #referenceSearch { #fields-container input, #fields-container select { - width: 19.5%; + width: 32.5%; display: inline-block; } @@ -234,6 +232,65 @@ div#refQuery #referenceSearch { font-size: 12pt; } +#ref ol, +#ref ul { + list-style-position: inside; +} + #referenceBook { display: none; +} + +.recording-link { + font-size: 8pt; + color: blue; + text-decoration: none; +} + +.fas-trash-alt { + color: red; + cursor: pointer; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #000; + opacity: 0.5; + z-index: 1000; +} + +.modal-container { + position: absolute; + width: 300px; + /* adjust this to your desired modal width */ + margin: 0 auto; + padding: 20px; + border: 1px solid #ddd; + background-color: #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + z-index: 1001; +} + +.modal-header { + background-color: #f5f5f5; + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.modal-body { + padding: 20px; +} + +.modal-footer { + background-color: #f5f5f5; + padding: 10px; + border-top: 1px solid #ddd; +} + +.btn-secondary { + color: #337ab7; }/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/assets/styles/style.css.map b/assets/styles/style.css.map new file mode 100644 index 0000000..561374b --- /dev/null +++ b/assets/styles/style.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA,kCAAA;AACA;EACI,SAAA;ACCJ;;ADEA,6BAAA;AAEA;EACI,aAAA;EACA,mBAAA;EACA,eAAA;EACA,6BAAA;EACA,oBAAA;EACA,yBAAA;EACA,iBAAA;EACA,iCAAA;EACA,cAAA;ACAJ;;ADGA;EACI,WAAA;EACA,4BAAA;ACAJ;;ADGA;EACI,SAAA;EACA,UAAA;ACAJ;;ADGA;EACI,gBAAA;EACA,qBAAA;ACAJ;;ADGA;EACI,kBAAA;ACAJ;;ADGA,GAAA;AACA;EACI,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,eAAA;EACA,kBAAA;ACAJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;EACI,mBAAA;EACA,2BAAA;EACA,yBAAA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,uBAAA;EACA,kBAAA;EACA,wCAAA;ACDJ;;ADIA;EACI,wBAAA;ACDJ;;ADIA;EACI,YAAA;ACDJ;;ADIA;EACI,aAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,cAAA;ACDJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;EACI,WAAA;EACA,YAAA;EACA,eAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;EACA,kBAAA;ACDJ;;ADIA;;EAEI,2BAAA;ACDJ;;ADIA;;;;EAII,oCAAA;EACA,uBAAA;ACDJ;;ADIA;EACI,oBAAA;EACA,mBAAA;EACA,UAAA;ACDJ;;ADIA;EACI,oBAAA;EACA,2BAAA;EACA,UAAA;ACDJ;;ADIA;EACI,iBAAA;ACDJ;;ADIA;EACI,aAAA;EACA,YAAA;EACA,kBAAA;ACDJ;;ADIA;EACI,SAAA;EACA,UAAA;EACA,qBAAA;EACA,gBAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;EACA,YAAA;EACA,oCAAA;EACA,YAAA;EACA,aAAA;ACDJ;;ADIA;EACI,YAAA;EACA,kBAAA;EACA,kBAAA;EACA,eAAA;EACA,gBAAA;EACA,WAAA;EACA,yBAAA;EACA,YAAA;EACA,YAAA;EACA,eAAA;EACA,aAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;EACA,YAAA;EACA,sBAAA;EACA,YAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,wCAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;ACDJ;;ADIA;EACI,UAAA;EACA,iBAAA;EACA,eAAA;EACA,iBAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;;EAEI,YAAA;EACA,qBAAA;ACDJ;;ADIA;EACI,2BAAA;EACA,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,qBAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;;EAEI,2BAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,cAAA;EACA,WAAA;EACA,qBAAA;ACDJ;;ADIA;EACI,UAAA;EACA,eAAA;ACDJ;;ADIA;EACI,eAAA;EACA,MAAA;EACA,QAAA;EACA,SAAA;EACA,OAAA;EACA,sBAAA;EACA,YAAA;EACA,aAAA;ACDJ;;ADIA;EACI,kBAAA;EACA,YAAA;EACA,4CAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,sBAAA;EACA,uCAAA;EACA,aAAA;ACDJ;;ADIA;EACI,yBAAA;EACA,aAAA;EACA,6BAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,yBAAA;EACA,aAAA;EACA,0BAAA;ACDJ;;ADIA;EACI,cAAA;ACDJ","file":"style.css"} \ No newline at end of file diff --git a/public/css/style.scss b/assets/styles/style.scss similarity index 72% rename from public/css/style.scss rename to assets/styles/style.scss index fee1b29..994c820 100644 --- a/public/css/style.scss +++ b/assets/styles/style.scss @@ -19,7 +19,7 @@ body { .ref-tab { width: 60px; - padding-top: 75px !important; + padding-top: 60px !important; } .ref-tab ul { @@ -44,25 +44,13 @@ body { height: 80px; width: 100%; text-align: center; - background-color: #f56a6a; - color: #fff !important; - border: none; - border-radius: 3px; - box-shadow: 0 4px 5px rgba(0, 0, 0, 0.8); font-size: 14px; margin-bottom: 3px; - - &:active { - background-color: #7a0016; - } } -.activeRef { - background-color: #3e8e41 !important; -} .ref { - width: 35%; + width: 36%; } .ref>div#ref { @@ -78,7 +66,7 @@ body { } #ref { - padding: 3px 3px 3px 10px; + padding: 3px 3px 3px 3px; } #passage { @@ -104,7 +92,7 @@ body { } .notes { - width: 55% + width: 57% } textarea#notes { @@ -116,10 +104,18 @@ textarea#notes { #notePreview { display: none; overflow-x: scroll; + padding-left: 10px; +} + +#notePreview ul, +#notePreview ol { + list-style-position: inside; } #previewBtn.active, -#show-hide-btn.active { +#show-hide-btn.active, +.tab button.active, +#openRefBtn.active { background-color: #f56a6a !important; color: white !important; } @@ -142,6 +138,8 @@ textarea#notes { #note-list { display: none; + height: 100%; + overflow-y: scroll; } #note-list ul { @@ -207,7 +205,7 @@ div#refQuery #referenceSearch { #fields-container input, #fields-container select { - width: 19.5%; + width: 32.5%; display: inline-block; } @@ -236,6 +234,65 @@ div#refQuery #referenceSearch { font-size: 12pt; } +#ref ol, +#ref ul { + list-style-position: inside; +} + #referenceBook { display: none; +} + +.recording-link { + font-size: 8pt; + color: blue; + text-decoration: none; +} + +.fas-trash-alt { + color: red; + cursor: pointer; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #000; + opacity: 0.5; + z-index: 1000; +} + +.modal-container { + position: absolute; + width: 300px; + /* adjust this to your desired modal width */ + margin: 0 auto; + padding: 20px; + border: 1px solid #ddd; + background-color: #fff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + z-index: 1001; +} + +.modal-header { + background-color: #f5f5f5; + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.modal-body { + padding: 20px; +} + +.modal-footer { + background-color: #f5f5f5; + padding: 10px; + border-top: 1px solid #ddd; +} + +.btn-secondary { + color: #337ab7; } \ No newline at end of file diff --git a/public/theme/assets/webfonts/fa-brands-400.eot b/assets/webfonts/fa-brands-400.eot similarity index 100% rename from public/theme/assets/webfonts/fa-brands-400.eot rename to assets/webfonts/fa-brands-400.eot diff --git a/public/theme/assets/webfonts/fa-brands-400.svg b/assets/webfonts/fa-brands-400.svg similarity index 100% rename from public/theme/assets/webfonts/fa-brands-400.svg rename to assets/webfonts/fa-brands-400.svg diff --git a/public/theme/assets/webfonts/fa-brands-400.ttf b/assets/webfonts/fa-brands-400.ttf similarity index 100% rename from public/theme/assets/webfonts/fa-brands-400.ttf rename to assets/webfonts/fa-brands-400.ttf diff --git a/public/theme/assets/webfonts/fa-brands-400.woff b/assets/webfonts/fa-brands-400.woff similarity index 100% rename from public/theme/assets/webfonts/fa-brands-400.woff rename to assets/webfonts/fa-brands-400.woff diff --git a/public/theme/assets/webfonts/fa-brands-400.woff2 b/assets/webfonts/fa-brands-400.woff2 similarity index 100% rename from public/theme/assets/webfonts/fa-brands-400.woff2 rename to assets/webfonts/fa-brands-400.woff2 diff --git a/public/theme/assets/webfonts/fa-regular-400.eot b/assets/webfonts/fa-regular-400.eot similarity index 100% rename from public/theme/assets/webfonts/fa-regular-400.eot rename to assets/webfonts/fa-regular-400.eot diff --git a/public/theme/assets/webfonts/fa-regular-400.svg b/assets/webfonts/fa-regular-400.svg similarity index 100% rename from public/theme/assets/webfonts/fa-regular-400.svg rename to assets/webfonts/fa-regular-400.svg diff --git a/public/theme/assets/webfonts/fa-regular-400.ttf b/assets/webfonts/fa-regular-400.ttf similarity index 100% rename from public/theme/assets/webfonts/fa-regular-400.ttf rename to assets/webfonts/fa-regular-400.ttf diff --git a/public/theme/assets/webfonts/fa-regular-400.woff b/assets/webfonts/fa-regular-400.woff similarity index 100% rename from public/theme/assets/webfonts/fa-regular-400.woff rename to assets/webfonts/fa-regular-400.woff diff --git a/public/theme/assets/webfonts/fa-regular-400.woff2 b/assets/webfonts/fa-regular-400.woff2 similarity index 100% rename from public/theme/assets/webfonts/fa-regular-400.woff2 rename to assets/webfonts/fa-regular-400.woff2 diff --git a/public/theme/assets/webfonts/fa-solid-900.eot b/assets/webfonts/fa-solid-900.eot similarity index 100% rename from public/theme/assets/webfonts/fa-solid-900.eot rename to assets/webfonts/fa-solid-900.eot diff --git a/public/theme/assets/webfonts/fa-solid-900.svg b/assets/webfonts/fa-solid-900.svg similarity index 100% rename from public/theme/assets/webfonts/fa-solid-900.svg rename to assets/webfonts/fa-solid-900.svg diff --git a/public/theme/assets/webfonts/fa-solid-900.ttf b/assets/webfonts/fa-solid-900.ttf similarity index 100% rename from public/theme/assets/webfonts/fa-solid-900.ttf rename to assets/webfonts/fa-solid-900.ttf diff --git a/public/theme/assets/webfonts/fa-solid-900.woff b/assets/webfonts/fa-solid-900.woff similarity index 100% rename from public/theme/assets/webfonts/fa-solid-900.woff rename to assets/webfonts/fa-solid-900.woff diff --git a/public/theme/assets/webfonts/fa-solid-900.woff2 b/assets/webfonts/fa-solid-900.woff2 similarity index 100% rename from public/theme/assets/webfonts/fa-solid-900.woff2 rename to assets/webfonts/fa-solid-900.woff2 diff --git a/composer.json b/composer.json index 44c31e9..3a13de4 100644 --- a/composer.json +++ b/composer.json @@ -12,41 +12,42 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.1", + "erusev/parsedown": "^1.7", "phpdocumentor/reflection-docblock": "^5.4", "phpstan/phpdoc-parser": "^1.28", - "symfony/asset": "7.0.*", - "symfony/asset-mapper": "7.0.*", - "symfony/console": "7.0.*", - "symfony/doctrine-messenger": "7.0.*", - "symfony/dotenv": "7.0.*", - "symfony/expression-language": "7.0.*", + "symfony/asset": "7.3.*", + "symfony/asset-mapper": "7.3.*", + "symfony/console": "7.3.*", + "symfony/debug-bundle": "7.3.*", + "symfony/doctrine-messenger": "7.3.*", + "symfony/dotenv": "7.3.*", + "symfony/expression-language": "7.3.*", "symfony/flex": "^2", - "symfony/form": "7.0.*", - "symfony/framework-bundle": "7.0.*", - "symfony/http-client": "7.0.*", - "symfony/intl": "7.0.*", - "symfony/mailer": "7.0.*", - "symfony/mime": "7.0.*", + "symfony/form": "7.3.*", + "symfony/framework-bundle": "7.3.*", + "symfony/http-client": "7.3.*", + "symfony/intl": "7.3.*", + "symfony/mailer": "7.3.*", + "symfony/mime": "7.3.*", "symfony/monolog-bundle": "^3.0", - "symfony/notifier": "7.0.*", - "symfony/process": "7.0.*", - "symfony/property-access": "7.0.*", - "symfony/property-info": "7.0.*", - "symfony/runtime": "7.0.*", - "symfony/security-bundle": "7.0.*", - "symfony/serializer": "7.0.*", + "symfony/notifier": "7.3.*", + "symfony/process": "7.3.*", + "symfony/property-access": "7.3.*", + "symfony/property-info": "7.3.*", + "symfony/runtime": "7.3.*", + "symfony/security-bundle": "7.3.*", + "symfony/serializer": "7.3.*", "symfony/stimulus-bundle": "^2.17", - "symfony/string": "7.0.*", - "symfony/translation": "7.0.*", - "symfony/twig-bundle": "7.0.*", - "symfony/uid": "7.0.*", + "symfony/string": "7.3.*", + "symfony/translation": "7.3.*", + "symfony/twig-bundle": "7.3.*", + "symfony/uid": "7.3.*", "symfony/ux-turbo": "^2.17", - "symfony/validator": "7.0.*", - "symfony/web-link": "7.0.*", - "symfony/yaml": "7.0.*", + "symfony/validator": "7.3.*", + "symfony/web-link": "7.3.*", + "symfony/yaml": "7.3.*", "twig/extra-bundle": "^2.12|^3.0", - "twig/twig": "^2.12|^3.0", - "symfony/debug-bundle": "7.0.*" + "twig/twig": "^2.12|^3.0" }, "config": { "allow-plugins": { @@ -95,16 +96,16 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "7.0.*" + "require": "7.3.*" } }, "require-dev": { "phpunit/phpunit": "^9.5", - "symfony/browser-kit": "7.0.*", - "symfony/css-selector": "7.0.*", + "symfony/browser-kit": "7.3.*", + "symfony/css-selector": "7.3.*", "symfony/maker-bundle": "^1.0", - "symfony/phpunit-bridge": "^7.0", - "symfony/stopwatch": "7.0.*", - "symfony/web-profiler-bundle": "7.0.*" + "symfony/phpunit-bridge": "^7.2", + "symfony/stopwatch": "7.3.*", + "symfony/web-profiler-bundle": "7.3.*" } } diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml index 56a650d..eed6f03 100644 --- a/config/packages/mailer.yaml +++ b/config/packages/mailer.yaml @@ -1,3 +1,4 @@ framework: - mailer: - dsn: '%env(MAILER_DSN)%' + mailer: + dsn: "%env(MAILER_DSN)%" + message_bus: false diff --git a/data/data.db b/data/data.db index 2bb2e7a..0e2261d 100644 Binary files a/data/data.db and b/data/data.db differ diff --git a/docker-compose.yml b/docker-compose.yml index 08c01cd..02143c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,10 @@ -version: "3" - services: sermon-notes: container_name: sermon-notes - image: ryanprather/sermon-notes:latest + image: gitea.rkprather.com/ryan/sermon-notes:1.1 ports: - - 80:80 \ No newline at end of file + - 80:80 + volumes: + - ${PWD}/data:/data + - ${PWD}/.env:/var/www/html/.env + restart: unless-stopped diff --git a/importmap.php b/importmap.php index b73b323..12828f2 100644 --- a/importmap.php +++ b/importmap.php @@ -25,4 +25,13 @@ return [ '@hotwired/turbo' => [ 'version' => '7.3.0', ], + 'jquery' => [ + 'version' => '3.3.1', + ], + 'jquery-ui' => [ + 'version' => '1.14.1', + ], + 'datatables' => [ + 'version' => '1.10.18', + ], ]; diff --git a/install.sh b/install.sh index c9263f1..945c438 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,8 @@ #!/bin/bash -rm -rf .env* +if [ ! -f /var/www/html/.env ]; then + exit 0 +fi echo "APP_ENV=prod" > .env echo "APP_DEBUG=0" >> .env @@ -10,8 +12,11 @@ LENGTH=32 SECRET_KEY=$(openssl rand -base64 $LENGTH | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' ') TRIMMED_KEY=$(cut -c1-32 <<< $SECRET_KEY) echo "APP_SECRET=$TRIMMED_KEY" >> .env -echo "DATABASE_URL=\"sqlite:///%kernel.project_dir%/var/data.db\"" >> .env +echo "DATABASE_URL=\"sqlite:////data/data.db\"" >> .env echo "MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0" >> .env -symfony console doctrine:migrations:migrate --no-interaction COMPOSER_ALLOW_SUPERUSER=1 composer update +symfony console asset-map:compile +symfony console doctrine:migrations:migrate --no-interaction + +chown -R www-data:www-data /data diff --git a/logo/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.eps b/logo/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.eps deleted file mode 100644 index da3aab3..0000000 Binary files a/logo/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.eps and /dev/null differ diff --git a/migrations/Version20240527010736.php b/migrations/Version20240527010736.php new file mode 100644 index 0000000..e75befe --- /dev/null +++ b/migrations/Version20240527010736.php @@ -0,0 +1,43 @@ +addSql('ALTER TABLE note ADD COLUMN recording VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TEMPORARY TABLE __temp__note AS SELECT id, speaker_id, series_id, user_id, title, date, passage, refs, text FROM note'); + $this->addSql('DROP TABLE note'); + $this->addSql('CREATE TABLE note (id BLOB NOT NULL --(DC2Type:uuid) + , speaker_id BLOB DEFAULT NULL --(DC2Type:uuid) + , series_id BLOB DEFAULT NULL --(DC2Type:uuid) + , user_id BLOB DEFAULT NULL --(DC2Type:uuid) + , title VARCHAR(255) NOT NULL, date DATE NOT NULL, passage VARCHAR(255) NOT NULL, refs CLOB DEFAULT NULL --(DC2Type:json) + , text CLOB DEFAULT NULL, PRIMARY KEY(id), CONSTRAINT FK_CFBDFA14D04A0F27 FOREIGN KEY (speaker_id) REFERENCES speaker (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA145278319C FOREIGN KEY (series_id) REFERENCES series (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA14A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO note (id, speaker_id, series_id, user_id, title, date, passage, refs, text) SELECT id, speaker_id, series_id, user_id, title, date, passage, refs, text FROM __temp__note'); + $this->addSql('DROP TABLE __temp__note'); + $this->addSql('CREATE INDEX IDX_CFBDFA14D04A0F27 ON note (speaker_id)'); + $this->addSql('CREATE INDEX IDX_CFBDFA145278319C ON note (series_id)'); + $this->addSql('CREATE INDEX IDX_CFBDFA14A76ED395 ON note (user_id)'); + } +} diff --git a/migrations/Version20240622233923.php b/migrations/Version20240622233923.php new file mode 100644 index 0000000..621e0e9 --- /dev/null +++ b/migrations/Version20240622233923.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE user ADD COLUMN meta_data CLOB DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, email, roles, password, name FROM user'); + $this->addSql('DROP TABLE user'); + $this->addSql('CREATE TABLE user (id BLOB NOT NULL --(DC2Type:uuid) + , email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO user (id, email, roles, password, name) SELECT id, email, roles, password, name FROM __temp__user'); + $this->addSql('DROP TABLE __temp__user'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)'); + } +} diff --git a/migrations/Version20240717022017.php b/migrations/Version20240717022017.php new file mode 100644 index 0000000..a989200 --- /dev/null +++ b/migrations/Version20240717022017.php @@ -0,0 +1,43 @@ +addSql('CREATE TABLE shared_note (id BLOB NOT NULL --(DC2Type:uuid) + , note_id BLOB NOT NULL --(DC2Type:uuid) + , owner_id BLOB NOT NULL --(DC2Type:uuid) + , shared_user_id BLOB NOT NULL --(DC2Type:uuid) + , PRIMARY KEY(id), CONSTRAINT FK_754B918C26ED0855 FOREIGN KEY (note_id) REFERENCES note (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_754B918C26ED0855 ON shared_note (note_id)'); + $this->addSql('CREATE TABLE shared_series (id BLOB NOT NULL --(DC2Type:uuid) + , series_id BLOB NOT NULL --(DC2Type:uuid) + , owner_id BLOB NOT NULL --(DC2Type:uuid) + , shared_user_id BLOB NOT NULL --(DC2Type:uuid) + , PRIMARY KEY(id), CONSTRAINT FK_59E803195278319C FOREIGN KEY (series_id) REFERENCES series (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_59E803195278319C ON shared_series (series_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE shared_note'); + $this->addSql('DROP TABLE shared_series'); + } +} diff --git a/public/css/style.css.map b/public/css/style.css.map deleted file mode 100644 index 192f302..0000000 --- a/public/css/style.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA,kCAAA;AACA;EACI,SAAA;ACCJ;;ADEA,6BAAA;AAEA;EACI,aAAA;EACA,mBAAA;EACA,eAAA;EACA,6BAAA;EACA,oBAAA;EACA,yBAAA;EACA,iBAAA;EACA,iCAAA;EACA,cAAA;ACAJ;;ADGA;EACI,WAAA;EACA,4BAAA;ACAJ;;ADGA;EACI,SAAA;EACA,UAAA;ACAJ;;ADGA;EACI,gBAAA;EACA,qBAAA;ACAJ;;ADGA;EACI,kBAAA;ACAJ;;ADGA,GAAA;AACA;EACI,aAAA;EACA,uBAAA;EACA,mBAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,yBAAA;EACA,sBAAA;EACA,YAAA;EACA,kBAAA;EACA,wCAAA;EACA,eAAA;EACA,kBAAA;ACAJ;ADEI;EACI,yBAAA;ACAR;;ADIA;EACI,oCAAA;ACDJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;EACI,mBAAA;EACA,2BAAA;EACA,yBAAA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,uBAAA;EACA,kBAAA;EACA,wCAAA;ACDJ;;ADIA;EACI,yBAAA;ACDJ;;ADIA;EACI,YAAA;ACDJ;;ADIA;EACI,aAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,cAAA;ACDJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;EACI,WAAA;EACA,YAAA;EACA,eAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;ACDJ;;ADIA;;EAEI,oCAAA;EACA,uBAAA;ACDJ;;ADIA;EACI,oBAAA;EACA,mBAAA;EACA,UAAA;ACDJ;;ADIA;EACI,oBAAA;EACA,2BAAA;EACA,UAAA;ACDJ;;ADIA;EACI,iBAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,SAAA;EACA,UAAA;EACA,qBAAA;EACA,gBAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;EACA,YAAA;EACA,oCAAA;EACA,YAAA;EACA,aAAA;ACDJ;;ADIA;EACI,YAAA;EACA,kBAAA;EACA,kBAAA;EACA,eAAA;EACA,gBAAA;EACA,WAAA;EACA,yBAAA;EACA,YAAA;EACA,YAAA;EACA,eAAA;EACA,aAAA;ACDJ;;ADIA;EACI,aAAA;EACA,kBAAA;EACA,YAAA;EACA,sBAAA;EACA,YAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,wCAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;ACDJ;;ADIA;EACI,UAAA;EACA,iBAAA;EACA,eAAA;EACA,iBAAA;EACA,YAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ;;ADIA;EACI,UAAA;ACDJ;;ADIA;;EAEI,YAAA;EACA,qBAAA;ACDJ;;ADIA;EACI,2BAAA;EACA,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,qBAAA;ACDJ;;ADIA;EACI,eAAA;ACDJ;;ADIA;EACI,aAAA;ACDJ","file":"style.css"} \ No newline at end of file diff --git a/public/css/style.min.css b/public/css/style.min.css deleted file mode 100644 index 4892f32..0000000 --- a/public/css/style.min.css +++ /dev/null @@ -1 +0,0 @@ -body{margin:0}.inner{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-evenly;align-items:stretch;align-content:flex-start;max-width:1060px;margin:0 auto}.ref-tab{width:60px;padding-top:75px !important}.ref-tab ul{margin:0;padding:0}.ref-tab ul li{list-style:none;list-style-type:none}.tab{margin-bottom:3px}.tab button{display:flex;justify-content:center;align-items:center;height:80px;width:100%;text-align:center;background-color:#f56a6a;color:#fff !important;border:none;border-radius:3px;box-shadow:0 4px 5px rgba(0,0,0,.8);font-size:14px;margin-bottom:3px}.tab button:active{background-color:#7a0016}.activeRef{background-color:#3e8e41 !important}.ref{width:35%}.ref>div#ref{vertical-align:top;justify-content:flex-start;align-content:flex-start;overflow-y:scroll;width:100%;height:100%;border:#000 solid 1px;border-radius:3px;box-shadow:0 2px 5px rgba(0,0,0,.3)}#ref{padding:3px 3px 3px 10px}#passage{width:100px}#newSpeaker{display:none;width:110px}#newSeries{display:none;width:110px}#fields-container{display:none}#fields-container.show{display:block}.notes{width:55%}textarea#notes{width:100%;height:100%;font-size:14pt}#notePreview{display:none;overflow-x:scroll}#previewBtn.active,#show-hide-btn.active{background-color:#f56a6a !important;color:#fff !important}#note-header-left{display:inline-flex;flex-direction:row;width:25%}#note-header-right{display:inline-flex;flex-direction:row-reverse;width:74%}#note-header-left h2.dirty{color:#ff8c00}#note-list{display:none}#note-list ul{margin:0;padding:0;list-style-type:none;list-style:none}div#refQuery{display:none;position:absolute;z-index:100;background-color:rgba(0,0,0,.8);width:400px;height:200px}div#refQuery #referenceSearch{border:none;border-radius:5px;padding:10px 20px;font-size:16px;line-height:1.5;color:#333;background-color:#f4f4f4;width:150px;height:25px;cursor:pointer;display:none}#passage-popup{display:none;position:absolute;z-index:100;background-color:#fff;color:#000;padding:10px;border:1px solid #ccc;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);width:300px;height:300px;overflow-x:scroll}#save-check{opacity:0;text-align:right;font-size:20pt;margin-left:15px;color:green}#save-check.saving{color:orange}#save-check.error{color:red}#fields-container input,#fields-container select{width:19.5%;display:inline-block}#old-notes article p:first-child{margin-bottom:0 !important;font-size:10pt}#old-notes article a{font-size:12pt}#old-notes article p:last-child{font-size:12pt}.inner{padding-left:0}.ref h2{display:inline-block}#ref{font-size:12pt}#referenceBook{display:none}/*# sourceMappingURL=style.min.css.map */ \ No newline at end of file diff --git a/public/css/style.min.css.map b/public/css/style.min.css.map deleted file mode 100644 index 4c774a2..0000000 --- a/public/css/style.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["style.scss"],"names":[],"mappings":"AACA,KACI,QAAA,CAKJ,OACI,YAAA,CACA,kBAAA,CACA,cAAA,CACA,4BAAA,CACA,mBAAA,CACA,wBAAA,CACA,gBAAA,CAEA,aAAA,CAGJ,SACI,UAAA,CACA,2BAAA,CAGJ,YACI,QAAA,CACA,SAAA,CAGJ,eACI,eAAA,CACA,oBAAA,CAGJ,KACI,iBAAA,CAIJ,YACI,YAAA,CACA,sBAAA,CACA,kBAAA,CACA,WAAA,CACA,UAAA,CACA,iBAAA,CACA,wBAAA,CACA,qBAAA,CACA,WAAA,CACA,iBAAA,CACA,mCAAA,CACA,cAAA,CACA,iBAAA,CAEA,mBACI,wBAAA,CAIR,WACI,mCAAA,CAGJ,KACI,SAAA,CAGJ,aACI,kBAAA,CACA,0BAAA,CACA,wBAAA,CACA,iBAAA,CACA,UAAA,CACA,WAAA,CACA,qBAAA,CACA,iBAAA,CACA,mCAAA,CAGJ,KACI,wBAAA,CAGJ,SACI,WAAA,CAGJ,YACI,YAAA,CACA,WAAA,CAGJ,WACI,YAAA,CACA,WAAA,CAGJ,kBACI,YAAA,CAGJ,uBACI,aAAA,CAGJ,OACI,SAAA,CAGJ,eACI,UAAA,CACA,WAAA,CACA,cAAA,CAGJ,aACI,YAAA,CACA,iBAAA,CAGJ,yCAEI,mCAAA,CACA,qBAAA,CAGJ,kBACI,mBAAA,CACA,kBAAA,CACA,SAAA,CAGJ,mBACI,mBAAA,CACA,0BAAA,CACA,SAAA,CAGJ,2BACI,aAAA,CAGJ,WACI,YAAA,CAGJ,cACI,QAAA,CACA,SAAA,CACA,oBAAA,CACA,eAAA,CAGJ,aACI,YAAA,CACA,iBAAA,CACA,WAAA,CACA,+BAAA,CACA,WAAA,CACA,YAAA,CAGJ,8BACI,WAAA,CACA,iBAAA,CACA,iBAAA,CACA,cAAA,CACA,eAAA,CACA,UAAA,CACA,wBAAA,CACA,WAAA,CACA,WAAA,CACA,cAAA,CACA,YAAA,CAGJ,eACI,YAAA,CACA,iBAAA,CACA,WAAA,CACA,qBAAA,CACA,UAAA,CACA,YAAA,CACA,qBAAA,CACA,iBAAA,CACA,mCAAA,CACA,WAAA,CACA,YAAA,CACA,iBAAA,CAGJ,YACI,SAAA,CACA,gBAAA,CACA,cAAA,CACA,gBAAA,CACA,WAAA,CAGJ,mBACI,YAAA,CAGJ,kBACI,SAAA,CAGJ,iDAEI,WAAA,CACA,oBAAA,CAGJ,iCACI,0BAAA,CACA,cAAA,CAGJ,qBACI,cAAA,CAGJ,gCACI,cAAA,CAGJ,OACI,cAAA,CAGJ,QACI,oBAAA,CAGJ,KACI,cAAA,CAGJ,eACI,YAAA","file":"style.min.css"} \ No newline at end of file diff --git a/docs/markdown-cheat-sheet.md b/public/docs/markdown-cheat-sheet.md similarity index 100% rename from docs/markdown-cheat-sheet.md rename to public/docs/markdown-cheat-sheet.md diff --git a/public/index.php b/public/index.php index 9982c21..4734829 100644 --- a/public/index.php +++ b/public/index.php @@ -2,6 +2,40 @@ use App\Kernel; +if (file_exists(__DIR__.$_SERVER['REQUEST_URI']) && is_readable(__DIR__.$_SERVER['REQUEST_URI']) && is_file(__DIR__.$_SERVER['REQUEST_URI'])) { + $header = 'text/html'; + if (substr($_SERVER['REQUEST_URI'], -4) == '.css') + $header = 'text/css'; + elseif (substr($_SERVER['REQUEST_URI'], -3) == '.js') + $header = 'text/javascript'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.ico') + $header = 'image/x-icon'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.png') + $header = 'image/png'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.jpg') + $header = 'image/jpeg'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.gif') + $header = 'image/gif'; + elseif (substr($_SERVER['REQUEST_URI'], -3) == '.wo' || substr($_SERVER['REQUEST_URI'], -6) == '.woff2') + $header = 'font/woff2'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.ttf') + $header = 'font/truetype'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.eot' || substr($_SERVER['REQUEST_URI'], -5) == '.ttf-') + $header = 'application/vnd.ms-fontobject'; + elseif (substr($_SERVER['REQUEST_URI'], -4) == '.svg') + $header = 'image/svg+xml'; + elseif (substr($_SERVER['REQUEST_URI'], -5) == '.json') + $header = 'application/json'; + elseif (substr($_SERVER['REQUEST_URI'], -3) == '.otf') + $header = 'font/opentype'; + elseif (substr($_SERVER['REQUEST_URI'], -5) == '.woff') + $header = 'font/woff'; + + print header("Content-Type: $header; charset=UTF-8"); + print file_get_contents(__DIR__.$_SERVER['REQUEST_URI']); + exit; +} + require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { diff --git a/public/js/data.json b/public/js/data.json index f3dea84..982c997 100644 --- a/public/js/data.json +++ b/public/js/data.json @@ -1333,8 +1333,237 @@ 37 ], "hc": [ - 1, - 52 + [ + 1, + 2 + ], + [ + 3, + 4, + 5 + ], + [ + 6, + 7, + 8 + ], + [ + 9, + 10, + 11 + ], + [ + 12, + 13, + 14, + 15 + ], + [ + 16, + 17, + 18, + 19 + ], + [ + 20, + 21, + 22, + 23 + ], + [ + 24, + 25, + 26 + ], + [ + 27, + 28 + ], + [ + 29, + 30 + ], + [ + 31, + 32 + ], + [ + 33, + 34 + ], + [ + 35, + 36 + ], + [ + 37, + 38, + 39 + ], + [ + 40, + 41, + 42, + 43, + 44 + ], + [ + 45 + ], + [ + 46, + 47, + 48, + 49 + ], + [ + 50, + 51, + 52 + ], + [ + 53 + ], + [ + 54, + 55, + 56 + ], + [ + 57, + 58 + ], + [ + 59, + 60, + 61 + ], + [ + 62, + 63, + 64 + ], + [ + 65, + 66, + 67, + 68 + ], + [ + 69, + 70, + 71 + ], + [ + 72, + 73, + 74 + ], + [ + 75, + 76, + 77 + ], + [ + 78, + 79 + ], + [ + 80, + 81, + 82 + ], + [ + 83, + 84, + 85 + ], + [ + 86, + 87 + ], + [ + 88, + 89, + 90, + 91 + ], + [ + 92, + 93, + 94, + 95 + ], + [ + 96, + 97, + 98 + ], + [ + 99, + 100 + ], + [ + 101, + 102 + ], + [ + 103 + ], + [ + 104 + ], + [ + 105, + 106, + 107 + ], + [ + 108, + 109 + ], + [ + 110, + 111 + ], + [ + 112 + ], + [ + 113, + 114, + 115 + ], + [ + 116, + 117, + 118, + 119 + ], + [ + 120, + 121 + ], + [ + 122 + ], + [ + 123 + ], + [ + 124 + ], + [ + 125 + ], + [ + 126 + ], + [ + 127, + 128, + 129 + ] ], "cd": [ "1", @@ -1354,5 +1583,29 @@ "wlc": [ 1, 196 + ], + "lbc": [ + 1, + 32 + ], + "39a": [ + 1, + 39 + ], + "1hc": [ + 1, + 10 + ], + "2hc": [ + 1, + 30 + ], + "sd": [ + 1, + 32 + ], + "agc": [ + 1, + 28 ] } \ No newline at end of file diff --git a/public/theme/LICENSE.txt b/public/theme/LICENSE.txt deleted file mode 100644 index d447b56..0000000 --- a/public/theme/LICENSE.txt +++ /dev/null @@ -1,63 +0,0 @@ -Creative Commons Attribution 3.0 Unported -http://creativecommons.org/licenses/by/3.0/ - -License - -THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. - -BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. - -1. Definitions - - 1. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. - 2. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. - 3. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. - 4. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. - 5. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. - 6. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. - 7. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. - 8. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. - 9. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. - -2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. - -3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: - - 1. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; - 2. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; - 3. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, - 4. to Distribute and Publicly Perform Adaptations. - 5. - - For the avoidance of doubt: - 1. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; - 2. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, - 3. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. - -The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. - -4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: - - 1. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(b), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(b), as requested. - 2. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4 (b) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. - 3. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. - -5. Representations, Warranties and Disclaimer - -UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. - -6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. Termination - - 1. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. - 2. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. - -8. Miscellaneous - - 1. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. - 2. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. - 3. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - 4. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. - 5. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. - 6. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. diff --git a/public/theme/README.txt b/public/theme/README.txt deleted file mode 100644 index 7282988..0000000 --- a/public/theme/README.txt +++ /dev/null @@ -1,30 +0,0 @@ -Editorial by HTML5 UP -html5up.net | @ajlkn -Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) - - -Say hello to Editorial, a blog/magazine-ish template built around a toggleable "locking" -sidebar (scroll down to see what I mean) and an accordion-style menu. Not the usual landing -page/portfolio affair you'd expect to see at HTML5 UP, but I figured for my 41st (!!!) -template I'd change it up a little. Enjoy :) - -Demo images* courtesy of Unsplash, a radtastic collection of CC0 (public domain) images -you can use for pretty much whatever. - -(* = not included) - -AJ -aj@lkn.io | @ajlkn - - -Credits: - - Demo Images: - Unsplash (unsplash.com) - - Icons: - Font Awesome (fontawesome.io) - - Other: - jQuery (jquery.com) - Responsive Tools (github.com/ajlkn/responsive-tools) \ No newline at end of file diff --git a/public/theme/elements.html b/public/theme/elements.html deleted file mode 100644 index a724758..0000000 --- a/public/theme/elements.html +++ /dev/null @@ -1,543 +0,0 @@ - - - - - Elements - Editorial by HTML5 UP - - - - - - - -
- - -
-
- - - - - -
-
-

Elements

-
- - -

Sample Content

-

Praesent ac adipiscing ullamcorper semper ut amet ac risus. Lorem sapien ut odio odio nunc. Ac adipiscing nibh porttitor erat risus justo adipiscing adipiscing amet placerat accumsan. Vis. Faucibus odio magna tempus adipiscing a non. In mi primis arcu ut non accumsan vivamus ac blandit adipiscing adipiscing arcu metus praesent turpis eu ac lacinia nunc ac commodo gravida adipiscing eget accumsan ac nunc adipiscing adipiscing lorem ipsum dolor sit amet nullam veroeros adipiscing.

-
-
-

Sem turpis amet semper

-

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat commodo eu sed ante lacinia. Sapien a lorem in integer ornare praesent commodo adipiscing arcu in massa commodo lorem accumsan at odio massa ac ac. Semper adipiscing varius montes viverra nibh in adipiscing blandit tempus accumsan.

-
-
-

Magna odio tempus commodo

-

In arcu accumsan arcu adipiscing accumsan orci ac. Felis id enim aliquet. Accumsan ac integer lobortis commodo ornare aliquet accumsan erat tempus amet porttitor. Ante commodo blandit adipiscing integer semper orci eget. Faucibus commodo adipiscing mi eu nullam accumsan morbi arcu ornare odio mi adipiscing nascetur lacus ac interdum morbi accumsan vis mi accumsan.

-
- -
-

Interdum sapien gravida

-

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat eu sed ante lacinia sapien lorem accumsan varius montes viverra nibh in adipiscing blandit.

-
-
-

Faucibus consequat lorem

-

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat eu sed ante lacinia sapien lorem accumsan varius montes viverra nibh in adipiscing blandit.

-
-
-

Accumsan montes viverra

-

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat eu sed ante lacinia sapien lorem accumsan varius montes viverra nibh in adipiscing blandit.

-
-
- -
- - -

Elements

-
-
- - -

Text

-

This is bold and this is strong. This is italic and this is emphasized. - This is superscript text and this is subscript text. - This is underlined and this is code: for (;;) { ... }. - Finally, this is a link.

-
-

Heading Level 2

-

Heading Level 3

-

Heading Level 4

-
-

Nunc lacinia ante nunc ac lobortis. Interdum adipiscing gravida odio porttitor sem non mi integer non faucibus ornare mi ut ante amet placerat aliquet. Volutpat eu sed ante lacinia sapien lorem accumsan varius montes viverra nibh in adipiscing blandit tempus accumsan.

- - -

Lists

-
-
- -

Unordered

-
    -
  • Dolor etiam magna etiam.
  • -
  • Sagittis lorem eleifend.
  • -
  • Felis dolore viverra.
  • -
- -

Alternate

-
    -
  • Dolor etiam magna etiam.
  • -
  • Sagittis lorem eleifend.
  • -
  • Felis feugiat viverra.
  • -
- -
-
- -

Ordered

-
    -
  1. Dolor etiam magna etiam.
  2. -
  3. Etiam vel lorem sed viverra.
  4. -
  5. Felis dolore viverra.
  6. -
  7. Dolor etiam magna etiam.
  8. -
  9. Etiam vel lorem sed viverra.
  10. -
  11. Felis dolore viverra.
  12. -
- -

Icons

- - -
-
-

Definition

-
-
Item1
-
-

Lorem ipsum dolor vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Lorem ipsum dolor.

-
-
Item2
-
-

Lorem ipsum dolor vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Lorem ipsum dolor.

-
-
Item3
-
-

Lorem ipsum dolor vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Lorem ipsum dolor.

-
-
- -

Actions

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

Pagination

- - - -

Blockquote

-
Lorem ipsum dolor vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent. Lorem ipsum dolor. Lorem ipsum dolor vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus.
- - -

Table

- -

Default

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionPrice
Item1Ante turpis integer aliquet porttitor.29.99
Item2Vis ac commodo adipiscing arcu aliquet.19.99
Item3 Morbi faucibus arcu accumsan lorem.29.99
Item4Vitae integer tempus condimentum.19.99
Item5Ante turpis integer aliquet porttitor.29.99
100.00
-
- -

Alternate

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionPrice
Item1Ante turpis integer aliquet porttitor.29.99
Item2Vis ac commodo adipiscing arcu aliquet.19.99
Item3 Morbi faucibus arcu accumsan lorem.29.99
Item4Vitae integer tempus condimentum.19.99
Item5Ante turpis integer aliquet porttitor.29.99
100.00
-
- -
-
- - -

Buttons

- - - - - - -
    -
  • Primary
  • -
  • Default
  • -
- - -

Form

- -
-
-
- -
-
- -
- -
- -
- -
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
- -
- -
- -
-
    -
  • -
  • -
-
-
-
- - -

Image

- -

Fit

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

Left & Right

-

Lorem ipsum dolor sit accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent.

-

Lorem ipsum dolor sit accumsan interdum nisi, quis tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Vestibulum ante ipsum primis sagittis eget. tempus euismod. Vestibulum ante ipsum primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent.

- - -

Box

-
-

Felis sagittis eget tempus primis in faucibus vestibulum. Blandit adipiscing eu felis iaculis volutpat ac adipiscing accumsan eu faucibus. Integer ac pellentesque praesent tincidunt felis sagittis eget. tempus euismod. Magna sed etiam ante ipsum primis in faucibus vestibulum.

-
- - -

Preformatted

-
i = 0;
-
-while (!deck.isInOrder()) {
-    print 'Iteration ' + i;
-    deck.shuffle();
-    i++;
-}
-
-print 'It took ' + i + ' iterations to sort the deck.';
-
- -
-
- -
- -
-
- - - - -
- - - - - - - - - - \ No newline at end of file diff --git a/public/theme/generic.html b/public/theme/generic.html deleted file mode 100644 index 7bfc528..0000000 --- a/public/theme/generic.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Generic - Editorial by HTML5 UP - - - - - - - -
- - -
-
- - - - - -
-
-

Generic

-
- - - -

Donec eget ex magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fergiat. Pellentesque in mi eu massa lacinia malesuada et a elit. Donec urna ex, lacinia in purus ac, pretium pulvinar mauris. Curabitur sapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dapibus rutrum facilisis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam tristique libero eu nibh porttitor fermentum. Nullam venenatis erat id vehicula viverra. Nunc ultrices eros ut ultricies condimentum. Mauris risus lacus, blandit sit amet venenatis non, bibendum vitae dolor. Nunc lorem mauris, fringilla in aliquam at, euismod in lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In non lorem sit amet elit placerat maximus. Pellentesque aliquam maximus risus, vel sed vehicula.

-

Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fersapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit tristique lorem ipsum dolor.

- -
- -

Interdum sed dapibus

-

Donec eget ex magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fergiat. Pellentesque in mi eu massa lacinia malesuada et a elit. Donec urna ex, lacinia in purus ac, pretium pulvinar mauris. Curabitur sapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dapibus rutrum facilisis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam tristique libero eu nibh porttitor fermentum. Nullam venenatis erat id vehicula viverra. Nunc ultrices eros ut ultricies condimentum. Mauris risus lacus, blandit sit amet venenatis non, bibendum vitae dolor. Nunc lorem mauris, fringilla in aliquam at, euismod in lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In non lorem sit amet elit placerat maximus. Pellentesque aliquam maximus risus, vel sed vehicula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fersapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit tristique lorem ipsum dolor.

- -
- -

Magna etiam veroeros

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dapibus rutrum facilisis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam tristique libero eu nibh porttitor fermentum. Nullam venenatis erat id vehicula viverra. Nunc ultrices eros ut ultricies condimentum. Mauris risus lacus, blandit sit amet venenatis non, bibendum vitae dolor. Nunc lorem mauris, fringilla in aliquam at, euismod in lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In non lorem sit amet elit placerat maximus. Pellentesque aliquam maximus risus, vel sed vehicula.

-

Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fersapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit tristique lorem ipsum dolor.

- -
- -

Lorem aliquam bibendum

-

Donec eget ex magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fergiat. Pellentesque in mi eu massa lacinia malesuada et a elit. Donec urna ex, lacinia in purus ac, pretium pulvinar mauris. Curabitur sapien risus, commodo eget turpis at, elementum convallis elit. Pellentesque enim turpis, hendrerit.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis dapibus rutrum facilisis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam tristique libero eu nibh porttitor fermentum. Nullam venenatis erat id vehicula viverra. Nunc ultrices eros ut ultricies condimentum. Mauris risus lacus, blandit sit amet venenatis non, bibendum vitae dolor. Nunc lorem mauris, fringilla in aliquam at, euismod in lectus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In non lorem sit amet elit placerat maximus. Pellentesque aliquam maximus risus, vel sed vehicula.

- -
- -
-
- - - - -
- - - - - - - - - - \ No newline at end of file diff --git a/public/theme/images/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg b/public/theme/images/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg deleted file mode 100644 index 27e503f..0000000 Binary files a/public/theme/images/vecteezy_notes-icon-in-trendy-flat-style-isolated-on-white_29722382.jpg and /dev/null differ diff --git a/public/theme/index.html b/public/theme/index.html deleted file mode 100644 index 1505ad9..0000000 --- a/public/theme/index.html +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - Sermon Notes - - - - - - - - -
- - -
-
- - - - - - - - -
-
-

Erat lacinia

-
-
-
- -
-

Portitor ullamcorper

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

-
-
-
- -
-

Sapien veroeros

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

-
-
-
- -
-

Quam lorem ipsum

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

-
-
-
- -
-

Sed magna finibus

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

-
-
-
-
- - -
-
-

Ipsum sed dolor

-
-
-
- -

Interdum aenean

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
- -

Nulla amet dolore

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
- -

Tempus ullamcorper

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
- -

Sed etiam facilis

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
- -

Feugiat lorem aenean

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
- -

Amet varius aliquam

-

Aenean ornare velit lacus, ac varius enim lorem ullamcorper dolore. Proin aliquam - facilisis ante interdum. Sed nulla amet lorem feugiat tempus aliquam.

- -
-
-
- -
-
- - - - -
- - - - - - - - - - - \ No newline at end of file diff --git a/src/Command/ImportHeidelbergCommand.php b/src/Command/ImportHeidelbergCommand.php new file mode 100644 index 0000000..b17b6ef --- /dev/null +++ b/src/Command/ImportHeidelbergCommand.php @@ -0,0 +1,95 @@ +emi = $emi; + } + + protected function configure(): void + { + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + + $files = glob('references/Heidelberg Catechism/*.md'); + natsort($files); + + $this->io->progressStart(count($files)); + foreach ($files as $file) { + $basename = basename($file); + //$io->info("Processing ".basename($file)); + $this->processFile($file); + $this->io->progressAdvance(); + } + $this->io->progressFinish(); + + return Command::SUCCESS; + } + + private function processFile(string $file): Reference|bool + { + $md = trim(file_get_contents($file)); + $ref = new Reference(); + + if (!$md) { + $this->io->warning("File is empty\n{$file}"); + return false; + } + + $match = []; + if (preg_match("/LD(\d+)\-HC(\d+)/", $file, $match)) { + $type = 'hc'; + $label = "LD{$match[1]}-HC{$match[2]}"; + $name = 'Heidelberg'; + } + + $ref->setNdx($match[2]); + $ref->setLabel($label); + $ref->setType($type); + $ref->setName($name); + $ref->setContent($md); + + $this->emi->persist($ref); + $this->emi->flush(); + + return $ref; + } +} diff --git a/src/Controller/AjaxController.php b/src/Controller/AjaxController.php index a8dccfc..213cea3 100644 --- a/src/Controller/AjaxController.php +++ b/src/Controller/AjaxController.php @@ -4,6 +4,7 @@ namespace App\Controller; use DateTime; +use Parsedown; use App\Entity\Reference; use App\Entity\Template; use App\Entity\User; @@ -11,10 +12,16 @@ use App\Entity\Bible; use App\Entity\Speaker; use App\Entity\Series; use App\Entity\Note; +use App\Entity\SharedNote; +use App\Utils\Utils; +use Doctrine\DBAL\Exception\ReadOnlyException; use Doctrine\ORM\EntityManagerInterface; +use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; use Symfony\Component\Routing\Attribute\Route; class AjaxController extends AbstractController @@ -49,7 +56,7 @@ class AjaxController extends AbstractController $user = $this->getUser(); - if($template_id) { + if ($template_id) { $template = $emi->getRepository(Template::class)->find($template_id); $template->setName($template_name); $template->setContent($template_value); @@ -161,11 +168,16 @@ class AjaxController extends AbstractController $ref = new Reference(); $ref->setType($data->type); - if(is_numeric($data->book)) { + if (is_numeric($data->book)) { $ref->setNdx($data->book); + } elseif ($data->type == 'ld' || $data->type == 'hc') { + $type = substr($data->book, 0, 2); + $ref->setType($type); + $ref->setLabel($data->book); } ReferenceController::$emi = $emi; + $user = $this->getUser(); $ret = match(strtolower($data->type)) { 'bible' => ReferenceController::retrieveBible("{$data->book} {$search}"), @@ -175,16 +187,37 @@ class AjaxController extends AbstractController 'wcf' => ReferenceController::retrieveWCF($ref), 'wsc' => ReferenceController::retrieveWSC($ref), 'wlc' => ReferenceController::retrieveWLC($ref), - 'creed' => ReferenceController::retrieveCreed($data->book) + 'lbc' => ReferenceController::retrieveLBC($ref), + 'creed' => ReferenceController::retrieveCreed($data->book), + '39a' => ReferenceController::retrieve39a($ref), + '1hc' => ReferenceController::retrieve1HC($ref), + '2hc' => ReferenceController::retrieve2HC($ref), + 'sd' => ReferenceController::retrieveSD($ref), + 'agc' => ReferenceController::retrieveAGC($ref), + 'note' => ReferenceController::retrieveNote($user) }; - if (!is_a($ret, Reference::class)) { - $ret = new Reference(); + $ref = new Reference(); + + if (is_a($ret, Reference::class)) { + $ref = $ret; + } + + if ($data->type == 'hc' && is_array($ret)) { + $ref->setLabel( + substr($ret[0]->getLabel(), 0, strpos($ret[0]->getLabel(), '-')) + ); + + $content = null; + foreach ($ret as $row) { + $content .= $row->getContent().PHP_EOL.PHP_EOL; + } + $ref->setContent($content); } $res->setContent(json_encode([ - 'text' => $ret->getContent(), - 'title' => "{$ret->getLabel()}", + 'text' => $ref->getContent(), + 'title' => $ref->getLabel(), ])); return $res; @@ -195,19 +228,12 @@ class AjaxController extends AbstractController { $res = new Response(); $data = json_decode($req->getContent()); - $ret = match ($data->type) { - 'creed' => '/Creeds/', - 'bc' => '/Belgic/', - 'hc' => '/Heidelberg/', - 'cd' => '/Dort/', - 'wcf' => '/Westminster/Confessions/', - 'wsc' => '/Westminster/Shorter Catechism/', - 'wlc' => '/Westminster/Larger Catechism/' - }; + $ref = $emi->getRepository(Reference::class)->find($data->id); - $fc = file_get_contents(dirname(dirname(__DIR__))."/references{$ret}{$data->file}"); - - $res->setContent(json_encode(['text' => $fc])); + if (!is_a($ref, Reference::class)) { + $ref = new Reference(); + } + $res->setContent(json_encode(['text' => $ref->getContent()])); return $res; } @@ -217,22 +243,13 @@ class AjaxController extends AbstractController { $res = new Response(); $data = json_decode($req->getContent()); - $path = match($data->type) { - 'creed' => 'Creeds', - 'bc' => 'Belgic', - 'hc' => 'Heidelberg', - 'cd' => 'Dort', - 'wcf' => 'Westminster/Confessions', - 'wsc' => 'Westminster/Shorter Catechism', - 'wlc' => 'Westminster/Larger Catechism' - }; + $ref = $emi->getRepository(Reference::class)->find($data->refId); - $ret = file_put_contents(dirname(dirname(__DIR__))."/references/{$path}/{$data->file}", $data->text); - - if($ret !== false) { - $res->setContent(json_encode(['msg' => 'File Saved'])); - } else { - $res->setContent(json_encode(['msg' => 'Failed to save file'])); + if ($ref) { + $ref->setContent($data->text); + $emi->persist($ref); + $emi->flush(); + $res = $this->json(['msg' => 'Reference updated.']); } return $res; @@ -251,6 +268,18 @@ class AjaxController extends AbstractController return $res; } + #[Route('/get-notes', name: 'app_get_notes')] + public function getNotes(EntityManagerInterface $emi): Response + { + /** @var User $user */ + $user = $this->getUser(); + $notes = $emi->getRepository(Note::class)->reverseNoteSort($user); + $res = new Response(); + $res->setContent(json_encode(['data'=> $notes])); + + return $res; + } + #[Route('/get-note', name: 'app_get_note')] public function getNote(Request $req, EntityManagerInterface $emi): Response { @@ -274,6 +303,10 @@ class AjaxController extends AbstractController if (is_array($note) && count($note) > 0) { /** @var Note $note */ $note = $note[0]; + } else { + $note = new Note(); + $newNote = true; + $note->setUser($this->getUser()); } } else { $note = new Note(); @@ -293,6 +326,7 @@ class AjaxController extends AbstractController ->setSpeaker($speaker) ->setText($data->note) ->setPassage($data->passage) + ->setRecording($data->recording) ->setRefs($refs); $emi->persist($note); @@ -308,7 +342,7 @@ class AjaxController extends AbstractController return $res; } - #[Route('/discard-note', name: 'app_discard_note', methods: ['POST'])] + #[Route('/delete-note', name: 'app_discard_note', methods: ['POST'])] public function discardNote(Request $req, EntityManagerInterface $emi): Response { $data = json_decode($req->getContent()); @@ -323,6 +357,61 @@ class AjaxController extends AbstractController return $res; } + #[Route('/share-note', name: 'app_share_note', methods: ['POST'])] + public function shareNote(Request $req, EntityManagerInterface $emi, MailerInterface $mailer): Response + { + $data = json_decode($req->getContent()); + $note = $emi->getRepository(Note::class)->find($data->id); + /** @var User $user */ + $user = $this->getUser(); + $shared = $emi->getRepository(User::class)->findBy(['email' => $data->email]); + $email = $data->email; + $isRegistered = false; + + if (!$note) { + $res = new Response(); + $res->setContent(json_encode([ + 'msg' => 'Note Not Found' + ])); + } + + if (is_array($shared) && count($shared) > 0) { + $ns = new SharedNote(); + $ns->setNote($note) + ->setOwnerId($user->getId()) + ->setSharedUserId($shared[0]->getId()); + $emi->persist($ns); + $emi->flush(); + + $email = $shared[0]->getEmail(); + $isRegistered = true; + } + + $Parsedown = new Parsedown(); + + $util = new Utils(); + $util->sendEmail( + $mailer, + new Address('ryan@rkprather.com', 'Sermon Notes'), + new Address($email), + 'Note Shared', + $this->renderView('emails/note-shared.html.twig', [ + 'note' => $note, + 'owner' => $user, + 'isRegistered' => $isRegistered, + 'formattedText' => $Parsedown->text($note->getText()), + 'domain' => $_ENV['DOMAIN'], + ]) + ); + + $res = new Response(); + $res->setContent(json_encode([ + 'msg' => 'shared' + ])); + + return $res; + } + #[Route('/get-passage/{passage}', name: 'app_get_passage')] public function getPassage($passage, EntityManagerInterface $emi): Response { @@ -350,7 +439,7 @@ class AjaxController extends AbstractController if (is_array($ret)) { $text = null; - foreach($ret as $b) { + foreach ($ret as $b) { $text .= "{$b->getVerse()}. {$b->getContent()}".PHP_EOL; } $bible->setContent($text); @@ -363,4 +452,56 @@ class AjaxController extends AbstractController return $res; } + + #[Route('/save-settings', name: 'app_save_settings', methods: ['POST'])] + public function saveSettings(Request $req, EntityManagerInterface $emi): Response + { + $data = json_decode($req->getContent()); + /** @var User $user */ + $user = $this->getUser(); + + if (!$user) { + return new Response(json_encode([ + 'msg' => 'No User' + ])); + } + + if (!$data->saveInterval) { + $data->saveInterval = 15; + } + + if (!$data->saveReferences) { + $data->saveReferences = true; + } + + if (!$data->noteTextSize) { + $data->noteTextSize = 12; + } + + if (!$data->trackSaveSize) { + $data->trackSaveSize = false; + } + + try { + $meta = $user->getMetaData(); + $meta['saveInterval'] = $data->saveInterval; + $meta['saveReferences'] = $data->saveReferences; + $meta['noteTextSize'] = $data->noteTextSize; + $meta['trackSaveSize'] = $data->trackSaveSize; + $meta['saveTimeout'] = $data->saveTimeout; + $meta['save-failure-count'] = $data->saveFailureCount; + $user->setMetaData($meta); + $emi->persist($user); + $emi->flush(); + } catch (ReadOnlyException $e) { + return new Response('Read Only '.$_ENV['DATABASE_URL'], 403); + } + + $res = new Response(); + $res->setContent(json_encode([ + 'msg' => 'Settings Saved' + ])); + + return $res; + } } diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 12c00a9..97b48f4 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -3,22 +3,26 @@ namespace App\Controller; use App\Entity\Note; -use App\Entity\Reference; use App\Entity\User; +use App\Entity\Speaker; +use App\Entity\Series; +use App\Entity\SharedNote; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\CurrentUser; -use Symfony\Component\Uid\Uuid; class DefaultController extends AbstractController { #[Route('/', name: 'app_index')] public function index(): Response { - return $this->render('default/index.html.twig'); + if ($this->isGranted('IS_AUTHENTICATED_FULLY')) { + return $this->redirect('/home'); + } + return $this->render('default/index.html.twig', ['onLoad' => null]); } #[Route('/home', name: 'app_home')] @@ -27,11 +31,61 @@ class DefaultController extends AbstractController $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $last4Notes = $emi->getRepository(Note::class)->getLast4Notes($user); $openNotes = $emi->getRepository(Note::class)->reverseNoteSort($user); + $meta = $user->getMetaData(); + $speakers = $emi->getRepository(Speaker::class)->findBy(['user' => $user], ['name' => 'ASC']); + $series = $emi->getRepository(Series::class)->findBy(['user' => $user], ['name' => 'ASC']); return $this->render('default/home.html.twig', [ + //'onLoad' => 'initHome()', 'last4Notes' => $last4Notes, 'reverseNoteSort' => $openNotes, 'isAdmin' => $this->isGranted('ROLE_ADMIN'), + 'meta' => $meta, + 'speakers' => $speakers, + 'series' => $series, + ]); + } + + #[Route('/cheat-sheet', name: 'app_cheat_sheet')] + public function cheatSheet(): Response + { + return $this->render('default/cheat-sheet.html.twig'); + } + + #[Route('/profile', name: 'app_profile')] + public function profile(EntityManagerInterface $emi): Response + { + /** @var User $user */ + $user = $this->getUser(); + if (!$user) { + return $this->redirectToRoute('app_login'); + } + $meta = $user->getMetaData(); + if (!$meta) { + $meta = [ + 'onLoad' => null, + 'saveInterval' => 15, + 'saveReferences' => 'checked', + 'noteTextSize' => 12, + 'trackSaveSize' => null, + 'saveFailureCount' => 3, + 'saveTimeout' => 5, + ]; + } else { + $meta['saveReferences'] = $meta['saveReferences'] ? 'checked' : null; + $meta['trackSaveSize'] = $meta['trackSaveSize'] ? 'checked' : null; + } + + $sharedWithMe = $emi->getRepository(SharedNote::class)->getNotesSharedWithMe($user); + $shared = $emi->getRepository(SharedNote::class)->getNotesSharedByMe($user); + //dump($shared); + + return $this->render('default/profile.html.twig', [ + 'onLoad' => 'rollUp("user");rollUp("settings")', + 'meta' => $meta, + 'sharedWithMe' => $sharedWithMe, + 'shared' => $shared, + 'user' => $this->getUser(), ]); } @@ -40,23 +94,7 @@ class DefaultController extends AbstractController { $this->denyAccessUnlessGranted('ROLE_ADMIN'); - $creeds = $emi->getRepository(Reference::class)->findByType('creed'); - $belgic = $emi->getRepository(Reference::class)->findByType('belgic'); - $heidelberg = $emi->getRepository(Reference::class)->findByType('heidelberg'); - $dort = $emi->getRepository(Reference::class)->findByType('dort'); - $wcf = $emi->getRepository(Reference::class)->findByType('wcf'); - $wsc = $emi->getRepository(Reference::class)->findByType('wsc'); - $wlc = $emi->getRepository(Reference::class)->findByType('wlc'); - - return $this->render('editors/reference-editor.html.twig', [ - 'creeds' => $creeds, - 'belgic' => $belgic, - 'heidelberg' => $heidelberg, - 'dort' => $dort, - 'wcf' => $wcf, - 'wsc' => $wsc, - 'wlc' => $wlc - ]); + return $this->render('editors/reference-editor.html.twig'); } #[Route('/template-editor', name: 'app_template_editor')] diff --git a/src/Controller/ReferenceController.php b/src/Controller/ReferenceController.php index 90b7fcb..a9d5bea 100644 --- a/src/Controller/ReferenceController.php +++ b/src/Controller/ReferenceController.php @@ -3,7 +3,9 @@ namespace App\Controller; use App\Entity\Bible; +use App\Entity\Note; use App\Entity\Reference; +use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -39,7 +41,7 @@ class ReferenceController extends AbstractController $bible = self::$emi->getRepository(Bible::class)->findRange($bible, [$passage_start, $passage_end]); $passage = "{$passage_start}-{$passage_end}"; - $label = "{$bible[0]->getLabel()}\n{$bible[0]->getChapter()}:{$passage}"; + $label = "{$bible[0]->getLabel()}\n{$bible[0]->getChapter()}\n{$passage}"; } elseif (is_int($passage)) { $bible = self::$emi->getRepository(Bible::class)->findBy(['book' => $book, 'chapter' => $chapter, 'verse' => $passage]); $label = "{$bible[0]->getLabel()}\n{$bible[0]->getChapter()}:{$passage}"; @@ -50,9 +52,9 @@ class ReferenceController extends AbstractController $passage = null; } - if(is_array($bible)) { + if (is_array($bible)) { $text = []; - foreach($bible as $b) { + foreach ($bible as $b) { $text[] = "{$b->getVerse()}. {$b->getContent()}"; } } else { @@ -91,15 +93,16 @@ class ReferenceController extends AbstractController * * @param Reference $ref * - * @return Reference + * @return Reference|array */ - public static function retrieveHC($ref): Reference + public static function retrieveHC(Reference $ref): Reference|array { - $r = self::$emi->getRepository(Reference::class)->findBy(['type' => 'hc', 'ndx' => $ref->getNdx()]); + $r = self::$emi->getRepository(Reference::class)->findHeidelberg($ref->getLabel()); + if (!$r) { return new Reference(); } - return $r[0]; + return $r; } /** @@ -112,7 +115,7 @@ class ReferenceController extends AbstractController public static function retrieveBC($ref): Reference { $r = self::$emi->getRepository(Reference::class)->findBy(['type' => 'belgic', 'ndx' => $ref->getNdx()]); - if(!$r) { + if (!$r) { return new Reference(); } return $r[0]; @@ -144,6 +147,9 @@ class ReferenceController extends AbstractController public static function retrieveWSC($ref): Reference { $r = self::$emi->getRepository(Reference::class)->findBy(['type' => $ref->getType(), 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } return $r[0]; } @@ -157,6 +163,9 @@ class ReferenceController extends AbstractController public static function retrieveWLC($ref): Reference { $r = self::$emi->getRepository(Reference::class)->findBy(['type' => $ref->getType(), 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } return $r[0]; } @@ -170,6 +179,79 @@ class ReferenceController extends AbstractController public static function retrieveWCF($ref): Reference { $r = self::$emi->getRepository(Reference::class)->findBy(['type' => $ref->getType(), 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } return $r[0]; } + + public static function retrieveLBC($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => 'lbc', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieveAGC($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => 'agc', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieve39a($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => '39a', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieve1HC($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => '1hc', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieve2HC($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => '2hc', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieveSD($ref): Reference + { + $r = self::$emi->getRepository(Reference::class)->findBy(['type' => 'sd', 'ndx' => $ref->getNdx()]); + if (!$r) { + return new Reference(); + } + return $r[0]; + } + + public static function retrieveNote(?User $user): array + { + $notes = self::$emi->getRepository(Note::class)->findBy(['user' => $user], ['date' => 'DESC']); + return $notes; + } + + #[Route('/reference/{type}', name: 'app_reference_by_type', methods: ['GET'])] + public function retrieveReferenceByType(string $type, EntityManagerInterface $emi): Response + { + $res = new Response(); + $data = $emi->getRepository(Reference::class)->findByType($type); + + $res->setContent(json_encode($data)); + return $res; + } } diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index dcb2fef..914871c 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -4,18 +4,25 @@ namespace App\Controller; use App\Entity\User; use App\Form\RegistrationFormType; +use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; class RegistrationController extends AbstractController { #[Route('/register', name: 'app_register')] - public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response - { + public function register( + Request $request, + UserPasswordHasherInterface $userPasswordHasher, + EntityManagerInterface $entityManager, + MailerInterface $mailer + ): Response { $user = new User(); $form = $this->createForm(RegistrationFormType::class, $user); @@ -39,11 +46,30 @@ class RegistrationController extends AbstractController $entityManager->persist($user); $entityManager->flush(); + $this->sendEmail($user, $mailer); + return $this->redirectToRoute('app_home'); } + else { + dd($form); + } return $this->render('registration/register.html.twig', [ 'registrationForm' => $form, ]); } + + private function sendEmail(User $user, MailerInterface $mailer): void + { + $util = new Utils(); + $util->sendEmail( + $mailer, + new Address('ryan@rkprather.com'), + new Address('ryan@rkprather.com'), + 'New Account', + $this->renderView('emails/registration.html.twig', [ + 'user' => $user + ]) + ); + } } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 76bf5c4..c3ce368 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -19,6 +19,7 @@ class SecurityController extends AbstractController $lastUsername = $authenticationUtils->getLastUsername(); return $this->render('security/login.html.twig', [ + 'onLoad' => null, 'last_username' => $lastUsername, 'error' => $error, ]); diff --git a/src/Entity/Note.php b/src/Entity/Note.php index 703ae85..0148d58 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -42,6 +42,9 @@ class Note implements JsonSerializable #[ORM\ManyToOne(inversedBy: 'notes')] private ?User $user = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $recording = null; + public function getId(): ?Uuid { return $this->id; @@ -166,12 +169,21 @@ class Note implements JsonSerializable { return "id}')\">". $this->title. - ""; + " ". + "id}', this)\">". + "". + "". + ( + $this->recording ? "
". + "". + "". + "" : null + ); } public function toTableRow(): string { - return "". + return "". "{$this->toLink()}". "{$this->speaker->getName()}". "{$this->passage}". @@ -183,6 +195,7 @@ class Note implements JsonSerializable { return [ 'id' => $this->getId(), + 'link' => $this->toLink(), 'title' => $this->getTitle(), 'date' => $this->getDate(), 'passage' => $this->getPassage(), @@ -193,4 +206,16 @@ class Note implements JsonSerializable 'user' => $this->getUser(), ]; } + + public function getRecording(): ?string + { + return $this->recording; + } + + public function setRecording(?string $recording): static + { + $this->recording = $recording; + + return $this; + } } diff --git a/src/Entity/User.php b/src/Entity/User.php index 16493cf..ec74ed1 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -11,6 +11,7 @@ use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Uid\Uuid; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] @@ -23,6 +24,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer private ?Uuid $id = null; #[ORM\Column(length: 180)] + #[Assert\Email( + message: 'The email {{ value }} is not a valid email.', + )] private ?string $email = null; /** @@ -64,6 +68,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer #[ORM\OneToMany(targetEntity: Note::class, mappedBy: 'user')] private Collection $notes; + #[ORM\Column(nullable: true)] + private ?array $metaData = null; + public function __construct() { $this->series = new ArrayCollection(); @@ -77,6 +84,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer return $this->id; } + public function getHexId(): string + { + return $this->id->toHex(); + } + public function setId(?Uuid $id): static { $this->id = $id; @@ -299,4 +311,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer 'name' => $this->name, ]; } + + public function getMetaData(): ?array + { + return $this->metaData; + } + + public function setMetaData(?array $metaData): static + { + $this->metaData = $metaData; + + return $this; + } } diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php index d7ad600..92dcd4b 100644 --- a/src/Form/RegistrationFormType.php +++ b/src/Form/RegistrationFormType.php @@ -43,6 +43,9 @@ class RegistrationFormType extends AbstractType { $resolver->setDefaults([ 'data_class' => User::class, + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_token_id' => 'registration', ]); } } diff --git a/src/Repository/ReferenceRepository.php b/src/Repository/ReferenceRepository.php index 178e950..2abb849 100644 --- a/src/Repository/ReferenceRepository.php +++ b/src/Repository/ReferenceRepository.php @@ -25,6 +25,35 @@ class ReferenceRepository extends ServiceEntityRepository ->getResult(); } + public function findHeidelberg($ref): array|Reference + { + if (substr($ref, 0, 2) == 'ld') { + $num = substr($ref, 2); + $qb = $this->createQueryBuilder('r'); + $r = $qb->where($qb->expr()->like('r.label', ':ld')) + ->setParameter('ld', "ld{$num}-%") + ->getQuery() + ->getResult() + ; + } elseif (substr($ref, 0, 2) == 'hc') { + $num = substr($ref, 2); + $qb = $this->createQueryBuilder('r'); + $r = $qb->where($qb->expr()->like('r.label', ':hc')) + ->setParameter('hc', "%-hc{$num}") + ->getQuery() + ->getResult() + ; + + if(count($r) > 0) { + $r = $r[0]; + + $r->setLabel(implode("\n", explode("-", $r->getLabel()))); + } + } + + return $r; + } + // /** // * @return Reference[] Returns an array of Reference objects // */ diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php new file mode 100644 index 0000000..d15f4b5 --- /dev/null +++ b/src/Utils/Utils.php @@ -0,0 +1,35 @@ +from($from) + ->to($to) + ->subject($subject) + ->replyTo($from) + ->html($content); + + try { + $mailer->send($mail); + } catch (TransportExceptionInterface $e) { + die($e->getMessage()); + // some error prevented the email sending; display an + // error message or try to resend the message + } + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 8d33a01..00d22bb 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -12,7 +12,7 @@ {% block title %}Welcome!{% endblock %} {% block stylesheets %}{% endblock %} - + {% block body %}{% endblock %} {% block javascripts %}{% endblock %} diff --git a/templates/default/cheat-sheet.html.twig b/templates/default/cheat-sheet.html.twig new file mode 100644 index 0000000..f2219f1 --- /dev/null +++ b/templates/default/cheat-sheet.html.twig @@ -0,0 +1,163 @@ +{% extends 'base.html.twig' %} + +{% block title %}Markdown Cheat Sheet{% endblock %} + +{% block stylesheets %} + + + + + +{% endblock %} + +{% block javascripts %} + + + + + + + + + + +{% endblock %} + +{% block body %} +
+

Markdown Cheat Sheet

+ +

The following was provided by The Markdown Guid

+ +

This Markdown cheat sheet provides a quick overview of all the Markdown syntax elements. It can't cover every edge case, so if you need more information about any of these elements, refere to the reference guides for basic syntax and extended syntax.

+ +

Basic Syntax

+ +

These are the elements outlined in John Gruberal's original design document. All Markdown applications support these elements. The quoted text is what is needed to render what is displayed right below

+ +

Headings

+ +"# H1"
+

# H1

+ +"## H2"
+

## H2

+ +"### H3"
+

### H3

+ +

Bold

+ +"**bold text**"
+bold text + +

Italic

+ +"*italicized text*"
+italicized text + +

Blockquote

+ +"> blockquote"
+
blockquote
+ +

Ordered List

+ +"1. First item"
+"2. Second item"
+"3. Third item"
+
+ +
    +
  1. First item
  2. +
  3. Second item
  4. +
  5. Third item
  6. +
+ +

Unordered List

+ +"- First item"
+"- Second item"
+"- Third item"
+
+ +
    +
  • First item
  • +
  • Second item
  • +
  • Third item
  • +
+ +

Code

+ +"`code`"
+ +code
+ +

Horizontal Rule

+ +"---"
+ +
+ +

Link

+ +"[Markdown Guide](https://www.markdownguide.org)"
+ +Markdown Guide
+ +

Image

+ +"![alt text](https://www.markdownguide.org/assets/images/tux.png)"
+ +alt text
+ +

Extended Syntax

+ +

These elements extend the basic syntax by adding additional features. Not all Markdown applications support these elements.

+ +

Table

+ +| Syntax | Description |
+| ----------- | ----------- |
+| Header | Title |
+| Paragraph | Text |
+
+ + + + + + + + + + + + + + + + +
SyntaxDescription
HeaderTitle
ParagraphText
+ +

Fenced Code Block

+ +

Specifying the language after the first 3 backticks may help with text formatting and highlighting based on the language entered, may or may not be supported in a given Markdown tools implementation

+ +```json
+{
+  "firstName": "John",
+  "lastName": "Smith",
+  "age": 25,
+}
+```
+ +
+{
+  "firstName": "John",
+  "lastName": "Smith",
+  "age": 25
+}
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/default/home.html.twig b/templates/default/home.html.twig index c1c0381..0c6936d 100644 --- a/templates/default/home.html.twig +++ b/templates/default/home.html.twig @@ -3,21 +3,51 @@ {% block title %}Sermon Notes{% endblock %} {% block stylesheets %} - - - - + + + + + + {% endblock %} {% block javascripts %} - - - - - - - - + + + + + + + + + + + + {% endblock %} {% block body %} @@ -34,11 +64,11 @@
    - +
-
+
- +
@@ -47,11 +77,6 @@ - - {% for n in reverseNoteSort %} - {{ n.toTableRow()|raw }} - {% endfor %} -
TitleDate
@@ -59,10 +84,10 @@

Notes

- +
- {% for t in app.user.templates %} @@ -70,22 +95,27 @@   -   +     +
- + + +
+ @@ -93,14 +123,13 @@ -
- +
@@ -121,6 +150,12 @@ + + + + + + @@ -134,4 +169,25 @@
+ + + + + + {% endblock %} \ No newline at end of file diff --git a/templates/default/index.html.twig b/templates/default/index.html.twig index 1bee19e..e8560d3 100644 --- a/templates/default/index.html.twig +++ b/templates/default/index.html.twig @@ -3,16 +3,18 @@ {% block title %}Sermon Notes{% endblock %} {% block stylesheets %} - - + + + + {% endblock %} {% block javascripts %} - - - - - + + + + + {% endblock %} @@ -22,15 +24,9 @@
@@ -42,7 +38,7 @@
- +
diff --git a/templates/default/profile.html.twig b/templates/default/profile.html.twig new file mode 100644 index 0000000..3b49346 --- /dev/null +++ b/templates/default/profile.html.twig @@ -0,0 +1,199 @@ +{% extends 'base.html.twig' %} + +{% block title %}Profile | Sermon Notes{% endblock %} + +{% block stylesheets %} + + + + + + +{% endblock %} + +{% block javascripts %} + + + + + + + + + +{% endblock %} + +{% block body %} +
+ +
+
+

User Profile

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

Site Settings

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

My Shared Notes

+ + + + + + + + {% for s in shared %} + {% endfor %} + +
RecipientTitlePassage
+
+ +
+

Notes Shared w/Me

+ + + + + + + + + +
OwnerTitlePassage
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/default/sidebar.html.twig b/templates/default/sidebar.html.twig index 55f4c00..ff70c98 100644 --- a/templates/default/sidebar.html.twig +++ b/templates/default/sidebar.html.twig @@ -13,26 +13,26 @@ diff --git a/templates/editors/reference-editor.html.twig b/templates/editors/reference-editor.html.twig index d490bc5..f37af15 100644 --- a/templates/editors/reference-editor.html.twig +++ b/templates/editors/reference-editor.html.twig @@ -2,51 +2,40 @@ Reference Editor + - - - {% for c in creeds %} - - {% endfor %} - - - {% for c in belgic %} - - {% endfor %} - - - {% for c in heidelberg %} - - {% endfor %} - - - {% for c in dort %} - - {% endfor %} - - - {% for c in wcf %} - - {% endfor %} - - - {% for c in wsc %} - - {% endfor %} - - - {% for c in wlc %} - - {% endfor %} - - + + + + + + + + + + + + + + +    +    +    - Back
+ Back
- + {{ encore_entry_script_tags('app') }} \ No newline at end of file diff --git a/templates/editors/series-editor.html.twig b/templates/editors/series-editor.html.twig index b059c32..4ffa33c 100644 --- a/templates/editors/series-editor.html.twig +++ b/templates/editors/series-editor.html.twig @@ -13,8 +13,8 @@          - Back + Back - + \ No newline at end of file diff --git a/templates/editors/speaker-editor.html.twig b/templates/editors/speaker-editor.html.twig index 6868c36..ee7eb4f 100644 --- a/templates/editors/speaker-editor.html.twig +++ b/templates/editors/speaker-editor.html.twig @@ -13,8 +13,8 @@          - Back + Back - + \ No newline at end of file diff --git a/templates/editors/template-editor.html.twig b/templates/editors/template-editor.html.twig index 557187e..bf19c1d 100644 --- a/templates/editors/template-editor.html.twig +++ b/templates/editors/template-editor.html.twig @@ -14,12 +14,17 @@          - Back + Back
- + + \ No newline at end of file diff --git a/templates/emails/note-shared.html.twig b/templates/emails/note-shared.html.twig new file mode 100644 index 0000000..bf2e75a --- /dev/null +++ b/templates/emails/note-shared.html.twig @@ -0,0 +1,31 @@ + + + + + + +

+ {{ owner.name }} has shared a note with you, what follows are their formatted notes. + {% if isRegistered %} + You can see shared notes here. + {% else %} + You can register for an account here, or just review the notes below + {% endif %} +

+

{{ note.title }}

+
+ Passage: {{ note.passage }}
+ Date: {{ note.date | date("F j Y") }}
+ Speaker: {{ note.speaker.name }}
+ Series: {{ note.series.name }}
+
+ + {{ formattedText | raw }} + + \ No newline at end of file diff --git a/templates/emails/registration.html.twig b/templates/emails/registration.html.twig new file mode 100644 index 0000000..e66e8d9 --- /dev/null +++ b/templates/emails/registration.html.twig @@ -0,0 +1,9 @@ + + + + +

New User Registered

+ Name: {{ user.name }}
+ Email: {{ user.email }} + + diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig index 521a291..c751ac1 100644 --- a/templates/registration/register.html.twig +++ b/templates/registration/register.html.twig @@ -3,17 +3,17 @@ {% block title %}Register{% endblock %} {% block stylesheets %} - - + + {% endblock %} {% block javascripts %} - - - - - - + + + + + + {% endblock %} {% block body %} @@ -27,41 +27,19 @@ {{ form_errors(registrationForm) }} {{ form_start(registrationForm) }} - {{ form_row(registrationForm.name) }} - {{ form_row(registrationForm.email) }} + + + + + + + {{ form_row(registrationForm.plainPassword, { label: 'Password' }) }} - + {{ form_end(registrationForm) }} - {% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index e6a6b3d..7cefb37 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -3,17 +3,17 @@ {% block title %}Login{% endblock %} {% block stylesheets %} - - + + {% endblock %} {% block javascripts %} - - - - - - + + + + + + {% endblock %} {% block body %}