-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/issue 16 upcoming events (#37)
* 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
1 parent
841c9c6
commit a6945a1
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
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 }; |
This file contains 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
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); |
This file contains 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
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; | ||
}); | ||
|
||
}); |
This file contains 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
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 | ||
}; |