/* exported app */ const cookies = { /** * Save a value in a cookie * @param {string} name * @param {string} value * @param {number | undefined} days */ set: function (name, value, days = undefined) { const maxAge = !days ? undefined : days * 864e2; document.cookie = `${name}=${encodeURIComponent(value)}${maxAge ? `;max-age=${maxAge};` : ''}`; }, /** * Get a value from a cookie * @param {string} name * @return {string} value from cookie or empty if not found */ get: function (name) { return document.cookie.split('; ').reduce(function (r, v) { const parts = v.split('='); return parts[0] === name ? decodeURIComponent(parts[1]) : r; }, ''); }, /** * Delete a cookie * @param {string} name */ delete: function (name) { this.set(name, '', -1); }, /** * Clear all cookies */ clear: function () { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i]; const eqPos = cookie.indexOf('='); const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`; } }, }; Date.prototype.addDays = function(days) { const date = new Date(this.getTime()); date.setDate(this.getDate() + parseInt(days)); return date; }; Date.prototype.getWeek = function() { const oneJan = new Date(this.getFullYear(), 0, 1); const numberOfDays = Math.floor((this - oneJan) / (24 * 60 * 60 * 1000)); return Math.ceil(( this.getDay() + 1 + numberOfDays) / 7); }; Date.prototype.formatSimple = function() { return `${this.getFullYear()}-${('00' + (this.getMonth() + 1)).substr(-2)}-${('00' + this.getDate()).substr(-2)}`; }; const cloneObject = function(obj) { return JSON.parse(JSON.stringify(obj)); }; const COLOR_PALETTE = [ '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FDD835', '#FFC107', '#FF9800', '#FF5722', '#795548', ]; const selectColor = { props: [ 'modelValue' ], data: function () { return { colorPalette: COLOR_PALETTE, }; }, emits: [ 'update:modelValue' ], template: ` `, }; const NEW_PERIOD = { color: '', startDate: '', endDate: '', text: '', }; const serialize = function(birtdate, view, events, views) { return LZString.compressToBase64(JSON.stringify([ birtdate, view, events.map(event => [ event.color, event.date, event.text, event.display ? 1 : 0 ]), views.map(view => [ view.name, view.periods.map(period => [ period.color, period.startDate, period.endDate, period.text ]) ]) ])); }; const deserialize = function(rawData) { const data = JSON.parse(LZString.decompressFromBase64(rawData)); return { birthdate: data[0], view: data[1], events: data[2].map(subData => { return { color: subData[0], date: subData[1], text: subData[2], display: subData[3] === 1, }; }), views: data[3].map(subData1 => { return { name: subData1[0], periods: subData1[1].map(subData2 => { return { color: subData2[0], startDate: subData2[1], endDate: subData2[2], text: subData2[3], }; }), newPeriod: cloneObject(NEW_PERIOD), }; }), }; }; let app = { data() { return { life: [], birth: '1996-07-25', view: '', newEvent: { color: '', date: '', text: '', display: true, }, events: [], views: [], }; }, computed: { birthdate() { return new Date(this.birth); }, }, methods: { showApp() { document.getElementById('app').setAttribute('style', ''); }, getDate(row, col) { return new Date(this.birthdate.getFullYear() + row, this.birthdate.getMonth(), this.birthdate.getDate()).addDays(col * 7); }, getEvents(row, col) { const startDate = this.getDate(row, col); const endDate = startDate.addDays(7); const strStartDate = startDate.formatSimple(); const strEndDate = endDate.formatSimple(); return this.events.concat( [ { type: 'special', color: null, date: this.birth, text: 'Birth', display: true, }, { type: 'special', color: null, date: new Date().formatSimple(), text: 'Today', display: true, }, ], ).filter(event => event.display && event.date >= strStartDate && event.date < strEndDate); }, getViews(row, col) { const strStartDate = this.getDate(row, col).formatSimple(); const strNow = (new Date()).formatSimple(); const out = {}; this.views.forEach(view => { const extract = view.periods.filter(period => period.startDate <= strStartDate && (!period.endDate && strStartDate <= strNow || period.endDate > strStartDate)); if (extract.length) { out[view.name] = extract[0]; } }); return out; }, getTooltip(row, col) { const date = this.getDate(row, col); let text = `${date.getFullYear()} (Week ${date.getWeek()})
Age: ${row}`; const views = this.getViews(row, col); for (const view in views) { text += `
${view}: ${views[view].text} (${views[view].percent})`; } this.getEvents(row, col).forEach(event => { text += `
- ${new Date(event.date).formatSimple()}: ${event.text}`; }); return text; }, getColor(row, col) { const events = this.getEvents(row, col); for (const i in events) { if (events[i].type === 'special') { return events[i].color; } } if (!this.view) { if (events.length > 0) { return events[0].color; } } else { const views = this.getViews(row, col); if (views[this.view]) { return views[this.view].color; } } return undefined; }, getStyle(row, col) { const color = this.getColor(row, col); return { 'background-color': color ? COLOR_PALETTE[color] : (color === null ? '#212121' : null), 'border-color': color ? COLOR_PALETTE[color] : (color === null ? '#212121' : null), }; }, deleteEvent(eventIndex) { this.events.pop(eventIndex); }, addEvent() { if (this.newEvent.color && this.newEvent.text && this.newEvent.date) { this.events.push(cloneObject(this.newEvent)); } }, deleteView(viewIndex) { this.views.pop(viewIndex); }, addView() { this.views.push({ name: 'New View', periods: [], newPeriod: cloneObject(NEW_PERIOD), }); }, deletePeriod(viewIndex, periodIndex) { this.views[viewIndex].periods.pop(periodIndex); }, addPeriod(viewIndex) { if (this.views[viewIndex].newPeriod.color && this.views[viewIndex].newPeriod.text && this.views[viewIndex].newPeriod.startDate) { this.views[viewIndex].periods.push(cloneObject(this.views[viewIndex].newPeriod)); this.views[viewIndex].newPeriod.startDate = this.views[viewIndex].newPeriod.endDate; this.views[viewIndex].newPeriod.endDate = ''; } }, reset() { this.view = ''; this.events = []; this.views = []; this.$force; }, updateStatistics() { const total = (new Date() - this.birthdate); this.views.forEach(view => { console.log(view); const data = {}; view.periods.forEach(period => { data[period.text + period.color] = (data[period.text + period.color] ?? 0) + ((period.endDate ? new Date(period.endDate) : new Date()) - (new Date(period.startDate))); }); view.periods.forEach(period => { period.percent = `${(data[period.text + period.color] / (7 * 24 * 60 * 60 * 1000)).toFixed(0)}w, ${(100 * data[period.text + period.color] / total).toFixed(2)}%`; }); }); }, }, beforeMount() { this.life = Array(90).fill(Array(52)); const url = new URL(window.location); if (url.searchParams.get('d')) { const data = deserialize(url.searchParams.get('d')); this.birtdate = data.birthdate; this.view = data.view; this.events = data.events; this.views = data.views; } else if (cookies.get('d')) { const data = deserialize(cookies.get('d')); this.birtdate = data.birthdate; this.view = data.view; this.events = data.events; this.views = data.views; } this.updateStatistics(); }, mounted() { setTimeout(this.showApp); }, beforeUpdate() { this.updateStatistics(); }, updated() { const data = serialize(this.birtdate, this.view, this.events, this.views); const url = new URL(window.location); if (url.searchParams.get('d') !== data) { url.searchParams.set('d', data); window.history.pushState({}, '', url); } cookies.set('d', data); }, }; window.onload = () => { app = Vue.createApp(app); app.component('select-color', selectColor); app.mount('#app'); };