Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/chdir/mr-activity-table/mr-activity-table.js b/static_src/elements/chdir/mr-activity-table/mr-activity-table.js
new file mode 100644
index 0000000..a0f4715
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-activity-table.js
@@ -0,0 +1,129 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+import './mr-day-icon.js';
+
+const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'];
+const WEEKDAY_ABBREVIATIONS = 'M T W T F S S'.split(' ');
+const SECONDS_PER_DAY = 24 * 60 * 60;
+// Only show comments from this many days ago and later.
+const MAX_COMMENT_AGE = 31 * 3;
+
+export class MrActivityTable extends LitElement {
+ /** @override */
+ static get styles() {
+ return css`
+ :host {
+ display: grid;
+ grid-auto-flow: column;
+ grid-auto-columns: repeat(13, auto);
+ grid-template-rows: repeat(7, auto);
+ margin: auto;
+ width: 90%;
+ text-align: center;
+ line-height: 110%;
+ align-items: center;
+ justify-content: space-between;
+ }
+ :host[hidden] {
+ display: none;
+ }
+ `;
+ }
+
+ /** @override */
+ render() {
+ return html`
+ ${WEEKDAY_ABBREVIATIONS.map((weekday) => html`<span>${weekday}</span>`)}
+ ${this._weekdayOffset.map(() => html`<span></span>`)}
+ ${this._activityArray.map((day) => html`
+ <mr-day-icon
+ .selected=${this.selectedDate === day.date}
+ .commentCount=${day.commentCount}
+ .date=${day.date}
+ @click=${this._selectDay}
+ ></mr-day-icon>
+ `)}
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ comments: {type: Array},
+ selectedDate: {type: Number},
+ };
+ }
+
+ _selectDay(event) {
+ const target = event.target;
+ if (this.selectedDate === target.date) {
+ this.selectedDate = undefined;
+ } else {
+ this.selectedDate = target.date;
+ }
+
+ this.dispatchEvent(new CustomEvent('dateChange', {
+ detail: {
+ date: this.selectedDate,
+ },
+ }));
+ }
+
+ get months() {
+ const currentMonth = (new Date()).getMonth();
+ return [MONTH_NAMES[currentMonth],
+ MONTH_NAMES[currentMonth - 1],
+ MONTH_NAMES[currentMonth - 2]];
+ }
+
+ get _weekdayOffset() {
+ const startDate = new Date(this._activityArray[0].date * 1000);
+ const startWeekdayNum = startDate.getDay()-1;
+ const emptyDays = [];
+ for (let i = 0; i < startWeekdayNum; i++) {
+ emptyDays.push(' ');
+ }
+ return emptyDays;
+ }
+
+ get _todayUnixTime() {
+ const now = new Date();
+ const today = new Date(Date.UTC(
+ now.getUTCFullYear(),
+ now.getUTCMonth(),
+ now.getUTCDate(),
+ 24, 0, 0));
+ const todayEndTime = today.getTime() / 1000;
+ return todayEndTime;
+ }
+
+ get _activityArray() {
+ const todayUnixEndTime = this._todayUnixTime;
+ const comments = this.comments || [];
+
+ const activityArray = [];
+ for (let i = 0; i < MAX_COMMENT_AGE; i++) {
+ const arrayDate = (todayUnixEndTime - ((i) * SECONDS_PER_DAY));
+ activityArray.unshift({
+ commentCount: 0,
+ date: arrayDate,
+ });
+ }
+
+ for (let i = 0; i < comments.length; i++) {
+ const commentAge = Math.floor(
+ (todayUnixEndTime - comments[i].timestamp) / SECONDS_PER_DAY);
+ if (commentAge < MAX_COMMENT_AGE) {
+ const pos = MAX_COMMENT_AGE - commentAge - 1;
+ activityArray[pos].commentCount++;
+ }
+ }
+
+ return activityArray;
+ }
+}
+customElements.define('mr-activity-table', MrActivityTable);
diff --git a/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js b/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js
new file mode 100644
index 0000000..0eb9d30
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-activity-table.test.js
@@ -0,0 +1,57 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrActivityTable} from './mr-activity-table.js';
+import sinon from 'sinon';
+
+const SECONDS_PER_DAY = 24 * 60 * 60;
+
+let element;
+
+describe('mr-activity-table', () => {
+ beforeEach(() => {
+ element = document.createElement('mr-activity-table');
+ document.body.appendChild(element);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(element);
+ });
+
+ it('initializes', () => {
+ assert.instanceOf(element, MrActivityTable);
+ });
+
+ it('no comments makes empty activity array', () => {
+ element.comments = [];
+
+ for (let i = 0; i < 93; i++) {
+ assert.equal(0, element._activityArray[i].commentCount);
+ }
+ });
+
+ it('activity array handles old comments', () => {
+ // 94 days since EPOCH.
+ sinon.stub(element, '_todayUnixTime').get(() => 94 * SECONDS_PER_DAY);
+
+ element.comments = [
+ {content: 'blah', timestamp: 0}, // too old.
+ {content: 'ignore', timestamp: 100}, // too old.
+ {
+ content: 'comment',
+ timestamp: SECONDS_PER_DAY + 1, // barely young enough.
+ },
+ {content: 'hello', timestamp: SECONDS_PER_DAY + 10}, // same day as above.
+ {content: 'world', timestamp: SECONDS_PER_DAY * 94}, // today
+ ];
+
+ assert.equal(93, element._activityArray.length);
+ assert.equal(2, element._activityArray[0].commentCount);
+ for (let i = 1; i < 92; i++) {
+ assert.equal(0, element._activityArray[i].commentCount);
+ }
+ assert.equal(1, element._activityArray[92].commentCount);
+ });
+});
diff --git a/static_src/elements/chdir/mr-activity-table/mr-day-icon.js b/static_src/elements/chdir/mr-activity-table/mr-day-icon.js
new file mode 100644
index 0000000..82f62b3
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-day-icon.js
@@ -0,0 +1,91 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+export class MrDayIcon extends LitElement {
+ /** @override */
+ static get styles() {
+ return css`
+ :host {
+ background-color: hsl(0, 0%, 95%);
+ margin: 0.25em 8px;
+ height: 20px;
+ width: 20px;
+ border: 2px solid white;
+ transition: border-color .5s ease-in-out;
+ }
+ :host(:hover) {
+ cursor: pointer;
+ border-color: hsl(87, 20%, 45%);
+ }
+ :host([activityLevel="0"]) {
+ background-color: var(--chops-blue-gray-50);
+ }
+ :host([activityLevel="1"]) {
+ background-color: hsl(87, 70%, 87%);
+ }
+ :host([activityLevel="2"]) {
+ background-color: hsl(88, 67%, 72%);
+ }
+ :host([activityLevel="3"]) {
+ background-color: hsl(87, 80%, 40%);
+ }
+ :host([selected]) {
+ border-color: hsl(0, 0%, 13%);
+ }
+ .hover-card {
+ display: none;
+ }
+ :host(:hover) .hover-card {
+ display: block;
+ position: relative;
+ width: 150px;
+ padding: 0.5em 8px;
+ background: rgba(0, 0, 0, 0.6);
+ color: var(--chops-white);
+ border-radius: 8px;
+ top: 120%;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+ `;
+ }
+
+ /** @override */
+ render() {
+ return html`
+ <div class="hover-card">
+ ${this.commentCount} Comments<br>
+ <chops-timestamp .timestamp=${this.date}></chops-timestamp>
+ </div>
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ activityLevel: {
+ type: Number,
+ reflect: true,
+ },
+ commentCount: {type: Number},
+ date: {type: Number},
+ selected: {
+ type: Boolean,
+ reflect: true,
+ },
+ };
+ }
+
+ /** @override */
+ update(changedProperties) {
+ if (changedProperties.has('commentCount')) {
+ const level = Math.ceil(this.commentCount / 2);
+ this.activityLevel = Math.min(level, 3);
+ }
+ super.update(changedProperties);
+ }
+}
+customElements.define('mr-day-icon', MrDayIcon);
diff --git a/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js b/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js
new file mode 100644
index 0000000..3c35a10
--- /dev/null
+++ b/static_src/elements/chdir/mr-activity-table/mr-day-icon.test.js
@@ -0,0 +1,24 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import {MrDayIcon} from './mr-day-icon.js';
+
+
+let element;
+
+describe('mr-day-icon', () => {
+ beforeEach(() => {
+ element = document.createElement('mr-day-icon');
+ document.body.appendChild(element);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(element);
+ });
+
+ it('initializes', () => {
+ assert.instanceOf(element, MrDayIcon);
+ });
+});