Skip to content

Commit

Permalink
feat: add a countdown timer example to the demo
Browse files Browse the repository at this point in the history
Resolves #47 by providing a code snippet to inspire a developer to create their
own countdown component.

- add countdown to showcase page
- add html page and css containing countdown timer
  • Loading branch information
amtins committed Dec 9, 2024
1 parent 1bed771 commit fc8a09d
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 1 deletion.
23 changes: 22 additions & 1 deletion src/layout/content/showcase/showcase-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import rawDisplayCurrentChapterExample from '../../../../static/showcases/chapte
import rawSkipCreditsExample from '../../../../static/showcases/skip-credits.html?raw';
import rawPlaylistExample from '../../../../static/showcases/playlist.html?raw';
import rawqualityMenuExample from '../../../../static/showcases/quality-menu.html?raw';
import rawCountdown from '../../../../static/showcases/countdown.html?raw';
import { getTextFromHTML } from './example-parser.js';

const startTimeExampleTxt = getTextFromHTML(rawStartTimeExample);
Expand All @@ -22,6 +23,7 @@ const displayCurrentChapterExampleTxt =
const skipCreditsExampleTxt = getTextFromHTML(rawSkipCreditsExample);
const playlistExampleTxt = getTextFromHTML(rawPlaylistExample);
const qualityMenuExampleTxt = getTextFromHTML(rawqualityMenuExample);
const countdownExampleTxt = getTextFromHTML(rawCountdown);

export class ShowCasePage extends LitElement {
static styles = [theme, animations, unsafeCSS(showcasePageCss)];
Expand All @@ -35,6 +37,7 @@ export class ShowCasePage extends LitElement {
${this.#renderSkipCredits()}
${this.#renderPlaylist()}
${this.#renderQualityMenu()}
${this.#renderCountdown()}
`;
}

Expand Down Expand Up @@ -149,7 +152,7 @@ export class ShowCasePage extends LitElement {
<showcase-component href="playlist.html">
<h2 slot="title">Playlist</h2>
<p slot="description">
This example shows how to fetch media data for a set of video sources
This example shows how to fetch media data for a set of video sources
and load them into the <a href="https://github.com/SRGSSR/pillarbox-web-suite/tree/main/packages/pillarbox-playlist#readme" target="_blank">Pillarbox Playlist plugin</a>
with metadata such as title and duration.
</p>
Expand Down Expand Up @@ -181,6 +184,24 @@ export class ShowCasePage extends LitElement {
</div>
`;
}

#renderCountdown() {
return html`
<div class="fade-in"
@animationend="${e => e.target.classList.remove('fade-in')}">
<showcase-component href="countdown.html">
<h2 slot="title">Countdown Timer</h2>
<p slot="description">
In this showcase, we'll demonstrate how to display a countdown timer.
</p>
<code-block slot="code" language="javascript">${countdownExampleTxt}</code-block>
</showcase-component>
<a part="showcase-link" href="./static/showcases/countdown.html" target="_blank">
Open this showcase
</a>
</div>
`;
}
}

customElements.define('showcase-page', ShowCasePage);
Expand Down
226 changes: 226 additions & 0 deletions static/showcases/countdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pillarbox Demo - Display Countdown</title>
<link rel="icon" type="image/x-icon" href="../../img/favicon.ico">
<link rel="stylesheet" href="./countdown.scss" />
</head>

<body>
<core-demo-header></core-demo-header>
<div class="showcase-content">
<h2>Display Countdown</h2>
<div class="video-container">
<video-js id="video-element-id" class="pillarbox-js" controls></video-js>
</div>

<button class="showcase-btn" id="close-btn">Close this window</button>
</div>

<script type="module" data-implementation>
// Import the pillarbox library
import pillarbox from '@srgssr/pillarbox-web';

const ModalDialog = pillarbox.getComponent('ModalDialog');

/**
* Provides a countdown timer functionality within a modal dialog.
*/
class Countdown extends ModalDialog {
/**
* Creates an instance of Countdown.
*
* @param {Player} player The video.js player instance
* @param {Object} options Configuration options for the modal dialog
*/
constructor(player, options) {
const opts = pillarbox.obj.merge(options, {
pauseOnOpen: false,
fillAlways: true,
temporary: false,
uncloseable: true
});

super(player, opts);

this.intervalId = undefined;
this.reset = this.reset.bind(this);

this.on(player, ['loadstart', 'playerreset', 'dispose', 'error'], this.reset);
}

/**
* The CSS class name for the countdown modal dialog.
*
* @returns {string} The CSS class name
*/
buildCSSClass() {
return `pillarbox-countdown ${super.buildCSSClass()}`;
}

/**
* Disposes of the countdown component.
*
* Cleans up any resources and event listeners.
*/
dispose() {
this.reset();
this.off(this.player(), ['loadstart', 'playerreset', 'dispose', 'error'], this.reset);

super.dispose();
}

/**
* Resets the countdown timer.
*
* Clears the interval, closes the modal, and empties its content.
*/
reset() {
this.clearInterval(this.intervalId);
this.close();
this.empty();

this.intervalId = undefined;
}

/**
* Starts the countdown timer.
*
* @param {number} timestamp The target timestamp in milliseconds
* @param {string} source The source to play when the countdown ends
*
* @returns {boolean} True if the countdown started successfully
*/
start(timestamp, source) {
this.reset();

if (typeof timestamp !== 'number') return;

this.intervalId = this.setInterval(() => {
const remainingDuration = this.remainingDuration(timestamp);

if (remainingDuration.totalInMilliseconds <= 0) {
this.reset();
this.player().src(source);

return;
}

this.fillWith(`${remainingDuration.days
}d ${remainingDuration.hours
}h ${remainingDuration.minutes
}m ${remainingDuration.seconds
}s remaining`);
}, 1_000);

this.open();

return true;
}

/**
* The remaining duration until the target timestamp.
*
* @param {number} timestamp The target timestamp in milliseconds
*
* @returns {Object} An object containing the remaining days, hours, minutes, seconds, and total in milliseconds
*/
remainingDuration(timestamp) {
const SECONDS_IN_MS = 1_000;
const MINUTES = 60 * SECONDS_IN_MS;
const HOURS = 60 * MINUTES;
const DAYS = 24 * HOURS;
const totalInMilliseconds = timestamp - Date.now();

let diff = totalInMilliseconds;
// Calculate days, hours, minutes, and seconds
let days = Math.floor(diff / (DAYS));
diff -= days * (DAYS);

let hours = Math.floor(diff / (HOURS));
diff -= hours * (HOURS);

let minutes = Math.floor(diff / (MINUTES));
diff -= minutes * (MINUTES);

let seconds = Math.floor(diff / SECONDS_IN_MS);

return {
days: days.toString().padStart(2, '0'),
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
seconds: seconds.toString().padStart(2, '0'),
totalInMilliseconds
}
}
}

// Register Countdown component
pillarbox.registerComponent('Countdown', Countdown);

// Create a pillarbox player instance with the currentChapter plugin
const player = pillarbox(
'video-element-id',
{
fill: true,
// Add the countdown component to the player
countdown: true,
}
);

// Listen for player errors
player.on('error', () => {
// Get the metadata associated with the error
const { metadata = {} } = player.error() || {};

// If the error is not of type STARTDATE we do nothing
if (metadata.errorType !== 'STARTDATE') return;

// Extract the validFrom property from the mediaData
const {
src: {
mediaData: {
chapters: [{ validFrom } = {}] = []
} = {}
} = {}
} = metadata;
const timestamp = new Date(validFrom).getTime();

if (!player.countdown.start(timestamp, player.currentSource())) return;

// Closes error display component to prevent overlapping
if (player.errorDisplay && player.errorDisplay.opened()) {
player.errorDisplay.close();
}
});

// In the absence of media generating a STARTDATE error, this function
// manually activates the countdown
player.countdown.start(Date.now() + 5_977_235_000, { src: 'urn:rts:video:10894383', type: 'srgssr/urn' });

// If a media sends a STARTDATE error, the countdown is automatically
// activated, since the logic for activating the countdown is handled by
// the error event handler
// player.src({ src: 'urn:rts:video:10894383', type: 'srgssr/urn' });

window.player = player;
</script>

<script type="module">
import pillarbox from '@srgssr/pillarbox-web';
import '../../src/layout/header/core-demo-header-component.js';

document.querySelector('#close-btn').addEventListener('click', () => {
window.close();
});

window.pillarbox = pillarbox;
</script>

</body>

</html>
11 changes: 11 additions & 0 deletions static/showcases/countdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import './static-showcase';


.pillarbox-countdown .vjs-modal-dialog-content {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 2em;
text-align: center;
}

0 comments on commit fc8a09d

Please sign in to comment.