diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 0075667615399fb5022c2cb43e426bb2f56ddd51..d4c1c29cc1b68810408f6048748918729c463d12 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -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: diff --git a/app/Controllers/Admin/BaseController.php b/app/Controllers/Admin/BaseController.php index 5d7de7d273ea8bf224860f4f1bfffc81787eb6a6..6bc9ff8567855c4d327ea072f761d9ffe4a0d86c 100644 --- a/app/Controllers/Admin/BaseController.php +++ b/app/Controllers/Admin/BaseController.php @@ -26,7 +26,7 @@ class BaseController extends Controller * * @var array */ - protected $helpers = ['auth', 'breadcrumb', 'svg', 'components']; + protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc']; /** * Constructor. diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index e10c40609c244d544d4fbc4630850f282e54cf1c..82a73a14c20726e3f517350e8ce345d22777a9f4 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -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'); diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 05503ca221f2946da66d770c9f9f0e7673002535..afac930d4a8f1bcc79fe81ae0e3054cff66eb726 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -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(); diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 3bfbd4b0ffe5094dd49e53a9bee1afbff985845e..ab249c803d71019e70a9cdcd43e52164f4aa3dac 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -26,7 +26,7 @@ class BaseController extends Controller * * @var array */ - protected $helpers = ['analytics', 'svg', 'components']; + protected $helpers = ['analytics', 'svg', 'components', 'misc']; /** * Constructor. diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index 38c83da97d5e852a3ff66a5cad80a2c30e927b6e..7b5dc9f7ab10cca53c4ad91728984fdc3b10cbaf 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -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, ]); } diff --git a/app/Controllers/Feed.php b/app/Controllers/Feed.php index 3da87f0dd3bc177467851f154c2b8013d6ed46a4..0170e05ea3fa9c35f2bc2dc1e76546447604ab95 100644 --- a/app/Controllers/Feed.php +++ b/app/Controllers/Feed.php @@ -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); } diff --git a/app/Controllers/Install.php b/app/Controllers/Install.php index cc9e26d48de297334fd9222e22c9736a4338091e..053e3faed4430e7de618373afe9fc5a94ea20778 100644 --- a/app/Controllers/Install.php +++ b/app/Controllers/Install.php @@ -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', diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 5d555f293e99d6cfe4bde920afd1a354db3915f6..1e1dbda3b31bdeb5da027eff837cef30a993f0bb 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -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, ]); } diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 70ab1ed3f0d6f4d81d3b9cceef382e859b3a375f..23f7737c24273c01c771e7176d628ef9e0937b63 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -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; + } } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 5b26da6df17adf746e434dd8f14f134e2f6ff78a..0e31cb651a4388ac27031ed6fbca9520677c0893 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -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>'; + } +} +// ------------------------------------------------------------------------ diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php index b1d1b168a34a99922dd040ae3e254ee1d60aa4a4..b87051c4792268c460fb7b0aa4ab2e4e85316a7e 100644 --- a/app/Helpers/misc_helper.php +++ b/app/Helpers/misc_helper.php @@ -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 + ); + } +} diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index 7e90eb891b98a342f684fef3e0fe7b2ad3bab90b..f1d101e1e6a6f897a8c91307eed21b14aad03951 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -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}', diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index d2a294c26b8afb130b0cf0ea4783f8a130369559..61f0132db020d2b2686abf32943bd36d6f1a9cb3 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -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?', diff --git a/app/Language/fr/Common.php b/app/Language/fr/Common.php index 0bef7680c41db232101d21c54dedfc2bc4b18f68..59a9514ecab161bfbf0fa790ed346ed0cdd92925 100644 --- a/app/Language/fr/Common.php +++ b/app/Language/fr/Common.php @@ -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}', diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index 1471e307cb40213fb474a7cc532ff0f229eeae14..2b5716cd766daa4acc8abd3c1784b5fde89fc13a 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -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 ?', diff --git a/app/Language/fr/MyAccount.php b/app/Language/fr/MyAccount.php index 5a2181f28dceaf7ef1ddea7080b63b703b42877a..3d75c589dc61e872acc34896e864582c6b697572 100644 --- a/app/Language/fr/MyAccount.php +++ b/app/Language/fr/MyAccount.php @@ -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 !', ], ]; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 2242266c263cd61e21ea071c9d863e0e2b1139ae..e01d5e35ca126707ddb79f237844357a95b20bd4 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -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(); diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 5c75640eba42403979ed195fe365851e76864743..0cc21617f129d8489fb8e739f49555de5c13833f 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -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']; diff --git a/app/Views/_assets/admin.ts b/app/Views/_assets/admin.ts index 4adc1b5b757582fa615c23aaf20b7106ec8f960e..d05abf005a8d3966672ab5ad97e2552df187c9ea 100644 --- a/app/Views/_assets/admin.ts +++ b/app/Views/_assets/admin.ts @@ -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(); diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index 2f6053afd7b67ab58aeb0fd4d27c3b98462919e3..41149edd4b74936c337b4857a6d2e9c79fc854d9 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -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")); diff --git a/app/Views/_assets/modules/ClientTimezone.ts b/app/Views/_assets/modules/ClientTimezone.ts new file mode 100644 index 0000000000000000000000000000000000000000..94ad23686fe4d3857ce3d43f0a69d5ec8e6a6dc6 --- /dev/null +++ b/app/Views/_assets/modules/ClientTimezone.ts @@ -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; diff --git a/app/Views/_assets/modules/DateTimePicker.ts b/app/Views/_assets/modules/DateTimePicker.ts new file mode 100644 index 0000000000000000000000000000000000000000..235cf69f789583598738e46f7d7d15db92ee1817 --- /dev/null +++ b/app/Views/_assets/modules/DateTimePicker.ts @@ -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; diff --git a/app/Views/_assets/modules/Time.ts b/app/Views/_assets/modules/Time.ts new file mode 100644 index 0000000000000000000000000000000000000000..58ea0f2695f47c72b83f8292dfc6ba2a59545a0a --- /dev/null +++ b/app/Views/_assets/modules/Time.ts @@ -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; diff --git a/app/Views/_assets/podcast.ts b/app/Views/_assets/podcast.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5e125b3f500ac2640e87f14a812fb198b8e86cc --- /dev/null +++ b/app/Views/_assets/podcast.ts @@ -0,0 +1,3 @@ +import Time from "./modules/Time"; + +Time(); diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index d04edd4e19c58e22d3ff2e3e00028ca0207eac20..cc46a59136bec4cd44c494e9f0432ea95e0c3fab 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -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> diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php index 8f3b5a2f05472873a903040eb78cbaeaa29f2b8e..e72961cac3a39fce832f41607121890f7623487b 100644 --- a/app/Views/admin/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -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> diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php index 8ef2850a8e829782686bd54a29feeb7fb1d7f0ce..cbce3925ab6bb7e66f3ee72e00452e3ff18079ae 100644 --- a/app/Views/admin/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -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) diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index 5adf9c8cd9b05f3321954a16b5ca30dad5805bb5..9f0330086b33b4c0efa8fcd4ba26f46f81c0d686 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -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() ?> diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index 07a88fd9a1b3281f8305436509643bbf60db48d4..d36151f19e277e6dd523a46344c210ad5cc4a0e3 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -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') ?> diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php index 3c34726ba43269c896cd81a039fdce6d6382ec47..0ebf49946d5c9c43b7d8bfac6630a929177f3d03 100644 --- a/app/Views/admin/my_account/change_password.php +++ b/app/Views/admin/my_account/change_password.php @@ -44,5 +44,4 @@ <?= form_close() ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/my_account/view.php b/app/Views/admin/my_account/view.php index 6dc6b8d93254f3c327c1548fbe809de878e843c4..77d30922192e2935fed8a9bb12e3a5e86b08d5ff 100644 --- a/app/Views/admin/my_account/view.php +++ b/app/Views/admin/my_account/view.php @@ -13,5 +13,4 @@ <?= view('admin/_partials/_user_info.php', ['user' => user()]) ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index 7b08bd8bf126ad723f036ff72ccc0e20626a76ba..09b2aa08c8b8690254a957a9a72442d42ee411d4 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -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', diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php index 840bfa556986787f29db2d754a0d64598025d9b3..b4dadbfa8cd849fb7be792a262748fa48f0b47de 100644 --- a/app/Views/admin/podcast/latest_episodes.php +++ b/app/Views/admin/podcast/latest_episodes.php @@ -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, diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php index a6a00c7237d8d35ac99d730822ceaedecb601700..28aa7ef6259c09576fb3cd548548cb448da357b7 100644 --- a/app/Views/admin/podcast/list.php +++ b/app/Views/admin/podcast/list.php @@ -62,5 +62,4 @@ <?php endif; ?> </div> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php index b55c0d136e58c31b0289fe5ee17d21bf82483ecf..aaf8369b52c9056e907f70e7d8b37c9e238104bf 100644 --- a/app/Views/admin/user/create.php +++ b/app/Views/admin/user/create.php @@ -50,5 +50,4 @@ <?= form_close() ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php index 1e54610cb0ae15df0183dac4fc19ba1829f9e2ef..9f409dc126e03f5786bee55f02ae42c2931c866e 100644 --- a/app/Views/admin/user/list.php +++ b/app/Views/admin/user/list.php @@ -85,5 +85,4 @@ $users ) ?> -<?= $this->endSection() -?> +<?= $this->endSection() ?> diff --git a/app/Views/episode.php b/app/Views/episode.php index 9baa9270db016bed8632ec3bb53c20e16034619c..c3c9e7f8a618ba2b1fc474014a8d67e86237f3a1 100644 --- a/app/Views/episode.php +++ b/app/Views/episode.php @@ -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' => diff --git a/app/Views/errors/cli/error_exception.php b/app/Views/errors/cli/error_exception.php index 1ad33d0679deab7de1b3d140b4782d57a5028d0b..c161311cc505a63a428402d3db7bc617cbf02c8d 100644 --- a/app/Views/errors/cli/error_exception.php +++ b/app/Views/errors/cli/error_exception.php @@ -20,4 +20,5 @@ Line Number: <?= $exception->getLine() ?> <?php endif; ?> <?php endforeach; ?> -<?php endif; ?> +<?php endif; +?> diff --git a/app/Views/podcast.php b/app/Views/podcast.php index 346e296fa288755b92f412bc8bb378573dbb7806..c70db9fa25da936366b8fea7f885149502a0df9f 100644 --- a/app/Views/podcast.php +++ b/app/Views/podcast.php @@ -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' => diff --git a/composer.lock b/composer.lock index 38bacea659f60947bc5a5000c46ea2f5b4b6ee24..b893284509e3b460de07bdd4f3607bdef515d3e5 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/package-lock.json b/package-lock.json index 047c1bfae6c36a4ec566da014d10828ab60a6b00..0267e0055ca53f0da112476cdbf65c02e60ac04d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 812c0818e08ae7920975c1f165db9007882052b6..d1b9cf549e0e67cd27d82bbf7c198bf312f105e9 100644 --- a/package.json +++ b/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": { diff --git a/tests/README.md b/tests/README.md index 24f7c828c6b4cae0d07c74287c55854197362aab..ba8aa0e2542883445d210af8f21d1f7c0f953b07 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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**. diff --git a/tsconfig.json b/tsconfig.json index 30890ef327ce74143b58231bd1525ef9ed3b345e..6cfc4c30f928f1a0ae96cd26c5cc27f76d5ac4b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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/**/*"] }