diff --git a/content/reading-tracker.html b/content/reading-tracker.html new file mode 100644 index 0000000..e81d016 --- /dev/null +++ b/content/reading-tracker.html @@ -0,0 +1,353 @@ +--- +title: "Time Spent reading the Word" +draft: false +--- + + + +
+

hours and minutes total

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

Weekly Reading

+ + +

Hours of the day

+ + +

Days of the week

+ +
+ + + + + + \ No newline at end of file diff --git a/db/scripts/create-db.sql b/db/scripts/create-db.sql index 822c678..01c583e 100644 --- a/db/scripts/create-db.sql +++ b/db/scripts/create-db.sql @@ -3,4 +3,12 @@ create table if not exists prayers ( groupname varchar(25), duration numeric not null, submitted_time timestamp +); + +create table if not exists reading ( + id serial primary key, + groupname varchar(25), + duration numeric not null, + submitted_time timestamp, + passage varchar(25) ); \ No newline at end of file diff --git a/pi/backend/.dockerignore b/pi/backend/.dockerignore new file mode 100644 index 0000000..bac6d50 --- /dev/null +++ b/pi/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore diff --git a/pi/backend/Dockerfile b/pi/backend/Dockerfile new file mode 100644 index 0000000..6c9d28a --- /dev/null +++ b/pi/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM node + +RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app +WORKDIR /home/node/app +COPY package*.json ./ +RUN npm install +COPY --chown=node:node app.js . +EXPOSE 3000 +CMD [ "node", "app.js" ] \ No newline at end of file diff --git a/pi/backend/app.js b/pi/backend/app.js index 1cc0083..4ee3cb9 100644 --- a/pi/backend/app.js +++ b/pi/backend/app.js @@ -35,6 +35,29 @@ app.post('/prayer',async (request,response) => { }); +app.get('/reading', async (req, res) => { + const readingMinutes = await getReadingMinutes(); + res.send(200, readingMinutes); +}) + +app.get('/reading/timespan', async (req, res) => { + const timespans = await getReadingByTimespan(); + res.send(200, timespans); +}) + +app.post('/reading',async (request,response) => { + const duration = parseInt(request.query.minutes); + const passage = request.query.passage; + try { + await saveReadingEntry(duration, passage) + response.sendStatus(200); + } + catch(error) { + response.sendStatus(500); + } + +}); + app.listen(port, '127.0.0.1', () => { console.log(`Example app listening on port ${port}`) }) @@ -47,12 +70,26 @@ async function savePrayerEntry(duration) { await queryDB(writeQuery, values); } +async function saveReadingEntry(duration, passage) { + const writeQuery = 'insert into reading (duration, passage, submitted_time)' + + `values ($1, $2, CURRENT_TIMESTAMP)`; + const values = [duration, passage]; + + await queryDB(writeQuery, values); +} + async function getPrayerMinutes() { const readQuery = 'select sum(duration) from prayers;'; let readResult = await queryDB(readQuery); return readResult.rows[0].sum || 0; } +async function getReadingMinutes() { + const readQuery = 'select sum(duration) from reading;'; + let readResult = await queryDB(readQuery); + return readResult.rows[0].sum || 0; +} + async function getEntriesByTimespan() { const readQuery = `select json_agg(subquery) as timespans from (select *, extract(hour from submitted_time) as hour, @@ -64,6 +101,17 @@ async function getEntriesByTimespan() { return readResult.rows[0]; } +async function getReadingByTimespan() { + const readQuery = `select json_agg(subquery) as timespans from (select *, + extract(hour from submitted_time) as hour, + extract(dow from submitted_time) as day + from reading where submitted_time is not null order by hour) as subquery;` + + const readResult = await queryDB(readQuery); + + return readResult.rows[0]; +} + async function queryDB(query, values = null) { const client = await pool.connect(); let response; diff --git a/static/reading.js b/static/reading.js new file mode 100644 index 0000000..09a573f --- /dev/null +++ b/static/reading.js @@ -0,0 +1,248 @@ +const apiUrl = 'https://api.carrollmedia.dev'; +let tally = 0; + function init() { + // enable active states for buttons in mobile safari + document.addEventListener("touchstart", function () {}, false); + updateTally(); + // getTimespans(); + setInputButtonState(); +} + +function updateTally(tallyType = 'reading') { + fetch(`${apiUrl}/${tallyType}`) + .then((response) => response.json()) + .then((data) => { + totalMinutes = data; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + let tallyDisplayHours = document.getElementsByClassName('tally-display-hours'); + let tallyDisplayMinutes = document.getElementsByClassName('tally-display-minutes'); + + for(element of tallyDisplayHours) { + element.innerText = hours; + } + + for(element of tallyDisplayMinutes) { + element.innerText = minutes; + } +}); +} + +async function getTimespans(tallyType = 'reading') { + const response = await fetch(`${apiUrl}/${tallyType}.timespans`); + const json = await response.json(); + let hoursMap = new Map(); + let daysMap = new Map(); + let timespans = json.timespans; + timespans.forEach(element => { + populateHoursMap(hoursMap, element); + populateDaysMap(daysMap, element); + }); + daysMap = formatDaysMap(daysMap); + const weeklyMap = populateWeeklyMap(timespans); + return [hoursMap, daysMap, weeklyMap]; +} + +function populateDaysMap(daysMap, element) { + if (daysMap.get(element.day)) { + const currentSum = daysMap.get(element.day); + daysMap.set(element.day, currentSum + element.duration); + } else { + daysMap.set(element.day, element.duration); + } +} + +function populateHoursMap(hoursMap, element) { + const formattedHour = element.hour > 12 ? element.hour - 12 + ' pm' : element.hour + ' am'; + element.hour = formattedHour; + if (hoursMap.get(element.hour)) { + const currentSum = hoursMap.get(element.hour); + hoursMap.set(element.hour, currentSum + element.duration); + } else { + hoursMap.set(element.hour, element.duration); + } +} + +function populateWeeklyMap(timespans) { + const sunday = getSunday(); + let weeklyMap = createBaseWeeklyMap(); + timespans = timespans.filter(element => isInCurrentWeek(element, sunday)) + timespans.forEach(element => { + const currentSum = weeklyMap.get(translateDays(element.day)); + weeklyMap.set(translateDays(element.day), currentSum + element.duration); + }); + + return weeklyMap; +} + +function isInCurrentWeek(element, sunday) { + const submittedTime = new Date(element.submitted_time); + return (submittedTime > sunday); +} + +function getSunday() { + const today = new Date(); + const sunday = new Date(today.setDate(today.getDate() - today.getDay())); + sunday.setHours(0); + sunday.setMinutes(0); + return sunday; +} + +function formatDaysMap(daysMap) { + daysMap = new Map([...daysMap.entries()].sort()); + let formattedMap = new Map(); + daysMap.forEach((value, key) => { + formattedMap.set(translateDays(key), value); + }) + return formattedMap; +} + +function createBaseWeeklyMap() { + return new Map([ + ['Sunday', 0], + ['Monday', 0], + ['Tuesday', 0], + ['Wednesday', 0], + ['Thursday', 0], + ['Friday', 0], + ['Saturday', 0] + ]) +} + +function translateDays(dayOfWeek) { + let formattedDay; + switch(dayOfWeek) { + case 0: + formattedDay = 'Sunday'; + break; + case 1: + formattedDay = 'Monday'; + break; + case 2: + formattedDay = 'Tuesday'; + break; + case 3: + formattedDay = 'Wednesday'; + break; + case 4: + formattedDay = 'Thursday'; + break; + case 5: + formattedDay = 'Friday'; + break; + case 6: + formattedDay = 'Saturday'; + break; + } + return formattedDay; +} + +function addTime(event, tallyType = 'reading') { +const hue = document.getElementById('hue'); +const minutes = hue.value; + +fetch(`${apiUrl}/${tallyType}?minutes=${minutes}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } +}) + .then((response) => + { + response.json(); + updateTally(); + }) + .then((data) => { + console.log('Success:', data); + }) + .catch((error) => { + console.error('Error:', error); + }); + +} + +function handleNumberInput() { + setInputButtonState(); +} + +function handleNumberInputBlur(event) { + const value = event.target.value; + + if (event.target.hasAttribute("min") && value < parseFloat(event.target.min)) + event.target.value = event.target.min; + + if (event.target.hasAttribute("max") && value > parseFloat(event.target.max)) + event.target.value = event.target.max; +} + +function setInputButtonState() { + const inputs = document.getElementsByClassName("number-input-text-box"); + + for (let input of inputs) { + if (input.id.length > 0) { // during value transition the old input won't have an id + const value = input.value; + const parent = input.parentElement.parentElement; + + if (parent.children[0] && input.hasAttribute("min")) + parent.children[0].disabled = value <= parseFloat(input.min); + + if (parent.children[2] && input.hasAttribute("max")) + parent.children[2].disabled = value >= parseFloat(input.max); + } + } +} + +function setNumber(event) { + let button = event.target; + let input = document.getElementById(button.dataset.inputId); + + if (input) { + let value = parseFloat(input.value); + let step = parseFloat(input.dataset.step); + + if (button.dataset.operation === "decrement") { + value -= isNaN(step) ? 1 : step; + } else if (button.dataset.operation === "increment") { + value += isNaN(step) ? 1 : step; + } + + if (input.hasAttribute("min") && value < parseFloat(input.min)) { + value = input.min; + } + + if (input.hasAttribute("max") && value > parseFloat(input.max)) { + value = input.max; + } + + if (input.value !== value) { + setInputValue(input, value); + setInputButtonState(); + } + } +} + +function setInputValue(input, value) { + let newInput = input.cloneNode(true); + const parentBox = input.parentElement.getBoundingClientRect(); + + input.id = ""; + + newInput.value = value; + + if (value > input.value) { + // right to left + input.parentElement.appendChild(newInput); + input.style.marginLeft = -parentBox.width + "px"; + } else if (value < input.value) { + // left to right + newInput.style.marginLeft = -parentBox.width + "px"; + input.parentElement.prepend(newInput); + window.setTimeout(function () { + newInput.style.marginLeft = 0 + }, 20); + } + + window.setTimeout(function () { + input.parentElement.removeChild(input); + }, 250); +} \ No newline at end of file