Created
July 15, 2014 08:13
-
-
Save A-gambit/e8be4f2fb356e1df9df8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {% extends "page/vacation-base.html" %} | |
| {%block page_class%}p-vacation{%endblock%} | |
| {% block content %} | |
| <div class="login"> | |
| <h1 class="login-h1">You need to work in Grammarly.</h1> | |
| <input type="button" value="Login" id="login"/> | |
| </div> | |
| <div class="load-spiner"> | |
| <div class="spinner"> | |
| <div class="double-bounce1"></div> | |
| <div class="double-bounce2"></div> | |
| </div> | |
| </div> | |
| <div class="content"> | |
| <div> | |
| <div class="content__top"> | |
| <div class="content-col"></div> | |
| <div class="content__top__profile-inf"> | |
| <img src="/pic/logo.png" height="30px" class="content__top__profile-inf__logo"> | |
| <div class="content__top__profile-inf__img"> | |
| <img src="" class="content__top__profile-inf__img-img img" width="170px" height="170px"> | |
| </div> | |
| <div class="content__top__profile-inf__txt"> | |
| <h1 class="content__top__profile-inf__txt-name name">Name</h1> | |
| <span class="content__top__profile-inf__txt-vacation vacation">Vacation <span class="content__top__profile-inf__txt-vacation__span-inf">(total/left)</span>:</span><br> | |
| <span class="content__top__profile-inf__txt-wfh wfh">Work from Home:</span><br> | |
| <span class="content__top__profile-inf__txt-sick sick">Sick:</span> | |
| </div> | |
| </div> | |
| <div class="content-col"></div> | |
| </div> | |
| <div class="content__bottom"> | |
| <div class="content-col"></div> | |
| <div class="content__bottom__absent"> | |
| <h1 class="content__bottom__titel-h1 absent-h1">Absent</h1> | |
| </div> | |
| <div class="content__bottom__wft"> | |
| <h1 class="content__bottom__titel-h1 wfh-h1">Work from Home</h1> | |
| </div> | |
| <div class="content__bottom__news"> | |
| <h1 class="content__bottom__titel-h1 news-h1">News</h1> | |
| </div> | |
| <div class="content-col"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="/js/vacation-team.js"></script> | |
| <script src="/js/vacation.js"></script> | |
| <script src="https://apis.google.com/js/client.js"></script> | |
| {%endblock%} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var firstTime = true; | |
| var USHoliday = [], | |
| UAHoliday = [], | |
| email, | |
| vacation=0, | |
| wfh=0, | |
| sick=0, | |
| myJSON = []; | |
| $("#login").onclick = function () { | |
| login(); | |
| $('#login').style.display='none'; | |
| $('.load-spiner').style.display='block'; | |
| $('.login-h1').style.display = "none"; | |
| } | |
| function $(el){ | |
| return document.querySelector(el); | |
| } | |
| function makeRequest() { | |
| req(gapi.client.calendar.calendarList.list()) | |
| } | |
| function req(request) { | |
| request.execute(function(response) { | |
| console.log(response); | |
| if(firstTime) | |
| knowCalendar(response); | |
| else | |
| workWithCalendar(response); | |
| }); | |
| } | |
| function knowCalendar(response){ | |
| email = response.items[0].id; | |
| console.log(email); | |
| var isEmail= email.indexOf("@grammarly.com") >= 0; | |
| if(isEmail) | |
| trueEmail(response.items); | |
| else | |
| repeatEmail(); | |
| function repeatEmail(){ | |
| $("#login").style.cssText = "display: block;"; | |
| $('.load-spiner').style.display='none'; | |
| $('.login-h1').style.display = "block"; | |
| } | |
| function trueEmail(list){ | |
| getHoliday(); | |
| otherСalendars(list); | |
| firstTime = false; | |
| var lastTime = new Date(); | |
| var startTime = new Date(lastTime.getFullYear(), 0, 1); | |
| lastTime.setFullYear(lastTime.getFullYear()+1) | |
| req(gapi.client.calendar.events.list({ | |
| calendarId: 'grammarly.com_471831rd40r114bmjnf7r49uh8@group.calendar.google.com', | |
| maxResults: 2500, | |
| timeMin: startTime, | |
| timeMax: lastTime | |
| })); | |
| } | |
| } | |
| function getHoliday(){ | |
| startTime = new Date((new Date()).getFullYear(), 0, 1); | |
| lastTime = new Date((new Date()).getFullYear(), 11, 31, 23, 59, 59); | |
| function reqHoliday(request) { | |
| request.execute(function(response) { | |
| console.log(response); | |
| sortHoliday(response.items); | |
| }); | |
| } | |
| function sortHoliday(items){ | |
| for (var i = 0; i < items.length; i++) { | |
| var summary = items[i].summary; | |
| var isUA = summary.indexOf("Ukraine") >= 0, | |
| isUS = summary.indexOf("US") >= 0; | |
| if(isUA) | |
| UAHoliday.push(items[i]); | |
| if(isUS) | |
| USHoliday.push(items[i]); | |
| }; | |
| } | |
| reqHoliday(gapi.client.calendar.events.list({ | |
| calendarId: 'grammarly.com_471831rd40r114bmjnf7r49uh8@group.calendar.google.com', | |
| maxResults: 2500, | |
| timeMin: startTime, | |
| timeMax: lastTime, | |
| q: 'Public' | |
| })); | |
| } | |
| function otherСalendars(list){ | |
| var startTime = new Date((new Date()).getFullYear(), (new Date()).getMonth(),(new Date()).getDate(), 0, 0), | |
| lastTime = new Date((new Date()).getFullYear(), (new Date()).getMonth(),(new Date()).getDate(), 23, 59, 59); | |
| for (var i = 1; i < list.length; i++) { | |
| if(i!=3){ | |
| var id = list[i].id; | |
| toList(id); | |
| } | |
| }; | |
| function toList(id){ | |
| reqToday(gapi.client.calendar.events.list({ | |
| calendarId: id, | |
| maxResults: 2500, | |
| timeMin: startTime, | |
| timeMax: lastTime | |
| })); | |
| } | |
| function reqToday(request) { | |
| request.execute(function(response) { | |
| console.log(response.summary,response); | |
| var items = response.items, | |
| elNews = $(".content__bottom__news"); | |
| if(items) | |
| for (var i = 0; i < items.length; i++) { | |
| var summary = items[i].summary; | |
| elNews.innerHTML+= "<h2>"+summary+"</h2>"; | |
| }; | |
| }); | |
| } | |
| } | |
| function workWithCalendar(response){ | |
| var myInf = []; | |
| myInf = getDataAboutMe(response); | |
| console.log(myInf); | |
| getMyJson(myInf); | |
| getInf(myInf); | |
| todayList = getTodaytNews(response); | |
| console.log(todayList); | |
| printList(todayList); | |
| function getMyJson(myInf){ | |
| var userName = myInf[0].creator.displayName; | |
| for (var i = 0; i < members.length; i++) { | |
| var isName = members[i].name == userName; | |
| if(isName){ | |
| myJSON = members[i]; | |
| break; | |
| } | |
| }; | |
| } | |
| } | |
| function printList(list){ | |
| var elNews = $(".content__bottom__news"), | |
| elAbsent = $(".content__bottom__absent"), | |
| elWFH = $(".content__bottom__wft"); | |
| var startNews = elNews.innerHTML, | |
| startAbsent = elAbsent.innerHTML, | |
| startWFH = elWFH.innerHTML | |
| for (var i = 0; i < list.length; i++) { | |
| var summary = list[i].summary; | |
| elNews.innerHTML+= "<h2>"+summary+"</h2>"; | |
| var conditionVacation = summary.indexOf("vacation") >= 0 || summary.indexOf("Vacation") >= 0, | |
| conditionSick = summary.indexOf("sick") >= 0 || summary.indexOf("Sick") >= 0 , | |
| conditionWTH = summary.indexOf("wfh") >=0 || summary.indexOf("WFH") >= 0; | |
| var name = list[i].creator.displayName; | |
| var isSrc = false; | |
| for (var j = 0; j < members.length; j++) { | |
| if(name == members[j].name){ | |
| if(conditionWTH) | |
| elWFH.innerHTML+= "<h2><img src='/pic/team/"+members[j].photo+"' class='content__bottom-img'>"+name+"</h2>"; | |
| else if(conditionVacation || conditionSick) | |
| elAbsent.innerHTML+= "<h2><img src='/pic/team/"+members[j].photo+"' class='content__bottom-img'></span>"+name+"</h2>"; | |
| isSrc = true; | |
| j= members.length; | |
| } | |
| } | |
| if(!isSrc){ | |
| if(conditionWTH) | |
| elWFH.innerHTML+= "<h2><img src='/pic/team/undefined.png' class='content__bottom-img'>"+name+"</h2>"; | |
| else if(conditionVacation || conditionSick) | |
| elAbsent.innerHTML+= "<h2><img src='/pic/team/undefined.png' class='content__bottom-img'></span>"+name+"</h2>"; | |
| } | |
| } | |
| if(startAbsent==elAbsent.innerHTML){ | |
| elAbsent.innerHTML+="<h2>Nobody is Absent</h2>" | |
| } | |
| else{ | |
| var el = $(".absent-h1"); | |
| el.style.cssText="margin-bottom: -4px !important;"; | |
| } | |
| if(startWFH==elWFH.innerHTML){ | |
| elWFH.innerHTML+="<h2>Nobody is Work from Home</h2>" | |
| } | |
| else{ | |
| var el = $(".wfh-h1"); | |
| el.style.cssText="margin-bottom: -4px !important;"; | |
| } | |
| if(startNews==elNews.innerHTML){ | |
| elNews.innerHTML+="<h2>No News</h2>" | |
| } | |
| } | |
| function getTodaytNews(response){ | |
| var events = response.items, | |
| todayList = [], | |
| stopFind = false; | |
| for(var i = events.length-1; i>0 && !stopFind; i--) | |
| if(events[i].creator) addList(i); | |
| return todayList; | |
| function addList(i){ | |
| var start = events[i].start.date ? events[i].start.date : events[i].start.dateTime, | |
| end = events[i].end.date ? events[i].end.date : events[i].end.dateTime, | |
| curDate = new Date(), | |
| eventStart = new Date(start), | |
| eventEnd = new Date(end), | |
| isToday = curDate > eventStart && curDate < eventEnd; | |
| if(isToday) | |
| todayList.push(events[i]); | |
| stopFind = curDate > eventEnd+1; | |
| } | |
| } | |
| function getInf(myInf){ | |
| for(var i=0; i<myInf.length; i++){ | |
| var summary = myInf[i].summary, | |
| start = myInf[i].start.date ? myInf[i].start.date : myInf[i].start.dateTime, | |
| end = myInf[i].end.date ? myInf[i].end.date : myInf[i].end.dateTime; | |
| var conditionVacation = summary.indexOf("vacation") >= 0 || summary.indexOf("Vacation") >= 0, | |
| conditionSick = summary.indexOf("sick") >= 0 || summary.indexOf("Sick") >= 0 , | |
| conditionWTH = summary.indexOf("wfh") >=0 || summary.indexOf("WFH") >= 0; | |
| var startDate = new Date(start), | |
| endDate = new Date(end); | |
| var minusDay=0; | |
| minusDay = dayOff(startDate, endDate, new Date()); | |
| //console.log(minusDay); | |
| startDate = new Date(start), | |
| endDate = new Date(end); | |
| if(conditionVacation) | |
| vacation+=countDays(startDate, endDate)-minusDay; | |
| else if(conditionWTH) | |
| wfh+=countDays(startDate, endDate); | |
| else if(conditionSick) | |
| sick+=countDays(startDate, endDate)-minusDay; | |
| } | |
| addDate(); | |
| function addDate(){ | |
| var userName = myInf[0].creator.displayName; | |
| $(".name").innerHTML=userName; | |
| var curDay = new Date(), | |
| endYear = new Date((new Date).getFullYear(), 11, 31), | |
| startYear = new Date((new Date).getFullYear(), 0, 1), | |
| vacationDays = 0, | |
| maxVacation = 0; | |
| var daysInYear = countDays(startYear, endYear); | |
| var startWorkDate = new Date(myJSON.started); | |
| startYear = startYear.getFullYear() > startWorkDate.getFullYear() ? startYear : startWorkDate; | |
| var isUA = myJSON.location == "Kyiv"; | |
| if(isUA) | |
| maxVacation = 20; | |
| else | |
| maxVacation = 15; | |
| curDay = countDays(startYear ,curDay); | |
| vacationDays = (maxVacation*curDay/daysInYear).toFixed(); | |
| console.log(vacationDays); | |
| preYearDays(); | |
| $(".vacation").innerHTML+="<span class='vacation-day'> "+vacation+"/"+vacationDays+"</span>"; | |
| $(".wfh").innerHTML+=" "+wfh; | |
| $(".sick").innerHTML+=" "+sick; | |
| var isSrc = false; | |
| for (var i = 0; i < members.length; i++) { | |
| if(userName == members[i].name){ | |
| $(".img").src="/pic/team/"+members[i].photo; | |
| isSrc = true; | |
| i = members.length; | |
| } | |
| } | |
| if(!isSrc) | |
| $(".img").src="/pic/team/undefined.png"; | |
| } | |
| } | |
| function countDays(start, end){ | |
| var days = 0; | |
| var timeDiff = Math.abs(end.getTime() - start.getTime()); | |
| days = Math.ceil(timeDiff / (1000 * 3600 * 24)); | |
| return days; | |
| } | |
| function dayOff(start, end, nowTime){ | |
| var day=0, | |
| isDate = start.getFullYear() == end.getFullYear() && start.getMonth() == end.getMonth() && start.getDate() == end.getDate(); | |
| while(!isDate){ | |
| var curTime = start.getFullYear() != nowTime.getFullYear() || start > nowTime, | |
| dayOff = start.getDay() == 0 || start.getDay() == 6, | |
| dayHoliday = isHoliday(start), | |
| isMinusDay = curTime || dayOff || dayHoliday; | |
| //console.log("DAY: "+start+" Status: "+dayHoliday); | |
| if(isMinusDay) | |
| day++; | |
| start.setDate(start.getDate()+1); | |
| isDate = start.getFullYear() == end.getFullYear() && start.getMonth() == end.getMonth() && start.getDate() == end.getDate(); | |
| } | |
| return day; | |
| } | |
| function isHoliday(day){ | |
| var loc = myJSON.location == "Kyiv", | |
| isToday = false; | |
| if(loc) | |
| isToday = findHoliday(UAHoliday, day); | |
| else | |
| isToday = findHoliday(USHoliday, day); | |
| return isToday; | |
| function findHoliday(holiday, day){ | |
| var flag=false; | |
| for (var i = 0; i < holiday.length; i++) { | |
| var start = holiday[i].start.date ? holiday[i].start.date : holiday[i].start.dateTime, | |
| end = holiday[i].end.date ? holiday[i].end.date : holiday[i].end.dateTime, | |
| startDate = new Date(start), | |
| endDate = new Date(start); | |
| startDate.setFullYear(day.getFullYear()); | |
| endDate.setFullYear(day.getFullYear()); | |
| var isToday = day.getTime() >= startDate.getTime() && day.getTime() <= endDate.getTime(); | |
| if(isToday){ | |
| flag = true; | |
| return flag; | |
| } | |
| }; | |
| return flag; | |
| } | |
| } | |
| function preYearDays(){ | |
| var days=0, | |
| lastInf = []; | |
| var lastTime = new Date((new Date()).getFullYear(), 0, 1), | |
| startTime = new Date(lastTime.getFullYear()-1, 0, 1); | |
| reqLast(gapi.client.calendar.events.list({ | |
| calendarId: 'grammarly.com_471831rd40r114bmjnf7r49uh8@group.calendar.google.com', | |
| maxResults: 2500, | |
| timeMin: startTime, | |
| timeMax: lastTime | |
| })); | |
| function reqLast(request){ | |
| request.execute(function(response){ | |
| console.log(response); | |
| lastInf = getDataAboutMe(response); | |
| console.log(lastInf); | |
| days = getLastDays(lastInf); | |
| var maxLastYear = maxLastYearDays(); | |
| days = days > maxLastYear ? maxLastYear : days; | |
| console.log(days, maxLastYear); | |
| var el = $(".vacation-day").innerHTML, | |
| index = el.indexOf("/"), | |
| totalNumberOfVacation = parseInt(el.substring(index+1, el.length))+days, | |
| haveDays = (totalNumberOfVacation - vacation) > 0 ? totalNumberOfVacation - vacation : 0; | |
| $(".vacation-day").innerHTML = " "+vacation + "/" + haveDays; | |
| endWork(); | |
| }); | |
| } | |
| function maxLastYearDays(){ | |
| var startDate = new Date(myJSON.started), | |
| lastYear = (new Date()).getFullYear() - 1, | |
| maxDays=10, | |
| days=0; | |
| //return 10; | |
| if(startDate.getFullYear()<lastYear){ | |
| days = maxDays; | |
| return days; | |
| } | |
| else if(startDate.getFullYear()==lastYear+1) | |
| return days; | |
| var endYear = new Date((new Date).getFullYear(), 0, 1, 0), | |
| startYear = new Date((new Date).getFullYear()-1, 0, 1, 0), | |
| dayInLastYear = countDays(startYear, endYear), | |
| workedDays = countDays(startDate, endYear); | |
| days = maxDays * workedDays/dayInLastYear; | |
| return days; | |
| } | |
| function getLastDays(lastInf){ | |
| var preVacation = 0; | |
| for (var i = 0; i < lastInf.length; i++) { | |
| var summary = lastInf[i].summary, | |
| start = lastInf[i].start.date ? lastInf[i].start.date : lastInf[i].start.dateTime, | |
| end = lastInf[i].end.date ? lastInf[i].end.date : lastInf[i].end.dateTime; | |
| var conditionVacation = summary.indexOf("vacation") >= 0 || summary.indexOf("Vacation") >= 0; | |
| var startDate = new Date(start), | |
| endDate = new Date(end); | |
| var minusDay=0; | |
| curTime = new Date((new Date()).getFullYear()-1, 11, 31); | |
| minusDay = dayOff(startDate, endDate, curTime); | |
| startDate = new Date(start), | |
| endDate = new Date(end); | |
| if(conditionVacation) | |
| preVacation+=countDays(startDate, endDate)-minusDay; | |
| } | |
| var maxVacation = 0; | |
| var isUA = myJSON.location == "Kyiv"; | |
| if(isUA) | |
| maxVacation = 20; | |
| else | |
| maxVacation = 15; | |
| preVacation = (maxVacation - preVacation)>0 ? maxVacation - preVacation : 0; | |
| return preVacation; | |
| } | |
| } | |
| function getDataAboutMe(response){ | |
| var myInf=[]; | |
| for( var i =0 ; i < response.items.length; i++){ | |
| var user = response.items[i].creator ? response.items[i].creator.email : ""; | |
| if (user != 'eugene.chechurin@grammarly.com') continue | |
| //if (user != 'yuriy.tanskiy@grammarly.com') continue | |
| //if (user != email) continue | |
| myInf.push(response.items[i]); | |
| } | |
| return myInf; | |
| } | |
| function calendar() { | |
| gapi.client.setApiKey('AIzaSyB4qUardN0ac-p4xYKWQKk_PxdSjPYBtY8'); | |
| gapi.client.load('calendar', 'v3', makeRequest); | |
| }; | |
| function login(){ | |
| if(!firstTime) return | |
| var config = { | |
| 'client_id': '365560126958-l83mn3tkkgaqrtkbskvocb3mvrn28i9d.apps.googleusercontent.com', | |
| 'scope': 'https://www.googleapis.com/auth/calendar', | |
| 'authuser': -1, | |
| 'login_hint': 'email address' | |
| }; | |
| gapi.auth.authorize(config, calendar); | |
| } | |
| function endWork(){ | |
| $(".content").style.display = "block"; | |
| $(".load-spiner").style.display = "none"; | |
| $(".login").style.display="none"; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body | |
| background-color #f2f8fa | |
| div | |
| margin 0px | |
| padding 0px | |
| .content | |
| display none | |
| &-col | |
| display table-cell !important | |
| width 20% !important | |
| display inline-block | |
| .vacation-day | |
| font-size: 21px | |
| .content__top | |
| background-color #313b43 | |
| width 100% | |
| height 300px | |
| display table | |
| &__profile-inf | |
| display table-cell | |
| min-width 750px | |
| padding-left 2% | |
| padding-right 2% | |
| width: 60%!important | |
| padding-top: 50px | |
| div | |
| display inline-block | |
| &__txt | |
| position relative | |
| transform translateY(-30%) | |
| &__img | |
| width 200px | |
| height 200px | |
| &-img | |
| border-radius 100px | |
| &__img | |
| margin-right 30px | |
| &__logo | |
| float right | |
| opacity 0.3 | |
| &__txt- | |
| &name | |
| color #f2f7f9 | |
| font-family "Open Sans", sans-serif | |
| font-size 24px | |
| font-weight bold | |
| margin-bottom 10px | |
| &wfh, | |
| &vacation, | |
| &sick | |
| color #f2f7f9 | |
| font-family "Open Sans", sans-serif | |
| font-size 16px | |
| &vacation__span-inf | |
| font-size 14px | |
| .content__bottom | |
| display table | |
| width 100% | |
| margin-bottom 20px | |
| div | |
| display table-cell | |
| width 20% | |
| padding 5px | |
| &__titel-h1 | |
| padding 0px | |
| margin 0px | |
| margin-top 25px | |
| margin-bottom 13px | |
| color #313b44 | |
| font-family "Open Sans", sans-serif | |
| font-size 20px | |
| font-weight bold | |
| &__absent, | |
| &__wft, | |
| &__news | |
| min-width 250px | |
| h2 | |
| font-weight normal | |
| color #313b43 | |
| font-family "Open Sans", sans-serif | |
| font-size 16px | |
| padding 0px | |
| margin 0px | |
| margin-bottom 15px | |
| img | |
| margin-bottom: 0 !important | |
| &__absent | |
| padding-left 2% | |
| &__news | |
| padding-right 2% | |
| h2 | |
| margin-bottom 15px | |
| &-img | |
| width 35px | |
| height 35px | |
| border-radius 90px | |
| background-color #313b43 | |
| position relative | |
| top 12px | |
| margin-right 10px | |
| .login | |
| position: absolute | |
| width: 100% | |
| height: 100% | |
| h1 | |
| position relative | |
| text-align center | |
| font-size 21px | |
| margin-bottom -1.5% | |
| h1 | |
| top 45% | |
| transform translateY(-50%) | |
| display none | |
| #login{ | |
| color #f2f8fa | |
| font-family 'Open Sans', sans-serif | |
| border-radius 3px | |
| outline none | |
| padding 5px | |
| position relative | |
| left 50% | |
| transform: translateX(-50%) | |
| border none | |
| background-color #313b43 | |
| transform translateY(-50%) | |
| top 50% | |
| } | |
| .load-spiner{ | |
| width 100% | |
| height 100% | |
| position absolute | |
| display none | |
| } | |
| .spinner{ | |
| transform: translateY(-50%) | |
| top 50% | |
| width 40px | |
| height 40px | |
| position relative | |
| margin 0px auto | |
| } | |
| .double-bounce1, .double-bounce2 { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| background-color: #333; | |
| opacity: 0.6; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| -webkit-animation: bounce 2.0s infinite ease-in-out; | |
| animation: bounce 2.0s infinite ease-in-out; | |
| } | |
| .double-bounce2 { | |
| -webkit-animation-delay: -1.0s; | |
| animation-delay: -1.0s; | |
| } | |
| @-webkit-keyframes bounce { | |
| 0%, 100% { -webkit-transform: scale(0.0) } | |
| 50% { -webkit-transform: scale(1.0) } | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { | |
| transform: scale(0.0); | |
| -webkit-transform: scale(0.0); | |
| } 50% { | |
| transform: scale(1.0); | |
| -webkit-transform: scale(1.0); | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment