initial commit

This commit is contained in:
Klemek
2022-01-14 19:16:26 +01:00
commit 4aa6ca4798
8 changed files with 16278 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/libs
+56
View File
@@ -0,0 +1,56 @@
module.exports = {
env: {
commonjs: true,
es2021: true,
browser: true,
},
globals: {
Vue: 'readonly',
LZString: 'readonly',
},
extends: [ 'eslint:recommended' ],
parserOptions: {
ecmaVersion: 12,
},
rules: {
'indent': [ 'error', 4 ],
'linebreak-style': [ 'error', 'unix' ],
'quotes': [ 'error', 'single' ],
'semi': [ 'error', 'always' ],
'curly': [ 'error', 'all' ],
'brace-style': [ 'error', '1tbs' ],
'jest/no-done-callback': 'off',
'jest/expect-expect': 'off',
'comma-dangle': [ 'error', 'always-multiline' ],
'complexity': 'error',
'consistent-return': 'error',
'dot-location': [ 'error', 'property' ],
'eqeqeq': [ 'error', 'always', { null: 'ignore' } ],
'no-empty-function': 'error',
'no-floating-decimal': 'error',
'no-multi-spaces': 'error',
'camelcase': [ 'error', { properties: 'never' } ],
'comma-spacing': [ 'error', { before: false, after: true } ],
'array-bracket-newline': [ 'error', { multiline: true } ],
'array-element-newline': [ 'error', { multiline: true, minItems: 6 } ],
'array-bracket-spacing': [ 'error', 'always' ],
'object-curly-spacing': [ 'error', 'always' ],
'comma-style': 'error',
'computed-property-spacing': 'error',
'eol-last': 'error',
'func-call-spacing': 'error',
'key-spacing': 'error',
'keyword-spacing': 'error',
'multiline-comment-style': 'error',
'newline-per-chained-call': 'error',
'no-lonely-if': 'error',
'no-multiple-empty-lines': 'error',
'no-trailing-spaces': 'error',
'no-unneeded-ternary': 'error',
'no-whitespace-before-property': 'error',
'operator-assignment': 'error',
'quote-props': [ 'error', 'consistent-as-needed' ],
'space-before-blocks': 'error',
'space-infix-ops': 'error',
},
};
+1
View File
@@ -0,0 +1 @@
.idea
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Life Calendar</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript" src="libs/vue.global.js"></script>
<script type="text/javascript" src="libs/lz-string.min.js"></script>
<script type="text/javascript" src="main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- card related -->
<!--
<meta name="twitter:card" content="summary_large_image">
<meta property="og:title" content="">
<meta property="twitter:title" content="">
<meta property="og:description" content="">
<meta property="twitter:description" content="">
<meta property="og:image" content="https://.../preview_640x320.jpg">
<meta property="twitter:image" content="">
<meta property="org:url" content="https://...">
-->
</head>
<body>
<main id="app" style="display:none;">
<h1>Life Calendar in weeks</h1>
View:&nbsp;<select v-model="view">
<option value="">Events</option>
<option v-for="view in views" v-bind:value="view.name">{{view.name}}</option>
</select>
<br>
<br>
<table class="life">
<tr v-for="(year, row) in life">
<td v-for="(week, col) in year" class="week tooltip" v-bind:style="getStyle(row, col)"><span class="tooltiptext" v-html="getTooltip(row, col)" /></td>
</tr>
</table>
<br>
<hr>
<div>
<h2>Config</h2>
<table class="config">
<tr>
<td>Birthdate:</td>
<td><input type="date" v-model="birth"></td>
<td><button v-on:click="reset">Reset all data</button></td>
</tr>
</table>
<h3>Events:</h3>
<table class="config">
<tr v-for="(event, i) in events">
<td><input type="checkbox" v-model="event.display"></td>
<td><input type="date" v-model="event.date"></td>
<td><input v-model="event.text"></td>
<td><select-color v-model="event.color"></select-color></td>
<td><button v-on:click="deleteEvent(i)">Delete</button></td>
</tr>
<tr>
<td><input type="checkbox" checked disabled></td>
<td><input type="date" v-model="newEvent.date"></td>
<td><input v-model="newEvent.text"></td>
<td><select-color v-model="newEvent.color"></select-color></td>
<td><button v-on:click="addEvent">Add</button></td>
</tr>
</table>
<h3>Views:</h3>
<table class="config">
<tbody v-for="(view, viewIndex) in views">
<tr>
<th colspan="5"><input style="width:100%" v-model="view.name"></th>
<th><button v-on:click="deleteView(viewIndex)">Delete</button></th>
</tr>
<tr v-for="(period, periodIndex) in view.periods">
<td></td>
<td><input type="date" v-model="period.startDate"></td>
<td><input type="date" v-model="period.endDate"></td>
<td><input v-model="period.text"></td>
<td><select-color v-model="period.color"></select-color></td>
<td><button v-on:click="deletePeriod(viewIndex, periodIndex)">Delete</button></td>
</tr>
<tr>
<td></td>
<td><input type="date" v-model="view.newPeriod.startDate"></td>
<td><input type="date" v-model="view.newPeriod.endDate"></td>
<td><input v-model="view.newPeriod.text"></td>
<td><select-color v-model="view.newPeriod.color"></select-color></td>
<td><button v-on:click="addPeriod(viewIndex)">Add</button></td>
</tr>
</tbody>
<tbody>
<tr>
<th colspan="6"><button style="width:100%" v-on:click="addView">Add</button></th>
</tr>
</tbody>
</table>
</div>
<br>
<hr>
<small><a href="https://twitter.com/_klemek" target="_blank">@Klemek</a> - <a href="" target="_blank">Github Repository</a> - 2022</small>
</main>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);
+15704
View File
File diff suppressed because it is too large Load Diff
+310
View File
@@ -0,0 +1,310 @@
/* 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 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: `
<select class="select-color" v-bind:value="modelValue" v-on:input="$emit('update:modelValue', $event.target.value)" v-bind:style="{'background-color':colorPalette[modelValue]}">
<option v-for="(color, i) in colorPalette" v-bind:value="i" v-bind:style="{'background-color':color}">Color {{i + 1}}</option>
</select>
`,
};
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: 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()})<br>Age: ${row}`;
const views = this.getViews(row, col);
for (const view in views) {
text += `<br>${view}: ${views[view].text}`; //TODO percent
}
this.getEvents(row, col).forEach(event => {
text += `<br>- ${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(this.newEvent);
}
},
deleteView(viewIndex) {
this.views.pop(viewIndex);
},
addView() {
this.views.push({
name: 'New View',
periods: [],
newPeriod: 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(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;
},
},
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;
}
},
mounted() {
setTimeout(this.showApp);
},
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');
};
+104
View File
@@ -0,0 +1,104 @@
* {
box-sizing: border-box;
font-family: Verdana, serif;
color: #424242;
}
html, body {
margin: 0;
padding: 0;
height: 100vh;
max-width: 120%;
}
body {
background-color: #F5F5F5;
}
main {
padding: 1.5rem;
margin: auto;
background-color: #EEEEEE;
min-height: 100%;
overflow: hidden;
}
h1 {
margin-bottom: .5em;
}
table.life {
margin:-1.5rem;
table-layout: fixed;
}
td.week{
margin:3px;
width:12px;
height: calc(100vw/52 - 2px);
padding:0;
border: 1px solid #424242;
}
table.config{
border-collapse: collapse;
}
table.config td{
padding:.2em;
}
table.config td button{
width:100%;
}
@media only screen and (min-width: 768px) {
main {
max-width: 48rem;
overflow: visible;
}
table.life{
margin: auto;
}
td.week{
height: 12px;
}
}
.select-color {
width:100px;
color: white;
}
.select-color option {
color: white;
}
.tooltip {
position: relative;
}
.tooltip .tooltiptext {
visibility: hidden;
background-color: #424242;
padding: 5px;
text-align: left;
font-size: .5em;
border-radius: 0 6px 6px 6px;
position: absolute;
z-index: 1;
top: -5px;
left: 105%;
top: 105%;
opacity: 0;
display: table;
width: fit-content;
transition: opacity .5s;
white-space: nowrap;
color: #fff;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}