feat(episodes): schedule episode with future publication_date by using cache expiration time
- merge publication date fields into one field instanciated with flatpickr datetime picker - get user timezone to convert user publication_date input to UTC - remove setPublishedAt() method from episode entity - add publication pill component to display the episode publication date info - clear cache after episode insert - use CI is_really_writable() helper in install instead of is_writable() - fix latest episodes layout - update tsconfig to only include ts folders - update DEPENDENCIES.md to include flatpickr - add format_duration helper to format episode enclosure duration instead of translating it (causes translation bug) - add Time.ts module to convert UTC time to user localized time for episode publication dates - fix some layout issues - update php and js dependencies to latest versions closes #47
This commit is contained in:
parent
0ab17d1075
commit
4f1e773c0f
|
@ -37,6 +37,8 @@ Javascript dependencies:
|
|||
([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE))
|
||||
- [Choices.js](https://joshuajohnson.co.uk/Choices/)
|
||||
([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
|
||||
- [flatpickr](https://flatpickr.js.org/)
|
||||
([MIT License](https://github.com/flatpickr/flatpickr/blob/master/LICENSE.md))
|
||||
|
||||
Other:
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class BaseController extends Controller
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['auth', 'breadcrumb', 'svg', 'components'];
|
||||
protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc'];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace App\Controllers\Admin;
|
|||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class Episode extends BaseController
|
||||
{
|
||||
|
@ -95,9 +96,7 @@ class Episode extends BaseController
|
|||
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
|
||||
'image' =>
|
||||
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
|
||||
'publication_date' => 'valid_date[Y-m-d]|permit_empty',
|
||||
'publication_time' =>
|
||||
'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty',
|
||||
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
|
@ -125,11 +124,12 @@ class Episode extends BaseController
|
|||
'block' => $this->request->getPost('block') == 'yes',
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
'published_at' => Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$this->request->getPost('publication_date'),
|
||||
$this->request->getPost('client_timezone')
|
||||
)->setTimezone('UTC'),
|
||||
]);
|
||||
$newEpisode->setPublishedAt(
|
||||
$this->request->getPost('publication_date'),
|
||||
$this->request->getPost('publication_time')
|
||||
);
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
||||
|
@ -185,9 +185,7 @@ class Episode extends BaseController
|
|||
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
|
||||
'image' =>
|
||||
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
|
||||
'publication_date' => 'valid_date[Y-m-d]|permit_empty',
|
||||
'publication_time' =>
|
||||
'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty',
|
||||
'publication_date' => 'valid_date[Y-m-d H:i]|permit_empty',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
|
@ -210,10 +208,11 @@ class Episode extends BaseController
|
|||
: null;
|
||||
$this->episode->type = $this->request->getPost('type');
|
||||
$this->episode->block = $this->request->getPost('block') == 'yes';
|
||||
$this->episode->setPublishedAt(
|
||||
$this->episode->published_at = Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$this->request->getPost('publication_date'),
|
||||
$this->request->getPost('publication_time')
|
||||
);
|
||||
$this->request->getPost('client_timezone')
|
||||
)->setTimezone('UTC');
|
||||
$this->episode->updated_by = user();
|
||||
|
||||
$enclosure = $this->request->getFile('enclosure');
|
||||
|
|
|
@ -388,11 +388,8 @@ class Podcast extends BaseController
|
|||
: $nsItunes->block === 'yes',
|
||||
'created_by' => user(),
|
||||
'updated_by' => user(),
|
||||
'published_at' => strtotime($item->pubDate),
|
||||
]);
|
||||
$newEpisode->setPublishedAt(
|
||||
date('Y-m-d', strtotime($item->pubDate)),
|
||||
date('H:i:s', strtotime($item->pubDate))
|
||||
);
|
||||
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class BaseController extends Controller
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['analytics', 'svg', 'components'];
|
||||
protected $helpers = ['analytics', 'svg', 'components', 'misc'];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
|
|
@ -48,7 +48,8 @@ class Episode extends BaseController
|
|||
$cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_{$locale}";
|
||||
|
||||
if (!($cachedView = cache($cacheName))) {
|
||||
$previousNextEpisodes = (new EpisodeModel())->getPreviousNextEpisodes(
|
||||
$episodeModel = new EpisodeModel();
|
||||
$previousNextEpisodes = $episodeModel->getPreviousNextEpisodes(
|
||||
$this->episode,
|
||||
$this->podcast->type
|
||||
);
|
||||
|
@ -60,9 +61,15 @@ class Episode extends BaseController
|
|||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id
|
||||
);
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('episode', $data, [
|
||||
'cache' => DECADE,
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
|
@ -31,15 +32,29 @@ class Feed extends Controller
|
|||
// If things go wrong the show must go on and the user must be able to download the file
|
||||
log_message('critical', $e);
|
||||
}
|
||||
|
||||
$cacheName =
|
||||
"podcast{$podcast->id}_feed" .
|
||||
($service ? "_{$service['slug']}" : '');
|
||||
|
||||
if (!($found = cache($cacheName))) {
|
||||
$found = get_rss_feed(
|
||||
$podcast,
|
||||
$service ? '?s=' . urlencode($service['name']) : ''
|
||||
);
|
||||
cache()->save($cacheName, $found, DECADE);
|
||||
|
||||
// The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$podcast->id
|
||||
);
|
||||
|
||||
cache()->save(
|
||||
$cacheName,
|
||||
$found,
|
||||
$secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE
|
||||
);
|
||||
}
|
||||
return $this->response->setXML($found);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class Install extends Controller
|
|||
}
|
||||
|
||||
// Check if the created .env file is writable to continue install process
|
||||
if (is_writable(ROOTPATH . '.env')) {
|
||||
if (is_really_writable(ROOTPATH . '.env')) {
|
||||
try {
|
||||
$dotenv->required([
|
||||
'app.baseURL',
|
||||
|
|
|
@ -113,7 +113,7 @@ class Podcast extends BaseController
|
|||
'podcast' => $this->podcast,
|
||||
'episodesNav' => $episodesNavigation,
|
||||
'activeQuery' => $activeQuery,
|
||||
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
|
||||
'episodes' => $episodeModel->getPodcastEpisodes(
|
||||
$this->podcast->id,
|
||||
$this->podcast->type,
|
||||
$yearQuery,
|
||||
|
@ -121,8 +121,14 @@ class Podcast extends BaseController
|
|||
),
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id
|
||||
);
|
||||
|
||||
return view('podcast', $data, [
|
||||
'cache' => DECADE,
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace App\Entities;
|
|||
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class Episode extends Entity
|
||||
|
@ -49,6 +50,11 @@ class Episode extends Entity
|
|||
*/
|
||||
protected $description_html;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $is_published;
|
||||
|
||||
protected $dates = [
|
||||
'published_at',
|
||||
'created_at',
|
||||
|
@ -232,17 +238,6 @@ class Episode extends Entity
|
|||
return $converter->convertToHtml($this->attributes['description']);
|
||||
}
|
||||
|
||||
public function setPublishedAt($date, $time)
|
||||
{
|
||||
if (empty($date)) {
|
||||
$this->attributes['published_at'] = null;
|
||||
} else {
|
||||
$this->attributes['published_at'] = $date . ' ' . $time;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedBy(\App\Entities\User $user)
|
||||
{
|
||||
$this->attributes['created_by'] = $user->id;
|
||||
|
@ -256,4 +251,17 @@ class Episode extends Entity
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsPublished()
|
||||
{
|
||||
if ($this->is_published) {
|
||||
return $this->is_published;
|
||||
}
|
||||
|
||||
helper('date');
|
||||
|
||||
$this->is_published = $this->published_at->isBefore(Time::now());
|
||||
|
||||
return $this->is_published;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,3 +256,51 @@ if (!function_exists('data_table')) {
|
|||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('publication_pill')) {
|
||||
/**
|
||||
* Data table component
|
||||
*
|
||||
* Creates a stylized table.
|
||||
*
|
||||
* @param \CodeIgniter\I18n\Time $publicationDate publication datetime of the episode
|
||||
* @param boolean $isPublished whether or not the episode has been published
|
||||
* @param string $customClass css class to add to the component
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function publication_pill(
|
||||
$publicationDate,
|
||||
$isPublished,
|
||||
$customClass = ''
|
||||
): string {
|
||||
$class = $isPublished
|
||||
? 'text-green-500 border-green-500'
|
||||
: 'text-orange-600 border-orange-600';
|
||||
|
||||
$label = lang(
|
||||
$isPublished ? 'Episode.published' : 'Episode.scheduled',
|
||||
[
|
||||
'<time
|
||||
pubdate
|
||||
datetime="' .
|
||||
$publicationDate->format(DateTime::ATOM) .
|
||||
'"
|
||||
title="' .
|
||||
$publicationDate .
|
||||
'">' .
|
||||
lang('Common.mediumDate', [$publicationDate]) .
|
||||
'</time>',
|
||||
]
|
||||
);
|
||||
|
||||
return '<span class="px-1 border ' .
|
||||
$class .
|
||||
' ' .
|
||||
$customClass .
|
||||
'">' .
|
||||
$label .
|
||||
'</span>';
|
||||
}
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
|
@ -143,3 +143,27 @@ function slugify($text)
|
|||
|
||||
return $text;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('format_duration')) {
|
||||
/**
|
||||
* Formats duration in seconds to an hh:mm:ss string
|
||||
*
|
||||
* @param int $seconds seconds to format
|
||||
* @param string $separator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function format_duration($seconds, $separator = ':')
|
||||
{
|
||||
return sprintf(
|
||||
'%02d%s%02d%s%02d',
|
||||
floor($seconds / 3600),
|
||||
$separator,
|
||||
($seconds / 60) % 60,
|
||||
$separator,
|
||||
$seconds % 60
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ return [
|
|||
'home' => 'Home',
|
||||
'explicit' => 'Explicit',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'duration' => '{0,duration}',
|
||||
'powered_by' => 'Powered by {castopod}.',
|
||||
'actions' => 'Actions',
|
||||
'pageInfo' => 'Page {currentPage} out of {pageCount}',
|
||||
|
|
|
@ -22,6 +22,8 @@ return [
|
|||
'delete' => 'Delete',
|
||||
'go_to_page' => 'Go to page',
|
||||
'create' => 'Add an episode',
|
||||
'published' => 'Published on {0}',
|
||||
'scheduled' => 'Scheduled for {0}',
|
||||
'form' => [
|
||||
'enclosure' => 'Audio file',
|
||||
'enclosure_hint' => 'Choose an .mp3 or .m4a audio file.',
|
||||
|
@ -54,11 +56,9 @@ return [
|
|||
'This text is added at the end of each episode description, it is a good place to input your social links for example.',
|
||||
'publication_section_title' => 'Publication info',
|
||||
'publication_section_subtitle' => '',
|
||||
'published_at' => [
|
||||
'label' => 'Publication date',
|
||||
'date' => 'Date',
|
||||
'time' => 'Time',
|
||||
],
|
||||
'publication_date' => 'Publication date',
|
||||
'publication_date_hint' =>
|
||||
'You can schedule the episode release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm',
|
||||
'parental_advisory' => [
|
||||
'label' => 'Parental advisory',
|
||||
'hint' => 'Does the episode contain explicit content?',
|
||||
|
|
|
@ -14,7 +14,6 @@ return [
|
|||
'home' => 'Accueil',
|
||||
'explicit' => 'Explicite',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'duration' => '{0,duration}',
|
||||
'powered_by' => 'Propulsé par {castopod}.',
|
||||
'actions' => 'Actions',
|
||||
'pageInfo' => 'Page {currentPage} sur {pageCount}',
|
||||
|
|
|
@ -22,6 +22,8 @@ return [
|
|||
'delete' => 'Supprimer',
|
||||
'go_to_page' => 'Voir',
|
||||
'create' => 'Ajouter un épisode',
|
||||
'published' => 'Publié le {0}',
|
||||
'scheduled' => 'Planifié pour le {0}',
|
||||
'form' => [
|
||||
'enclosure' => 'Fichier audio',
|
||||
'enclosure_hint' => 'Sélectionnez un fichier audio .mp3 ou .m4a.',
|
||||
|
@ -54,11 +56,9 @@ return [
|
|||
'Ce texte est ajouté à la fin de chaque description d’épisode, c’est un bon endroit pour placer vos liens sociaux par exemple.',
|
||||
'publication_section_title' => 'Information de publication',
|
||||
'publication_section_subtitle' => '',
|
||||
'published_at' => [
|
||||
'label' => 'Date de publication',
|
||||
'date' => 'Date',
|
||||
'time' => 'Heure',
|
||||
],
|
||||
'publication_date' => 'Date de publication',
|
||||
'publication_date_hint' =>
|
||||
'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm',
|
||||
'parental_advisory' => [
|
||||
'label' => 'Avertissement parental',
|
||||
'hint' => 'L’épisode contient-il un contenu explicite ?',
|
||||
|
|
|
@ -12,6 +12,7 @@ return [
|
|||
'messages' => [
|
||||
'wrongPasswordError' =>
|
||||
'Le mot de passe que vous avez saisi est invalide.',
|
||||
'passwordChangeSuccess' => 'Le mot de passe a été modifié avec succès !',
|
||||
'passwordChangeSuccess' =>
|
||||
'Le mot de passe a été modifié avec succès !',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -57,32 +57,21 @@ class EpisodeModel extends Model
|
|||
];
|
||||
protected $validationMessages = [];
|
||||
|
||||
protected $afterInsert = ['writeEnclosureMetadata'];
|
||||
protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
|
||||
// clear cache beforeUpdate because if slug changes, so will the episode link
|
||||
protected $beforeUpdate = ['clearCache'];
|
||||
protected $afterUpdate = ['writeEnclosureMetadata'];
|
||||
protected $beforeDelete = ['clearCache'];
|
||||
|
||||
protected function writeEnclosureMetadata(array $data)
|
||||
{
|
||||
helper('id3');
|
||||
|
||||
$episode = (new EpisodeModel())->find(
|
||||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
write_enclosure_tags($episode);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getEpisodeBySlug($podcastId, $episodeSlug)
|
||||
{
|
||||
if (!($found = cache("podcast{$podcastId}_episode@{$episodeSlug}"))) {
|
||||
$found = $this->where([
|
||||
'podcast_id' => $podcastId,
|
||||
'slug' => $episodeSlug,
|
||||
])->first();
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->first();
|
||||
|
||||
cache()->save(
|
||||
"podcast{$podcastId}_episode@{$episodeSlug}",
|
||||
|
@ -120,6 +109,7 @@ class EpisodeModel extends Model
|
|||
'podcast_id' => $episode->podcast_id,
|
||||
$sortNumberField . ' <' => $sortNumberValue,
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->first();
|
||||
|
||||
$nextData = $this->orderBy('(' . $sortNumberField . ') ASC')
|
||||
|
@ -127,6 +117,7 @@ class EpisodeModel extends Model
|
|||
'podcast_id' => $episode->podcast_id,
|
||||
$sortNumberField . ' >' => $sortNumberValue,
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->first();
|
||||
|
||||
return [
|
||||
|
@ -160,7 +151,9 @@ class EpisodeModel extends Model
|
|||
);
|
||||
|
||||
if (!($found = cache($cacheName))) {
|
||||
$where = ['podcast_id' => $podcastId];
|
||||
$where = [
|
||||
'podcast_id' => $podcastId,
|
||||
];
|
||||
if ($year) {
|
||||
$where['YEAR(published_at)'] = $year;
|
||||
$where['season_number'] = null;
|
||||
|
@ -172,15 +165,27 @@ class EpisodeModel extends Model
|
|||
if ($podcastType == 'serial') {
|
||||
// podcast is serial
|
||||
$found = $this->where($where)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('season_number DESC, number ASC')
|
||||
->findAll();
|
||||
} else {
|
||||
$found = $this->where($where)
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->orderBy('published_at', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
cache()->save($cacheName, $found, DECADE);
|
||||
$secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
|
||||
$podcastId
|
||||
);
|
||||
|
||||
cache()->save(
|
||||
$cacheName,
|
||||
$found,
|
||||
$secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE
|
||||
);
|
||||
}
|
||||
|
||||
return $found;
|
||||
|
@ -197,12 +202,23 @@ class EpisodeModel extends Model
|
|||
'season_number' => null,
|
||||
$this->deletedField => null,
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->groupBy('year')
|
||||
->orderBy('year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
cache()->save("podcast{$podcastId}_years", $found, DECADE);
|
||||
$secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
|
||||
$podcastId
|
||||
);
|
||||
|
||||
cache()->save(
|
||||
"podcast{$podcastId}_years",
|
||||
$found,
|
||||
$secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE
|
||||
);
|
||||
}
|
||||
|
||||
return $found;
|
||||
|
@ -219,12 +235,23 @@ class EpisodeModel extends Model
|
|||
'season_number is not' => null,
|
||||
$this->deletedField => null,
|
||||
])
|
||||
->where('`published_at` <= NOW()', null, false)
|
||||
->groupBy('season_number')
|
||||
->orderBy('season_number', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
cache()->save("podcast{$podcastId}_seasons", $found, DECADE);
|
||||
$secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode(
|
||||
$podcastId
|
||||
);
|
||||
|
||||
cache()->save(
|
||||
"podcast{$podcastId}_seasons",
|
||||
$found,
|
||||
$secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE
|
||||
);
|
||||
}
|
||||
|
||||
return $found;
|
||||
|
@ -264,6 +291,43 @@ class EpisodeModel extends Model
|
|||
return $defaultQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp difference in seconds between the next episode to publish and the current timestamp
|
||||
* Returns false if there's no episode to publish
|
||||
*
|
||||
* @param int $podcastId
|
||||
*
|
||||
* @return int|false seconds
|
||||
*/
|
||||
public function getSecondsToNextUnpublishedEpisode(int $podcastId)
|
||||
{
|
||||
$result = $this->select(
|
||||
'TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff'
|
||||
)
|
||||
->where([
|
||||
'podcast_id' => $podcastId,
|
||||
])
|
||||
->where('`published_at` > NOW()', null, false)
|
||||
->orderBy('published_at', 'asc')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return (int) $result ? $result[0]['timestamp_diff'] : false;
|
||||
}
|
||||
|
||||
protected function writeEnclosureMetadata(array $data)
|
||||
{
|
||||
helper('id3');
|
||||
|
||||
$episode = (new EpisodeModel())->find(
|
||||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
write_enclosure_tags($episode);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function clearCache(array $data)
|
||||
{
|
||||
$episodeModel = new EpisodeModel();
|
||||
|
|
|
@ -59,7 +59,7 @@ class PodcastModel extends Model
|
|||
];
|
||||
protected $validationMessages = [];
|
||||
|
||||
// clear cache before update if by any chance, the podcast name changes, and so will the podcast link
|
||||
// clear cache before update if by any chance, the podcast name changes, so will the podcast link
|
||||
protected $beforeUpdate = ['clearCache'];
|
||||
protected $beforeDelete = ['clearCache'];
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import ClientTimezone from "./modules/ClientTimezone";
|
||||
import DateTimePicker from "./modules/DateTimePicker";
|
||||
import Dropdown from "./modules/Dropdown";
|
||||
import MarkdownEditor from "./modules/MarkdownEditor";
|
||||
import MultiSelect from "./modules/MultiSelect";
|
||||
import SidebarToggler from "./modules/SidebarToggler";
|
||||
import Slugify from "./modules/Slugify";
|
||||
import Time from "./modules/Time";
|
||||
import Tooltip from "./modules/Tooltip";
|
||||
|
||||
Dropdown();
|
||||
|
@ -11,3 +14,6 @@ MarkdownEditor();
|
|||
MultiSelect();
|
||||
Slugify();
|
||||
SidebarToggler();
|
||||
ClientTimezone();
|
||||
DateTimePicker();
|
||||
Time();
|
||||
|
|
|
@ -68,7 +68,10 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
|
|||
chart.scrollbarX = new am4core.Scrollbar();
|
||||
};
|
||||
|
||||
const drawXYDurationChart = (chartDivId: string, dataUrl: string | null): void => {
|
||||
const drawXYDurationChart = (
|
||||
chartDivId: string,
|
||||
dataUrl: string | null
|
||||
): void => {
|
||||
// Create chart instance
|
||||
const chart = am4core.create(chartDivId, am4charts.XYChart);
|
||||
am4core.percent(100);
|
||||
|
@ -203,7 +206,10 @@ const DrawCharts = (): void => {
|
|||
drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||
break;
|
||||
case "xy-duration-chart":
|
||||
drawXYDurationChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||
drawXYDurationChart(
|
||||
chartDiv.id,
|
||||
chartDiv.getAttribute("data-chart-url")
|
||||
);
|
||||
break;
|
||||
case "xy-series-chart":
|
||||
drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
const ClientTimezone = (): void => {
|
||||
const input: HTMLInputElement | null = document.querySelector(
|
||||
"input[name='client_timezone']"
|
||||
);
|
||||
|
||||
if (input) {
|
||||
input.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientTimezone;
|
|
@ -0,0 +1,41 @@
|
|||
import flatpickr from "flatpickr";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
|
||||
/*
|
||||
* Detects navigator locale 24h time preference
|
||||
* It works by checking whether hour output contains AM ('1 AM' or '01 h')
|
||||
*/
|
||||
const isBrowserLocale24h = () =>
|
||||
!new Intl.DateTimeFormat(navigator.language, { hour: "numeric" })
|
||||
.format(0)
|
||||
.match(/AM/);
|
||||
|
||||
const DateTimePicker = (): void => {
|
||||
const dateTimeContainers: NodeListOf<HTMLInputElement> = document.querySelectorAll(
|
||||
"input[data-picker='datetime']"
|
||||
);
|
||||
|
||||
for (let i = 0; i < dateTimeContainers.length; i++) {
|
||||
const dateTimeContainer = dateTimeContainers[i];
|
||||
|
||||
const flatpickrInstance = flatpickr(dateTimeContainer, {
|
||||
enableTime: true,
|
||||
time_24hr: isBrowserLocale24h(),
|
||||
});
|
||||
|
||||
// convert container UTC date value to user timezone
|
||||
const dateTime = new Date(dateTimeContainer.value);
|
||||
const dateUTC = Date.UTC(
|
||||
dateTime.getFullYear(),
|
||||
dateTime.getMonth(),
|
||||
dateTime.getDate(),
|
||||
dateTime.getHours(),
|
||||
dateTime.getMinutes()
|
||||
);
|
||||
|
||||
// set converted date as field value
|
||||
flatpickrInstance.setDate(new Date(dateUTC));
|
||||
}
|
||||
};
|
||||
|
||||
export default DateTimePicker;
|
|
@ -0,0 +1,24 @@
|
|||
const Time = (): void => {
|
||||
const timeElements: NodeListOf<HTMLTimeElement> = document.querySelectorAll(
|
||||
"time"
|
||||
);
|
||||
|
||||
console.log(timeElements);
|
||||
|
||||
for (let i = 0; i < timeElements.length; i++) {
|
||||
const timeElement = timeElements[i];
|
||||
|
||||
// convert UTC date value to user timezone
|
||||
const timeElementDateTime = timeElement.getAttribute("datetime");
|
||||
|
||||
// check if timeElementDateTime is not null and not a duration
|
||||
if (timeElementDateTime && !timeElementDateTime.startsWith("PT")) {
|
||||
const dateTime = new Date(timeElementDateTime);
|
||||
|
||||
// replace <time/> title with localized datetime
|
||||
timeElement.setAttribute("title", dateTime.toLocaleString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Time;
|
|
@ -0,0 +1,3 @@
|
|||
import Time from "./modules/Time";
|
||||
|
||||
Time();
|
|
@ -26,7 +26,7 @@
|
|||
<div class="container flex flex-wrap items-end justify-between px-2 py-10 mx-auto md:px-12 gap-y-6 gap-x-6">
|
||||
<div class="flex flex-col">
|
||||
<?= render_breadcrumb('text-gray-300') ?>
|
||||
<h1 class="text-3xl leading-none"><?= $this->renderSection(
|
||||
<h1 class="text-3xl"><?= $this->renderSection(
|
||||
'pageTitle'
|
||||
) ?></h1>
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
<?= form_hidden('client_timezone', 'UTC') ?>
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.info_section_title'),
|
||||
|
@ -193,35 +194,19 @@
|
|||
lang('Episode.form.publication_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.published_at.label') ?></legend>
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.publication_date'), 'publication_date', [
|
||||
'class' => 'sr-only',
|
||||
]) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_date',
|
||||
'name' => 'publication_date',
|
||||
'class' => 'form-input',
|
||||
'value' => old('publication_date', date('Y-m-d')),
|
||||
'type' => 'date',
|
||||
]) ?>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.publication_time'), 'publication_time', [
|
||||
'class' => 'sr-only',
|
||||
]) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_time',
|
||||
'name' => 'publication_time',
|
||||
'class' => 'form-input',
|
||||
'value' => old('publication_time', date('H:i')),
|
||||
'placeholder' => '--:--',
|
||||
'type' => 'time',
|
||||
]) ?>
|
||||
</div>
|
||||
<?= form_fieldset_close() ?>
|
||||
<?= form_label(
|
||||
lang('Episode.form.publication_date'),
|
||||
'publication_date',
|
||||
[],
|
||||
lang('Episode.form.publication_date_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_date',
|
||||
'name' => 'publication_date',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('publication_date', date('Y-m-d H:i')),
|
||||
'data-picker' => 'datetime',
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?>
|
||||
<legend>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
<?= form_hidden('client_timezone', 'UTC') ?>
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.info_section_title'),
|
||||
|
@ -197,44 +198,24 @@
|
|||
lang('Episode.form.publication_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.published_at.label') ?></legend>
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.publication_date'), 'publication_date', [
|
||||
'class' => 'sr-only',
|
||||
]) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_date',
|
||||
'name' => 'publication_date',
|
||||
'class' => 'form-input',
|
||||
'value' => old(
|
||||
'publication_date',
|
||||
$episode->published_at
|
||||
? $episode->published_at->format('Y-m-d')
|
||||
: ''
|
||||
),
|
||||
'type' => 'date',
|
||||
]) ?>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.publication_time'), 'publication_time', [
|
||||
'class' => 'sr-only',
|
||||
]) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_time',
|
||||
'name' => 'publication_time',
|
||||
'class' => 'form-input',
|
||||
'value' => old(
|
||||
'publication_time',
|
||||
$episode->published_at ? $episode->published_at->format('H:i') : ''
|
||||
),
|
||||
'placeholder' => '--:--',
|
||||
'type' => 'time',
|
||||
]) ?>
|
||||
</div>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Episode.form.publication_date'),
|
||||
'publication_date',
|
||||
[],
|
||||
lang('Episode.form.publication_date_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'publication_date',
|
||||
'name' => 'publication_date',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old(
|
||||
'publication_date',
|
||||
$episode->published_at
|
||||
? $episode->published_at->format('Y-m-d H:i')
|
||||
: ''
|
||||
),
|
||||
'data-picker' => 'datetime',
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'mb-6']) ?>
|
||||
<legend>
|
||||
|
@ -288,6 +269,7 @@
|
|||
<?= form_switch(
|
||||
lang('Episode.form.block') .
|
||||
hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'),
|
||||
|
||||
['id' => 'block', 'name' => 'block'],
|
||||
'yes',
|
||||
old('block', $episode->block)
|
||||
|
|
|
@ -97,19 +97,13 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mb-2 text-xs">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
<?= publication_pill(
|
||||
$episode->published_at,
|
||||
]) ?>
|
||||
</time>
|
||||
$episode->is_published
|
||||
) ?>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang('Common.duration', [
|
||||
$episode->enclosure_duration,
|
||||
]) ?>
|
||||
<?= format_duration($episode->enclosure_duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
|
@ -126,5 +120,4 @@
|
|||
|
||||
<?= $pager->links() ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= $episode->title ?>
|
||||
<?= $episode->title .
|
||||
publication_pill(
|
||||
$episode->published_at,
|
||||
$episode->is_published,
|
||||
'text-sm ml-2 align-middle'
|
||||
) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
|
@ -44,5 +44,4 @@
|
|||
|
||||
<?= form_close() ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -13,5 +13,4 @@
|
|||
|
||||
<?= view('admin/_partials/_user_info.php', ['user' => user()]) ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -238,7 +238,14 @@
|
|||
'value' => old('publisher'),
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.copyright'),
|
||||
'copyright',
|
||||
[],
|
||||
|
||||
'',
|
||||
true
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'copyright',
|
||||
'name' => 'copyright',
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
</a>
|
||||
</header>
|
||||
<?php if ($episodes): ?>
|
||||
<div class="flex justify-between gap-4 overflow-x-auto">
|
||||
<div class="flex p-2 space-x-4 overflow-x-auto">
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<article class="flex flex-col w-56 mb-4 bg-white border rounded shadow" style="min-width: 12rem;">
|
||||
<article class="flex flex-col w-56 bg-white border rounded shadow" style="min-width: 12rem;">
|
||||
<img
|
||||
src="<?= $episode->image->thumbnail_url ?>"
|
||||
alt="<?= $episode->title ?>" class="object-cover" />
|
||||
|
@ -61,7 +61,9 @@
|
|||
<span class="mx-1">•</span>
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
datetime="<?= $episode->published_at->format(
|
||||
DateTime::ATOM
|
||||
) ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
$episode->published_at,
|
||||
|
|
|
@ -62,5 +62,4 @@
|
|||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -50,5 +50,4 @@
|
|||
|
||||
<?= form_close() ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -85,5 +85,4 @@
|
|||
$users
|
||||
) ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
<script src="/assets/podcast.js" type="module" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen mx-auto">
|
||||
|
@ -85,17 +86,13 @@
|
|||
<div class="text-sm">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
datetime="<?= $episode->published_at->format(DateTime::ATOM) ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [$episode->published_at]) ?>
|
||||
</time>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang(
|
||||
'Common.duration',
|
||||
[$episode->enclosure_duration],
|
||||
'en'
|
||||
) ?>
|
||||
<?= format_duration($episode->enclosure_duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
|
@ -110,9 +107,9 @@
|
|||
</section>
|
||||
</main>
|
||||
<footer class="px-2 py-4 border-t ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row ">
|
||||
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
|
||||
<div class="flex flex-col items-end text-xs">
|
||||
<div class="flex flex-col items-end">
|
||||
<p><?= $podcast->copyright ?></p>
|
||||
<p><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
|
|
|
@ -20,4 +20,5 @@ Line Number: <?= $exception->getLine() ?>
|
|||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php endif;
|
||||
?>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
<link type="application/rss+xml" rel="alternate" title="<?= $podcast->title ?>" href="<?= $podcast->feed_url ?>"/>
|
||||
<script src="/assets/podcast.js" type="module" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen">
|
||||
|
@ -127,7 +128,9 @@
|
|||
<div class="mb-2 text-xs">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
datetime="<?= $episode->published_at->format(
|
||||
DateTime::ATOM
|
||||
) ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
$episode->published_at,
|
||||
|
@ -135,9 +138,9 @@
|
|||
</time>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang('Common.duration', [
|
||||
$episode->enclosure_duration,
|
||||
]) ?>
|
||||
<?= format_duration(
|
||||
$episode->enclosure_duration
|
||||
) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
|
@ -159,9 +162,9 @@
|
|||
</section>
|
||||
</main>
|
||||
<footer class="px-2 py-4 border-t ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row ">
|
||||
<div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row ">
|
||||
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
|
||||
<div class="flex flex-col items-center text-xs md:items-end">
|
||||
<div class="flex flex-col items-center md:items-end">
|
||||
<p><?= $podcast->copyright ?></p>
|
||||
<p><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "37551523e4097a9341bc00dd317f573d",
|
||||
"content-hash": "58e59ff661eaa3553d3f9f9f88b9d274",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/codeigniter4",
|
||||
|
@ -12,12 +12,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/codeigniter4/CodeIgniter4.git",
|
||||
"reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a"
|
||||
"reference": "58993fbbab54a2523be25e8230337b855f465a7a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/13ff147fa4cd9db15888b041ef35bc22ed94252a",
|
||||
"reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a",
|
||||
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/58993fbbab54a2523be25e8230337b855f465a7a",
|
||||
"reference": "58993fbbab54a2523be25e8230337b855f465a7a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -53,7 +53,6 @@
|
|||
},
|
||||
"scripts": {
|
||||
"post-update-cmd": [
|
||||
"@composer dump-autoload",
|
||||
"CodeIgniter\\ComposerScripts::postUpdate",
|
||||
"bash admin/setup.sh"
|
||||
],
|
||||
|
@ -75,7 +74,7 @@
|
|||
"slack": "https://codeigniterchat.slack.com",
|
||||
"issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
|
||||
},
|
||||
"time": "2020-10-20T18:13:11+00:00"
|
||||
"time": "2020-10-21T16:26:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
|
@ -805,12 +804,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lonnieezell/myth-auth.git",
|
||||
"reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c"
|
||||
"reference": "fe9739e1a410d9a30292faee9e8b6369667241e8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/e9d6a2f557bd275158e0b84624534b2abeeb539c",
|
||||
"reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c",
|
||||
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/fe9739e1a410d9a30292faee9e8b6369667241e8",
|
||||
"reference": "fe9739e1a410d9a30292faee9e8b6369667241e8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -860,7 +859,7 @@
|
|||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2020-10-16T18:51:37+00:00"
|
||||
"time": "2020-10-22T03:25:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "opawg/user-agents-php",
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"requires": true,
|
||||
"dependencies": {
|
||||
"@amcharts/amcharts4": {
|
||||
"version": "4.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.7.tgz",
|
||||
"integrity": "sha512-XWITAuewadEnkX9XgZTqT6CUn91gCJpvLJYrnSdnwu4GOGV4Siu6esoEb4JEYQYEDCzDIK3zlmOT5+a0fulcTw==",
|
||||
"version": "4.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.8.tgz",
|
||||
"integrity": "sha512-2xIPHkvuxhsN49btE+K0ThO0CxvEgHC+n2aFa05GLwIH2JKgSjFBmjSvELrEqlEYf2mEPjmKjuYe6d4TgHfGUA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"core-js": "^3.0.0",
|
||||
|
@ -53,16 +53,16 @@
|
|||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.1.tgz",
|
||||
"integrity": "sha512-6bGmltqzIJrinwRRdczQsMhruSi9Sqty9Te+/5hudn4Izx/JYRhW1QELpR+CIL0gC/c9A7WroH6FmkDGxmWx3w==",
|
||||
"version": "7.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz",
|
||||
"integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.1",
|
||||
"@babel/helper-module-transforms": "^7.12.1",
|
||||
"@babel/helpers": "^7.12.1",
|
||||
"@babel/parser": "^7.12.1",
|
||||
"@babel/parser": "^7.12.3",
|
||||
"@babel/template": "^7.10.4",
|
||||
"@babel/traverse": "^7.12.1",
|
||||
"@babel/types": "^7.12.1",
|
||||
|
@ -102,6 +102,12 @@
|
|||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz",
|
||||
"integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz",
|
||||
|
@ -2379,13 +2385,13 @@
|
|||
"integrity": "sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg=="
|
||||
},
|
||||
"@prettier/plugin-php": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.15.0.tgz",
|
||||
"integrity": "sha512-OnzCmDTDdWLkm2nsvtiWKip1ePoy+KucY1h9zHDVXIFWBrd+OZATeZZgC7JU7gjly96g86hW1ZHpbF9ip9KHfg==",
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.15.1.tgz",
|
||||
"integrity": "sha512-uQiaGGXCs0uqpck1LyDU+V4Z50Qqml7ltajPQL+DB43r5aHVawDCSkgLGYZJSb1g+hK5eBmdVBqMa7ED8EBjbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"linguist-languages": "^7.5.1",
|
||||
"mem": "^6.0.1",
|
||||
"mem": "^8.0.0",
|
||||
"php-parser": "3.0.2"
|
||||
}
|
||||
},
|
||||
|
@ -3133,13 +3139,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.4.1.tgz",
|
||||
"integrity": "sha512-O+8Utz8pb4OmcA+Nfi5THQnQpHSD2sDUNw9AxNHpuYOo326HZTtG8gsfT+EAYuVrFNaLyNb2QnUNkmTRDskuRA==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.5.0.tgz",
|
||||
"integrity": "sha512-mjb/gwNcmDKNt+6mb7Aj/TjKzIJjOPcoCJpjBQC9ZnTRnBt1p4q5dJSSmIqAtsZ/Pff5N+hJlbiPc5bl6QN4OQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.4.1",
|
||||
"@typescript-eslint/scope-manager": "4.4.1",
|
||||
"@typescript-eslint/experimental-utils": "4.5.0",
|
||||
"@typescript-eslint/scope-manager": "4.5.0",
|
||||
"debug": "^4.1.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.0.0",
|
||||
|
@ -3156,55 +3162,55 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.1.tgz",
|
||||
"integrity": "sha512-Nt4EVlb1mqExW9cWhpV6pd1a3DkUbX9DeyYsdoeziKOpIJ04S2KMVDO+SEidsXRH/XHDpbzXykKcMTLdTXH6cQ==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.5.0.tgz",
|
||||
"integrity": "sha512-bW9IpSAKYvkqDGRZzayBXIgPsj2xmmVHLJ+flGSoN0fF98pGoKFhbunIol0VF2Crka7z984EEhFi623Rl7e6gg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/scope-manager": "4.4.1",
|
||||
"@typescript-eslint/types": "4.4.1",
|
||||
"@typescript-eslint/typescript-estree": "4.4.1",
|
||||
"@typescript-eslint/scope-manager": "4.5.0",
|
||||
"@typescript-eslint/types": "4.5.0",
|
||||
"@typescript-eslint/typescript-estree": "4.5.0",
|
||||
"eslint-scope": "^5.0.0",
|
||||
"eslint-utils": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.4.1.tgz",
|
||||
"integrity": "sha512-S0fuX5lDku28Au9REYUsV+hdJpW/rNW0gWlc4SXzF/kdrRaAVX9YCxKpziH7djeWT/HFAjLZcnY7NJD8xTeUEg==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.5.0.tgz",
|
||||
"integrity": "sha512-xb+gmyhQcnDWe+5+xxaQk5iCw6KqXd8VQxGiTeELTMoYeRjpocZYYRP1gFVM2C8Yl0SpUvLa1lhprwqZ00w3Iw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.4.1",
|
||||
"@typescript-eslint/types": "4.4.1",
|
||||
"@typescript-eslint/typescript-estree": "4.4.1",
|
||||
"@typescript-eslint/scope-manager": "4.5.0",
|
||||
"@typescript-eslint/types": "4.5.0",
|
||||
"@typescript-eslint/typescript-estree": "4.5.0",
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz",
|
||||
"integrity": "sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.5.0.tgz",
|
||||
"integrity": "sha512-C0cEO0cTMPJ/w4RA/KVe4LFFkkSh9VHoFzKmyaaDWAnPYIEzVCtJ+Un8GZoJhcvq+mPFXEsXa01lcZDHDG6Www==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.4.1",
|
||||
"@typescript-eslint/visitor-keys": "4.4.1"
|
||||
"@typescript-eslint/types": "4.5.0",
|
||||
"@typescript-eslint/visitor-keys": "4.5.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.4.1.tgz",
|
||||
"integrity": "sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.5.0.tgz",
|
||||
"integrity": "sha512-n2uQoXnyWNk0Les9MtF0gCK3JiWd987JQi97dMSxBOzVoLZXCNtxFckVqt1h8xuI1ix01t+iMY4h4rFMj/303g==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz",
|
||||
"integrity": "sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.5.0.tgz",
|
||||
"integrity": "sha512-gN1mffq3zwRAjlYWzb5DanarOPdajQwx5MEWkWCk0XvqC8JpafDTeioDoow2L4CA/RkYZu7xEsGZRhqrTsAG8w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.4.1",
|
||||
"@typescript-eslint/visitor-keys": "4.4.1",
|
||||
"@typescript-eslint/types": "4.5.0",
|
||||
"@typescript-eslint/visitor-keys": "4.5.0",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
|
@ -3222,12 +3228,12 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz",
|
||||
"integrity": "sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.5.0.tgz",
|
||||
"integrity": "sha512-UHq4FSa55NDZqscRU//O5ROFhHa9Hqn9KWTEvJGTArtTQp5GKv9Zqf6d/Q3YXXcFv4woyBml7fJQlQ+OuqRcHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.4.1",
|
||||
"@typescript-eslint/types": "4.5.0",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -5909,9 +5915,9 @@
|
|||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.13.0.tgz",
|
||||
"integrity": "sha512-LcT0i0LSmnzqK2t764pyIt7kKH2AuuqKRTtJTdddWxOiUja9HdG5GXBVF2gmCTvVYWVsTu8J2MhJLVGRh+pj8w==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.14.0.tgz",
|
||||
"integrity": "sha512-DbVwh0qZhAC7CNDWcq8cBdK6FcVHiMTKmCypOPWeZkp9hJ8xYwTaWSa6bb6cjfi8KOeJy0e9a8Izxyx+O4+gCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-stdin": "^6.0.0"
|
||||
|
@ -6488,6 +6494,11 @@
|
|||
"write": "1.0.3"
|
||||
}
|
||||
},
|
||||
"flatpickr": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.6.tgz",
|
||||
"integrity": "sha512-EZ48CJMttMg3maMhJoX+GvTuuEhX/RbA1YeuI19attP3pwBdbYy6+yqAEVm0o0hSBFYBiLbVxscLW6gJXq6H3A=="
|
||||
},
|
||||
"flatted": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
|
||||
|
@ -7888,9 +7899,9 @@
|
|||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.1.tgz",
|
||||
"integrity": "sha512-E2Y6Mu1haUD3ZefzwBG8tqy3QDQ9udWRS946YcuDCU8Mi22RjwxrEhLrqTLszxl80DG/sCtKdGCArzEkTsBzJQ==",
|
||||
"version": "10.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.2.tgz",
|
||||
"integrity": "sha512-OLCA9K1hS+Sl179SO6kX0JtnsaKj/MZalEhUj5yAgXsb63qPI/Gfn6Ua1KuZdbfkZNEu3/n5C/obYCu70IMt9g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
|
@ -8612,13 +8623,13 @@
|
|||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"mem": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-6.1.1.tgz",
|
||||
"integrity": "sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-8.0.0.tgz",
|
||||
"integrity": "sha512-qrcJOe6uD+EW8Wrci1Vdiua/15Xw3n/QnaNXE7varnB6InxSk7nu3/i5jfy3S6kWxr8WYJ6R1o0afMUtvorTsA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"map-age-cleaner": "^0.1.3",
|
||||
"mimic-fn": "^3.0.0"
|
||||
"mimic-fn": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"meow": {
|
||||
|
@ -15403,9 +15414,9 @@
|
|||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.31.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.31.0.tgz",
|
||||
"integrity": "sha512-0d8S3XwEZ7aCP910/9SjnelgLvC+ZXziouVolzxPOM1zvKkHioGkWGJIWmlOULlmvB8BZ6S0wrgsT4yMz0eyMg==",
|
||||
"version": "2.32.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz",
|
||||
"integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.1.2"
|
||||
|
@ -17027,9 +17038,9 @@
|
|||
}
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.2.tgz",
|
||||
"integrity": "sha512-D3uKSZZkh4GaKiZWmPEfNrqEmEuYdwaqXOQ7trYSQQFI5laSD9+b2FUUj5g39nk5R1omKp5tBW9wZsfJq+KIVA==",
|
||||
"version": "1.9.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.9.5.tgz",
|
||||
"integrity": "sha512-Je5t1fAfyW333YTpSxF+8uJwbnrkpyBskDtZYgSMMKQbNp6QUhEKJ4g/JIevZjD2Zidz9VxLraEUq/yWOx6nQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@fullhuman/postcss-purgecss": "^2.1.2",
|
||||
|
|
19
package.json
19
package.json
|
@ -25,23 +25,24 @@
|
|||
"release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amcharts/amcharts4": "^4.10.7",
|
||||
"@amcharts/amcharts4": "^4.10.8",
|
||||
"@amcharts/amcharts4-geodata": "^4.1.17",
|
||||
"@popperjs/core": "^2.5.3",
|
||||
"choices.js": "^9.0.1",
|
||||
"flatpickr": "^4.6.6",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.5.0",
|
||||
"prosemirror-state": "^1.3.3",
|
||||
"prosemirror-view": "^1.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.1",
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@commitlint/cli": "^11.0.0",
|
||||
"@commitlint/config-conventional": "^11.0.0",
|
||||
"@prettier/plugin-php": "^0.15.0",
|
||||
"@prettier/plugin-php": "^0.15.1",
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
|
@ -55,22 +56,22 @@
|
|||
"@tailwindcss/typography": "^0.2.0",
|
||||
"@types/prosemirror-markdown": "^1.0.3",
|
||||
"@types/prosemirror-view": "^1.16.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.1",
|
||||
"@typescript-eslint/parser": "^4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
||||
"@typescript-eslint/parser": "^4.5.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cssnano": "^4.1.10",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.13.0",
|
||||
"eslint-config-prettier": "^6.14.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.4.1",
|
||||
"lint-staged": "^10.4.2",
|
||||
"postcss-cli": "^8.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "2.1.2",
|
||||
"prettier-plugin-organize-imports": "^1.1.1",
|
||||
"rollup": "^2.31.0",
|
||||
"rollup": "^2.32.1",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"rollup-plugin-postcss": "^3.1.8",
|
||||
|
@ -79,7 +80,7 @@
|
|||
"stylelint": "^13.7.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"svgo": "^1.3.2",
|
||||
"tailwindcss": "^1.9.2",
|
||||
"tailwindcss": "^1.9.5",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"husky": {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Running Application Tests
|
||||
|
||||
This is the quick-start to CodeIgniter testing. Its intent is to describe what
|
||||
it takes to set up your application and get it ready to run unit tests.
|
||||
It is not intended to be a full description of the test features that you can
|
||||
use to test your application. Those details can be found in the documentation.
|
||||
it takes to set up your application and get it ready to run unit tests. It is
|
||||
not intended to be a full description of the test features that you can use to
|
||||
test your application. Those details can be found in the documentation.
|
||||
|
||||
## Resources
|
||||
|
||||
|
@ -15,33 +15,36 @@ use to test your application. Those details can be found in the documentation.
|
|||
It is recommended to use the latest version of PHPUnit. At the time of this
|
||||
writing we are running version 8.5.2. Support for this has been built into the
|
||||
**composer.json** file that ships with CodeIgniter and can easily be installed
|
||||
via [Composer](https://getcomposer.org/) if you don't already have it installed globally.
|
||||
via [Composer](https://getcomposer.org/) if you don't already have it installed
|
||||
globally.
|
||||
|
||||
> composer install
|
||||
|
||||
If running under OS X or Linux, you can create a symbolic link to make running tests a touch nicer.
|
||||
If running under OS X or Linux, you can create a symbolic link to make running
|
||||
tests a touch nicer.
|
||||
|
||||
> ln -s ./vendor/bin/phpunit ./phpunit
|
||||
|
||||
You also need to install [XDebug](https://xdebug.org/index.php) in order
|
||||
for code coverage to be calculated successfully.
|
||||
You also need to install [XDebug](https://xdebug.org/index.php) in order for
|
||||
code coverage to be calculated successfully.
|
||||
|
||||
## Setting Up
|
||||
|
||||
A number of the tests use a running database.
|
||||
In order to set up the database edit the details for the `tests` group in
|
||||
**app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine
|
||||
that is currently running on your machine. More details on a test database setup are in the
|
||||
A number of the tests use a running database. In order to set up the database
|
||||
edit the details for the `tests` group in **app/Config/Database.php** or
|
||||
**phpunit.xml**. Make sure that you provide a database engine that is currently
|
||||
running on your machine. More details on a test database setup are in the
|
||||
_Docs>>Testing>>Testing Your Database_ section of the documentation.
|
||||
|
||||
If you want to run the tests without using live database you can
|
||||
exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** -
|
||||
call it **phpunit.xml** - and comment out the <testsuite> named "database". This will make
|
||||
the tests run quite a bit faster.
|
||||
If you want to run the tests without using live database you can exclude
|
||||
@DatabaseLive group. Or make a copy of **phpunit.dist.xml** - call it
|
||||
**phpunit.xml** - and comment out the <testsuite> named "database". This will
|
||||
make the tests run quite a bit faster.
|
||||
|
||||
## Running the tests
|
||||
|
||||
The entire test suite can be run by simply typing one command-line command from the main directory.
|
||||
The entire test suite can be run by simply typing one command-line command from
|
||||
the main directory.
|
||||
|
||||
> ./phpunit
|
||||
|
||||
|
@ -52,59 +55,62 @@ directory name after phpunit.
|
|||
|
||||
## Generating Code Coverage
|
||||
|
||||
To generate coverage information, including HTML reports you can view in your browser,
|
||||
you can use the following command:
|
||||
To generate coverage information, including HTML reports you can view in your
|
||||
browser, you can use the following command:
|
||||
|
||||
> ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
|
||||
|
||||
This runs all of the tests again collecting information about how many lines,
|
||||
functions, and files are tested. It also reports the percentage of the code that is covered by tests.
|
||||
It is collected in two formats: a simple text file that provides an overview as well
|
||||
as a comprehensive collection of HTML files that show the status of every line of code in the project.
|
||||
functions, and files are tested. It also reports the percentage of the code that
|
||||
is covered by tests. It is collected in two formats: a simple text file that
|
||||
provides an overview as well as a comprehensive collection of HTML files that
|
||||
show the status of every line of code in the project.
|
||||
|
||||
The text file can be found at **tests/coverage.txt**.
|
||||
The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser.
|
||||
The text file can be found at **tests/coverage.txt**. The HTML files can be
|
||||
viewed by opening **tests/coverage/index.html** in your favorite browser.
|
||||
|
||||
## PHPUnit XML Configuration
|
||||
|
||||
The repository has a `phpunit.xml.dist` file in the project root that's used for
|
||||
PHPUnit configuration. This is used to provide a default configuration if you
|
||||
do not have your own configuration file in the project root.
|
||||
PHPUnit configuration. This is used to provide a default configuration if you do
|
||||
not have your own configuration file in the project root.
|
||||
|
||||
The normal practice would be to copy `phpunit.xml.dist` to `phpunit.xml`
|
||||
(which is git ignored), and to tailor it as you see fit.
|
||||
For instance, you might wish to exclude database tests, or automatically generate
|
||||
HTML code coverage reports.
|
||||
The normal practice would be to copy `phpunit.xml.dist` to `phpunit.xml` (which
|
||||
is git ignored), and to tailor it as you see fit. For instance, you might wish
|
||||
to exclude database tests, or automatically generate HTML code coverage reports.
|
||||
|
||||
## Test Cases
|
||||
|
||||
Every test needs a _test case_, or class that your tests extend. CodeIgniter 4
|
||||
provides a few that you may use directly:
|
||||
|
||||
- `CodeIgniter\Test\CIUnitTestCase` - for basic tests with no other service needs
|
||||
- `CodeIgniter\Test\CIUnitTestCase` - for basic tests with no other service
|
||||
needs
|
||||
- `CodeIgniter\Test\CIDatabaseTestCase` - for tests that need database access
|
||||
|
||||
Most of the time you will want to write your own test cases to hold functions and services
|
||||
common to your test suites.
|
||||
Most of the time you will want to write your own test cases to hold functions
|
||||
and services common to your test suites.
|
||||
|
||||
## Creating Tests
|
||||
|
||||
All tests go in the **tests/** directory. Each test file is a class that extends a
|
||||
**Test Case** (see above) and contains methods for the individual tests. These method
|
||||
names must start with the word "test" and should have descriptive names for precisely what
|
||||
they are testing:
|
||||
`testUserCanModifyFile()` `testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()`
|
||||
All tests go in the **tests/** directory. Each test file is a class that extends
|
||||
a **Test Case** (see above) and contains methods for the individual tests. These
|
||||
method names must start with the word "test" and should have descriptive names
|
||||
for precisely what they are testing: `testUserCanModifyFile()`
|
||||
`testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()`
|
||||
|
||||
Writing tests is an art, and there are many resources available to help learn how.
|
||||
Review the links above and always pay attention to your code coverage.
|
||||
Writing tests is an art, and there are many resources available to help learn
|
||||
how. Review the links above and always pay attention to your code coverage.
|
||||
|
||||
### Database Tests
|
||||
|
||||
Tests can include migrating, seeding, and testing against a mock or live<sup>1</sup> database.
|
||||
Be sure to modify the test case (or create your own) to point to your seed and migrations
|
||||
and include any additional steps to be run before tests in the `setUp()` method.
|
||||
Tests can include migrating, seeding, and testing against a mock or
|
||||
live<sup>1</sup> database. Be sure to modify the test case (or create your own)
|
||||
to point to your seed and migrations and include any additional steps to be run
|
||||
before tests in the `setUp()` method.
|
||||
|
||||
<sup>1</sup> Note: If you are using database tests that require a live database connection
|
||||
you will need to rename **phpunit.xml.dist** to **phpunit.xml**, uncomment the database
|
||||
configuration lines and add your connection details. Prevent **phpunit.xml** from being
|
||||
tracked in your repo by adding it to **.gitignore**.
|
||||
<sup>1</sup> Note: If you are using database tests that require a live database
|
||||
connection you will need to rename **phpunit.xml.dist** to **phpunit.xml**,
|
||||
uncomment the database configuration lines and add your connection details.
|
||||
Prevent **phpunit.xml** from being tracked in your repo by adding it to
|
||||
**.gitignore**.
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
/* Advanced Options */
|
||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
},
|
||||
"include": ["app/Views/_assets/**/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue