HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<div class="container mt-4 mb-2" style="max-width: 600px">
<h2>Human Feed</h2>
<p class="lead">This is a demo of human-friendly dates created as a companion
to a PhraseApp blog post. The localized date strings in the feed below have been dynamically
generated from <code>Date</code> objects. </p>
<div class="mx-auto text-center">
<p class="lead mb-1">View in</p>
<div class="btn-group mb-2" id="localeControls">
<button
id="localeControl-en"
style="width: 4rem"
class="btn btn-sm btn-secondary active"
aria-pressed="true"
>
English
</button>
<button
id="localeControl-ar"
style="width: 4rem"
class="btn btn-sm btn-secondary"
>
عربي
</button>
</div>
</div>
<h3 id="feedHeader">What's New</h3>
<ul class="list-group pr-0" id="feed">
</ul>
</div>
// ----------------------------------------------------------------------
// CONFIG
// ----------------------------------------------------------------------
/**
* Past this number of days we'll no longer display the
* day of the week and instead we'll display the date
* with the month
*/
const DATE_WITH_MONTH_THRESHOLD_IN_DAYS: number = 6;
/**
* Past this number of seconds it's now longer "now" when
* we're displaying dates
*/
const NOW_THRESHOLD_IN_SECONDS: number = 10;
/**
* Past this number of hours we'll no longer display "hours
* ago" and instead we'll display "today"
*/
const TODAY_AT_THRESHOLD_IN_HOURS: number = 12;
/**
* Date formats for different human-friendly thresholds
*/
const FORMATS = {
YEARS_AGO: {
year: "numeric",
month: "short",
day: "numeric",
},
DAYS_AGO: {
weekday: "long",
},
};
// ----------------------------------------------------------------------
// TYPES
// ----------------------------------------------------------------------
/**
* Supported locales
*/
type Locale = "en" | "ar";
/**
* Keys allowed in plural forms
*/
type PluralFormKey = "1" | "2" | "3-10" | "other";
/**
* Definition of a phrase's plural form
*/
interface PluralForms {
"1"?: string;
"2"?: string;
"3-10"?: string;
"other": string;
}
/**
* Translation key/value pairs for supported locales
*/
type Translations = {
[locale in Locale]: {
[key: string]: string | PluralForms;
}
}
/**
* Representation of a date & time in components
*/
interface DateTimeComponents {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
}
/**
* Options when formatting a date
*/
interface DateFormatOptions {
includeYear?: Boolean;
}
/**
* A post in our data set
*/
interface Post {
name: string;
photo: string;
post: string;
posted_at: Date;
}
// ----------------------------------------------------------------------
// CONSTANTS
// ----------------------------------------------------------------------
const MILLISECONDS_TO_SECONDS: number = 0.001;
const SECONDS_IN_YEAR: number = 31557600;
const SECONDS_IN_MONTH: number = 2629800;
const SECONDS_IN_DAY: number = 86400;
const SECONDS_IN_HOUR: number = 3600;
const SECONDS_IN_MINUTE: number = 60;
// ----------------------------------------------------------------------
// i18n
// ----------------------------------------------------------------------
/**
* Determines localized UI elements and data shown
*/
let currentLocale: Locale = "en";
/**
* Localized UI strings
*/
const translations: Translations = {
"en": {
"What's New": "What's New",
"posted": "posted",
"just now": "just now",
"today at": "today at",
"yesterday": "yesterday",
"hours ago": {
"1": "{count} hour ago",
"other": "{count} hours ago",
},
"minutes ago": {
"1": "{count} minute ago",
"other": "{count} minutes ago",
},
"seconds ago": {
"1": "{count} second ago",
"other": "{count} seconds ago",
},
},
"ar": {
"What's New": "ما الجديد",
"posted": "نشر/ت",
"just now": "الآن",
"today at": "اليوم الساعة",
"yesterday": "الأمس",
"hours ago": {
"1": "من ساعة",
"2": "من ساعتان",
"3-10": "من {count} ساعات",
"other": "من {count} ساعة",
},
"minutes ago": {
"1": "من دقيقة",
"2": "من دقيقتين",
"3-10": "من {count} دقائق",
"other": "من {count} دقيقة",
},
"seconds ago": {
"1": "من ثانية",
"2": "من ثانيتين",
"3-10": "من {count} ثواني",
"other": "من {count} ثانية",
},
},
};
/**
* Retrieve a localized string by key
*/
function __(key: string, locale: Locale = currentLocale): string {
return translations[locale][key] as string;
}
/**
* Retrieve a plural form for a phrase by the phrase's key
*/
function _p(key: string, count: number, locale: Locale = currentLocale): string {
const forms = translations[locale][key] as PluralForms;
const { other, ...definiteForms } = forms;
const sortedDefiniteForms = Object.keys(definiteForms).sort();
let hit: string = "";
for (let i: number = 0; i < sortedDefiniteForms.length; i += 1) {
const currentFormKey: PluralFormKey = sortedDefiniteForms[i] as PluralFormKey;
if (currentFormKey.includes("-")) {
const [lowerLimit, upperLimit] = currentFormKey.split("-").map(s => parseInt(s.trim(), 10));
if (lowerLimit <= count && count <= upperLimit) {
hit = forms[currentFormKey]!;
}
} else {
if (count === parseInt(currentFormKey, 10)) {
hit = forms[currentFormKey]!;
}
}
}
if (hit === "") {
hit = other || key;
}
const normalized: string = hit.replace("{count}", count.toString());
return locale.toLowerCase().startsWith("ar") ?
// Convert Arabic numerals to Indian numerals
normalized.replace(/\d/g, d => "٠١٢٣٤٥٦٧٨٩"[parseInt(d, 10)]) :
normalized ;
}
// ----------------------------------------------------------------------
// HUMAN-FRIENDLY DATES
// ----------------------------------------------------------------------
/**
* Retrieve a human-friendly date string relative to now and in the
* current locale e.g. "two minutes ago"
*/
function humanFriendlyDate(date: Date): string {
const unixTimestamp: number = millisecondsToSeconds(date.valueOf());
const now: number = millisecondsToSeconds(Date.now());
const diffComponents: DateTimeComponents = getDateTimeComponents(now - unixTimestamp);
const { years, months, days, hours, minutes, seconds } = diffComponents;
if (years > 0) {
return formatLocalizedDateWithOrdinal(currentLocale, date, { includeYear: true });
}
if (months > 0 || days > DATE_WITH_MONTH_THRESHOLD_IN_DAYS) {
return formatLocalizedDateWithOrdinal(currentLocale, date, { includeYear: false });
}
if (days > 1) {
return date.toLocaleDateString(currentLocale, { weekday: "long" });
}
if (days === 1) {
return __("yesterday");
}
if (hours > TODAY_AT_THRESHOLD_IN_HOURS) {
return __("today at") + " " +
date.toLocaleTimeString(currentLocale, { hour: "numeric", minute: "2-digit" });
}
if (hours > 0) {
return _p("hours ago", hours);
}
if (minutes > 0) {
return _p("minutes ago", minutes);
}
if (seconds > NOW_THRESHOLD_IN_SECONDS) {
return _p("seconds ago", seconds);
}
return __("just now");
}
/**
* For English, format a date with given options, adding an ordinal
* e.g. "May 1st, 1992" (note the "1st"). For non-English locales,
* format a date with given options (and no ordinal);
*/
function formatLocalizedDateWithOrdinal(locale: Locale, date: Date, options: DateFormatOptions = { includeYear: false }) {
if (locale.toLowerCase().startsWith("en")) {
return formatEnglishDateWithOrdinal(date, options);
}
return formatNonEnglishDate(locale, date, options);
}
/**
* Format an English date with it ordinal e.g. "May 1st, 1992"
*/
function formatEnglishDateWithOrdinal(date: Date, { includeYear }: DateFormatOptions): string {
const month: string = date.toLocaleDateString("en", { month: "long" });
const day: string = getOrdinal(date.getDate());
let formatted: string = `${month} ${day}`;
if (includeYear) {
formatted += `, ${date.getFullYear()}`;
}
return formatted;
}
/**
* Format a non-English date
*/
function formatNonEnglishDate(locale: Locale, date: Date, { includeYear }: DateFormatOptions): string {
const options: Intl.DateTimeFormatOptions = { day: "numeric", month: "long" };
if (includeYear) {
options.year = "numeric";
}
return date.toLocaleDateString(locale, options);
}
/**
* Retrieve an English ordinal for a number, e.g. "2nd" for 2
*/
function getOrdinal(n: number): string {
// From https://community.shopify.com/c/Shopify-Design/Ordinal-Number-in-javascript-1st-2nd-3rd-4th/m-p/72156
var s=["th","st","nd","rd"],
v=n%100;
return n+(s[(v-20)%10]||s[v]||s[0]);
}
/**
* Convert milliseconds to seconds
*/
function millisecondsToSeconds(milliseconds: number): number {
return Math.floor(milliseconds * MILLISECONDS_TO_SECONDS);
}
/**
* Break up a unix timestamp into its date & time components
*/
function getDateTimeComponents(timestamp: number): DateTimeComponents {
const components: DateTimeComponents = {
years: 0,
months: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
};
let remaining: number = timestamp;
// years
components.years = Math.floor(remaining / SECONDS_IN_YEAR);
remaining -= components.years * SECONDS_IN_YEAR;
// months
components.months = Math.floor(remaining / SECONDS_IN_MONTH);
remaining -= components.months * SECONDS_IN_MONTH;
// days
components.days = Math.floor(remaining / SECONDS_IN_DAY);
remaining -= components.days * SECONDS_IN_DAY;
// hours
components.hours = Math.floor(remaining / SECONDS_IN_HOUR);
remaining -= components.hours * SECONDS_IN_HOUR;
// minutes
components.minutes = Math.floor(remaining / SECONDS_IN_MINUTE);
remaining -= components.minutes * SECONDS_IN_MINUTE;
// seconds
components.seconds = remaining;
return components;
}
// ----------------------------------------------------------------------
// DATA & RENDERING
// ----------------------------------------------------------------------
/**
* All dates are displayed relative to this
*/
const baseTime: number = Date.now();
/**
* Mock English data for our demo
*/
const en_data: Post[] = [
{
"name": "Fawzi Abdulrahman",
"photo": "https://tinyfac.es/data/avatars/475605E3-69C5-4D2B-8727-61B7BB8C4699-500w.jpeg",
"post": "Gun hornswaggle furl long clothes hands spike hardtack plunder log mizzen. Landlubber or just lubber chandler long clothes jib holystone yard warp lateen sail Cat o'nine tails fore.",
"posted_at": new Date(baseTime),
},
{
"name": "June Cha",
"photo": "https://tinyfac.es/data/avatars/8B510E03-96BA-43F0-A85A-F38BB3005AF1-500w.jpeg",
"post": "Halvah fruitcake donut brownie chocolate bear claw. Muffin biscuit tootsie roll candy. Cheesecake jelly-o lemon drops sweet chocolate bar marshmallow marshmallow. Cotton candy donut cheesecake sweet.",
"posted_at": new Date(baseTime - 1 / MILLISECONDS_TO_SECONDS)
},
{
"name": "Iida Niskanen",
"photo": "https://tinyfac.es/data/avatars/BA0CB1F2-8C79-4376-B13B-DD5FB8772537-200w.jpeg",
"post": "Melted cheese bocconcini mozzarella. Halloumi swiss pecorino dolcelatte monterey jack cream cheese cheese triangles dolcelat",
"posted_at": new Date(baseTime - 30 / MILLISECONDS_TO_SECONDS),
},
{
"name": "Renee Sims",
"photo": "https:\/\/randomuser.me\/api\/portraits\/women\/65.jpg",
"post": "Check it out, y'all. Everyone who was invited is here. So, how 'bout them Knicks? Why would a robot need to drink?",
"posted_at": new Date(baseTime - 2 * SECONDS_IN_MINUTE / MILLISECONDS_TO_SECONDS),
},
{
"name": "Jonathan Nu\u00f1ez",
"photo": "https:\/\/randomuser.me\/api\/portraits\/men\/43.jpg",
"post": "Viral air plant aesthetic, williamsburg iPhone glossier vape. Try-hard hell of chicharrones aesthetic.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
},
{
"name": "Sasha Ho",
"photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
"post": "What's the status on the deliverables for eow? root-and-branch review best practices, or if you want to motivate these clowns, try less carrot and more stick.",
"posted_at": new Date(baseTime - 13 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
},
{
"name": "Abdullah Hadley",
"photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
"post": "Tunguska event Euclid two ghostly white figures in coveralls and helmets are soflty dancing Apollonius of Perga hundreds of thousands intelligent beings?",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "Veeti Seppanen",
"photo": "https:\/\/randomuser.me\/api\/portraits\/men\/97.jpg",
"post": "De carne lumbering animata corpora quaeritis. Summus brains sit, morbo vel maleficia?",
"posted_at": new Date(baseTime - 2 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "Thomas Stock",
"photo": "https:\/\/tinyfac.es\/data\/avatars\/B0298C36-9751-48EF-BE15-80FB9CD11143-500w.jpeg",
"post": "Ignore the squirrels, you'll never catch them anyway vommit food and eat it again.",
"posted_at": new Date(baseTime - 7 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "Bonnie Riley",
"photo": "https:\/\/randomuser.me\/api\/portraits\/women\/26.jpg",
"post": "No borders, no limits… go ahead, touch the Cornballer… you know best? Steve Holt? The moron jock? I am going to my spin class.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),
},
{
"name": "Abdullah Hadley",
"photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
"post": "At the pump-room I saw Mr. Macartney; I courtsied to him twice ere he would speak to me. ",
"posted_at": new Date(baseTime - 5 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),
},
{
"name": "Sasha Ho",
"photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
"post": "Hand-crafted sleepy Asia-Pacific elegant Scandinavian Zürich Marylebone carefully curated Sunspel essential ryokan. Delightful flat white bespoke hub essential espresso Porter Melbourne Sunspel wardrobe.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_YEAR / MILLISECONDS_TO_SECONDS),
},
];
/**
* Mock Arabic data for our demo
*/
const ar_data: Post[] = [
{
"name": "فوزي عبدالرحمان",
"photo": "https://tinyfac.es/data/avatars/475605E3-69C5-4D2B-8727-61B7BB8C4699-500w.jpeg",
"post": "بندقية hornswaggle furl ملابس طويلة الأيدي سبايك hardtack نهب سجل mizzen. Landlubber أو مجرد lubber chandler ملابس طويلة jib holystone yard warp lateen الشراع Cat o'nine ذيول الصدارة.",
"posted_at": new Date(baseTime),
},
{
"name": "جون شا",
"photo": "https://tinyfac.es/data/avatars/8B510E03-96BA-43F0-A85A-F38BB3005AF1-500w.jpeg",
"post": "الحلاوة فواكه الكعك دونات براوني الشوكولاته الدب مخلب. الكعك بسكويت tootsie لفة الحلوى. تشيز كيك جيلي-ليمون يسقط حلوى الشوكولاته بار الخطمي الخطمي. حلوى القطن دونات تشيز كيك حلوة",
"posted_at": new Date(baseTime - 1 / MILLISECONDS_TO_SECONDS)
},
{
"name": "ايدا نسكانين",
"photo": "https://tinyfac.es/data/avatars/BA0CB1F2-8C79-4376-B13B-DD5FB8772537-200w.jpeg",
"post": "ذاب الجبن بوكونسيني موزاريلا. حلومي سويس بيكورينو دولسيلاتي مونتيري جاك جبنة مثلثات دولسيلات",
"posted_at": new Date(baseTime - 30 / MILLISECONDS_TO_SECONDS),
},
{
"name": "ريني سيمز",
"photo": "https:\/\/randomuser.me\/api\/portraits\/women\/65.jpg",
"post": "التحقق من ذلك ، كل شيء. كل من تمت دعوته هنا. لذا ، كيف نوبة لهم نيكس؟ لماذا يحتاج الروبوت للشرب؟",
"posted_at": new Date(baseTime - 2 * SECONDS_IN_MINUTE / MILLISECONDS_TO_SECONDS),
},
{
"name": "جوناثان نونز",
"photo": "https:\/\/randomuser.me\/api\/portraits\/men\/43.jpg",
"post": "جمالية الهواء النباتية الفيروسية ، وليامز فون المصقول vape. محاولة من الصعب الجحيم chicharrones الجمالية.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
},
{
"name": "ساشا هو",
"photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
"post": "ما هو الوضع على التسليمات ل eow؟ راجع أفضل الممارسات الخاصة بالجذر والفروع ، أو إذا كنت ترغب في تحفيز هؤلاء المهرجين ، فحاول استخدام عدد أقل من الجزرة والعصا.",
"posted_at": new Date(baseTime - 13 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
},
{
"name": "عبد الله هيدلي",
"photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
"post": "حدث تونجوسكا إقليدس شخصيتان أبيضتان شبحتان في المعاطف والخوذات يرقصان أبولونيوس من بيرغا مئات الآلاف من الكائنات الذكية؟",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "ڤيتي سيبانين",
"photo": "https:\/\/randomuser.me\/api\/portraits\/men\/97.jpg",
"post": "De carne lumbering animata corpora quaeritis. أدمغة Summus الجلوس ، morbo vel ينكوليا؟",
"posted_at": new Date(baseTime - 2 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "توماس ستوك",
"photo": "https:\/\/tinyfac.es\/data\/avatars\/B0298C36-9751-48EF-BE15-80FB9CD11143-500w.jpeg",
"post": "تجاهل السناجب ، فلن تصطادهم أبداً بتقيؤ الطعام وتناوله مرة أخرى.",
"posted_at": new Date(baseTime - 7 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
},
{
"name": "بوني رايلي",
"photo": "https:\/\/randomuser.me\/api\/portraits\/women\/26.jpg",
"post": "لا حدود ، لا حدود ... المضي قدما ، المس Cornballer ... أنت تعرف أفضل؟ ستيف هولت؟ جوك معتوه؟ أنا ذاهب إلى صفي تدور.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),
},
{
"name": "عبد الله هيدلي",
"photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
"post": "في غرفة المضخة ، رأيت السيد مكارتني. كنت مغازلة له مرتين إذا كان سيتحدث معي. ",
"posted_at": new Date(baseTime - 5 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),
},
{
"name": "ساشا هو",
"photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
"post": "صُنعت الاسكندنافية زيوريخ ماريليبوني الأنيقة المصنّعة يدوياً في منطقة آسيا والمحيط الهادئ برعاية Sunspel الأساسية. مبهج مفصل أبيض مسطح محور إسبرسو الأساسية بورتر ملبورن Sunspel خزانة الملابس.",
"posted_at": new Date(baseTime - 1 * SECONDS_IN_YEAR / MILLISECONDS_TO_SECONDS),
},
];
/**
* Retrieve HTML for a single post, humanizing the post's date
*/
function renderPost(post: Post): string {
const avatarMargin = currentLocale === "en" ? "mr-3" : "ml-3";
return (
`<li class="list-group-item">
<div class="d-flex">
<div
class="${avatarMargin} rounded-circle"
style="width: 4rem; height: 4rem; overflow: hidden;"
>
<img src="${post.photo}"
style="object-fit: cover; object-position: center; min-height: 100%; width: 100%;"
/>
</div>
<div style="flex: 1;">
<h4 class="h6 d-flex justify-content-between align-items-baseline">
<span>${post.name} ${__("posted")}</span>
<span class="small">${humanFriendlyDate(post.posted_at)}</span>
</h4>
<p style="text-align: ${currentLocale === "en" ? "left" : "right"}">
${post.post}
</p>
</div>
</div>
</li>`
);
}
/**
* Update button state based on active button
*/
function updateLocaleButtons(): void {
const buttons = {
en: document.getElementById("localeControl-en"),
ar: document.getElementById("localeControl-ar"),
}
Object.keys(buttons).forEach((locale) => {
const btn = buttons[locale as Locale];
if (btn === null) { return; }
btn.setAttribute("aria-pressed", "false");
btn.className = btn.className.replace("active", "");
});
const selectedBtn = buttons[currentLocale];
if (selectedBtn === null) { return; }
selectedBtn.setAttribute("aria-pressed", "true");
selectedBtn.className += " active";
}
/**
* Refresh UI based on selected locale
*/
function selectLocale(newLocale: Locale): void {
currentLocale = newLocale;
const data = newLocale === "en" ? en_data : ar_data;
const feed = document.getElementById('feed');
if (feed !== null) {
feed.dir = newLocale === "en" ? "ltr" : "rtl";
feed.innerHTML = data.map(renderPost).join("");
}
const feedHeader = document.getElementById("feedHeader");
if (feedHeader !== null) {
feedHeader.innerHTML = __("What's New");
feedHeader.style.textAlign = newLocale === "en" ? "left" : "right";
}
updateLocaleButtons();
}
/**
* Locale-switcher
*/
const localeControls = document.getElementById('localeControls');
if (localeControls !== null) {
// Switch locale when respective button is clicked
localeControls.addEventListener("click", e => {
const btn = e.target as HTMLButtonElement;
if (btn === null) { return; }
const newLocale: Locale = btn.id.split("-")[1].trim() as Locale;
selectLocale(newLocale);
});
}
selectLocale("en");
Also see: Tab Triggers