diff --git a/frontend/src/ketr-photos/ketr-photos.html b/frontend/src/ketr-photos/ketr-photos.html index 2d966cf..c25356f 100755 --- a/frontend/src/ketr-photos/ketr-photos.html +++ b/frontend/src/ketr-photos/ketr-photos.html @@ -304,6 +304,15 @@ #requestAccess #createButton { margin-top: 1.5em; } + + #holiday > div:first-child { + font-weight: bold; + } + + #holiday [tabindex]:hover { + text-decoration: underline; + cursor: pointer; + } @@ -314,12 +323,18 @@ - +
+
+
Holidays
+ +
... time slider ...
@@ -386,7 +401,7 @@
[[item.name]] /
-
Thanksgiving!
+
[[holidayTitle]]
time
Photos taken on [[memoryDate]]
@@ -551,6 +566,10 @@ date: { type: String, value: window.moment().format("YYYY-MM-DD") + }, + holiday: { + type: String, + value: "Christmas" } }, @@ -846,6 +865,12 @@ "iron-resize" : "onResize" }, + loadHoliday: function(event) { + this.holiday = event.model.item; + this.resetPhotos(); + this._loadPhotos(); + }, + loadPath: function(event) { this._pathLoad(event.model.item.path); }, @@ -1695,8 +1720,8 @@ path = ""; } else if (mode == "memories") { path = "memories/" + (this.date.replace(this.year + "-", "") || ""); - } else if (mode == "thanksgiving") { - path = "thanksgiving/"; + } else if (mode == "holiday") { + path = "holiday/" + this.holiday; } } var username = this.user ? this.user.username : ""; @@ -1711,7 +1736,7 @@ if ((username != (this.user ? this.user.username : "")) || (mode != this.mode) || ((mode == "albums") && (path != (this.path || ""))) || - ((mode == "thanksgiving") && (path != ("thanksgiving/"))) || + ((mode == "holiday") && (path != ("holiday/" + this.holiday))) || ((mode == "memories") && (path != ("memories/" + (this.date.replace(this.year + "-", "") || ""))))) { console.log("Skipping results for old query. Triggering re-fetch of photos for new path or mode."); this._loadPhotos(); @@ -1750,6 +1775,9 @@ this._loadPhotos(results.cursor, true, this.limit * 2); } + if (this.mode == "holiday") { + this.holidayTitle = results.holiday; + } }.bind(this, path)); }, @@ -1903,6 +1931,30 @@ } }, + loadHolidays: function() { + window.fetch("api/v1/holidays", function(error, xhr) { + if (error) { + console.error(JSON.stringify(error, null, 2)); + return; + } + + var results; + try { + results = JSON.parse(xhr.responseText); + } catch (___) { + this.$.toast.text = "Unable to parse holidays."; + this.$.toast.setAttribute("error", true); + this.$.toast.updateStyles(); + this.$.toast.show(); + console.log(xhr.responseText); + return; + } + + this.holiday = results.next; + this.holidays = results.holidays; + }.bind(this)); + }, + userChanged: function(user) { if (!this.firstRequest) { this.mode = "loading"; @@ -1923,6 +1975,7 @@ if (!user.restriction) { this.loginStatus = null; this.mode = "memories"; + this.loadHolidays(); this.setActions(); return; } diff --git a/package.json b/package.json index 08f5d3e..6071bc4 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "ldapauth-fork": "^4.0.2", "mariasql": "^0.2.6", "moment": "^2.22.2", + "moment-holiday": "^1.5.1", "morgan": "^1.9.0", "mustache": "^3.0.0", "nodemailer": "^4.6.8", diff --git a/server/app.js b/server/app.js index d7eede7..ca98062 100755 --- a/server/app.js +++ b/server/app.js @@ -263,6 +263,7 @@ app.use(basePath, express.static(picturesPath, { index: false })); app.use(basePath + "api/v1/photos", require("./routes/photos")); app.use(basePath + "api/v1/days", require("./routes/days")); app.use(basePath + "api/v1/albums", require("./routes/albums")); +app.use(basePath + "api/v1/holidays", require("./routes/holidays")); app.use(basePath + "api/v1/scan", require("./routes/scan")(scanner)); /* Declare the "catch all" index route last; the final route is a 404 dynamic router */ diff --git a/server/lib/pascha.js b/server/lib/pascha.js new file mode 100644 index 0000000..74692dd --- /dev/null +++ b/server/lib/pascha.js @@ -0,0 +1,108 @@ +//! moment-holiday.js locale configuration +//! locale : pascha Related Holidays +//! author : Kodie Grantham : https://github.com/kodie + +//(function() { +// var moment = (typeof require !== 'undefined' && require !== null) && !require.amd ? require('moment') : this.moment; +function init(moment) { +// moment.holidays.pascha = { + moment.modifyHolidays.add({ + "Lent": { + date: 'pascha-46|pascha-3' + }, + /* + "Holy Monday": { + date: 'pascha-6', + keywords_y: ['great', 'monday'] + }, + "Holy Tuesday": { + date: 'pascha-5', + keywords_y: ['great', 'tuesday'] + }, + "Holy Wednesday": { + date: 'pascha-4', + keywords_y: ['great', 'wednesday'] + }, + "Holy Thursday": { + date: 'pascha-3', + keywords_y: ['great', 'thursday'] + }, + "Holy Friday": { + date: 'pascha-2', + keywords_y: ['great', 'friday'] + }, + "Holy Saturday": { + date: 'pascha-1', + keywords_y: ['holy', 'saturday'] + }, + */ + "Pascha Sunday": { + date: 'pascha', + keywords_y: ['pascha'], + keywords: ['sunday'] + }, + "Bright Week": { + date: 'pascha+1|pascha+6' + }, + "Pentecost Sunday": { + date: 'pascha+49', + keywords_y: ['pentecost'], + keywords: ['sunday'] + }, + //}; + }); + + /** + * Calculates Easter in the Gregorian/Western (Catholic and Protestant) calendar + * based on the algorithm by Oudin (1940) from http://www.tondering.dk/claus/cal/easter.php + * @returns {array} [int month, int day] + */ + var pascha = function(year) { + var f = Math.floor, + // Golden Number - 1 + G = year % 19, + C = f(year / 100), + // related to Epact + H = (C - f(C / 4) - f((8 * C + 13)/25) + 19 * G + 15) % 30, + // number of days from 21 March to the Paschal full moon + I = H - f(H/28) * (1 - f(29/(H + 1)) * f((21-G)/11)), + // weekday for the Paschal full moon + J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7, + // number of days from 21 March to the Sunday on or before the Paschal full moon + L = I - J, + month = 3 + f((L + 40)/44), + day = L + 28 - 31 * f(month / 4); + + return moment([year, (month - 1),day]); + } + + moment.modifyHolidays.extendParser(function(m, date){ + if (~date.indexOf('pascha')) { + var dates = date.split('|'); + var ds = []; + + for (var i = 0; i < dates.length; i++) { + if (dates[i].substring(0, 6) === 'pascha') { + var e = pascha(m.year()); + + if (dates[i].charAt(6) === '-') { e.subtract(dates[i].substring(7), 'days'); } + if (dates[i].charAt(6) === '+') { e.add(dates[i].substring(7), 'days'); } + + if (dates.length === 1) { return e; } + ds.push(e.format('M/D')); + } else { + ds.push(dates[i]); + } + } + + if (ds.length) { return ds.join('|'); } + } + }); + + console.log("Pascha initialized"); +} + +module.exports = init; + +// if ((typeof module !== 'undefined' && module !== null ? module.exports : void 0) != null) { module.exports = moment; } +//}).call(this); \ No newline at end of file diff --git a/server/routes/holidays.js b/server/routes/holidays.js new file mode 100644 index 0000000..3340a29 --- /dev/null +++ b/server/routes/holidays.js @@ -0,0 +1,49 @@ +"use strict"; + +const express = require("express"), + moment = require("moment-holiday"); + +require("../lib/pascha.js")(moment); + +const router = express.Router(); + +/* Remove the western Easter dates, except for Easter itself */ +[ 'Good Friday' ].forEach(function(holiday) { + moment.modifyHolidays.remove(holiday); +}); + +router.get("/", function(req, res/*, next*/) { + let holidays = [], skip = {}; + + + moment("2000-01-01", "YYYY-MM-DD").holidaysBetween("2000-12-31").forEach(function(holiday) { + /* Dates with multiple holidays will return an array of items */ + let names = holiday.isHoliday(); + if (!Array.isArray(names)) { + names = [ names ]; + } + names.forEach(function(name) { + if (name in skip) { + return; + } + + /* If this holiday already exists, remove it from the holidays list + * as we only want single day events returned, and add to the 'skip' + * list */ + let index = holidays.indexOf(name); + if (index != -1) { + holidays.splice(index, 1); + skip[name] = true; + } else { + holidays.push(name); + } + }); + }); + + return res.status(200).send({ + holidays: holidays, + next: moment().nextHoliday().isHoliday() + }); +}); + +module.exports = router; diff --git a/server/routes/photos.js b/server/routes/photos.js index 0b5ed48..75f7459 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -3,11 +3,13 @@ const express = require("express"), fs = require("fs"), config = require("config"), - moment = require("moment"), + moment = require("moment-holiday"), crypto = require("crypto"), util = require("util"), Promise = require("bluebird"); +require("../lib/pascha.js")(moment); + const execFile = util.promisify(require("child_process").execFile); let photoDB; @@ -538,56 +540,40 @@ router.delete("/:id?", function(req, res/*, next*/) { }); }); -router.get("/thanksgiving", function(req, res/*, next*/) { - let thanksgiving = [ - "1995-11-23", - "1996-11-28", - "1997-11-27", - "1998-11-26", - "1999-11-25", - "2000-11-23", - "2001-11-22", - "2002-11-28", - "2003-11-27", - "2004-11-25", - "2005-11-24", - "2006-11-23", - "2007-11-22", - "2008-11-27", - "2009-11-26", - "2010-11-25", - "2011-11-24", - "2012-11-22", - "2013-11-28", - "2014-11-27", - "2015-11-26", - "2016-11-24", - "2017-11-23", - "2018-11-22", - "2019-11-28", - "2020-11-26" - ]; +router.get("/holiday/:holiday", function(req, res/*, next*/) { + let startYear = 1990, + dayIsHoliday = "", + holidayName; - let dayIsThanksgiving = ""; - thanksgiving.forEach(function(date) { - let comparison = "strftime('%Y-%m-%d',taken)='" + date + "'"; - if (!dayIsThanksgiving) { - dayIsThanksgiving = comparison; - } else { - dayIsThanksgiving += " OR " + comparison; + let lookup = moment().holidays([req.params.holiday]); + if (!lookup) { + return res.status(404).send(req.params.holiday + " holiday not found."); + } + holidayName = Object.getOwnPropertyNames(lookup)[0]; + + for (let year = startYear; year <= moment().year(); year++) { + let holiday = moment(year + "-01-01", "YYYY-MM-DD").holiday(req.params.holiday); + if (!holiday) { + /* 'Leap Year' doesn't exist every year... */ + continue; } - }); + + let comparison = "strftime('%Y-%m-%d',taken)='" + holiday.format("YYYY-MM-DD") + "'" + if (!dayIsHoliday) { + dayIsHoliday = comparison; + } else { + dayIsHoliday += " OR " + comparison; + } + } let query = "SELECT photos.*,albums.path AS path FROM photos " + "INNER JOIN albums ON (albums.id=photos.albumId) " + - "WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND (" + dayIsThanksgiving + ")) " + + "WHERE (photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL AND (" + dayIsHoliday + ")) " + "ORDER BY strftime('%Y-%m-%d', taken) DESC,id DESC"; return photoDB.sequelize.query(query, { type: photoDB.Sequelize.QueryTypes.SELECT }).then(function(photos) { - console.log(query); - console.log(JSON.stringify(photos)); photos.forEach(function(photo) { for (var key in photo) { if (photo[key] instanceof Date) { @@ -597,6 +583,7 @@ router.get("/thanksgiving", function(req, res/*, next*/) { }); return res.status(200).json({ + holiday: holidayName, items: photos });