Skip to content

Commit

Permalink
✨ [bento][amp-date-countdown] Initial Preact component for amp-date-c…
Browse files Browse the repository at this point in the history
…ountdown (ampproject#30092)

* First commit Preact component

* Add clear comments for helper functions

* Various review comment updates

* More review updates

* More review updates

* Add useMemo

* Updates for code review

* Review comments and rebase

* Remove rebase conflict

* remove quotes on playable

* Remove date format from epoch

* add quotes for object keys that are output to template
  • Loading branch information
krdwan authored and ed-bird committed Dec 10, 2020
1 parent 3935f29 commit a7e5909
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 0 deletions.
245 changes: 245 additions & 0 deletions extensions/amp-date-countdown/1.0/date-countdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Preact from '../../../src/preact';
import {getLocaleStrings} from './messages';
import {useAmpContext} from '../../../src/preact/context';
import {useEffect, useMemo, useRef, useState} from '../../../src/preact';
import {useResourcesNotify} from '../../../src/preact/utils';

const NAME = 'DateCountdown';

// Constants
/** @const {number} */
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;

/** @const {number} */
const MILLISECONDS_IN_HOUR = 60 * 60 * 1000;

/** @const {number} */
const MILLISECONDS_IN_MINUTE = 60 * 1000;

/** @const {number} */
const MILLISECONDS_IN_SECOND = 1000;

/** @const {number} */
const DELAY = 1000;

/** @const {Object<string, number>} */
const TimeUnit = {
DAYS: 1,
HOURS: 2,
MINUTES: 3,
SECONDS: 4,
};

// Default prop values
const DEFAULT_OFFSET_SECONDS = 0;
const DEFAULT_LOCALE = 'en';
const DEFAULT_WHEN_ENDED = 'stop';
const DEFAULT_BIGGEST_UNIT = 'DAYS';

/**
* @param {!DateCountdownPropsDef} props
* @return {PreactDef.Renderable}
*/
export function DateCountdown({
endDate,
timeleftMs,
timestampMs,
timestampSeconds,
offsetSeconds = DEFAULT_OFFSET_SECONDS,
whenEnded = DEFAULT_WHEN_ENDED,
locale = DEFAULT_LOCALE,
biggestUnit = DEFAULT_BIGGEST_UNIT,
render,
children,
...rest
}) {
useResourcesNotify();
const {playable} = useAmpContext();
const epoch = useMemo(
() =>
getEpoch(endDate, timeleftMs, timestampMs, timestampSeconds) +
offsetSeconds * MILLISECONDS_IN_SECOND,
[endDate, timeleftMs, timestampMs, timestampSeconds, offsetSeconds]
);
const [timeleft, setTimeleft] = useState(epoch - Date.now());
const localeStrings = useMemo(() => getLocaleWord(locale), [locale]);
const rootRef = useRef(null);

useEffect(() => {
if (!playable || !rootRef.current) {
return;
}
const win = rootRef.current.ownerDocument.defaultView;
const interval = win.setInterval(() => {
const newTimeleft = epoch - Date.now() + DELAY;
setTimeleft(newTimeleft);
if (whenEnded === DEFAULT_WHEN_ENDED && newTimeleft < 1000) {
win.clearInterval(interval);
}
}, DELAY);
return () => win.clearInterval(interval);
}, [playable, epoch, whenEnded]);

const data = {
...getYDHMSFromMs(timeleft, biggestUnit),
...localeStrings,
};
return (
<div ref={rootRef} {...rest}>
{render(data, children)}
</div>
);
}

/**
* Calculate the epoch time that this component should countdown to from
* one of multiple input options.
* @param {string|undefined} endDate
* @param {number|undefined} timeleftMs
* @param {number|undefined} timestampMs
* @param {number|undefined} timestampSeconds
* @return {number}
*/
function getEpoch(endDate, timeleftMs, timestampMs, timestampSeconds) {
let epoch;

if (endDate) {
epoch = Date.parse(endDate);
} else if (timeleftMs) {
epoch = Date.now() + timeleftMs;
} else if (timestampMs) {
epoch = timestampMs;
} else if (timestampSeconds) {
epoch = timestampSeconds * 1000;
}

if (epoch === undefined) {
throw new Error(
`One of endDate, timeleftMs, timestampMs, timestampSeconds` +
`is required. ${NAME}`
);
}
return epoch;
}

/**
* Return an object with a label for 'years', 'months', etc. based on the
* user provided locale string.
* @param {string} locale
* @return {!JsonObject}
*/
function getLocaleWord(locale) {
if (getLocaleStrings(locale) === undefined) {
displayWarning(
`Invalid locale ${locale}, defaulting to ${DEFAULT_LOCALE}. ${NAME}`
);
locale = DEFAULT_LOCALE;
}
const localeWordList = getLocaleStrings(locale);
return {
'years': localeWordList[0],
'months': localeWordList[1],
'days': localeWordList[2],
'hours': localeWordList[3],
'minutes': localeWordList[4],
'seconds': localeWordList[5],
};
}

/**
* Converts a time represented in milliseconds (ms) into a representation with
* days, hours, minutes, etc. and returns formatted strings in an object.
* @param {number} ms
* @param {string} biggestUnit
* @return {JsonObject}
*/
function getYDHMSFromMs(ms, biggestUnit) {
//Math.trunc is used instead of Math.floor to support negative past date
const d =
TimeUnit[biggestUnit] == TimeUnit.DAYS
? supportBackDate(Math.floor(ms / MILLISECONDS_IN_DAY))
: 0;
const h =
TimeUnit[biggestUnit] == TimeUnit.HOURS
? supportBackDate(Math.floor(ms / MILLISECONDS_IN_HOUR))
: TimeUnit[biggestUnit] < TimeUnit.HOURS
? supportBackDate(
Math.floor((ms % MILLISECONDS_IN_DAY) / MILLISECONDS_IN_HOUR)
)
: 0;
const m =
TimeUnit[biggestUnit] == TimeUnit.MINUTES
? supportBackDate(Math.floor(ms / MILLISECONDS_IN_MINUTE))
: TimeUnit[biggestUnit] < TimeUnit.MINUTES
? supportBackDate(
Math.floor((ms % MILLISECONDS_IN_HOUR) / MILLISECONDS_IN_MINUTE)
)
: 0;
const s =
TimeUnit[biggestUnit] == TimeUnit.SECONDS
? supportBackDate(Math.floor(ms / MILLISECONDS_IN_SECOND))
: supportBackDate(
Math.floor((ms % MILLISECONDS_IN_MINUTE) / MILLISECONDS_IN_SECOND)
);

return {
'd': d,
'dd': padStart(d),
'h': h,
'hh': padStart(h),
'm': m,
'mm': padStart(m),
's': s,
'ss': padStart(s),
};
}

/**
* Format a number for output to the template. Adds a leading zero if the
* input is only one digit and a negative sign for inputs less than 0.
* @param {number} input
* @return {string}
*/
function padStart(input) {
if (input < -9 || input > 9) {
return String(input);
} else if (input >= -9 && input < 0) {
return '-0' + -input;
}
return '0' + input;
}

/**
* @param {number} input
* @return {number}
*/
function supportBackDate(input) {
if (input < 0) {
return input + 1;
}
return input;
}

/**
* @param {?string} message
*/
function displayWarning(message) {
console /*OK*/
.warn(message);
}
33 changes: 33 additions & 0 deletions extensions/amp-date-countdown/1.0/date-countdown.type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** @externs */

/**
* @typedef {{
* endDate: (string|undefined),
* timeleftMs: (number|undefined),
* timestampMs: (number|undefined),
* timestampSeconds: (number|undefined),
* offsetSeconds: (number|undefined),
* whenEnded: (string|undefined),
* locale: (string|undefined),
* biggestUnit: (string|undefined),
* render: (function(!JsonObject, (?PreactDef.Renderable|undefined)):PreactDef.Renderable),
* children: (?PreactDef.Renderable|undefined),
* }}
*/
var DateCountdownPropsDef;
47 changes: 47 additions & 0 deletions extensions/amp-date-countdown/1.0/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Get locale strings or undefined.
* @param {string} locale
* @return {!Array<string>|undefined}
*/
export function getLocaleStrings(locale) {
return LOCALE_WORD[locale];
}

/**
* Strings representing years, minutes, etc. in various locales
* @type {Object<string, !Array<string>>}
*/
const LOCALE_WORD = {
'de': ['Jahren', 'Monaten', 'Tagen', 'Stunden', 'Minuten', 'Sekunden'],
'en': ['Years', 'Months', 'Days', 'Hours', 'Minutes', 'Seconds'],
'es': ['años', 'meses', 'días', 'horas', 'minutos', 'segundos'],
'fr': ['ans', 'mois', 'jours', 'heures', 'minutes', 'secondes'],
'id': ['tahun', 'bulan', 'hari', 'jam', 'menit', 'detik'],
'it': ['anni', 'mesi', 'giorni', 'ore', 'minuti', 'secondi'],
'ja': ['年', 'ヶ月', '日', '時間', '分', '秒'],
'ko': ['년', '달', '일', '시간', '분', '초'],
'nl': ['jaar', 'maanden', 'dagen', 'uur', 'minuten', 'seconden'],
'pt': ['anos', 'meses', 'dias', 'horas', 'minutos', 'segundos'],
'ru': ['год', 'месяц', 'день', 'час', 'минута', 'секунда'],
'th': ['ปี', 'เดือน', 'วัน', 'ชั่วโมง', 'นาที', 'วินาที'],
'tr': ['yıl', 'ay', 'gün', 'saat', 'dakika', 'saniye'],
'vi': ['năm', 'tháng', 'ngày', 'giờ', 'phút', 'giây'],
'zh-cn': ['年', '月', '天', '小时', '分钟', '秒'],
'zh-tw': ['年', '月', '天', '小時', '分鐘', '秒'],
};

0 comments on commit a7e5909

Please sign in to comment.