Skip to content

Commit

Permalink
feat: add filters, remove times and dates (#29)
Browse files Browse the repository at this point in the history
* feat: add filters (WIP)

* tweaks to card sizes on mobile

* fix: mobile css for filters

* feat: remove schedule

Co-authored-by: Nealevf <neale@rogueamoeba.com>
  • Loading branch information
greatislander and Nealevf committed Aug 6, 2020
1 parent bdd67ce commit 83418eb
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 159 deletions.
32 changes: 19 additions & 13 deletions _data/streams.js
Expand Up @@ -5,15 +5,12 @@ const airtable = require('airtable');
const Bottleneck = require('bottleneck');
const base = new airtable({apiKey}).base('appIDPnCgGApCGUtF');

const compareStreams = (a, b) => {
const streamA = new Date(a.startTime);
const streamB = new Date(b.startTime);

const compareStreams = (a, b) => {
let comparison = 0;

if (streamA.getTime() > streamB.getTime()) {
if (a.name > b.name) {
comparison = 1;
} else if (streamA.getTime() < streamB.getTime()) {
} else if (a.name < b.name) {
comparison = -1;
}

Expand All @@ -25,29 +22,38 @@ module.exports = async function() {
const limiter = new Bottleneck({minTime: 1000 / 5});
const streams = [];

limiter.wrap(base('Streams').select({
filterByFormula: "IS_AFTER({End Time}, NOW())"
}).eachPage(function page(records, fetchNextPage) {
limiter.wrap(base('Streams').select().eachPage(function page(records, fetchNextPage) {
records.forEach(function(record) {
const name = record.get('Name');
const link = record.get('Link');
const photo = record.get('Photo');
const startTime = record.get('Start Time');
const endTime = record.get('End Time');
const creatorName = record.get('Creator Name');
const creatorLink = record.get('Creator Link');
const platform = record.get('Platform') || false;
const subject = record.get('Subject');
const ageRange = record.get('Age Range');
streams.push({name, link, photo, startTime, endTime, creatorName, creatorLink, platform, subject, ageRange});
streams.push({name, link, photo, creatorName, creatorLink, platform, subject, ageRange});
});
fetchNextPage();
}, function done(err) {
if (err) {
console.error(err);
reject(error);
}
resolve({items: streams.sort(compareStreams)});

const subjects = streams.reduce((accumulator, currentStream) => {
if (!accumulator.includes(currentStream.subject)) {
accumulator.push(currentStream.subject);
}

return accumulator;
}, []);

resolve({
items: streams.sort(compareStreams),
ageRanges: ["3+", "6+", "9+", "12+", "Teens+", "All Ages"],
subjects: subjects.sort()
});
}));
});
}
3 changes: 3 additions & 0 deletions _includes/layouts/base.njk
Expand Up @@ -33,6 +33,9 @@
<div class="card card--navigation">
{% include 'partials/navigation.njk' %}
</div>
{% if page.url === '/' %}
{% include 'partials/filters.njk' %}
{% endif %}
</header>
<main>
{% block content %}
Expand Down
20 changes: 1 addition & 19 deletions _includes/layouts/home.njk
Expand Up @@ -2,24 +2,6 @@

{% block content %}
{% for item in streams.items %}
<article class="card card--stream" data-status>
<div class="card-image">
<img src="{{ item.photo[0].url }}" alt="" loading="lazy" />
</div>
<time class="card-date" datetime="{{ item.startTime }}" data-end={{ item.endTime }}></time>
<h2><a rel="external noopener noreferrer" href="{{ item.link }}" target="_blank">{{ item.name }}<span class="sr-only"> (opens in new window)</span></a></h2>
<p>By <a rel="external noopener noreferrer" href="{{ item.creatorLink }}">{{ item.creatorName }}</a>{% if item.platform !== 'Web' %} on {{ item.platform }}{% endif %}</p>
<div class="meta">
<span class="subject">{{ item.subject }}</span>
</div>
<div class="badge">
<span class="label">Done</span>
<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path d="M0,0 L60,0 C71.045695,0 80,8.954305 80,20 L80,80 L80,80 L0,0 Z"></path>
</g>
</svg>
</div>
</article>
{% include 'partials/stream.njk' %}
{% endfor %}
{% endblock %}
21 changes: 21 additions & 0 deletions _includes/partials/filters.njk
@@ -0,0 +1,21 @@
<div class="card card--filters">
<h2>Filter Streams</h2>
<p>
<label for="subject">Filter by Subject:</label>
<select id="subject" name="subject">
<option value="" selected>--</option>
{% for value in streams.subjects %}
<option value="{{ value }}">{{ value }}</option>
{% endfor %}
</select>
</p>
<p>
<label for="ages">Filter by Age:</label>
<select id="ages" name="ages">
<option value="" selected>--</option>
{% for value in streams.ageRanges %}
<option value="{{ value }}">{{ value }}</option>
{% endfor %}
</select>
</p>
</div>
18 changes: 18 additions & 0 deletions _includes/partials/stream.njk
@@ -0,0 +1,18 @@
<article class="card card--stream" data-status data-subject="{{ item.subject }}" data-ages="{{ item.ageRange }}">
<div class="card-image">
<img src="{{ item.photo[0].url }}" alt="" loading="lazy" />
</div>
<h2><a rel="external noopener noreferrer" href="{{ item.link }}" target="_blank">{{ item.name }}<span class="sr-only"> (opens in new window)</span></a></h2>
<p>By <a rel="external noopener noreferrer" href="{{ item.creatorLink }}">{{ item.creatorName }}</a>{% if item.platform !== 'Web' %} on {{ item.platform }}{% endif %}</p>
<div class="meta">
<span class="subject">{{ item.subject }}</span>
</div>
<div class="badge">
<span class="label">Done</span>
<svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path d="M0,0 L60,0 C71.045695,0 80,8.954305 80,20 L80,80 L80,80 L0,0 Z"></path>
</g>
</svg>
</div>
</article>
143 changes: 23 additions & 120 deletions app.js
@@ -1,129 +1,32 @@
import { format, isPast, isAfter, isBefore, isToday, isTomorrow, addHours, addDays, endOfTomorrow } from "/web_modules/date-fns.js";

function insertHeading(stream, label) {
const heading = document.createElement('h2');
heading.className = 'clearfix date-title';
heading.textContent = label;
stream.parentNode.insertBefore(heading, stream);
}

function formatDates(streams) {
streams.forEach(function(stream) {
const date = stream.querySelector('.card-date');
const startTime = date.getAttribute('datetime');
date.innerText = `${format(new Date(startTime), 'PP')} at ${format(new Date(startTime), 'p')}`;
});
}

function groupStreams(streams) {
let offset = false;
streams.forEach(function(stream) {
const startTime = stream.querySelector('.card-date').getAttribute('datetime');
// Stream is today.
if (isToday(new Date(startTime))) {
if (!offset) {
insertHeading(stream, 'Today’s Livestreams');
offset = '0';
}
}

// Stream is tomorrow.
if (isTomorrow(new Date(startTime))) {
if (!offset || offset === '0') {
insertHeading(stream, 'Tomorrow’s Livestreams');
offset = '1';
}
}

// Stream is in two days.
if (isAfter(new Date(startTime), endOfTomorrow()) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 1))) {
if (offset === '1') {
insertHeading(stream, `${format(new Date(startTime), 'EEEE')}’s Livestreams`);
offset = '2';
}
}
// Stream is in three days.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 1)) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 2))) {
if (offset === '2') {
insertHeading(stream, `${format(new Date(startTime), 'EEEE')}’s Livestreams`);
offset = '3';
}
}
// Stream is in four days.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 2)) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 3))) {
if (offset === '3') {
insertHeading(stream, `${format(new Date(startTime), 'EEEE')}’s Livestreams`);
offset = '4';
}
}
// Stream is in five days.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 3)) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 4))) {
if (offset === '4') {
insertHeading(stream, `${format(new Date(startTime), 'EEEE')}’s Livestreams`);
offset = '5';
}
}
// Stream is in six days.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 4)) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 5))) {
if (offset === '5') {
insertHeading(stream, `${format(new Date(startTime), 'EEEE')}’s Livestreams`);
offset = '6';
}
}
// Stream is next week.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 5)) && isBefore(new Date(startTime), addDays(endOfTomorrow(), 13))) {
if (offset === '6') {
insertHeading(stream, 'Next Week’s Livestreams');
offset = 'nextweek';
}
}
// Stream is later.
if (isAfter(new Date(startTime), addDays(endOfTomorrow(), 13))) {
if (offset === 'nextweek') {
insertHeading(stream, 'Future Livestreams');
offset = 'later';
}
function filterStreams() {
const streams = document.querySelectorAll('article.card');
const ageRange = document.getElementById('ages').value !== '' ? document.getElementById('ages').value : false;
const subject = document.getElementById('subject').value !== '' ? document.getElementById('subject').value : false;

const matchedStreams = [...streams].filter(stream => {
if (ageRange && subject) {
return stream.dataset.ages === ageRange && stream.dataset.subject === subject;
} else if (ageRange) {
return stream.dataset.ages === ageRange;
} else if (subject) {
return stream.dataset.subject === subject;
} else {
return stream;
}
});
}

function updateStreams() {
const streams = document.querySelectorAll('article.card');

streams.forEach(function(stream) {
const date = stream.querySelector('.card-date');
const startTime = date.getAttribute('datetime');
const endTime = date.dataset.end;

// Stream end time is in the past.
if (isPast(new Date(endTime))) {
stream.dataset.status = 'done';
stream.querySelector('.badge .label').innerText = 'done';
}
// Stream start time is within the next hour.
if (isAfter(new Date(startTime), new Date()) && isBefore(new Date(startTime), addHours(new Date(), 1))) {
stream.dataset.status = 'soon';
stream.querySelector('.badge .label').innerText = 'soon';
}
// Stream is live.
if (isAfter(new Date(), new Date(startTime)) && isBefore(new Date(), new Date(endTime))) {
stream.dataset.status = 'live';
stream.querySelector('.badge .label').innerText = 'live';
streams.forEach((stream) => {
if (!matchedStreams.includes(stream)) {
stream.setAttribute('hidden', '');
} else {
stream.removeAttribute('hidden');
}
});
}

function timedUpdate() {
updateStreams();
setTimeout(timedUpdate, 60000);
}

const streams = document.querySelectorAll('article.card');

if (streams.length > 0) {
formatDates(streams);
groupStreams(streams);
timedUpdate();
}
document.addEventListener('input', function (event) {
if (event.target.id !== 'ages' && event.target.id !=='subject') return;

filterStreams();
}, false);

41 changes: 34 additions & 7 deletions style.css
Expand Up @@ -282,10 +282,6 @@ p {
transition: all 0.3s ease;
}

.card--brand, .card--subhead, .card--navigation {
height: 14rem;
}

.card--subhead p.subhead {
font-size: 1.5em;
padding: 1.5rem 0.5rem 0;
Expand All @@ -310,6 +306,7 @@ header .card.card--navigation {

header .card {
background: var(--green-dark);
height: 14rem;
padding: 1rem;
}

Expand Down Expand Up @@ -360,6 +357,35 @@ header .card a:hover {
transition: all 0.1s ease;
}

/* Filters Card */
.card.card--filters {
padding: 1.5rem 0.5rem;
}

.card.card--filters h2 {
font-size: 1.5em;
color: var(--white);
}

.card.card--filters p {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
color: var(--white);
padding: 1.5rem 0 0;
}

.card.card--filters label {
margin-right: 0.5rem;
text-align: right;
width: calc(60% - 0.5rem);
}

.card.card--filters select {
width: 40%;
}

/* Stream Card */

.card--stream:hover,
Expand All @@ -374,7 +400,8 @@ header .card a:hover {
transition: all 0.1s ease;
}

.card[data-status="done"] {
.card[data-status="done"],
.card[hidden] {
display: none;
}

Expand Down Expand Up @@ -510,8 +537,8 @@ header .card a:hover {
text-align: center;
}

.card--brand, .card--subhead, .card--navigation {
height: auto;
.card--brand, .card--subhead, .card--navigation, .card--filters {
height: auto !important;
width: 90%;
padding: 0.625rem;
}
Expand Down

0 comments on commit 83418eb

Please sign in to comment.