Skip to content

Commit

Permalink
Feature/issue 16 upcoming events (#37)
Browse files Browse the repository at this point in the history
* init commit of WIP upcoming events component

* handle multiple months, multiple events, and out order sorting

* support no upcoming events

* keep events in the current month in the list

* spruce up event alignment and justification

* add unit tests

* updated styling

* leave note about JSON serialization issue
  • Loading branch information
thescientist13 authored Oct 16, 2022
1 parent 841c9c6 commit a6945a1
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/components/upcoming-events/mock-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const SINGLE_EVENT = [{
title: 'Tuesdays Tunes Season 3 - Premier!',
timestamp: Date.now() + 300000,
link: 'http://www.facebook.com/'
}];

// includes testing for multiple months, events list "overflow", and out of order events
const MULTIPLE_EVENTS = [{
title: 'Tuesdays Tunes Season 3 - Episode 1',
timestamp: SINGLE_EVENT[0].timestamp + (86400000 * 15),
link: 'http://www.facebook.com/'
}, {
...SINGLE_EVENT[0]
}, {
title: 'Tuesdays Tunes Season 3 - Episode 2',
timestamp: SINGLE_EVENT[0].timestamp + (86400000 * 30),
link: 'http://www.facebook.com/'
}, {
title: 'Tuesdays Tunes Season 3 - Teaser Trailer',
timestamp: SINGLE_EVENT[0].timestamp - (86400000 * 5),
link: 'http://www.facebook.com/'
}, {
title: 'Tuesdays Tunes Season 3 - Episode 3',
timestamp: SINGLE_EVENT[0].timestamp + (86400000 * 45),
link: 'http://www.facebook.com/'
}];

const NO_EVENTS = [];

export { SINGLE_EVENT, MULTIPLE_EVENTS, NO_EVENTS };
109 changes: 109 additions & 0 deletions src/components/upcoming-events/upcoming-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const MONTH_INDEX_MAPPER = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];

export default class UpcomingEvents extends HTMLElement {
connectedCallback() {
const eventsByMonth = {};
const events = (JSON.parse(this.getAttribute('events')) || [])
.filter((event) => {
// filter out old events except ones that are also in the current month
const { timestamp } = event;
const now = new Date();

// set to be the beginning of the current month
now.setDate(1);
now.setFullYear(now.getFullYear());
now.setMonth(now.getMonth());

const isInCurrentMonth = timestamp >= now.getTime();

return event.timestamp >= now.getTime() || isInCurrentMonth;
})
.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1); // sort newest to latest
const noEvents = events.length === 0
? '<h2 class="text-center">No Upcoming Events</h2>'
: '';

// group events by month
events.forEach((event) => {
const time = new Date(event.timestamp);
const month = time.getMonth();
const monthKey = MONTH_INDEX_MAPPER[month];

if (!eventsByMonth[monthKey]) {
eventsByMonth[monthKey] = [];
}

eventsByMonth[monthKey].push(event);
});

/* eslint-disable indent */
this.innerHTML = `
<div class="upcoming-events">
${noEvents}
${
Object.keys(eventsByMonth).map((month) => {
return `
<div class="mb-6">
<h2
style="background-color:var(--color-secondary);color:var(--color-white);font-family:var(--font-secondary)"
class="text-center p-2 mb-4 text-3xl font-bold"
>
${month}
</h2>
${eventsByMonth[month].map((event) => {
const { link, timestamp, title } = event;
const time = new Date(timestamp);
const date = time.getDate();
const hour = time.getHours() - 12; // here we assume an 8pm (e.g. afternoon) start time
return `
<div>
<h3
style="color:var(--color-white); margin: .5rem auto;"
class="sm:w-1/2 md:w-1/3"
>
<a
href="${link}"
alt="${title}"
>
<span
class="inline-block w-8 text-center"
style="background-color:var(--color-accent);"
>
${date}
</span>
<span
style="color:var(--color-secondary);"
>
${title} @ ${hour}pm
</span>
</a>
</h3>
</div>
`;
}).join('')}
</div>
`;
}).join('')
}
</div>
`;
}
}

customElements.define('tt-upcoming-events', UpcomingEvents);
165 changes: 165 additions & 0 deletions src/components/upcoming-events/upcoming-events.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { expect } from '@esm-bundle/chai';
import './upcoming-events.js';
import { SINGLE_EVENT, NO_EVENTS, MULTIPLE_EVENTS } from './mock-events.js';

const MONTH_INDEX_MAPPER = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];

describe('Components/Upcoming Events', () => {
let events;

describe('Default Behavior', () => {
before(async () => {
events = document.createElement('tt-upcoming-events');

document.body.appendChild(events);

await events.updateComplete;
});

it('should not be null', () => {
expect(events).not.equal(undefined);
expect(events.querySelectorAll('div.upcoming-events').length).equal(1);
});
});

describe('Single event', () => {
before(async () => {
events = document.createElement('tt-upcoming-events');
events.setAttribute('events', JSON.stringify(SINGLE_EVENT));

document.body.appendChild(events);

await events.updateComplete;
});

it('should not display the no upcoming events text', () => {
const headings = events.querySelectorAll('h2');

expect(headings.length).to.equal(1);
expect(headings[0].textContent).to.not.equal('No Upcoming Events');
});

it('should display the correct month heading', () => {
const headings = events.querySelectorAll('h2');
const time = new Date(SINGLE_EVENT[0].timestamp);
const month = time.getMonth();
const monthLabel = MONTH_INDEX_MAPPER[month];

expect(headings[0].textContent).to.contain(monthLabel);
});

it('should display the correct date details', () => {
const { title, timestamp } = SINGLE_EVENT[0];
const headings = events.querySelectorAll('h3');
const time = new Date(timestamp);
const date = time.getDate();
const hour = time.getHours() - 12;
const display = `${date}${title}@${hour}pm`.replace(/ /g, '');

expect(headings[0].textContent.replace(/\n/g, '').replace(/ /g, '')).to.equal(display);
});
});

describe('Multiple events', () => {
let ORDERED_EVENTS = [];

before(async () => {
events = document.createElement('tt-upcoming-events');
events.setAttribute('events', JSON.stringify(MULTIPLE_EVENTS));

document.body.appendChild(events);

await events.updateComplete;

// 3, 1, 0, 2, 4
ORDERED_EVENTS = [{
...MULTIPLE_EVENTS[3]
}, {
...MULTIPLE_EVENTS[1]
}, {
...MULTIPLE_EVENTS[0]
}, {
...MULTIPLE_EVENTS[2]
}, {
...MULTIPLE_EVENTS[4]
}];
});

it('should not display the no upcoming events text', () => {
const headings = events.querySelectorAll('h2');

expect(headings.length).to.equal(2);
expect(headings[0].textContent).to.not.equal('No Upcoming Events');
});

it('should display the correct month heading', () => {
const headings = events.querySelectorAll('h2');
const eventsByMonth = {};

MULTIPLE_EVENTS.forEach((event) => {
const time = new Date(event.timestamp);
const month = time.getMonth();
const monthKey = MONTH_INDEX_MAPPER[month];

if (!eventsByMonth[monthKey]) {
eventsByMonth[monthKey] = true;
}
});

headings.forEach((heading, idx) => {
expect(heading.textContent).to.contain(Object.keys(eventsByMonth)[idx]);
});
});

it('should display the correct date details', () => {
const headings = events.querySelectorAll('h3');

headings.forEach((heading, idx) => {
const { title, timestamp } = ORDERED_EVENTS[idx];
const time = new Date(timestamp);
const date = time.getDate();
const hour = time.getHours() - 12;
const display = `${date}${title}@${hour}pm`.replace(/ /g, '');

expect(heading.textContent.replace(/\n/g, '').replace(/ /g, '')).to.equal(display);
});
});
});

describe('No events', () => {
before(async () => {
events = document.createElement('tt-upcoming-events');
events.setAttribute('events', JSON.stringify(NO_EVENTS));

document.body.appendChild(events);

await events.updateComplete;
});

it('should only display the no upcoming events text', () => {
const headings = events.querySelectorAll('h2');

expect(headings.length).to.equal(1);
expect(headings[0].textContent).to.equal('No Upcoming Events');
});
});

after(() => {
events.remove();
events = null;
});

});
31 changes: 31 additions & 0 deletions src/components/upcoming-events/upcoming-events.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import '../../styles/theme.css';
import './upcoming-events.js';
import { SINGLE_EVENT, MULTIPLE_EVENTS, NO_EVENTS } from './mock-events';

export default {
title: 'Components/Upcoming Events'
};

// TODO mock data with an apostrophe will not work here, e.g. 'Tuesday\'s Tunes Season 3 - Premier!'
// If I use double quote everything stops at the first double quote, e.g. `[{`
// If I use single quote, it stops at the first single quote, e.g. `[{"title":"Tuesday`
// works fine when using `setAttribute` and the DOM 🤷
const Template = ({ events }) => `<tt-upcoming-events events='${JSON.stringify(events)}'></tt-upcoming-events>`;

export const Primary = Template.bind({});

Primary.args = {
events: SINGLE_EVENT
};

export const MultipleEvents = Template.bind({});

MultipleEvents.args = {
events: MULTIPLE_EVENTS
};

export const NoEvents = Template.bind({});

NoEvents.args = {
events: NO_EVENTS
};

0 comments on commit a6945a1

Please sign in to comment.