paulo@81: <html>
paulo@81:     <head>
paulo@81:         <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
paulo@81:         <title>Life Calendar</title>
paulo@81:         <style type="text/css">
paulo@81:             body {
paulo@81:                 padding: 0;
paulo@81:                 margin: 0;
paulo@81:                 position: relative;
paulo@81:                 font-family: sans-serif;
paulo@81:                 background-size: cover;
paulo@81:             }
paulo@81: 
paulo@81:             a, a:visited {
paulo@81:                 color: #70BCF9;
paulo@81:             }
paulo@81: 
paulo@81:             table {
paulo@81:                 width: 100%;
paulo@81:                 height: 100%;
paulo@81:             }
paulo@81: 
paulo@81:             table, td {
paulo@81:                 border: 1px solid;
paulo@81:                 border-collapse: collapse;
paulo@81:                 padding: 0;
paulo@81:                 margin: 0;
paulo@81:             }
paulo@81: 
paulo@81:             div#welcome {
paulo@81:                 display: none;
paulo@81: 
paulo@81:                 width: 50vw;
paulo@81:                 margin: 8vh auto 0;
paulo@81:             }
paulo@81: 
paulo@81:             div#welcome h1 {
paulo@81:                 text-align: center;
paulo@81:                 text-transform: uppercase;
paulo@81:                 font-family: serif;
paulo@81:                 font-size: 4em;
paulo@81:             }
paulo@81:             div#welcome form {
paulo@81:                 border: 1px solid #999999;
paulo@81:                 padding: 10px 20px;
paulo@81:             }
paulo@81: 
paulo@81:             div#welcome .settings {
paulo@81:                 margin-left: 5px;
paulo@81:                 padding: 5px;
paulo@81:             }
paulo@81: 
paulo@81:             div#welcome div#footer {
paulo@81:                 color: #a6a6a6;
paulo@81:                 padding-top: 50px;
paulo@81:                 text-align: center;
paulo@81:             }
paulo@81: 
paulo@81:             #settingsForm span.unhappyMessage {
paulo@81:                 font-size: 0.8em;
paulo@81:                 padding-left: 10px;
paulo@81:             }
paulo@81: 
paulo@81:             div#tooltip {
paulo@81:                 background-color: #5070D0;
paulo@81:                 padding: 0.5em;
paulo@81:             }
paulo@81: 
paulo@81:             div#tooltip h1 {
paulo@81:                 color: #DFDFDF;
paulo@81:                 font-size: 1.25em;
paulo@81:                 margin: 1px;
paulo@81:             }
paulo@81: 
paulo@81:             div#tooltip h2 {
paulo@81:                 color: #AAAAAA;
paulo@81:                 font-size: 0.75em;
paulo@81:                 font-weight: normal;
paulo@81:                 margin: 1px;
paulo@81:             }
paulo@81: 
paulo@86:             div#tooltip h3 {
paulo@86:                 color: #333333;
paulo@86:                 font-size: 0.8em;
paulo@86:                 margin: 2px;
paulo@86:             }
paulo@86: 
paulo@81:             td {
paulo@81:                 border-color: #FFFFFF;
paulo@81:             }
paulo@81: 
paulo@81:             table {
paulo@81:                 border-color: #FFFFFF;
paulo@81:             }
paulo@81: 
paulo@81:             tr.previous td {
paulo@81:                 background-color: #BBBBBB;
paulo@81:             }
paulo@81: 
paulo@81:             tr.current td.partial {
paulo@81:                 background-color: #CCDDCC;
paulo@81:             }
paulo@81: 
paulo@81:             tr.current td.today {
paulo@81:                 background-color: #DDDDCC;
paulo@81:             }
paulo@81: 
paulo@81:             tr.current td.future {
paulo@81:                 background-color: #DDCCCC;
paulo@81:             }
paulo@81: 
paulo@81:             tr.future {
paulo@81:                 background-color: #DDDDDD;
paulo@81:             }
paulo@81: 
paulo@81:         </style>
paulo@81:         <script src="jquery-2.1.3.min.js"></script>
paulo@81:         <script src="happy.js"></script>
paulo@81:         <script src="tooltip.js"></script>
paulo@81: 
paulo@81:         <script type="text/javascript">
paulo@81:             var nWeeks = 52;
paulo@81:             var nMsPerDay = 3600 * 24 * 1000;
paulo@81:             var nMsPerWeek = 7 * nMsPerDay;
paulo@81:             var QSParams;
paulo@81: 
paulo@81:             var birthday;
paulo@81:             var birthdayString;
paulo@81:             var birthdayDate;
paulo@81: 
paulo@81:             var lifespan;
paulo@81: 
paulo@81:             var age;
paulo@81:             var ageYears;
paulo@81:             var remainder;
paulo@81: 
paulo@81:             var previousYears;
paulo@81:             var futureYears;
paulo@81:             var remainderWeeks;
paulo@81:             var restOfYearWeeks;
paulo@81: 
paulo@86:             var selecting = false;
paulo@86:             var start_wyc = null;
paulo@86:             var end_wyc = null;
paulo@86: 
paulo@81:             function validateBirthday(birthday) {
paulo@81:                 var birthdayPattern = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
paulo@81:                 return birthdayPattern.test( $("#birthday").val());
paulo@81:             }
paulo@81: 
paulo@81:             function validateLifespan(lifespan) {
paulo@81:                 var lifespanPattern = /^[0-9]{1,3}$/;
paulo@81:                 return lifespanPattern.test( $("#lifespan").val());
paulo@81:             }
paulo@81: 
paulo@81:             // Read a page's GET URL variables and return them as an associative array.
paulo@81:             // From http://jquery-howto.blogspot.ca/2009/09/get-url-parameters-values-with-jquery.html
paulo@81:             function getQSParams()
paulo@81:             {
paulo@81:                 var vars = [], hash;
paulo@81:                 var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
paulo@81:                 for(var i = 0; i < hashes.length; i++)
paulo@81:                 {
paulo@81:                     hash = hashes[i].split('=');
paulo@81:                     vars.push(hash[0]);
paulo@81:                     vars[hash[0]] = hash[1];
paulo@81:                 }
paulo@81:                 return vars;
paulo@81:             }
paulo@81: 
paulo@81:             function isLeapYear(year) {
paulo@81:                 var d = new Date(year, 1, 28);
paulo@81:                 d.setDate(d.getDate() + 1);
paulo@81:                 return d.getMonth() == 1;
paulo@81:             }
paulo@81: 
paulo@81:             function getAge(date) {
paulo@81:                 var d = new Date(date), now = new Date();
paulo@81:                 var years = now.getFullYear() - d.getFullYear();
paulo@81:                 d.setFullYear(d.getFullYear() + years);
paulo@81:                 if (d > now) {
paulo@81:                     years--;
paulo@81:                     d.setFullYear(d.getFullYear() - 1);
paulo@81:                 }
paulo@81:                 var days = (now.getTime() - d.getTime()) / nMsPerDay;
paulo@81:                 return years + days / (isLeapYear(now.getFullYear()) ? 366 : 365);
paulo@81:             }
paulo@81: 
paulo@81:             function getCalendar(d, week, year) {
paulo@81:                 var c = new Date(d);
paulo@81:                 c.setFullYear(d.getFullYear() + year);
paulo@81:                 c.setTime(c.getTime() + (week * nMsPerWeek)); 
paulo@81:                 return c;
paulo@81:             }
paulo@81: 
paulo@81:             function getWeekYearCal(e, week) {
paulo@81:                 var yearParent = e.parentNode;
paulo@81:                 var calParent = yearParent.parentNode;
paulo@81:                 var year;
paulo@81:                 for (year = 0; year < calParent.childNodes.length; year++) {
paulo@81:                     if (calParent.childNodes[year] === yearParent) {
paulo@81:                         break;
paulo@81:                     }
paulo@81:                 }
paulo@81:                 return {
paulo@81:                     week: week,
paulo@81:                     year: year,
paulo@86:                     cal: getCalendar(birthdayDate, week, year),
paulo@86:                     weeks: nWeeks*year + week,
paulo@81:                 }
paulo@81:             }
paulo@81: 
paulo@86:             function inSpan(x, s, e) {
paulo@86:                 return x >= s && x <= e;
paulo@86:             }
paulo@86: 
paulo@81:             function loop(x, f) {
paulo@81:                 for (var i = 0; i < x; f(i++));
paulo@81:             }
paulo@81: 
paulo@81:             function mapLoop(x, f) {
paulo@81:                 var ret = [];
paulo@81:                 for (var i = 0; i < x; i++) {
paulo@81:                   ret.push(f(i));
paulo@81:                 }
paulo@81:                 return ret;
paulo@81:             }
paulo@81: 
paulo@86:             function setStyle(e) {
paulo@86:                 var styles = [];
paulo@86: 
paulo@86:                 if (e["_fill_color"] != undefined) {
paulo@86:                     styles.push("background-color: " + e._fill_color);
paulo@86:                 } 
paulo@86:                 if (e["_border_color"] != undefined) {
paulo@86:                     styles.push("border-color: " + e._border_color);
paulo@86:                 }
paulo@86: 
paulo@86:                 if (styles.length == 0) {
paulo@86:                     e.setAttribute("style", undefined);
paulo@86:                 } else {
paulo@86:                     e.setAttribute("style", styles.join('; '));
paulo@86:                 }
paulo@86:             }
paulo@86: 
paulo@86:             function highlightDistance(e0, e1_wyc, e2_wyc) {
paulo@86:                 var calParent = e0.parentNode.parentNode;
paulo@86:                 var min_wyc = (e1_wyc.weeks <= e2_wyc.weeks) ? e1_wyc : e2_wyc;
paulo@86:                 var max_wyc = (e2_wyc == min_wyc) ? e1_wyc : e2_wyc;
paulo@86:                 for (var year = 0; year < lifespan; year++) {
paulo@86:                     for (var week = 0; week < nWeeks; week++) {
paulo@86:                         w = nWeeks*year + week;
paulo@86:                         e = calParent.childNodes[year].childNodes[week]  
paulo@86:                         if (inSpan(w, min_wyc.weeks, max_wyc.weeks)) {
paulo@86:                             e._border_color = "black";
paulo@86:                         } else {
paulo@86:                             e._border_color = undefined;
paulo@86:                         }
paulo@86:                         setStyle(e);
paulo@86:                     }
paulo@86:                 }
paulo@86:             }
paulo@86: 
paulo@81:             function cycleColors(e) {
paulo@81:                 var colors = [
paulo@81:                     undefined,
paulo@81:                     "black",
paulo@81:                     "red",
paulo@81:                     "green",
paulo@81:                     "blue",
paulo@81:                     "white",
paulo@81:                 ];
paulo@81: 
paulo@81:                 var i = 0;
paulo@81:                 for (; i < colors.length, colors[i] != e._fill_color; i++);
paulo@81:                 e._fill_color = colors[(i + 1) % colors.length];
paulo@86:                 setStyle(e);
paulo@81:             }
paulo@81: 
paulo@81:             function _createElement(tagName, className) {
paulo@81:                 var e = document.createElement(tagName);
paulo@81:                 if (className) {
paulo@81:                     e.className = className;
paulo@81:                 }
paulo@81:                 return e;
paulo@81:             }
paulo@81: 
paulo@81:             function weekElem(className) {
paulo@81:                 return _createElement("td", className);
paulo@81:             }
paulo@81: 
paulo@81:             function yearElem(className) {
paulo@81:                 return _createElement("tr", className);
paulo@81:             }
paulo@81: 
paulo@81:             function yearWeekElems() {
paulo@81:                 return mapLoop(nWeeks, function(i) {
paulo@81:                     var e = weekElem();
paulo@81:                     addWeekMouseEvents(e, i);
paulo@81:                     return e;
paulo@81:                 });
paulo@81:             }
paulo@81: 
paulo@81:             function addWeekMouseEvents(e, i) {
paulo@81:                 e.addEventListener("mouseover", function(evt) {
paulo@81:                     var wyc = getWeekYearCal(e, i);
paulo@81:                     var calYear = wyc.cal.getFullYear();
paulo@81:                     var calMonth = wyc.cal.getMonth() + 1;
paulo@81:                     var calDay = wyc.cal.getDate();
paulo@86:                     var title = calYear + '-' + calMonth + '-' + calDay;
paulo@86:                     var text = "(Week: " + wyc.week + ", Year: " + wyc.year + ")";
paulo@86:                     var subtext = null;
paulo@86:                     if (selecting) {
paulo@86:                         end_wyc = wyc;
paulo@86:                         highlightDistance(e, start_wyc, end_wyc);
paulo@86:                     }
paulo@86:                     if (start_wyc && end_wyc) {
paulo@86:                         var min_wyc = (start_wyc.weeks <= end_wyc.weeks) ? start_wyc : end_wyc;
paulo@86:                         var max_wyc = (end_wyc == min_wyc) ? start_wyc : end_wyc;
paulo@86:                         if (inSpan(wyc.weeks, min_wyc.weeks, max_wyc.weeks)) {
paulo@86:                             var sel_weeks = max_wyc.weeks - min_wyc.weeks;
paulo@86:                             var sel_years = sel_weeks/nWeeks;
paulo@86:                             subtext = "[Selected: weeks: " + sel_weeks + " years: " + sel_years.toFixed(2) + "]";
paulo@86:                         }
paulo@86:                     }
paulo@86:                     createTooltip(evt, title, text, subtext);
paulo@81:                 });
paulo@81:                 e.addEventListener("click", function() {
paulo@86:                     var wyc = getWeekYearCal(e, i);
paulo@86:                     highlightDistance(e, wyc, wyc);
paulo@86:                     if (start_wyc && end_wyc) {
paulo@86:                         start_wyc = null;
paulo@86:                         end_wyc = null;
paulo@86:                     } else {
paulo@86:                         cycleColors(e);
paulo@86:                     }
paulo@86:                 });
paulo@86:                 e.addEventListener("mousedown", function() {
paulo@86:                     selecting = true;
paulo@86:                     start_wyc = getWeekYearCal(e, i);
paulo@86:                 });
paulo@86:                 e.addEventListener("mouseup", function() {
paulo@86:                     selecting = false;
paulo@81:                 });
paulo@81:             }
paulo@81: 
paulo@81:             $( document ).ready(function() {
paulo@81:                 $( "#settingsForm" ).isHappy({
paulo@81:                     fields: {
paulo@81:                         '#birthday': {
paulo@81:                             required: true,
paulo@81:                             test: validateBirthday,
paulo@81:                             message: 'Please enter your birthday (like YYYY-MM-DD).'
paulo@81:                         },
paulo@81:                         '#lifespan': {
paulo@81:                             required: true,
paulo@81:                             test: validateLifespan,
paulo@81:                             message: 'Please enter your expected lifespan (like 90)'
paulo@81:                         }
paulo@81:                     }
paulo@81:                 });
paulo@81: 
paulo@81:                 QSParams = getQSParams();
paulo@81: 
paulo@81:                 if(!("birthday" in QSParams) | !("lifespan" in QSParams)) {
paulo@81:                     // Show the form
paulo@81:                     $("#welcome").css("display", "block");
paulo@81: 
paulo@81:                     // Give the first field focus
paulo@81:                     $("#birthday").focus();
paulo@81:                     return;
paulo@81:                 }
paulo@81: 
paulo@81:                 $("body").append( '<table id="calendar"></table>' );
paulo@81: 
paulo@81:                 birthday = QSParams["birthday"];
paulo@81:                 birthdayString = birthday + "T00:00:00";
paulo@81:                 birthdayDate = new Date(birthdayString);
paulo@81: 
paulo@81:                 lifespan = QSParams["lifespan"];
paulo@81: 
paulo@81:                 age = getAge(birthdayDate);
paulo@81:                 ageYears = Math.floor(age);
paulo@81:                 remainder = age - ageYears;
paulo@81: 
paulo@81:                 previousYears = (ageYears).toFixed(0);
paulo@81: 
paulo@81:                 if( lifespan > ageYears) {
paulo@81:                     futureYears = lifespan - ageYears - 1;
paulo@81:                     remainderWeeks = Math.floor(remainder * nWeeks);
paulo@81:                     restOfYearWeeks = Math.ceil(nWeeks - remainderWeeks) - 1;
paulo@81:                 } else {
paulo@81:                     futureYears = 0;
paulo@81:                     remainderWeeks = nWeeks;
paulo@81:                     restOfYearWeeks = 0;
paulo@81:                 }
paulo@81: 
paulo@81:                 // Fill in lived years
paulo@81:                 loop(previousYears, function() {
paulo@81:                     var tr = yearElem("previous");
paulo@81:                     $(tr).append(yearWeekElems());
paulo@81:                     $("#calendar").append(tr);
paulo@81:                 })
paulo@81: 
paulo@81:                 // Fill in the current birth-year (the number of weeks elapsed since the most recent birthday.)
paulo@81:                 var current_tr = yearElem("current");
paulo@81:                 loop(remainderWeeks, function(i) {
paulo@81:                     var e = weekElem("partial");
paulo@81:                     addWeekMouseEvents(e, i);
paulo@81:                     $(current_tr).append(e);
paulo@81:                 })
paulo@81:                 var e = weekElem("today");
paulo@81:                 addWeekMouseEvents(e, remainderWeeks);
paulo@81:                 $(current_tr).append(e);
paulo@81:                 loop(restOfYearWeeks, function(i) {
paulo@81:                     var e = weekElem("future");
paulo@81:                     addWeekMouseEvents(e, remainderWeeks + 1 + i);
paulo@81:                     $(current_tr).append(e);
paulo@81:                 })
paulo@81:                 $("#calendar").append(current_tr);
paulo@81: 
paulo@81:                 // Fill in future years
paulo@81:                 loop(futureYears, function() {
paulo@81:                     var tr = yearElem("future");
paulo@81:                     $(tr).append(yearWeekElems());
paulo@81:                     $("#calendar").append(tr);
paulo@81:                 })
paulo@81:             });
paulo@81:         </script>
paulo@81:     </head>
paulo@81:     <body>
paulo@81:         <div id="welcome">
paulo@81:             <h1>Life Calendar</h1>
paulo@81:             <p>
paulo@81:                 A minimalist life calendar. Shows the number of weeks you've lived and the number of weeks you
paulo@81:                 have left.
paulo@81:             </p>
paulo@81:             <form method="get" action="" id="settingsForm">
paulo@81:                 <p>
paulo@81:                     <label for="birthday">Birthday:
paulo@81:                         <input class="settings" id="birthday" name="birthday" placeholder="1985-01-01"/>
paulo@81:                     </label>
paulo@81:                 </p>
paulo@81:                 <p>
paulo@81:                     <label for="lifespan">Life expectancy:
paulo@81:                         <input class="settings" id="lifespan" name="lifespan" placeholder="90"/>
paulo@81:                     </label>
paulo@81:                 </p>
paulo@81:                 <p>
paulo@81:                     <input type="submit" id="submit" value="Show calendar" />
paulo@81:                 </p>
paulo@81:             </form>
paulo@81:             <div id="footer">
paulo@81:                 <p>
paulo@81:                     Based on <a href="http://waitbutwhy.com/2014/05/life-weeks.html">Your Life in Weeks</a>
paulo@81:                     and <a href="http://count.life">count.life</a>.
paulo@81:                 </p>
paulo@81:             </div>
paulo@81:         </div>
paulo@81:     </body>
paulo@81: </html>