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