Improve countdown logic

use `Intl.RelativeTimeFormat` to support different l10n add week as possible duration
This commit is contained in:
Thomas Rupprecht 2022-12-21 14:18:21 +01:00 committed by GitHub
parent 98a3187899
commit f24d31b928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 111 deletions

View File

@ -1,5 +1,6 @@
<?php <?php
use Carbon\Carbon;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Room; use Engelsystem\Models\Room;
use Engelsystem\Models\User\User; use Engelsystem\Models\User\User;
@ -89,9 +90,10 @@ function ShiftEntry_create_view_admin(
$signup_user, $signup_user,
$users_select $users_select
) { ) {
$start = Carbon::createFromTimestamp($shift['start'])->format(__('Y-m-d H:i'));
return page_with_title( return page_with_title(
ShiftEntry_create_title() . ': ' . $shift['name'] ShiftEntry_create_title() . ': ' . $shift['name']
. ' <small data-countdown-ts="' . $shift['start'] . '">%c</small>', . ' <small title="' . $start . '" data-countdown-ts="' . $shift['start'] . '">%c</small>',
[ [
Shift_view_header($shift, $room), Shift_view_header($shift, $room),
info(__('Do you want to sign up the following user for this shift?'), true), info(__('Do you want to sign up the following user for this shift?'), true),
@ -116,9 +118,10 @@ function ShiftEntry_create_view_admin(
*/ */
function ShiftEntry_create_view_supporter($shift, Room $room, AngelType $angeltype, $signup_user, $users_select) function ShiftEntry_create_view_supporter($shift, Room $room, AngelType $angeltype, $signup_user, $users_select)
{ {
$start = Carbon::createFromTimestamp($shift['start'])->format(__('Y-m-d H:i'));
return page_with_title( return page_with_title(
ShiftEntry_create_title() . ': ' . $shift['name'] ShiftEntry_create_title() . ': ' . $shift['name']
. ' <small data-countdown-ts="' . $shift['start'] . '">%c</small>', . ' <small title="' . $start . '" data-countdown-ts="' . $shift['start'] . '">%c</small>',
[ [
Shift_view_header($shift, $room), Shift_view_header($shift, $room),
info(sprintf( info(sprintf(
@ -144,9 +147,10 @@ function ShiftEntry_create_view_supporter($shift, Room $room, AngelType $angelty
*/ */
function ShiftEntry_create_view_user($shift, Room $room, AngelType $angeltype, $comment) function ShiftEntry_create_view_user($shift, Room $room, AngelType $angeltype, $comment)
{ {
$start = Carbon::createFromTimestamp($shift['start'])->format(__('Y-m-d H:i'));
return page_with_title( return page_with_title(
ShiftEntry_create_title() . ': ' . $shift['name'] ShiftEntry_create_title() . ': ' . $shift['name']
. ' <small data-countdown-ts="' . $shift['start'] . '">%c</small>', . ' <small title="' . $start . '" data-countdown-ts="' . $shift['start'] . '">%c</small>',
[ [
Shift_view_header($shift, $room), Shift_view_header($shift, $room),
info(sprintf(__('Do you want to sign up for this shift as %s?'), AngelType_name_render($angeltype)), true), info(sprintf(__('Do you want to sign up for this shift as %s?'), AngelType_name_render($angeltype)), true),

View File

@ -1,5 +1,6 @@
<?php <?php
use Carbon\Carbon;
use Engelsystem\Models\AngelType; use Engelsystem\Models\AngelType;
use Engelsystem\Models\Room; use Engelsystem\Models\Room;
use Engelsystem\Models\Shifts\ShiftType; use Engelsystem\Models\Shifts\ShiftType;
@ -195,8 +196,10 @@ function Shift_view($shift, ShiftType $shifttype, Room $room, $angeltypes_source
$content[] = Shift_editor_info_render($shift); $content[] = Shift_editor_info_render($shift);
} }
$start = Carbon::createFromTimestamp($shift['start'])->format(__('Y-m-d H:i'));
return page_with_title( return page_with_title(
$shift['name'] . ' <small data-countdown-ts="' . $shift['start'] . '">%c</small>', $shift['name'] . ' <small title="' . $start . '" data-countdown-ts="' . $shift['start'] . '">%c</small>',
$content $content
); );
} }

View File

@ -189,25 +189,28 @@ function User_shift_state_render($user)
$nextShift = array_shift($upcoming_shifts); $nextShift = array_shift($upcoming_shifts);
$start = Carbon::createFromTimestamp($nextShift['start'])->format(__('Y-m-d H:i'));
if ($nextShift['start'] > time()) { if ($nextShift['start'] > time()) {
if ($nextShift['start'] - time() > 3600) { if ($nextShift['start'] - time() > 3600) {
return '<span class="text-success" data-countdown-ts="' . $nextShift['start'] . '">' return '<span class="text-success" title="' . $start . '" data-countdown-ts="' . $nextShift['start'] . '">'
. __('Next shift %c') . __('Next shift %c')
. '</span>'; . '</span>';
} }
return '<span class="text-warning" data-countdown-ts="' . $nextShift['start'] . '">' return '<span class="text-warning" title="' . $start . '" data-countdown-ts="' . $nextShift['start'] . '">'
. __('Next shift %c') . __('Next shift %c')
. '</span>'; . '</span>';
} }
$halfway = ($nextShift['start'] + $nextShift['end']) / 2; $halfway = ($nextShift['start'] + $nextShift['end']) / 2;
if (time() < $halfway) { if (time() < $halfway) {
return '<span class="text-danger" data-countdown-ts="' . $nextShift['start'] . '">' return '<span class="text-danger" title="' . $start . '" data-countdown-ts="' . $nextShift['start'] . '">'
. __('Shift started %c') . __('Shift started %c')
. '</span>'; . '</span>';
} }
return '<span class="text-danger" data-countdown-ts="' . $nextShift['end'] . '">' $end = Carbon::createFromTimestamp($nextShift['end'])->format(__('Y-m-d H:i'));
return '<span class="text-danger" title="' . $end . '" data-countdown-ts="' . $nextShift['end'] . '">'
. __('Shift ends %c') . __('Shift ends %c')
. '</span>'; . '</span>';
} }
@ -224,7 +227,9 @@ function User_last_shift_render($user)
} }
$lastShift = array_shift($last_shifts); $lastShift = array_shift($last_shifts);
return '<span data-countdown-ts="' . $lastShift['end'] . '">' $end = Carbon::createFromTimestamp($lastShift['end'])->format(__('Y-m-d H:i'));
return '<span title="' . $end . '" data-countdown-ts="' . $lastShift['end'] . '">'
. __('Shift ended %c') . __('Shift ended %c')
. '</span>'; . '</span>';
} }

View File

@ -1,85 +1,47 @@
import { ready } from './ready'; import { ready } from './ready';
const lang = document.documentElement.getAttribute('lang');
const templateFuture = 'in %value %unit';
const templatePast = lang === 'en'
? '%value %unit ago'
: 'vor %value %unit';
const yearUnits = lang === 'en'
? ['year', 'years']
: ['Jahr', 'Jahren'];
const monthUnits = lang === 'en'
? ['month', 'months']
: ['Monat', 'Monaten'];
const dayUnits = lang === 'en'
? ['day', 'days']
: ['Tag', 'Tagen'];
const hourUnits = lang === 'en'
? ['hour', 'hours']
: ['Stunde', 'Stunden'];
const minuteUnits = lang === 'en'
? ['minute', 'minutes']
: ['Minute', 'Minuten'];
const secondUnits = lang === 'en'
? ['second', 'seconds']
: ['Sekunde', 'Sekunden'];
const nowString = lang === 'en' ? 'now' : 'jetzt';
const secondsHour = 60 * 60;
const timeFrames = [
[365 * 24 * 60 * 60, yearUnits],
[30 * 24 * 60 * 60, monthUnits],
[24 * 60 * 60, dayUnits],
[secondsHour, hourUnits],
[60, minuteUnits],
[1, secondUnits],
];
/**
* @param {number} timestamp
* @returns {string}
*/
function formatFromNow(timestamp) {
const now = Date.now() / 1000;
const diff = Math.abs(timestamp - now);
const ago = now > timestamp;
for (const [duration, [singular, plural]] of timeFrames) {
const value = diff < secondsHour
? Math.floor(diff / duration)
: Math.round(diff / duration);
if (value) {
const template = ago ? templatePast : templateFuture;
const unit = value === 1 ? singular : plural;
return template
.replace('%value', value)
.replace('%unit', unit);
}
}
return nowString;
}
/** /**
* Initialises all countdown fields on the page. * Initialises all countdown fields on the page.
*/ */
ready(() => { ready(() => {
const lang = document.documentElement.getAttribute('lang');
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
const timeFrames = [
[60 * 60 * 24 * 365, 'year'],
[60 * 60 * 24 * 30, 'month'],
[60 * 60 * 24 * 7, 'week'],
[60 * 60 * 24, 'day'],
[60 * 60, 'hour'],
[60, 'minute'],
[1, 'second'],
];
/**
* @param {number} timestamp
* @returns {string}
*/
function formatFromNow(timestamp) {
const now = Date.now() / 1000;
const diff = Math.round(timestamp - now);
const absValue = Math.abs(diff);
for (const [duration, unit] of timeFrames) {
if (absValue >= duration) {
return rtf.format(Math.round(diff / duration), unit);
}
}
return rtf.format(0, 'second');
}
document.querySelectorAll('[data-countdown-ts]').forEach((element) => { document.querySelectorAll('[data-countdown-ts]').forEach((element) => {
const timestamp = Number(element.dataset.countdownTs); const timestamp = Number(element.dataset.countdownTs);
const template = element.innerHTML; const template = element.textContent;
element.innerHTML = template.replace('%c', formatFromNow(timestamp)); element.textContent = template.replace('%c', formatFromNow(timestamp));
setInterval(() => { setInterval(() => {
element.innerHTML = template.replace('%c', formatFromNow(timestamp)); element.textContent = template.replace('%c', formatFromNow(timestamp));
}, 1000); }, 1000);
}); });
}); });

View File

@ -322,20 +322,20 @@
<div class="col"> <div class="col">
<h4>Countdowns</h4> <h4>Countdowns</h4>
<ul> <ul>
<li data-countdown-ts="{{ timestamp30s }}">30s: %c</li> <li title="{{ timestamp30s.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp30s.getTimestamp() }}">30s: %c</li>
<li data-countdown-ts="{{ timestamp30m }}">30m: %c</li> <li title="{{ timestamp30m.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp30m.getTimestamp() }}">30m: %c</li>
<li data-countdown-ts="{{ timestamp59m }}">59m: %c</li> <li title="{{ timestamp59m.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp59m.getTimestamp() }}">59m: %c</li>
<li data-countdown-ts="{{ timestamp1h }}">1h: %c</li> <li title="{{ timestamp1h.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp1h.getTimestamp() }}">1h: %c</li>
<li data-countdown-ts="{{ timestamp1h30m }}">1h 30m: %c</li> <li title="{{ timestamp1h30m.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp1h30m.getTimestamp() }}">1h 30m: %c</li>
<li data-countdown-ts="{{ timestamp1h31m }}">1h 31m: %c</li> <li title="{{ timestamp1h31m.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp1h31m.getTimestamp() }}">1h 31m: %c</li>
<li data-countdown-ts="{{ timestamp2h }}">2h: %c</li> <li title="{{ timestamp2h.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp2h.getTimestamp() }}">2h: %c</li>
<li data-countdown-ts="{{ timestamp2d }}">2d: %c</li> <li title="{{ timestamp2d.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp2d.getTimestamp() }}">2d: %c</li>
<li data-countdown-ts="{{ timestamp3m }}">3m: %c</li> <li title="{{ timestamp3m.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp3m.getTimestamp() }}">3m: %c</li>
<li data-countdown-ts="{{ timestamp22y }}">22y: %c</li> <li title="{{ timestamp22y.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp22y.getTimestamp() }}">22y: %c</li>
</ul> </ul>
<ul> <ul>
<li data-countdown-ts="{{ timestamp30mago }}">30m ago: %c</li> <li title="{{ timestamp30mago.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp30mago.getTimestamp() }}">30m ago: %c</li>
<li data-countdown-ts="{{ timestamp45mago }}">45m ago: %c</li> <li title="{{ timestamp45mago.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ timestamp45mago.getTimestamp() }}">45m ago: %c</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@
{% if date > date() %} {% if date > date() %}
<div class="col-sm-3 text-center d-none d-sm-block"> <div class="col-sm-3 text-center d-none d-sm-block">
<h4>{{ name }}</h4> <h4>{{ name }}</h4>
<div class="h2 text-body" data-countdown-ts="{{ date.getTimestamp }}">%c</div> <div class="h2 text-body" title="{{ date.format(__('Y-m-d H:i')) }}" data-countdown-ts="{{ date.getTimestamp() }}">%c</div>
<small>{{ date.format(__('Y-m-d')) }}</small> <small>{{ date.format(__('Y-m-d')) }}</small>
</div> </div>
{% endif %} {% endif %}

View File

@ -41,24 +41,26 @@ class DesignController extends BaseController
])); ]));
$themes = $this->config->get('themes'); $themes = $this->config->get('themes');
$date = new \DateTimeImmutable();
$data = [ $data = [
'demo_user' => $demoUser, 'demo_user' => $demoUser,
'demo_user_2' => $demoUser2, 'demo_user_2' => $demoUser2,
'themes' => $themes, 'themes' => $themes,
'bar_chart' => BarChart::render(...BarChart::generateChartDemoData(23)), 'bar_chart' => BarChart::render(...BarChart::generateChartDemoData(23)),
'timestamp30m' => time() + 30 * 60,
'timestamp59m' => time() + 59 * 60,
'timestamp1h' => time() + 1 * 60 * 60,
'timestamp1h30m' => time() + 90 * 60,
'timestamp1h31m' => time() + 91 * 60,
'timestamp2h' => time() + 2 * 60 * 60,
'timestamp2d' => time() + 2 * 24 * 60 * 60,
'timestamp3m' => time() + 3 * 30 * 24 * 60 * 60,
'timestamp22y' => time() + 22 * 365 * 24 * 60 * 60,
'timestamp30s' => time() + 30,
'timestamp30mago' => time() - 30 * 60, 'timestamp30m' => $date->add(new \DateInterval('PT30M')),
'timestamp45mago' => time() - 45 * 60, 'timestamp59m' => $date->add(new \DateInterval('PT59M')),
'timestamp1h' => $date->add(new \DateInterval('PT1H')),
'timestamp1h30m' => $date->add(new \DateInterval('PT1H30M')),
'timestamp1h31m' => $date->add(new \DateInterval('PT1H31M')),
'timestamp2h' => $date->add(new \DateInterval('PT2H')),
'timestamp2d' => $date->add(new \DateInterval('P2D')),
'timestamp3m' => $date->add(new \DateInterval('P3M')),
'timestamp22y' => $date->add(new \DateInterval('P22Y')),
'timestamp30s' => $date->add(new \DateInterval('PT30S')),
'timestamp30mago' => $date->sub(new \DateInterval('PT30M')),
'timestamp45mago' => $date->sub(new \DateInterval('PT45M')),
]; ];
return $this->response->withView( return $this->response->withView(