diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index fed484c99b569c3f8378e09f913a8ddc31b05ae3..df0c8a6e8d518b0bbe6961fd4269b6a28d38f05a 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -17,9 +17,9 @@ Javascript dependencies: - [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md)) - [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE)) -- [CodeMirror](https://github.com/codemirror/CodeMirror) ([MIT License](https://github.com/codemirror/CodeMirror/blob/master/LICENSE)) - [ProseMirror](https://prosemirror.net/) ([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE)) - [D3: Data-Driven Documents](https://d3js.org) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE)) +- [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE)) Other: diff --git a/app/Config/Pager.php b/app/Config/Pager.php index 50a4a5f97e8e78d4d4038f1598d7802f2794ba29..699ab90782c04d5fa3bb59e5dc0b0e610f3bf495 100644 --- a/app/Config/Pager.php +++ b/app/Config/Pager.php @@ -20,7 +20,7 @@ class Pager extends BaseConfig | */ public $templates = [ - 'default_full' => 'CodeIgniter\Pager\Views\default_full', + 'default_full' => 'App\Views\pager\default_full', 'default_simple' => 'CodeIgniter\Pager\Views\default_simple', 'default_head' => 'CodeIgniter\Pager\Views\default_head', ]; diff --git a/app/Controllers/Admin/BaseController.php b/app/Controllers/Admin/BaseController.php index 92f4849ab40c42b399f2264210c53558ad081147..5d7de7d273ea8bf224860f4f1bfffc81787eb6a6 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']; + protected $helpers = ['auth', 'breadcrumb', 'svg', 'components']; /** * Constructor. diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php index 693746d003a055e34212d49863d1225383419e41..01e66ee6959441c31f7808a5ff8ea7b2e5e6206b 100644 --- a/app/Controllers/Admin/Contributor.php +++ b/app/Controllers/Admin/Contributor.php @@ -166,7 +166,7 @@ class Contributor extends BaseController public function remove() { - if ($this->podcast->owner_id == $this->user->id) { + if ($this->podcast->created_by == $this->user->id) { return redirect() ->back() ->with('errors', [ diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index 39ce2e0c0bacb87f4b8be4fccefbc4033aa96a83..5e25bd00c157787b2b61bb4f92d195616436a25b 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -45,8 +45,14 @@ class Episode extends BaseController public function list() { + $episodes = (new EpisodeModel()) + ->where('podcast_id', $this->podcast->id) + ->orderBy('created_at', 'desc'); + $data = [ 'podcast' => $this->podcast, + 'episodes' => $episodes->paginate(10), + 'pager' => $episodes->pager, ]; replace_breadcrumb_params([ @@ -57,7 +63,10 @@ class Episode extends BaseController public function view() { - $data = ['episode' => $this->episode]; + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + ]; replace_breadcrumb_params([ 0 => $this->podcast->title, @@ -105,7 +114,10 @@ class Episode extends BaseController 'enclosure' => $this->request->getFile('enclosure'), 'description' => $this->request->getPost('description'), 'image' => $this->request->getFile('image'), - 'explicit' => $this->request->getPost('explicit') == 'yes', + 'parental_advisory' => + $this->request->getPost('parental_advisory') !== 'undefined' + ? $this->request->getPost('parental_advisory') + : null, 'number' => $this->request->getPost('episode_number'), 'season_number' => $this->request->getPost('season_number'), 'type' => $this->request->getPost('type'), @@ -120,14 +132,33 @@ class Episode extends BaseController $episodeModel = new EpisodeModel(); - if (!$episodeModel->save($newEpisode)) { + if (!($newEpisodeId = $episodeModel->insert($newEpisode, true))) { return redirect() ->back() ->withInput() ->with('errors', $episodeModel->errors()); } - return redirect()->route('episode-list', [$this->podcast->id]); + // update podcast's episode_description_footer if changed + $podcastModel = new PodcastModel(); + + if ($this->podcast->hasChanged('episode_description_footer')) { + $this->podcast->episode_description_footer = $this->request->getPost( + 'description_footer' + ); + + if (!$podcastModel->update($this->podcast->id, $this->podcast)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $podcastModel->errors()); + } + } + + return redirect()->route('episode-view', [ + $this->podcast->id, + $newEpisodeId, + ]); } public function edit() @@ -135,6 +166,7 @@ class Episode extends BaseController helper(['form']); $data = [ + 'podcast' => $this->podcast, 'episode' => $this->episode, ]; @@ -167,7 +199,10 @@ class Episode extends BaseController $this->episode->title = $this->request->getPost('title'); $this->episode->slug = $this->request->getPost('slug'); $this->episode->description = $this->request->getPost('description'); - $this->episode->explicit = $this->request->getPost('explicit') == 'yes'; + $this->episode->parental_advisory = + $this->request->getPost('parental_advisory') !== 'undefined' + ? $this->request->getPost('parental_advisory') + : null; $this->episode->number = $this->request->getPost('episode_number'); $this->episode->season_number = $this->request->getPost('season_number') ? $this->request->getPost('season_number') @@ -191,14 +226,32 @@ class Episode extends BaseController $episodeModel = new EpisodeModel(); - if (!$episodeModel->save($this->episode)) { + if (!$episodeModel->update($this->episode->id, $this->episode)) { return redirect() ->back() ->withInput() ->with('errors', $episodeModel->errors()); } - return redirect()->route('episode-list', [$this->podcast->id]); + // update podcast's episode_description_footer if changed + $this->podcast->episode_description_footer = $this->request->getPost( + 'description_footer' + ); + + if ($this->podcast->hasChanged('episode_description_footer')) { + $podcastModel = new PodcastModel(); + if (!$podcastModel->update($this->podcast->id, $this->podcast)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $podcastModel->errors()); + } + } + + return redirect()->route('episode-view', [ + $this->podcast->id, + $this->episode->id, + ]); } public function delete() diff --git a/app/Controllers/Admin/Myaccount.php b/app/Controllers/Admin/Myaccount.php index 7a058af07903a8db9d889693f3061d819045d928..50f2fb45345f69a8bf39493a676decc274ddf1cc 100644 --- a/app/Controllers/Admin/Myaccount.php +++ b/app/Controllers/Admin/Myaccount.php @@ -57,9 +57,8 @@ class MyAccount extends BaseController } user()->password = $this->request->getPost('new_password'); - $userModel->save(user()); - if (!$userModel->save(user())) { + if (!$userModel->update(user()->id, user())) { return redirect() ->back() ->withInput() diff --git a/app/Controllers/Admin/Page.php b/app/Controllers/Admin/Page.php index 384b72bf21485dfc80d7c098ad0f8977da2e859f..f2ce56db4b3379dbd450cdcb600ee1df223f4df8 100644 --- a/app/Controllers/Admin/Page.php +++ b/app/Controllers/Admin/Page.php @@ -59,7 +59,7 @@ class Page extends BaseController $pageModel = new PageModel(); - if (!$pageModel->save($page)) { + if (!$pageModel->insert($page)) { return redirect() ->back() ->withInput() @@ -92,7 +92,7 @@ class Page extends BaseController $pageModel = new PageModel(); - if (!$pageModel->save($this->page)) { + if (!$pageModel->update($this->page->id, $this->page)) { return redirect() ->back() ->withInput() diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index ab0dde47dfeef6dfccfe1250c29e23c29665da46..794bcef3847c14214000287b2eb0e917a95e3960 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -94,21 +94,20 @@ class Podcast extends BaseController 'title' => $this->request->getPost('title'), 'name' => $this->request->getPost('name'), 'description' => $this->request->getPost('description'), - 'episode_description_footer' => $this->request->getPost( - 'episode_description_footer' - ), 'image' => $this->request->getFile('image'), 'language' => $this->request->getPost('language'), 'category_id' => $this->request->getPost('category'), - 'explicit' => $this->request->getPost('explicit') == 'yes', - 'author' => $this->request->getPost('author'), + 'parental_advisory' => + $this->request->getPost('parental_advisory') !== 'undefined' + ? $this->request->getPost('parental_advisory') + : null, 'owner_name' => $this->request->getPost('owner_name'), 'owner_email' => $this->request->getPost('owner_email'), + 'publisher' => $this->request->getPost('publisher'), 'type' => $this->request->getPost('type'), 'copyright' => $this->request->getPost('copyright'), - 'block' => $this->request->getPost('block') == 'yes', - 'complete' => $this->request->getPost('complete') == 'yes', - 'custom_html_head' => $this->request->getPost('custom_html_head'), + 'block' => $this->request->getPost('block') === 'yes', + 'complete' => $this->request->getPost('complete') === 'yes', 'created_by' => user(), 'updated_by' => user(), ]); @@ -119,7 +118,7 @@ class Podcast extends BaseController $db->transStart(); if (!($newPodcastId = $podcastModel->insert($podcast, true))) { - $db->transComplete(); + $db->transRollback(); return redirect() ->back() ->withInput() @@ -135,6 +134,12 @@ class Podcast extends BaseController $podcastAdminGroup->id ); + // set Podcast categories + (new CategoryModel())->setPodcastCategories( + $newPodcastId, + $this->request->getPost('other_categories') + ); + $db->transComplete(); return redirect()->route('podcast-view', [$newPodcastId]); @@ -205,20 +210,22 @@ class Podcast extends BaseController 'image' => download_file($nsItunes->image->attributes()), 'language' => $this->request->getPost('language'), 'category_id' => $this->request->getPost('category'), - 'explicit' => empty($nsItunes->explicit) - ? false - : $nsItunes->explicit == 'yes', - 'author' => $nsItunes->author, + 'parental_advisory' => empty($nsItunes->explicit) + ? null + : (in_array($nsItunes->explicit, ['yes', 'true']) + ? 'explicit' + : null), 'owner_name' => $nsItunes->owner->name, 'owner_email' => $nsItunes->owner->email, + 'publisher' => $nsItunes->author, 'type' => empty($nsItunes->type) ? 'episodic' : $nsItunes->type, 'copyright' => $feed->channel[0]->copyright, 'block' => empty($nsItunes->block) ? false - : $nsItunes->block == 'yes', + : $nsItunes->block === 'yes', 'complete' => empty($nsItunes->complete) ? false - : $nsItunes->complete == 'yes', + : $nsItunes->complete === 'yes', 'created_by' => user(), 'updated_by' => user(), ]); @@ -229,7 +236,7 @@ class Podcast extends BaseController $db->transStart(); if (!($newPodcastId = $podcastModel->insert($podcast, true))) { - $db->transComplete(); + $db->transRollback(); return redirect() ->back() ->withInput() @@ -265,7 +272,7 @@ class Podcast extends BaseController ); $slug = slugify( - $this->request->getPost('slug_field') == 'title' + $this->request->getPost('slug_field') === 'title' ? $item->title : basename($item->link) ); @@ -285,22 +292,23 @@ class Podcast extends BaseController 'slug' => $slug, 'enclosure' => download_file($item->enclosure->attributes()), 'description' => $converter->convert( - $this->request->getPost('description_field') == 'summary' + $this->request->getPost('description_field') === 'summary' ? $nsItunes->summary - : ($this->request->getPost('description_field') == + : ($this->request->getPost('description_field') === 'subtitle_summary' - ? '<h3>' . - $nsItunes->subtitle . - "</h3>\n" . - $nsItunes->summary + ? $nsItunes->subtitle . "\n" . $nsItunes->summary : $item->description) ), 'image' => empty($nsItunes->image->attributes()) ? null : download_file($nsItunes->image->attributes()), - 'explicit' => $nsItunes->explicit == 'yes', + 'explicit' => $nsItunes->explicit + ? (in_array($nsItunes->explicit, ['yes', 'true']) + ? 'explicit' + : null) + : null, 'number' => - $this->request->getPost('force_renumber') == 'yes' + $this->request->getPost('force_renumber') === 'yes' ? $itemNumber : $nsItunes->episode, 'season_number' => empty( @@ -313,7 +321,7 @@ class Podcast extends BaseController : $nsItunes->episodeType, 'block' => empty($nsItunes->block) ? false - : $nsItunes->block == 'yes', + : $nsItunes->block === 'yes', 'created_by' => user(), 'updated_by' => user(), ]); @@ -324,8 +332,8 @@ class Podcast extends BaseController $episodeModel = new EpisodeModel(); - if (!$episodeModel->save($newEpisode)) { - // FIX: What shall we do? + if (!$episodeModel->insert($newEpisode)) { + // FIXME: What shall we do? return redirect() ->back() ->withInput() @@ -335,7 +343,7 @@ class Podcast extends BaseController $db->transComplete(); - return redirect()->route('podcast-list'); + return redirect()->route('podcast-view', [$newPodcastId]); } public function edit() @@ -372,9 +380,6 @@ class Podcast extends BaseController $this->podcast->title = $this->request->getPost('title'); $this->podcast->name = $this->request->getPost('name'); $this->podcast->description = $this->request->getPost('description'); - $this->podcast->episode_description_footer = $this->request->getPost( - 'episode_description_footer' - ); $image = $this->request->getFile('image'); if ($image->isValid()) { @@ -382,29 +387,50 @@ class Podcast extends BaseController } $this->podcast->language = $this->request->getPost('language'); $this->podcast->category_id = $this->request->getPost('category'); - $this->podcast->explicit = $this->request->getPost('explicit') == 'yes'; - $this->podcast->author = $this->request->getPost('author'); + $this->podcast->parental_advisory = + $this->request->getPost('parental_advisory') !== 'undefined' + ? $this->request->getPost('parental_advisory') + : null; + $this->podcast->publisher = $this->request->getPost('publisher'); $this->podcast->owner_name = $this->request->getPost('owner_name'); $this->podcast->owner_email = $this->request->getPost('owner_email'); $this->podcast->type = $this->request->getPost('type'); $this->podcast->copyright = $this->request->getPost('copyright'); - $this->podcast->block = $this->request->getPost('block') == 'yes'; - $this->podcast->complete = $this->request->getPost('complete') == 'yes'; - $this->podcast->custom_html_head = $this->request->getPost( - 'custom_html_head' - ); + $this->podcast->block = $this->request->getPost('block') === 'yes'; + $this->podcast->complete = + $this->request->getPost('complete') === 'yes'; $this->updated_by = user(); - $podcastModel = new PodcastModel(); + $db = \Config\Database::connect(); + $db->transStart(); - if (!$podcastModel->save($this->podcast)) { + $podcastModel = new PodcastModel(); + if (!$podcastModel->update($this->podcast->id, $this->podcast)) { + $db->transRollback(); return redirect() ->back() ->withInput() ->with('errors', $podcastModel->errors()); } - return redirect()->route('podcast-list'); + // set Podcast categories + (new CategoryModel())->setPodcastCategories( + $this->podcast->id, + $this->request->getPost('other_categories') + ); + + $db->transComplete(); + + return redirect()->route('podcast-view', [$this->podcast->id]); + } + + public function latestEpisodes(int $limit) + { + $episodes = (new EpisodeModel()) + ->orderBy('created_at', 'desc') + ->findAll($limit); + + return view('admin/podcast/latest_episodes', ['episodes' => $episodes]); } public function delete() diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php index 63874156ca3f959c4f8dcf675108096d8e069ed9..be174529ae69900abb85f1be722fd4f39a3c9f8c 100644 --- a/app/Controllers/Admin/User.php +++ b/app/Controllers/Admin/User.php @@ -86,7 +86,7 @@ class User extends BaseController // Force user to reset his password on first connection $user->forcePasswordReset(); - if (!$userModel->save($user)) { + if (!$userModel->insert($user)) { return redirect() ->back() ->withInput() @@ -150,7 +150,7 @@ class User extends BaseController $userModel = new UserModel(); $this->user->forcePasswordReset(); - if (!$userModel->save($this->user)) { + if (!$userModel->update($this->user->id, $this->user)) { return redirect() ->back() ->with('errors', $userModel->errors()); @@ -184,7 +184,7 @@ class User extends BaseController // TODO: add ban reason? $this->user->ban(''); - if (!$userModel->save($this->user)) { + if (!$userModel->update($this->user->id, $this->user)) { return redirect() ->back() ->with('errors', $userModel->errors()); @@ -205,7 +205,7 @@ class User extends BaseController $userModel = new UserModel(); $this->user->unBan(); - if (!$userModel->save($this->user)) { + if (!$userModel->update($this->user->id, $this->user)) { return redirect() ->back() ->with('errors', $userModel->errors()); diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php index aaac73ccc03e9e40458d7a8a67bb3d9f15a5fc65..1a36daf16f209672c3a87a31db91a7283592f50b 100644 --- a/app/Controllers/Auth.php +++ b/app/Controllers/Auth.php @@ -12,6 +12,14 @@ use App\Entities\User; class Auth extends \Myth\Auth\Controllers\AuthController { + /** + * An array of helpers to be automatically loaded + * upon class instantiation. + * + * @var array + */ + protected $helpers = ['components']; + /** * Attempt to register a new user. */ diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index ab5eef7da612ce87d096dabccdcd5d2516fe4683..2f5bdcff291b15a430e29ade0fdb7b390c6c3391 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']; + protected $helpers = ['analytics', 'svg', 'components']; /** * Constructor. diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index a0e0dfa2cb86bce91b03fb9852375fa95c37ddc2..8b3409014e5007fbd72f1b2cf7fc0fe66a4365c6 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -57,6 +57,7 @@ class Episode extends BaseController $data = [ 'previousEpisode' => $previousNextEpisodes['previous'], 'nextEpisode' => $previousNextEpisodes['next'], + 'podcast' => $this->podcast, 'episode' => $this->episode, ]; diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php index 4c024439dc7e9cfc32671814f28917bae561f2fa..a95e4db10869489e31b17f9e74b3bbc25a191a76 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -50,10 +50,11 @@ class AddPodcasts extends Migration 'unsigned' => true, 'default' => 0, ], - 'explicit' => [ - 'type' => 'TINYINT', - 'constraint' => 1, - 'default' => 0, + 'parental_advisory' => [ + 'type' => 'ENUM', + 'constraint' => ['clean', 'explicit'], + 'null' => true, + 'default' => null, ], 'owner_name' => [ 'type' => 'VARCHAR', @@ -63,7 +64,7 @@ class AddPodcasts extends Migration 'type' => 'VARCHAR', 'constraint' => 1024, ], - 'author' => [ + 'publisher' => [ 'type' => 'VARCHAR', 'constraint' => 1024, 'null' => true, @@ -92,10 +93,6 @@ class AddPodcasts extends Migration 'type' => 'TEXT', 'null' => true, ], - 'custom_html_head' => [ - 'type' => 'TEXT', - 'null' => true, - ], 'created_by' => [ 'type' => 'INT', 'constraint' => 11, diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index e965fd8e6ed1db7d7c94c70f89a5ca8d72233fd0..24b2f02fc5dd7497e9515abd1deb1813f41212a5 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -70,10 +70,11 @@ class AddEpisodes extends Migration 'constraint' => 1024, 'null' => true, ], - 'explicit' => [ - 'type' => 'TINYINT', - 'constraint' => 1, - 'default' => 0, + 'parental_advisory' => [ + 'type' => 'ENUM', + 'constraint' => ['clean', 'explicit'], + 'null' => true, + 'default' => null, ], 'number' => [ 'type' => 'INT', diff --git a/app/Database/Migrations/2020-09-29-150000_add_podcasts_categories.php b/app/Database/Migrations/2020-09-29-150000_add_podcasts_categories.php new file mode 100644 index 0000000000000000000000000000000000000000..4139b15011199e61f78d66ea3c578d92cdd08ffb --- /dev/null +++ b/app/Database/Migrations/2020-09-29-150000_add_podcasts_categories.php @@ -0,0 +1,42 @@ +<?php + +/** + * Class AddPodcastsCategories + * Creates podcasts_categories table in database + * + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Migrations; + +use CodeIgniter\Database\Migration; + +class AddPodcastsCategories extends Migration +{ + public function up() + { + $this->forge->addField([ + 'podcast_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + ], + 'category_id' => [ + 'type' => 'INT', + 'constraint' => 10, + 'unsigned' => true, + ], + ]); + $this->forge->addPrimaryKey(['podcast_id', 'category_id']); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->addForeignKey('category_id', 'categories', 'id'); + $this->forge->createTable('podcasts_categories'); + } + + public function down() + { + $this->forge->dropTable('podcasts_categories'); + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 9c7311231a906d71f1c624d58ad47e408ffa3ef0..7bbda5e53f62fb963ae588897f4128a2c8943a52 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -66,7 +66,7 @@ class Episode extends Entity 'enclosure_filesize' => 'integer', 'description' => 'string', 'image_uri' => '?string', - 'explicit' => 'boolean', + 'parental_advisory' => '?string', 'number' => '?integer', 'season_number' => '?integer', 'type' => 'string', diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 0a8a273ba075493ca51b1028ebb1ec5a6c75d6ae..c0dd2b7defbf5c961a792f1c74edfafec060a582 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -37,6 +37,16 @@ class Podcast extends Entity */ protected $category; + /** + * @var \App\Entities\Category[] + */ + protected $other_categories; + + /** + * @var integer[] + */ + protected $other_categories_ids; + /** * @var \App\Entities\User[] */ @@ -60,8 +70,8 @@ class Podcast extends Entity 'image_uri' => 'string', 'language' => 'string', 'category_id' => 'integer', - 'explicit' => 'boolean', - 'author' => '?string', + 'parental_advisory' => '?string', + 'publisher' => '?string', 'owner_name' => '?string', 'owner_email' => '?string', 'type' => 'string', @@ -69,7 +79,6 @@ class Podcast extends Entity 'block' => 'boolean', 'complete' => 'boolean', 'episode_description_footer' => '?string', - 'custom_html_head' => '?string', 'created_by' => 'integer', 'updated_by' => 'integer', 'imported_feed_url' => '?string', @@ -225,4 +234,33 @@ class Podcast extends Entity return $this->platforms; } + + public function getOtherCategories() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting other categories.' + ); + } + + if (empty($this->other_categories)) { + $this->other_categories = (new CategoryModel())->getPodcastCategories( + $this->id + ); + } + + return $this->other_categories; + } + + public function getOtherCategoriesIds() + { + if (empty($this->other_categories_ids)) { + $this->other_categories_ids = array_column( + $this->getOtherCategories(), + 'id' + ); + } + + return $this->other_categories_ids; + } } diff --git a/app/Entities/User.php b/app/Entities/User.php index ed6bc92029f17523ed166216cbcdf03dbeea9eaf..6a3e7a1fcfcb48f5b731b5705609aefe0f761f9b 100644 --- a/app/Entities/User.php +++ b/app/Entities/User.php @@ -1,5 +1,11 @@ <?php +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + namespace App\Entities; use App\Models\PodcastModel; diff --git a/app/Helpers/breadcrumb_helper.php b/app/Helpers/breadcrumb_helper.php index 52022860be06c48f5371bfa881ccd867972a8ab9..03d9c86b3ac5ddbe5b40914e2f41577a1e28ae95 100644 --- a/app/Helpers/breadcrumb_helper.php +++ b/app/Helpers/breadcrumb_helper.php @@ -9,16 +9,15 @@ use Config\Services; /** - * Returns the inline svg icon + * Renders the breadcrumb navigation through the Breadcrumb service * - * @param string $name name of the icon file without the .svg extension - * @param string $class to be added to the svg string + * @param string $class to be added to the breadcrumb nav * @return string html breadcrumb */ -function render_breadcrumb() +function render_breadcrumb($class = null) { $breadcrumb = Services::breadcrumb(); - return $breadcrumb->render(); + return $breadcrumb->render($class); } function replace_breadcrumb_params($newParams) diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..5b26da6df17adf746e434dd8f14f134e2f6ff78a --- /dev/null +++ b/app/Helpers/components_helper.php @@ -0,0 +1,258 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +if (!function_exists('button')) { + /** + * Button component + * + * Creates a stylized button or button like anchor tag if the URL is defined. + * + * @param string $label The button label + * @param mixed|null $uri URI string or array of URI segments + * @param array $customOptions button options: variant, size, iconLeft, iconRight + * @param array $customAttributes Additional attributes + * + * @return string + */ + function button( + string $label = '', + $uri = null, + $customOptions = [], + $customAttributes = [] + ): string { + $defaultOptions = [ + 'variant' => 'default', + 'size' => 'base', + 'iconLeft' => null, + 'iconRight' => null, + 'isRoundedFull' => false, + 'isSquared' => false, + ]; + $options = array_merge($defaultOptions, $customOptions); + + $baseClass = + 'inline-flex items-center shadow-xs outline-none focus:shadow-outline'; + + $variantClass = [ + 'default' => 'bg-gray-300 hover:bg-gray-400', + 'primary' => 'text-white bg-green-500 hover:bg-green-600', + 'secondary' => 'text-white bg-gray-700 hover:bg-gray-800', + 'success' => 'text-white bg-green-600 hover:bg-green-700', + 'danger' => 'text-white bg-red-600 hover:bg-red-700', + 'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600', + 'info' => 'text-white bg-teal-500 hover:bg-teal-600', + ]; + + $sizeClass = [ + 'small' => 'text-xs md:text-sm ', + 'base' => 'text-sm md:text-base', + 'large' => 'text-lg md:text-xl', + ]; + + $basePaddings = [ + 'small' => 'px-1 md:px-2 md:py-1', + 'base' => 'px-2 py-1 md:px-3 md:py-2', + 'large' => 'px-3 py-2 md:px-4 md:py-2', + ]; + + $squaredPaddings = [ + 'small' => 'p-1', + 'base' => 'p-2', + 'large' => 'p-3', + ]; + + $roundedClass = [ + 'full' => 'rounded-full', + 'small' => 'rounded-sm md:rounded', + 'base' => 'rounded md:rounded-md', + 'large' => 'rounded-md md:rounded-lg', + ]; + + $buttonClass = + $baseClass . + ' ' . + ($options['isRoundedFull'] + ? $roundedClass['full'] + : $roundedClass[$options['size']]) . + ' ' . + ($options['isSquared'] + ? $squaredPaddings[$options['size']] + : $basePaddings[$options['size']]) . + ' ' . + $sizeClass[$options['size']] . + ' ' . + $variantClass[$options['variant']]; + + if (!empty($customAttributes['class'])) { + $buttonClass .= ' ' . $customAttributes['class']; + unset($customAttributes['class']); + } + + if ($options['iconLeft']) { + $label = icon($options['iconLeft'], 'mr-2') . $label; + } + + if ($options['iconRight']) { + $label .= icon($options['iconRight'], 'ml-2'); + } + + if ($uri) { + return anchor( + $uri, + $label, + array_merge( + [ + 'class' => $buttonClass, + ], + $customAttributes + ) + ); + } + + $defaultButtonAttributes = [ + 'type' => 'button', + ]; + $attributes = array_merge($defaultButtonAttributes, $customAttributes); + + return '<button class="' . + $buttonClass . + '"' . + stringify_attributes($attributes) . + '>' . + $label . + '</button>'; + } +} + +// ------------------------------------------------------------------------ + +if (!function_exists('icon_button')) { + /** + * Icon Button component + * + * Abstracts the `button()` helper to create a stylized icon button + * + * @param string $label The button label + * @param mixed|null $uri URI string or array of URI segments + * @param array $customOptions button options: variant, size, iconLeft, iconRight + * @param array $customAttributes Additional attributes + * + * @return string + */ + function icon_button( + string $icon, + string $title, + $uri = null, + $customOptions = [], + $customAttributes = [] + ): string { + $defaultOptions = [ + 'isRoundedFull' => true, + 'isSquared' => true, + ]; + $options = array_merge($defaultOptions, $customOptions); + + $defaultAttributes = [ + 'title' => $title, + 'data-toggle' => 'tooltip', + 'data-placement' => 'bottom', + ]; + $attributes = array_merge($defaultAttributes, $customAttributes); + + return button(icon($icon), $uri, $options, $attributes); + } +} + +// ------------------------------------------------------------------------ + +if (!function_exists('hint_tooltip')) { + /** + * Hint component + * + * Used to produce tooltip with a question mark icon for hint texts + * + * @param string $hintText The hint text + * + * @return string + */ + function hint_tooltip(string $hintText = '', string $class = ''): string + { + $tooltip = + '<span data-toggle="tooltip" data-placement="bottom" tabindex="0" title="' . + $hintText . + '" class="inline-block align-middle outline-none focus:shadow-outline'; + + if ($class !== '') { + $tooltip .= ' ' . $class; + } + + return $tooltip . '">' . icon('question') . '</span>'; + } +} + +// ------------------------------------------------------------------------ + +if (!function_exists('data_table')) { + /** + * Data table component + * + * Creates a stylized table. + * + * @param array $columns array of associate arrays with `header` and `cell` keys where `cell` is a function with a row of $data as parameter + * @param array $data data to loop through and display in rows + * @param array ...$rest Any other argument to pass to the `cell` function + * + * @return string + */ + function data_table($columns, $data = [], ...$rest): string + { + $table = new \CodeIgniter\View\Table(); + + $template = [ + 'table_open' => '<table class="w-full whitespace-no-wrap">', + + 'thead_open' => + '<thead class="text-xs font-semibold text-left text-gray-500 uppercase border-b">', + + 'heading_cell_start' => '<th class="px-4 py-2">', + 'cell_start' => '<td class="px-4 py-2">', + 'cell_alt_start' => '<td class="px-4 py-2">', + + 'row_start' => '<tr class="bg-gray-100 hover:bg-green-100">', + 'row_alt_start' => '<tr class="hover:bg-green-100">', + ]; + + $table->setTemplate($template); + + $tableHeaders = []; + foreach ($columns as $column) { + array_push($tableHeaders, $column['header']); + } + + $table->setHeading($tableHeaders); + + if ($dataCount = count($data)) { + for ($i = 0; $i < $dataCount; $i++) { + $row = $data[$i]; + $rowData = []; + foreach ($columns as $column) { + array_push($rowData, $column['cell']($row, ...$rest)); + } + $table->addRow($rowData); + } + } else { + return lang('Common.no_data'); + } + + return '<div class="overflow-x-auto bg-white rounded-lg shadow" >' . + $table->generate() . + '</div>'; + } +} + +// ------------------------------------------------------------------------ diff --git a/app/Helpers/form_helper.php b/app/Helpers/form_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..5be15eecfaf78a9ecfd8c1625c930e84a808f270 --- /dev/null +++ b/app/Helpers/form_helper.php @@ -0,0 +1,187 @@ +<?php +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +if (!function_exists('form_section')) { + /** + * Form section + * + * Used to produce a responsive form section with a title and subtitle. To close section, + * use form_section_close() + * + * @param string $title The section title + * @param string $subtitle The section subtitle + * @param array $attributes Additional attributes + * + * @return string + */ + function form_section( + string $title = '', + string $subtitle = '', + array $attributes = [] + ): string { + $section = + '<div class="flex flex-wrap w-full gap-6 mb-8"' . + stringify_attributes($attributes) . + ">\n"; + + $info = + '<div class="w-full max-w-xs"><h2 class="text-lg font-semibold">' . + $title . + '</h2><p class="text-sm text-gray-600">' . + $subtitle . + '</p></div>'; + + return $section . $info . '<div class="flex flex-col w-full max-w-lg">'; + } +} + +//-------------------------------------------------------------------- + +if (!function_exists('form_section_close')) { + /** + * Form Section close Tag + * + * @param string $extra + * + * @return string + */ + function form_section_close(string $extra = ''): string + { + return '</div></div>' . $extra; + } +} + +//-------------------------------------------------------------------- + +if (!function_exists('form_switch')) { + /** + * Form Checkbox Switch + * + * Abstracts form_label to stylize it as a switch toggle + * + * @param array $data + * @param string $value + * @param boolean $checked + * @param mixed $extra + * + * @return string + */ + function form_switch( + $label = '', + $data = '', + string $value = '', + bool $checked = false, + $class = '', + $extra = '' + ): string { + $data['class'] = 'form-switch'; + + return '<label class="relative inline-flex items-center' . + ' ' . + $class . + '">' . + form_checkbox($data, $value, $checked, $extra) . + '<span class="form-switch-slider"></span>' . + '<span class="ml-2">' . + $label . + '</span></label>'; + } +} + +//-------------------------------------------------------------------- + +if (!function_exists('form_label')) { + /** + * Form Label Tag + * + * @param string $label_text The text to appear onscreen + * @param string $id The id the label applies to + * @param array $attributes Additional attributes + * @param string $hintText Hint text to add next to the label + * @param boolean $isOptional adds an optional text if true + * + * @return string + */ + function form_label( + string $label_text = '', + string $id = '', + array $attributes = [], + string $hintText = '', + bool $isOptional = false + ): string { + $label = '<label'; + + if ($id !== '') { + $label .= ' for="' . $id . '"'; + } + + if (is_array($attributes) && $attributes) { + foreach ($attributes as $key => $val) { + $label .= ' ' . $key . '="' . $val . '"'; + } + } + + $label_content = $label_text; + if ($isOptional) { + $label_content .= + '<small class="ml-1 lowercase">(' . + lang('Common.optional') . + ')</small>'; + } + + if ($hintText !== '') { + $label_content .= hint_tooltip($hintText, 'ml-1'); + } + + return $label . '>' . $label_content . '</label>'; + } +} + +//-------------------------------------------------------------------- + +if (!function_exists('form_multiselect')) { + /** + * Multi-select menu + * + * @param string $name + * @param array $options + * @param array $selected + * @param mixed $extra + * + * @return string + */ + function form_multiselect( + string $name = '', + array $options = [], + array $selected = [], + $customExtra = '' + ): string { + $defaultExtra = [ + 'data-class' => $customExtra['class'], + 'data-select-text' => lang('Common.forms.multiSelect.selectText'), + 'data-loading-text' => lang('Common.forms.multiSelect.loadingText'), + 'data-no-results-text' => lang( + 'Common.forms.multiSelect.noResultsText' + ), + 'data-no-choices-text' => lang( + 'Common.forms.multiSelect.noChoicesText' + ), + 'data-max-item-text' => lang( + 'Common.forms.multiSelect.maxItemText' + ), + ]; + $extra = stringify_attributes(array_merge($defaultExtra, $customExtra)); + + if (stripos($extra, 'multiple') === false) { + $extra .= ' multiple="multiple"'; + } + + return form_dropdown($name, $options, $selected, $extra); + } +} + +//-------------------------------------------------------------------- diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 68da0f2ae1d68daea201adbd74a46e7158b5defc..532b9bcb57a57e1ae16001fe109addd3fe1e16d8 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -7,7 +7,6 @@ */ use App\Libraries\SimpleRSSElement; -use App\Models\CategoryModel; use CodeIgniter\I18n\Time; /** @@ -18,14 +17,8 @@ use CodeIgniter\I18n\Time; */ function get_rss_feed($podcast) { - $category_model = new CategoryModel(); - $episodes = $podcast->episodes; - $podcast_category = $category_model - ->where('id', $podcast->category_id) - ->first(); - $itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; $rss = new SimpleRSSElement( @@ -60,39 +53,20 @@ function get_rss_feed($podcast) $itunes_image->addAttribute('href', $podcast->image->url); $channel->addChild('language', $podcast->language); - $itunes_category = $channel->addChild('category', null, $itunes_namespace); - $itunes_category->addAttribute( - 'text', - $podcast_category->parent - ? $podcast_category->parent->apple_category - : $podcast_category->apple_category - ); - - if ($podcast_category->parent) { - $itunes_category_child = $itunes_category->addChild( - 'category', - null, - $itunes_namespace - ); - $itunes_category_child->addAttribute( - 'text', - $podcast_category->apple_category - ); - $channel->addChild( - 'category', - $podcast_category->parent->apple_category - ); + // set main category first, then other categories as apple + add_category_tag($channel, $podcast->category); + foreach ($podcast->other_categories as $other_category) { + add_category_tag($channel, $other_category); } - $channel->addChild('category', $podcast_category->apple_category); $channel->addChild( 'explicit', - $podcast->explicit ? 'true' : 'false', + $podcast->parental_advisory === 'explicit' ? 'true' : 'false', $itunes_namespace ); - $podcast->author && - $channel->addChild('author', $podcast->author, $itunes_namespace); + $podcast->publisher && + $channel->addChild('author', $podcast->publisher, $itunes_namespace); $channel->addChild('link', $podcast->link); $owner = $channel->addChild('owner', null, $itunes_namespace); @@ -137,11 +111,13 @@ function get_rss_feed($podcast) $itunes_namespace ); $episode_itunes_image->addAttribute('href', $episode->image->feed_url); - $item->addChild( - 'explicit', - $episode->explicit ? 'true' : 'false', - $itunes_namespace - ); + + $episode->parental_advisory && + $item->addChild( + 'explicit', + $episode->parental_advisory === 'explicit' ? 'true' : 'false', + $itunes_namespace + ); $item->addChild('episode', $episode->number, $itunes_namespace); $episode->season_number && @@ -157,3 +133,35 @@ function get_rss_feed($podcast) return $rss->asXML(); } + +/** + * Adds <itunes:category> and <category> tags to node for a given category + * + * @param \SimpleXMLElement $node + * @param \App\Entities\Category $category + * + * @return void + */ +function add_category_tag($node, $category) +{ + $itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; + + $itunes_category = $node->addChild('category', null, $itunes_namespace); + $itunes_category->addAttribute( + 'text', + $category->parent + ? $category->parent->apple_category + : $category->apple_category + ); + + if ($category->parent) { + $itunes_category_child = $itunes_category->addChild( + 'category', + null, + $itunes_namespace + ); + $itunes_category_child->addAttribute('text', $category->apple_category); + $node->addChild('category', $category->parent->apple_category); + } + $node->addChild('category', $category->apple_category); +} diff --git a/app/Helpers/svg_helper.php b/app/Helpers/svg_helper.php index a3b353d7abd6e7948cf3359de3bb65f8e191ffe2..921284c9d6fea6088d9b8020e8038e8a517db08d 100644 --- a/app/Helpers/svg_helper.php +++ b/app/Helpers/svg_helper.php @@ -13,16 +13,17 @@ * @param string $class to be added to the svg string * @return string svg contents */ -function icon($name, $class = null) +function icon(string $name, string $class = '') { $svg_contents = file_get_contents('assets/icons/' . $name . '.svg'); - if ($class) { + if ($class !== '') { $svg_contents = str_replace( '<svg', '<svg class="' . $class . '"', $svg_contents ); } + return $svg_contents; } diff --git a/app/Language/en/AdminNavigation.php b/app/Language/en/AdminNavigation.php index 9b398f577d8a28c36d2fc8827d06a7177e813c67..212863f5d14fc7e6f01e2c83ab50ffd82651e91a 100644 --- a/app/Language/en/AdminNavigation.php +++ b/app/Language/en/AdminNavigation.php @@ -7,17 +7,17 @@ */ return [ + 'go_to_website' => 'Go to website', 'dashboard' => 'Dashboard', - 'podcasts' => 'Podcasts', - 'users' => 'Users', - 'pages' => 'Pages', 'admin' => 'Home', + 'podcasts' => 'Podcasts', 'podcast-list' => 'All podcasts', 'podcast-create' => 'New podcast', 'podcast-import' => 'Import a podcast', + 'users' => 'Users', 'user-list' => 'All users', 'user-create' => 'New user', + 'pages' => 'Pages', 'page-list' => 'All pages', 'page-create' => 'New Page', - 'go_to_website' => 'Go to website', ]; diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index ac242f8f74988b3be13cc12a11e748f78efd60cd..7e90eb891b98a342f684fef3e0fe7b2ad3bab90b 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -7,12 +7,25 @@ */ return [ + 'yes' => 'Yes', + 'no' => 'No', + 'optional' => 'Optional', + 'no_data' => 'No data found!', 'home' => 'Home', 'explicit' => 'Explicit', 'mediumDate' => '{0,date,medium}', 'duration' => '{0,duration}', 'powered_by' => 'Powered by {castopod}.', + 'actions' => 'Actions', + 'pageInfo' => 'Page {currentPage} out of {pageCount}', 'forms' => [ + 'multiSelect' => [ + 'selectText' => 'Press to select', + 'loadingText' => 'Loading...', + 'noResultsText' => 'No results found', + 'noChoicesText' => 'No choices to choose from', + 'maxItemText' => 'Cannot add more items', + ], 'image_size_hint' => 'Image must be squared with at least 1400px wide and tall.', ], diff --git a/app/Language/en/Contributor.php b/app/Language/en/Contributor.php index 46b95f04164116625447c356e597feed7bd1802a..540dc57113ffa9f8c86cdd75168660c7567756cc 100644 --- a/app/Language/en/Contributor.php +++ b/app/Language/en/Contributor.php @@ -14,6 +14,10 @@ return [ 'edit_role' => 'Update role for {0}', 'edit' => 'Edit', 'remove' => 'Remove', + 'list' => [ + 'username' => 'Username', + 'role' => 'Role', + ], 'form' => [ 'user' => 'User', 'role' => 'Role', diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index d2563a115f57181afb00ffc21ff15b4df23281f3..d6a01ac2d00b25163dedd90c2522b3ac38a7d2df 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -13,6 +13,9 @@ return [ 'next_season' => 'Next season', 'season' => 'Season {seasonNumber}', 'number' => 'Episode {episodeNumber}', + 'number_abbr' => 'Ep. {episodeNumber}', + 'season_episode' => 'Season {seasonNumber} episode {episodeNumber}', + 'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}', 'all_podcast_episodes' => 'All podcast episodes', 'back_to_podcast' => 'Go back to podcast', 'edit' => 'Edit', @@ -20,50 +23,51 @@ return [ 'go_to_page' => 'Go to page', 'create' => 'Add an episode', 'form' => [ - 'enclosure' => 'Audio file', + 'enclosure' => 'Choose an .mp3 or .m4a audio file…', + 'info_section_title' => 'Episode info', + 'info_section_subtitle' => '', + 'image' => 'Cover image', + 'image_hint' => + 'If you do not set an image, the podcast cover will be used instead.', 'title' => 'Title', - 'title_help' => - 'This episode title. It should contain a clear, concise name for your episode. Don’t specify the episode number or season number here.', + 'title_hint' => + 'Should contain a clear and concise episode name. Do not specify the episode or season numbers here.', 'slug' => 'Slug', - 'slug_help' => - 'This episode slug. It will be used for its URL address.', - 'description' => 'Description', - 'description_help' => - 'This is where you type the episode show notes. You may add rich text, links, images…', - 'image' => 'Image', - 'image_help' => - 'This episode image. If an image is already in the audio file, you don’t need to add one here. If you add no image to this episode, the podcast image will be used instead.', - 'explicit' => 'Explicit', - 'explicit_help' => - 'The episode parental advisory information for this episode.', - 'published_at' => [ - 'label' => 'Publication date', - 'date' => 'Publication date', - 'time' => 'Publication time', - ], - 'published_at_help' => - 'The date and time when this episode was released. It can be in the past or in the future.', + 'slug_hint' => 'Used for generating the episode URL.', + 'season_number' => 'Season', + 'episode_number' => 'Episode', 'type' => [ 'label' => 'Type', + 'hint' => + '- <strong>full</strong>: complete content the episode.<br/>- <strong>trailer</strong>: short, promotional piece of content that represents a preview of the current show.<br/>- <strong>bonus</strong>: extra content for the show (for example, behind the scenes info or interviews with the cast) or cross-promotional content for another show.', 'full' => 'Full', - 'full_help' => - 'Specify full when you are submitting the complete content of your episode.', 'trailer' => 'Trailer', - 'trailer_help' => - 'Specify trailer when you are submitting a short, promotional piece of content that represents a preview of your current show.', 'bonus' => 'Bonus', - 'bonus_help' => - 'Specify bonus when you are submitting extra content for your show (for example, behind the scenes information or interviews with the cast) or cross-promotional content for another show.', ], - 'episode_number' => 'Episode number', - 'episode_number_help' => - 'The episode number is mandatory for serial podcasts but optional for episodic podcasts.', - 'season_number' => 'Season number', - 'season_number_help' => - 'Season number is a non-zero integer (1, 2, 3, etc.) representing this episode season number.', - 'block' => 'Block', - 'block_help' => - 'This episode show or hide status. If you want this episode removed from the Apple directory, use this tag.', + 'show_notes_section_title' => 'Show notes', + 'show_notes_section_subtitle' => + 'Up to 4000 characters, be clear and concise. Show notes help potential listeners in finding the episode.', + 'description' => 'Description', + 'description_footer' => 'Description footer', + 'description_footer_hint' => + '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', + ], + 'parental_advisory' => [ + 'label' => 'Parental advisory', + 'hint' => 'Does the episode contain explicit content?', + 'undefined' => 'undefined', + 'clean' => 'Clean', + 'explicit' => 'Explicit', + ], + 'block' => 'Episode should be hidden from all platforms', + 'block_hint' => + 'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.', 'submit_create' => 'Create episode', 'submit_edit' => 'Save episode', ], diff --git a/app/Language/en/Page.php b/app/Language/en/Page.php index e0f5f032ee54de21a4e87acf186026763a83c18e..c15145b1ef80ab0f36c4547af06de3cc43dc03c2 100644 --- a/app/Language/en/Page.php +++ b/app/Language/en/Page.php @@ -7,6 +7,7 @@ */ return [ + 'page' => 'Page', 'all_pages' => 'All pages', 'create' => 'New page', 'go_to_page' => 'Go to page', diff --git a/app/Language/en/Pager.php b/app/Language/en/Pager.php new file mode 100644 index 0000000000000000000000000000000000000000..d18f15edbfaeb177c745e812ad0d281d387d30b4 --- /dev/null +++ b/app/Language/en/Pager.php @@ -0,0 +1,19 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'pageNavigation' => 'Page navigation', + 'first' => 'First', + 'previous' => 'Previous', + 'next' => 'Next', + 'last' => 'Last', + 'older' => 'Older', + 'newer' => 'Newer', + 'invalidTemplate' => '{0} is not a valid Pager template.', + 'invalidPaginationGroup' => '{0} is not a valid Pagination group.', +]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 2c573d72824d0295a817dc05074dc6fbe03a2c58..8a86df06c9cef6d47e7dce64b0c127ec9e88cb7d 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -9,8 +9,8 @@ return [ 'all_podcasts' => 'All podcasts', 'no_podcast' => 'No podcast found!', - 'create' => 'Create a Podcast', - 'import' => 'Create and Import a Podcast from an existing Feed', + 'create' => 'Create a podcast', + 'import' => 'Import a podcast', 'new_episode' => 'New Episode', 'feed' => 'RSS feed', 'view' => 'View podcast', @@ -19,93 +19,55 @@ return [ 'see_episodes' => 'See episodes', 'see_contributors' => 'See contributors', 'go_to_page' => 'Go to page', + 'latest_episodes' => 'Latest episodes', + 'see_all_episodes' => 'See all episodes', 'form' => [ + 'identity_section_title' => 'Podcast identity', + 'identity_section_subtitle' => 'These fields allow you to get noticed.', + 'image' => 'Cover image', 'title' => 'Title', - 'title_help' => - 'The podcast title will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).', 'name' => 'Name', - 'name_help' => - 'The podcast will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).', - 'description' => 'Description', - 'description_help' => - 'It will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).', - 'episode_description_footer' => 'Episode description footer', - 'episode_description_footer_help' => - 'This text will be automatically added at the end of each episode description, so that you don’t have to copy/paste it a gazillion times.', - 'image' => 'Image', - 'image_help' => - 'This podcast image. It must be square, JPEG or PNG, minimum 1400 x 1400 pixels and maximum 3000 x 3000 pixels.', - 'language' => 'Language', - 'language_help' => 'The language spoken on the podcast.', - 'category' => 'Category', - 'category_help' => - 'This podcast category. Because no one uses subcategories, Castopod does not allow you te use one.', - 'explicit' => 'Explicit', - 'explicit_help' => - 'The podcast parental advisory information. Does it contain explicit content?', - 'owner_name' => 'Owner name', - 'owner_name_help' => - 'For administrative use only. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', - 'owner_email' => 'Owner email', - 'owner_email_help' => - 'It will be used by most platforms to verify this podcast ownership. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.', - 'author' => 'Author', - 'author_help' => - 'The group responsible for creating the show. Show author most often refers to the parent company or network of a podcast. This field is sometimes labeled as ’Author’.', + 'name_hint' => 'Used for generating the podcast URL.', 'type' => [ 'label' => 'Type', + 'hint' => + '- <strong>episodic</strong>: if episodes are intended to be consumed without any specific order. Newest episodes will be presented first.<br/>- <strong>serial</strong>: if episodes are intended to be consumed in sequential order. The oldest episodes will be presented first.', 'episodic' => 'Episodic', - 'episodic_help' => - 'Specify episodic when episodes are intended to be consumed without any specific order. The newest episodes will be presented first.', 'serial' => 'Serial', - 'serial_help' => - 'Specify serial when episodes are intended to be consumed in sequential order. The oldest episodes will be presented first.', ], + 'description' => 'Description', + 'classification_section_title' => 'Classification', + 'classification_section_subtitle' => + 'These fields will impact your audience and competition.', + 'language' => 'Language', + 'category' => 'Category', + 'other_categories' => 'Other categories', + 'parental_advisory' => [ + 'label' => 'Parental advisory', + 'hint' => 'Does it contain explicit content?', + 'undefined' => 'undefined', + 'clean' => 'Clean', + 'explicit' => 'Explicit', + ], + 'author_section_title' => 'Author', + 'author_section_subtitle' => 'Who is managing the podcast?', + 'owner_name' => 'Owner name', + 'owner_name_hint' => + 'For administrative use only. Visible in the public RSS feed.', + 'owner_email' => 'Owner email', + 'owner_email_hint' => + 'Will be used by most platforms to verify the podcast ownership. Visible in the public RSS feed.', + 'publisher' => 'Publisher', + 'publisher_hint' => + 'The group responsible for creating the show. Often refers to the parent company or network of a podcast. This field is sometimes labeled as ’Author’.', 'copyright' => 'Copyright', - 'copyright_help' => - 'The podcast copyright details, such as "2020 (cc)(by-nc-sa)" or "©2020".', - 'block' => 'Block', - 'block_help' => - 'If you want your show removed from all platforms, use this tag.', - 'complete' => 'Complete', - 'complete_help' => - 'Check this if you will never publish another episode to your podcast.', - 'custom_html_head' => 'Custom HTML code in <head/>', - 'custom_html_head_help' => - 'Add here any HTML code that you would like to see on all this podcast pages within the <head/> tag.', + 'status_section_title' => 'Status', + 'status_section_subtitle' => 'Dead or alive?', + 'block' => 'Podcast should be hidden from all platforms', + 'complete' => 'Podcast will not be having new episodes', 'submit_create' => 'Create podcast', 'submit_edit' => 'Save podcast', ], - 'form_import' => [ - 'name' => 'Name', - 'name_help' => - 'This podcast name. It will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).', - 'imported_feed_url' => 'Feed URL', - 'imported_feed_url_help' => - 'Make sure you are legally allowed to copy that podcast.', - 'force_renumber' => 'Force episodes renumbering', - 'force_renumber_help' => - 'Use this if your old podcast does not have number but you want some on your new one.', - 'season_number' => 'Season number', - 'season_number_help' => - 'Use this if your old podcast does not have season number but you want one on your new one. Leave blank otherwise.', - 'slug_field' => [ - 'label' => 'Which field should be used to calculate episode slug', - 'link' => '<link>', - 'title' => '<title>', - ], - 'description_field' => [ - 'label' => 'Source field used for episode description / show notes', - 'description' => '<description>', - 'summary' => '<itunes:summary>', - 'subtitle_summary' => - '<itunes:subtitle> <itunes:summary>', - ], - 'max_episodes' => 'Maximum number of episodes to import', - 'max_episodes_helper' => 'Leave blank to import all episodes', - 'submit_import' => 'Import podcast', - 'submit_importing' => 'Importing podcast, this could take a while…', - ], 'category_options' => [ 'uncategorized' => 'uncategorized', 'arts' => 'Arts', @@ -219,7 +181,7 @@ return [ 'film_reviews' => 'Film Reviews', 'tv_reviews' => 'TV Reviews', ], - 'by' => 'By {author}', + 'by' => 'By {publisher}', 'season' => 'Season {seasonNumber}', 'list_of_episodes_year' => '{year} episodes', 'list_of_episodes_season' => 'Season {seasonNumber} episodes', diff --git a/app/Language/en/PodcastImport.php b/app/Language/en/PodcastImport.php new file mode 100644 index 0000000000000000000000000000000000000000..6b86eb1693dfe2dd8ec7cc2f06ea376a2f7ccbf1 --- /dev/null +++ b/app/Language/en/PodcastImport.php @@ -0,0 +1,43 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'old_podcast_section_title' => 'The podcast to import', + 'old_podcast_section_subtitle' => '', + 'imported_feed_url' => 'Feed URL', + 'imported_feed_url_hint' => + 'The feed must be in `.xml` format. Make sure you are legally allowed to copy the podcast.', + 'new_podcast_section_title' => 'The new podcast', + 'new_podcast_section_subtitle' => '', + 'name' => 'Name', + 'name_hint' => 'Used for generating the podcast URL.', + 'advanced_params_section_title' => 'Advanced parameters', + 'advanced_params_section_subtitle' => + 'Keep the default values if you have no idea of what the fields are for.', + 'slug_field' => [ + 'label' => 'Which field should be used to calculate episode slug', + 'link' => '<link>', + 'title' => '<title>', + ], + 'description_field' => [ + 'label' => 'Source field used for episode description / show notes', + 'description' => '<description>', + 'summary' => '<itunes:summary>', + 'subtitle_summary' => + '<itunes:subtitle> + <itunes:summary>', + ], + 'force_renumber' => 'Force episodes renumbering', + 'force_renumber_hint' => + 'Use this if your podcast does not have episode numbers but wish to set them during import.', + 'season_number' => 'Season number', + 'season_number_hint' => + 'Use this if your podcast does not have a season number but wish to set one during import. Leave blank otherwise.', + 'max_episodes' => 'Maximum number of episodes to import', + 'max_episodes_hint' => 'Leave blank to import all episodes', + 'submit' => 'Import podcast', +]; diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php new file mode 100644 index 0000000000000000000000000000000000000000..05fe31f198e986913d8a60437d0efba4c71439b6 --- /dev/null +++ b/app/Language/en/PodcastNavigation.php @@ -0,0 +1,23 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'go_to_page' => 'Go to podcast page', + 'dashboard' => 'Podcast dashboard', + 'podcast-view' => 'Home', + 'podcast-edit' => 'Edit podcast', + 'episodes' => 'Episodes', + 'episode-list' => 'All episodes', + 'episode-create' => 'New episode', + 'analytics' => 'Analytics', + 'contributors' => 'Contributors', + 'contributor-list' => 'All contributors', + 'contributor-add' => 'Add contributor', + 'settings' => 'Settings', + 'platforms' => 'Podcast platforms', +]; diff --git a/app/Language/en/User.php b/app/Language/en/User.php index 2e221aaf300058a47fa07b518fe64f945d2fc9c6..3c84d274bb0ebe7a312d8c2301b4c6493a895ce1 100644 --- a/app/Language/en/User.php +++ b/app/Language/en/User.php @@ -12,15 +12,21 @@ return [ 'ban' => 'Ban', 'unban' => 'Unban', 'delete' => 'Delete', - 'create' => 'Create a user', + 'create' => 'New user', 'view' => '{username}\'s info', 'all_users' => 'All users', + 'list' => [ + 'user' => 'User', + 'roles' => 'Roles', + 'banned' => 'Banned?', + ], 'form' => [ 'email' => 'Email', 'username' => 'Username', 'password' => 'Password', 'new_password' => 'New Password', 'roles' => 'Roles', + 'permissions' => 'Permissions', 'submit_create' => 'Create user', 'submit_edit' => 'Save', 'submit_password_change' => 'Change!', diff --git a/app/Libraries/Breadcrumb.php b/app/Libraries/Breadcrumb.php index 43cb2f951ac2db08ad8686004233d9effa29fa12..816f61eb3e67648fd2b4dd71f43d91f1d0c4dfe2 100644 --- a/app/Libraries/Breadcrumb.php +++ b/app/Libraries/Breadcrumb.php @@ -75,7 +75,7 @@ class Breadcrumb * * @return string */ - public function render() + public function render($class = null) { $listItems = ''; $keys = array_keys($this->links); @@ -97,7 +97,9 @@ class Breadcrumb return '<nav aria-label="' . lang('Breadcrumb.label') . - '"><ol class="breadcrumb">' . + '"><ol class="breadcrumb ' . + $class . + '">' . $listItems . '</ol></nav>'; } diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php index af52ff04e72cf7346fc3bee1c11b6c292b91ea75..6764f6ed04947901a4d16f2513e028c83aa37b16 100644 --- a/app/Models/CategoryModel.php +++ b/app/Models/CategoryModel.php @@ -53,4 +53,72 @@ class CategoryModel extends Model return $options; } + + /** + * Sets categories for a given podcast + * + * @param int $podcastId + * @param array $categories + * + * @return integer|false Number of rows inserted or FALSE on failure + */ + public function setPodcastCategories($podcastId, $categories) + { + cache()->delete("podcasts{$podcastId}_categories"); + + // Remove already previously set categories to overwrite them + $this->db + ->table('podcasts_categories') + ->delete(['podcast_id' => $podcastId]); + + if (!empty($categories)) { + // prepare data for `podcasts_categories` table + $data = array_reduce( + $categories, + function ($result, $categoryId) use ($podcastId) { + $result[] = [ + 'podcast_id' => $podcastId, + 'category_id' => $categoryId, + ]; + + return $result; + }, + [] + ); + + // Set podcast categories + return $this->db->table('podcasts_categories')->insertBatch($data); + } + + // no row has been inserted after deletion + return 0; + } + + /** + * Gets all the podcast categories + * + * @param int $podcastId + * + * @return \App\Entities\Category[] + */ + public function getPodcastCategories($podcastId) + { + if (!($categories = cache("podcasts{$podcastId}_categories"))) { + $categories = $this->select('categories.*') + ->join( + 'podcasts_categories', + 'podcasts_categories.category_id = categories.id' + ) + ->where('podcasts_categories.podcast_id', $podcastId) + ->findAll(); + + cache()->save( + "podcasts{$podcastId}_categories", + $categories, + DECADE + ); + } + + return $categories; + } } diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 9eb734d830d8386304bb5069e1298af10aabbe5e..d72f3f08654f29342656758a134251ed0efeba79 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -26,7 +26,7 @@ class EpisodeModel extends Model 'enclosure_filesize', 'description', 'image_uri', - 'explicit', + 'parental_advisory', 'number', 'season_number', 'type', @@ -47,7 +47,6 @@ class EpisodeModel extends Model 'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]', 'enclosure_uri' => 'required', 'description' => 'required', - 'image_uri' => 'required', 'number' => 'is_natural_no_zero|permit_empty', 'season_number' => 'is_natural_no_zero|permit_empty', 'type' => 'required', diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 16f50a264bfb4f125045d2571dd8a37a32b12fe1..2df723492184d0ba5ea6b0fc793ce3e7179bad6e 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -24,15 +24,14 @@ class PodcastModel extends Model 'image_uri', 'language', 'category_id', - 'explicit', + 'parental_advisory', 'owner_name', 'owner_email', - 'author', + 'publisher', 'type', 'copyright', 'block', 'complete', - 'custom_html_head', 'created_by', 'updated_by', 'imported_feed_url', diff --git a/app/Views/_assets/admin.ts b/app/Views/_assets/admin.ts index 2f3905567845e3deb1149f8af31f3c6975a929e1..7967215c1898bc23e5355eb4f86c3eaa70ca50ca 100644 --- a/app/Views/_assets/admin.ts +++ b/app/Views/_assets/admin.ts @@ -1,11 +1,15 @@ import Dropdown from "./modules/Dropdown"; -import HTMLEditor from "./modules/HTMLEditor"; +import EnclosureInput from "./modules/EnclosureInput"; import MarkdownEditor from "./modules/MarkdownEditor"; +import MultiSelect from "./modules/MultiSelect"; +import SidebarToggler from "./modules/SidebarToggler"; import Slugify from "./modules/Slugify"; import Tooltip from "./modules/Tooltip"; Dropdown(); Tooltip(); MarkdownEditor(); -HTMLEditor(); +MultiSelect(); Slugify(); +SidebarToggler(); +EnclosureInput(); diff --git a/app/Views/_assets/icons/arrow-left.svg b/app/Views/_assets/icons/arrow-left.svg index 6d82f7ba31909c6f68847bdd93ee1728edd48b33..d10d02b5042f233a334e8d4e23b21bc378223628 100644 --- a/app/Views/_assets/icons/arrow-left.svg +++ b/app/Views/_assets/icons/arrow-left.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <g> <path fill="none" d="M0 0h24v24H0z"/> - <path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/> + <path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z"/> </g> </svg> diff --git a/app/Views/_assets/icons/caret-right.svg b/app/Views/_assets/icons/caret-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..346cb156b81f26cec3c1fccfdccbde08ce3b57c3 --- /dev/null +++ b/app/Views/_assets/icons/caret-right.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M14 12l-4 4V8z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/chevron-left.svg b/app/Views/_assets/icons/chevron-left.svg new file mode 100644 index 0000000000000000000000000000000000000000..6d82f7ba31909c6f68847bdd93ee1728edd48b33 --- /dev/null +++ b/app/Views/_assets/icons/chevron-left.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/arrow-right.svg b/app/Views/_assets/icons/chevron-right.svg similarity index 100% rename from app/Views/_assets/icons/arrow-right.svg rename to app/Views/_assets/icons/chevron-right.svg diff --git a/app/Views/_assets/icons/download.svg b/app/Views/_assets/icons/download.svg new file mode 100644 index 0000000000000000000000000000000000000000..42702f57dbf6950b95553d4f18191790d7041f0a --- /dev/null +++ b/app/Views/_assets/icons/download.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M13 10h5l-6 6-6-6h5V3h2v7zm-9 9h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2v7z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/line-chart.svg b/app/Views/_assets/icons/line-chart.svg new file mode 100644 index 0000000000000000000000000000000000000000..c3080e57c76267efe565bf8b8e4d6e829aafac8b --- /dev/null +++ b/app/Views/_assets/icons/line-chart.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0H24V24H0z"/> + <path d="M5 3v16h16v2H3V3h2zm15.293 3.293l1.414 1.414L16 13.414l-3-2.999-4.293 4.292-1.414-1.414L13 7.586l3 2.999 4.293-4.292z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/menu.svg b/app/Views/_assets/icons/menu.svg new file mode 100644 index 0000000000000000000000000000000000000000..666764dc34e68e836d215a35c75a657831f9b3aa --- /dev/null +++ b/app/Views/_assets/icons/menu.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/question.svg b/app/Views/_assets/icons/question.svg new file mode 100644 index 0000000000000000000000000000000000000000..984376ae7d43d4ab2905522caa475a996b81d81e --- /dev/null +++ b/app/Views/_assets/icons/question.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-5h2v2h-2v-2zm2-1.645V14h-2v-1.5a1 1 0 0 1 1-1 1.5 1.5 0 1 0-1.471-1.794l-1.962-.393A3.501 3.501 0 1 1 13 13.355z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/settings.svg b/app/Views/_assets/icons/settings.svg new file mode 100644 index 0000000000000000000000000000000000000000..8ab66f65d22a13d10c8760f3417a2cfd57ee51ab --- /dev/null +++ b/app/Views/_assets/icons/settings.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M2 12c0-.865.11-1.703.316-2.504A3 3 0 0 0 4.99 4.867a9.99 9.99 0 0 1 4.335-2.505 3 3 0 0 0 5.348 0 9.99 9.99 0 0 1 4.335 2.505 3 3 0 0 0 2.675 4.63c.206.8.316 1.638.316 2.503 0 .865-.11 1.703-.316 2.504a3 3 0 0 0-2.675 4.629 9.99 9.99 0 0 1-4.335 2.505 3 3 0 0 0-5.348 0 9.99 9.99 0 0 1-4.335-2.505 3 3 0 0 0-2.675-4.63C2.11 13.704 2 12.866 2 12zm4.804 3c.63 1.091.81 2.346.564 3.524.408.29.842.541 1.297.75A4.993 4.993 0 0 1 12 18c1.26 0 2.438.471 3.335 1.274.455-.209.889-.46 1.297-.75A4.993 4.993 0 0 1 17.196 15a4.993 4.993 0 0 1 2.77-2.25 8.126 8.126 0 0 0 0-1.5A4.993 4.993 0 0 1 17.195 9a4.993 4.993 0 0 1-.564-3.524 7.989 7.989 0 0 0-1.297-.75A4.993 4.993 0 0 1 12 6a4.993 4.993 0 0 1-3.335-1.274 7.99 7.99 0 0 0-1.297.75A4.993 4.993 0 0 1 6.804 9a4.993 4.993 0 0 1-2.77 2.25 8.126 8.126 0 0 0 0 1.5A4.993 4.993 0 0 1 6.805 15zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/user-add.svg b/app/Views/_assets/icons/user-add.svg new file mode 100644 index 0000000000000000000000000000000000000000..ab808608ffe8b6a0d18fa794d21cd0170603bcd4 --- /dev/null +++ b/app/Views/_assets/icons/user-add.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M14 14.252v2.09A6 6 0 0 0 6 22l-2-.001a8 8 0 0 1 10-7.748zM12 13c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm6 6v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3z"/> + </g> +</svg> diff --git a/app/Views/_assets/icons/user.svg b/app/Views/_assets/icons/user.svg new file mode 100644 index 0000000000000000000000000000000000000000..9e64bb5632c084661232f8f069243c32d49b7e7a --- /dev/null +++ b/app/Views/_assets/icons/user.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <g> + <path fill="none" d="M0 0h24v24H0z"/> + <path d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/> + </g> +</svg> diff --git a/app/Views/_assets/modules/EnclosureInput.ts b/app/Views/_assets/modules/EnclosureInput.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef6b95e1bd9fb16fee220205061bf79220dfe74a --- /dev/null +++ b/app/Views/_assets/modules/EnclosureInput.ts @@ -0,0 +1,24 @@ +const EnclosureInput = (): void => { + const enclosureInput = document.querySelector( + ".form-enclosure-input" + ) as HTMLInputElement; + + if (enclosureInput) { + const label = enclosureInput?.nextElementSibling?.querySelector( + "span" + ) as HTMLSpanElement; + const labelVal = label.innerHTML; + + enclosureInput.addEventListener("change", (e: Event) => { + const fileName = (e.target as HTMLInputElement).value.split("\\").pop(); + + if (fileName) { + label.innerHTML = fileName; + } else { + label.innerHTML = labelVal; + } + }); + } +}; + +export default EnclosureInput; diff --git a/app/Views/_assets/modules/HTMLEditor.ts b/app/Views/_assets/modules/HTMLEditor.ts deleted file mode 100644 index 7e67b65fe2e64b68212fb01df32dc581475ce1cb..0000000000000000000000000000000000000000 --- a/app/Views/_assets/modules/HTMLEditor.ts +++ /dev/null @@ -1,19 +0,0 @@ -import CodeMirror from "codemirror"; -import "codemirror/lib/codemirror.css"; - -const HTMLEditor = (): void => { - const allHTMLEditors: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll( - "textarea[data-editor='html']" - ); - - for (let j = 0; j < allHTMLEditors.length; j++) { - const textarea = allHTMLEditors[j]; - - CodeMirror.fromTextArea(textarea, { - lineNumbers: true, - mode: { name: "xml", htmlMode: true }, - }); - } -}; - -export default HTMLEditor; diff --git a/app/Views/_assets/modules/MarkdownEditor.ts b/app/Views/_assets/modules/MarkdownEditor.ts index cb38bca969fc7f2f5c365d7b17dca6c039143edf..604e948ea38ab2409f33e84d1bdf44bd23bc766c 100644 --- a/app/Views/_assets/modules/MarkdownEditor.ts +++ b/app/Views/_assets/modules/MarkdownEditor.ts @@ -59,7 +59,7 @@ class ProseMirrorView { } }, attributes: { - class: "prose-sm px-3 py-2 overflow-y-auto", + class: "prose-sm px-3 py-2 overflow-y-auto focus:shadow-outline", style: "min-height: 200px; max-height: 500px", }, }); @@ -95,12 +95,22 @@ const MarkdownEditor = (): void => { "px-2", "bg-white", "border", - "text-xs" + "text-xs", + "outline-none", + "focus:shadow-outline" ); wysiwygBtn.setAttribute("type", "button"); wysiwygBtn.innerHTML = "Wysiwyg"; const markdownBtn = document.createElement("button"); - markdownBtn.classList.add("py-1", "px-2", "bg-white", "border", "text-xs"); + markdownBtn.classList.add( + "py-1", + "px-2", + "bg-white", + "border", + "text-xs", + "outline-none", + "focus:shadow-outline" + ); markdownBtn.setAttribute("type", "button"); markdownBtn.innerHTML = "Markdown"; diff --git a/app/Views/_assets/modules/MultiSelect.ts b/app/Views/_assets/modules/MultiSelect.ts new file mode 100644 index 0000000000000000000000000000000000000000..87ef62be34e1bb2183a5b3c57631a31d66e69483 --- /dev/null +++ b/app/Views/_assets/modules/MultiSelect.ts @@ -0,0 +1,40 @@ +import Choices from "choices.js"; + +const MultiSelect = (): void => { + // Pass single element + const multiSelects: NodeListOf<HTMLSelectElement> = document.querySelectorAll( + "select[multiple]" + ); + + for (let i = 0; i < multiSelects.length; i++) { + const multiSelect = multiSelects[i]; + + new Choices(multiSelect, { + maxItemCount: parseInt(multiSelect.dataset.maxItemCount || "-1"), + itemSelectText: multiSelect.dataset.selectText, + maxItemText: multiSelect.dataset.maxItemText, + removeItemButton: true, + classNames: { + containerOuter: + "multiselect" + + (multiSelect.dataset.class ? ` ${multiSelect.dataset.class}` : ""), + containerInner: "multiselect__inner", + input: "multiselect__input", + inputCloned: "multiselect__input--cloned", + list: "multiselect__list", + listItems: "multiselect__list--multiple", + listDropdown: "multiselect__list--dropdown", + item: "multiselect__item", + itemSelectable: "multiselect__item--selectable", + itemDisabled: "multiselect__item--disabled", + itemChoice: "multiselect__item--choice", + placeholder: "multiselect__placeholder", + group: "multiselect__group", + groupHeading: "multiselect__heading", + button: "multiselect__button", + }, + }); + } +}; + +export default MultiSelect; diff --git a/app/Views/_assets/modules/SidebarToggler.ts b/app/Views/_assets/modules/SidebarToggler.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9176f4f657bbf6fc1300e78d339701301984646 --- /dev/null +++ b/app/Views/_assets/modules/SidebarToggler.ts @@ -0,0 +1,62 @@ +const SidebarToggler = (): void => { + const sidebar = document.querySelector( + "aside[id='admin-sidebar']" + ) as HTMLElement; + const toggler = document.querySelector( + "button[id='sidebar-toggler']" + ) as HTMLButtonElement; + const sidebarBackdrop = document.querySelector( + "div[id='sidebar-backdrop']" + ) as HTMLElement; + + const setAriaExpanded = (isExpanded: "true" | "false") => { + toggler.setAttribute("aria-expanded", isExpanded); + sidebarBackdrop.setAttribute("aria-expanded", isExpanded); + }; + + const hideSidebar = () => { + setAriaExpanded("false"); + sidebar.classList.add("-translate-x-full"); + sidebarBackdrop.classList.add("hidden"); + toggler.style.transform = "translateX(0px)"; + }; + + const showSidebar = () => { + setAriaExpanded("true"); + sidebar.classList.remove("-translate-x-full"); + sidebarBackdrop.classList.remove("hidden"); + toggler.style.transform = + "translateX(" + sidebar.getBoundingClientRect().width + "px)"; + }; + + toggler.addEventListener("click", () => { + if (sidebar.classList.contains("-translate-x-full")) { + showSidebar(); + } else { + hideSidebar(); + } + }); + + sidebarBackdrop.addEventListener("click", () => { + if (!sidebar.classList.contains("-translate-x-full")) { + hideSidebar(); + } + }); + + const setAriaExpandedOnWindowEvent = () => { + const isExpanded = + !sidebar.classList.contains("-translate-x-full") || + window.innerWidth >= 768; + const ariaExpanded = toggler.getAttribute("aria-expanded"); + if (isExpanded && (!ariaExpanded || ariaExpanded === "false")) { + setAriaExpanded("true"); + } else if (!isExpanded && (!ariaExpanded || ariaExpanded === "true")) { + setAriaExpanded("false"); + } + }; + + window.addEventListener("load", setAriaExpandedOnWindowEvent); + window.addEventListener("resize", setAriaExpandedOnWindowEvent); +}; + +export default SidebarToggler; diff --git a/app/Views/_assets/modules/Tooltip.ts b/app/Views/_assets/modules/Tooltip.ts index 7f91ec0b85cfa199168ad4b99c290a0973d56ee3..b8d2ab2de09fb12b451eedd608a47362df33a221 100644 --- a/app/Views/_assets/modules/Tooltip.ts +++ b/app/Views/_assets/modules/Tooltip.ts @@ -10,10 +10,10 @@ const Tooltip = (): void => { const tooltipContent = tooltipReference.title; const tooltip = document.createElement("div"); - tooltip.setAttribute("id", "tooltip"); + tooltip.setAttribute("id", "tooltip" + i); tooltip.setAttribute( "class", - "px-2 py-1 text-sm bg-gray-900 text-white rounded" + "px-2 py-1 text-sm bg-gray-900 text-white rounded max-w-xs z-50" ); tooltip.innerHTML = tooltipContent; @@ -31,13 +31,13 @@ const Tooltip = (): void => { const show = () => { tooltipReference.removeAttribute("title"); - tooltipReference.setAttribute("aria-describedby", "tooltip"); + tooltipReference.setAttribute("aria-describedby", "tooltip" + i); document.body.appendChild(tooltip); popper.update(); }; const hide = () => { - const element = document.getElementById("tooltip"); + const element = document.getElementById("tooltip" + i); tooltipReference.removeAttribute("aria-describedby"); tooltipReference.setAttribute("title", tooltipContent); if (element) { diff --git a/app/Views/_assets/styles/breadcrumb.css b/app/Views/_assets/styles/breadcrumb.css index f2cb91620f3d705ceb52a267340bca7250dd1e81..0a89fe55b8f673b83698c2be32deae79a773d698 100644 --- a/app/Views/_assets/styles/breadcrumb.css +++ b/app/Views/_assets/styles/breadcrumb.css @@ -1,5 +1,5 @@ .breadcrumb { - @apply inline-flex flex-wrap px-1 py-2 text-sm text-gray-800; + @apply inline-flex flex-wrap px-1 py-2 text-sm; } .breadcrumb-item + .breadcrumb-item::before { diff --git a/app/Views/_assets/styles/enclosureInput.css b/app/Views/_assets/styles/enclosureInput.css new file mode 100644 index 0000000000000000000000000000000000000000..44ea532923919962b7b8cdf4b2175b4c7f3f1e09 --- /dev/null +++ b/app/Views/_assets/styles/enclosureInput.css @@ -0,0 +1,16 @@ +.form-enclosure-input { + @apply absolute w-0 h-0 opacity-0; +} + +.form-enclosure-input + label { + @apply inline-flex items-center justify-center w-full py-2 text-lg font-semibold text-green-600 bg-white border-2 border-green-500 rounded-lg shadow cursor-pointer; +} + +.form-enclosure-input:focus + label, +.form-enclosure-input + label:hover { + @apply text-green-700 border-green-700 shadow-md; +} + +.form-enclosure-input:focus + label { + @apply shadow-outline; +} diff --git a/app/Views/_assets/styles/index.css b/app/Views/_assets/styles/index.css index f12f46bb993ef86487ccce073593a77c8abe5439..d68082f6bf61fa02c596f13af0c9918d7cb89f6e 100644 --- a/app/Views/_assets/styles/index.css +++ b/app/Views/_assets/styles/index.css @@ -1,3 +1,7 @@ @import "./tailwind.css"; @import "./layout.css"; @import "./breadcrumb.css"; +@import "./multiSelect.css"; +@import "./radioBtn.css"; +@import "./switch.css"; +@import "./enclosureInput.css"; diff --git a/app/Views/_assets/styles/layout.css b/app/Views/_assets/styles/layout.css index bed5c1eb0cf7ac886f9d425eba169683dcc43446..b613b7473d0c5514e72c81fc6ade8a98427d8ee9 100644 --- a/app/Views/_assets/styles/layout.css +++ b/app/Views/_assets/styles/layout.css @@ -1,21 +1,26 @@ .holy-grail-grid { - @apply grid; - grid-template: auto 1fr auto / auto 1fr auto; + @apply grid min-h-screen overflow-y-auto; + grid-template: 1fr auto / auto 1fr; - & .holy-grail-header { - grid-column: 1 / 4; - } - - & .holy-grail-sidenav { - grid-column: 1 / 2; - grid-row: 2 / 4; + & .holy-grail-sidebar { + @apply w-64 col-start-1 col-end-2 row-start-1 row-end-3; } & .holy-grail-main { - grid-column: 2 / 4; + @apply w-full col-start-1 col-end-3 row-start-1 row-end-2; } & .holy-grail-footer { - grid-column: 2 / 4; + @apply w-full col-start-1 col-end-3 row-start-2 row-end-3; + } + + @screen md { + & .holy-grail-main { + @apply col-start-2; + } + + & .holy-grail-footer { + @apply col-start-2; + } } } diff --git a/app/Views/_assets/styles/multiSelect.css b/app/Views/_assets/styles/multiSelect.css new file mode 100644 index 0000000000000000000000000000000000000000..83fe9c76a7ae6f375c098709ced7be10cf432cf7 --- /dev/null +++ b/app/Views/_assets/styles/multiSelect.css @@ -0,0 +1,180 @@ +/*=============================== += MultiSelect = +===============================*/ +.multiselect { + @apply relative; + + &:focus { + @apply shadow-outline outline-none; + } + &:last-child { + @apply mb-0; + } + &.is-disabled { + &.multiselect__inner, + &.multiselect__input { + @apply bg-gray-300 cursor-not-allowed select-none; + } + &.multiselect__item { + @apply cursor-not-allowed; + } + } + + & [hidden] { + @apply hidden; + } +} + +.multiselect[data-type*="select-multiple"], +.multiselect[data-type*="text"] { + & .multiselect__inner { + @apply cursor-text; + } + & .multiselect__button { + @apply relative inline-block w-2 pl-4 mt-0 mb-0 ml-1 opacity-75; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==); + background-size: 8px; + + &:hover, + &:focus { + @apply opacity-100; + } + } +} + +.multiselect__inner { + @apply inline-block w-full px-2 pt-2 pb-1 overflow-hidden align-top bg-white border rounded; + + &.is-focused, + &.is-open { + @apply shadow-outline; + } + &.is-open { + @apply rounded-b-none; + } + &.is-flipped.is-open { + @apply rounded-t-none; + } +} + +.multiselect__list { + @apply p-0 m-0 list-none; +} + +.multiselect__list--multiple { + @apply inline; + + & .multiselect__item { + @apply inline-flex px-2 py-1 mb-1 mr-2 text-sm text-white break-all bg-green-500 rounded; + + &[data-deletable] { + @apply pr-1; + } + & [dir="rtl"] { + @apply ml-2 mr-0; + } + &.is-highlighted { + @apply bg-green-700; + } + &.is-disabled { + @apply bg-gray-500; + } + } +} + +.multiselect__list--dropdown { + @apply absolute z-10 invisible w-full overflow-hidden break-all bg-white border border-t-0 rounded-b shadow-lg; + top: 100%; + will-change: visibility; + + &.is-active { + @apply visible; + } + &.is-open { + @apply shadow-outline; + } + &.is-flipped { + @apply top-auto mt-0 rounded-t; + bottom: 100%; + } + & .multiselect__list { + @apply relative overflow-auto; + max-height: 300px; + -webkit-overflow-scrolling: touch; + will-change: scroll-position; + } + & .multiselect__item { + @apply relative p-3; + + & [dir="rtl"] { + @apply text-right; + } + } + & .multiselect__item--selectable { + @screen sm { + padding-right: 100px; + &:after { + @apply absolute text-sm transform -translate-y-1/2 opacity-0; + content: attr(data-select-text); + right: 10px; + top: 50%; + } + & [dir="rtl"] { + @apply text-right; + padding-left: 100px; + padding-right: 10px; + &:after { + @apply right-auto; + left: 10px; + } + } + } + &.is-highlighted { + @apply bg-gray-100; + &:after { + @apply opacity-50; + } + } + } +} + +.multiselect__item { + @apply cursor-default; +} + +.multiselect__item--selectable { + @apply cursor-pointer; +} + +.multiselect__item--disabled { + @apply opacity-50 cursor-not-allowed select-none; +} + +.multiselect__heading { + @apply p-3 font-semibold text-gray-600 border-b; +} + +.multiselect__button { + @apply bg-transparent bg-center bg-no-repeat border-0 appearance-none cursor-pointer; + text-indent: -9999px; + + &:focus { + @apply outline-none; + } +} + +.multiselect__input { + @apply inline-block max-w-full py-1 pl-1 mb-1 align-baseline bg-transparent border-0 rounded-none; + &:focus { + @apply outline-none; + } + & [dir="rtl"] { + @apply pl-0 pr-1; + } +} + +.multiselect__placeholder { + @apply opacity-50; +} + +/*===== End of Choices ======*/ diff --git a/app/Views/_assets/styles/radioBtn.css b/app/Views/_assets/styles/radioBtn.css new file mode 100644 index 0000000000000000000000000000000000000000..7e6045d6f4679201e5df9ec34d94407815f04cd4 --- /dev/null +++ b/app/Views/_assets/styles/radioBtn.css @@ -0,0 +1,24 @@ +.form-radio-btn { + @apply absolute opacity-0; +} + +.form-radio-btn:focus + label { + @apply shadow-outline; +} + +.form-radio-btn + label { + @apply px-2 py-1 text-sm text-black bg-white border rounded cursor-pointer; + + &:hover { + @apply bg-green-100; + } +} + +.form-radio-btn:checked + label { + @apply text-white bg-green-500; + + &::before { + @apply mr-2 text-green-200; + content: "✓"; + } +} diff --git a/app/Views/_assets/styles/switch.css b/app/Views/_assets/styles/switch.css new file mode 100644 index 0000000000000000000000000000000000000000..be0cbe0f35890519ba966806325c0fe37b6c5315 --- /dev/null +++ b/app/Views/_assets/styles/switch.css @@ -0,0 +1,26 @@ +.form-switch { + @apply absolute w-0 h-0 opacity-0; + + &:checked + .form-switch-slider { + @apply bg-green-500; + } + + &:focus + .form-switch-slider { + @apply shadow-outline; + } + + &:checked + .form-switch-slider::before { + @apply transform translate-x-5; + } +} + +.form-switch-slider { + @apply relative inset-0 flex-shrink-0 w-10 h-5 transition duration-200 bg-gray-400 rounded-full cursor-pointer; + + &::before { + @apply absolute w-4 h-4 transition duration-200 bg-white rounded-full shadow-xs; + content: ""; + left: 2px; + bottom: 2px; + } +} diff --git a/app/Views/_layout.php b/app/Views/_layout.php index 5bd01391eecfe163ffa327580961b93ace537aac..c2ce491826353a0c4dcc9c9f0ec0acf743e3cca1 100644 --- a/app/Views/_layout.php +++ b/app/Views/_layout.php @@ -1,6 +1,6 @@ <?= helper('page') ?> <!DOCTYPE html> -<html lang="en"> +<html lang="<?= service('request')->getLocale() ?>"> <head> <meta charset="UTF-8"/> @@ -9,9 +9,6 @@ <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"/> - <?php if (isset($podcast)): ?> - <?= $podcast->custom_html_head ?> - <?php endif; ?> </head> <body class="flex flex-col min-h-screen mx-auto"> @@ -25,6 +22,9 @@ </main> <footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t"> <?= render_page_links() ?> - <p>Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.</p> + <small><?= lang('Common.powered_by', [ + 'castopod' => + '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + ]) ?></small> </footer> </body> diff --git a/app/Views/admin/_header.php b/app/Views/admin/_header.php deleted file mode 100644 index 274f52dc63023277bc90fc5c71bb93a34116ef0a..0000000000000000000000000000000000000000 --- a/app/Views/admin/_header.php +++ /dev/null @@ -1,28 +0,0 @@ -<header class="<?= $class ?>"> - <div class="w-64"> - <a href="<?= route_to( - 'admin' - ) ?>" class="inline-flex items-center text-xl"> - <?= svg('logo-castopod', 'h-10 mr-2') ?> - Admin - </a> - </div> - <?= render_breadcrumb() ?> - <div class="relative ml-auto" data-toggle="dropdown"> - <button type="button" class="inline-flex items-center px-2 py-1 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> - Hey <?= user()->username ?> - <?= icon('caret-down', 'ml-2') ?> - </button> - <nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="bottom-end"> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'my-account' - ) ?>">My Account</a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'change-password' - ) ?>">Change password</a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'logout' - ) ?>">Logout</a> - </nav> - </div> -</header> \ No newline at end of file diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index f87c44f151c22d3f91e16bb9ea922f2a7e0f8b54..8cd3c3f166499691d2e82718a4f07a1444b5a029 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -1,31 +1,54 @@ <!DOCTYPE html> -<html lang="en"> +<html lang="<?= service('request')->getLocale() ?>"> <head> <meta charset="UTF-8"/> - <title>Castopod Admin</title> + <title><?= $this->renderSection('title') ?> | Castopod Admin</title> <meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/> <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/admin.css"/> <link rel="stylesheet" href="/assets/index.css"/> + <script src="/assets/admin.js" defer></script> </head> -<body class="min-h-screen bg-gray-100 holy-grail-grid"> - <?= view('admin/_header', [ - 'class' => 'flex items-center px-4 py-2 holy-grail-header', - ]) ?> - <?= view('admin/_sidenav', [ - 'class' => 'flex flex-col w-64 py-6 holy-grail-sidenav', - ]) ?> - <main class="container px-4 py-6 mx-auto holy-grail-main"> - <h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1> - <?= view('_message_block') ?> - <?= $this->renderSection('content') ?> +<body class="relative bg-gray-100 holy-grail-grid"> + <div id="sidebar-backdrop" role="button" tabIndex="0" aria-label="Close" class="fixed z-50 hidden w-full h-full bg-gray-900 bg-opacity-50 md:hidden"></div> + <aside id="admin-sidebar" class="sticky top-0 z-50 flex flex-col w-64 max-h-screen transition duration-200 ease-in-out transform -translate-x-full bg-white border-r holy-grail-sidebar md:translate-x-0"> + <?php if (isset($podcast)): ?> + <?= $this->include('admin/podcast/_sidebar') ?> + <?php else: ?> + <?= $this->include('admin/_sidebar') ?> + <?php endif; ?> + </aside> + <main class="overflow-hidden holy-grail-main"> + <header class="text-white bg-gradient-to-tr from-gray-900 to-gray-800"> + <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( + 'pageTitle' + ) ?></h1> + </div> + <div class="flex flex-wrap gap-y-2"><?= $this->renderSection( + 'headerRight' + ) ?></div> + </div> + </header> + <div class="container px-2 py-8 mx-auto md:px-12"> + <?= view('_message_block') ?> + <?= $this->renderSection('content') ?> + </div> </main> - <footer class="w-full px-2 py-4 mx-auto text-xs text-right border-t holy-grail-footer"> - Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative. + <footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer"> + <small><?= lang('Common.powered_by', [ + 'castopod' => + '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + ]) ?></small> </footer> - - <script src="/assets/admin.js"></script> + <button + type="button" + id="sidebar-toggler" + class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline" + style="transform: translateX(0px);"><?= icon('menu') ?></button> </body> diff --git a/app/Views/admin/_partials/_episode-card.php b/app/Views/admin/_partials/_episode-card.php deleted file mode 100644 index 4ff75d35ce94b6439bc99644c0713b4c1fce548f..0000000000000000000000000000000000000000 --- a/app/Views/admin/_partials/_episode-card.php +++ /dev/null @@ -1,44 +0,0 @@ -<article class="flex w-full max-w-lg mb-4 bg-white border rounded shadow"> - <img - loading="lazy" - src="<?= $episode->image->thumbnail_url ?>" - alt="<?= $episode->title ?>" class="object-cover w-32 h-32 rounded-l" /> - <div class="flex flex-col flex-1 px-4 py-2"> - <a href="<?= route_to( - 'episode-view', - $episode->podcast->id, - $episode->id - ) ?>"> - <h3 class="text-xl font-semibold"> - <span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span> - <span class="text-base font-bold text-gray-600">#<?= $episode->number ?></span> - </h3> - </a> - <div class="relative ml-auto" data-toggle="dropdown"> - <button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> - <?= icon('more') ?> - </button> - <nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="bottom-start" data-popper-offset-x="0" data-popper-offset-y="0" > - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode-edit', - $episode->podcast->id, - $episode->id - ) ?>"><?= lang('Episode.edit') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode', - $episode->podcast->name, - $episode->slug - ) ?>"><?= lang('Episode.go_to_page') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode-delete', - $episode->podcast->id, - $episode->id - ) ?>"><?= lang('Episode.delete') ?></a> - </nav> - </div> - <audio controls class="mt-auto" preload="none"> - <source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>"> - Your browser does not support the audio tag. - </audio> - </div> -</article> \ No newline at end of file diff --git a/app/Views/admin/_partials/_episode-list.php b/app/Views/admin/_partials/_episode-list.php deleted file mode 100644 index d396e43be9d93b02505fafc6d3d2922a66ba28b5..0000000000000000000000000000000000000000 --- a/app/Views/admin/_partials/_episode-list.php +++ /dev/null @@ -1,11 +0,0 @@ -<div class="flex flex-col py-4"> - <?php if ($episodes): ?> - <?php foreach ($episodes as $episode): ?> - <?= view('admin/_partials/_episode-card', [ - 'episode' => $episode, - ]) ?> - <?php endforeach; ?> - <?php else: ?> - <p class="italic"><?= lang('Podcast.no_episode') ?></p> - <?php endif; ?> -</div> \ No newline at end of file diff --git a/app/Views/admin/_partials/_podcast-card.php b/app/Views/admin/_partials/_podcast-card.php deleted file mode 100644 index 00d7f441166acabb091b2bcd3daea66e00869d4c..0000000000000000000000000000000000000000 --- a/app/Views/admin/_partials/_podcast-card.php +++ /dev/null @@ -1,29 +0,0 @@ -<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow"> - <img - alt="<?= $podcast->title ?>" - src="<?= $podcast->image - ->thumbnail_url ?>" class="object-cover w-full h-40" /> - <div class="p-2"> - <a href="<?= route_to( - 'podcast-view', - $podcast->id - ) ?>" class="hover:underline"> - <h2 class="font-semibold"><?= $podcast->title ?></h2> - </a> - <p class="text-gray-600">@<?= $podcast->name ?></p> - </div> - <footer class="flex items-center justify-end p-2"> - <a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to( - 'podcast-edit', - $podcast->id - ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang( - 'Podcast.edit' -) ?>"><?= icon('edit') ?></a> - <a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to( - 'podcast-view', - $podcast->id - ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang( - 'Podcast.view' -) ?>"><?= icon('eye') ?></a> - </footer> -</article> diff --git a/app/Views/admin/_partials/_user_info.php b/app/Views/admin/_partials/_user_info.php index 576311f197a6651403d56b3e10f0c3f15c70b53d..e116b04e5ea2d8b7241702f22c7a795510d119f8 100644 --- a/app/Views/admin/_partials/_user_info.php +++ b/app/Views/admin/_partials/_user_info.php @@ -1,6 +1,6 @@ <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <dt class="text-sm font-medium leading-5 text-gray-500"> - Email + <?= lang('User.form.email') ?> </dt> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <?= $user->email ?> @@ -8,7 +8,7 @@ </div> <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <dt class="text-sm font-medium leading-5 text-gray-500"> - Username + <?= lang('User.form.username') ?> </dt> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <?= $user->username ?> @@ -16,7 +16,7 @@ </div> <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <dt class="text-sm font-medium leading-5 text-gray-500"> - Roles + <?= lang('User.form.roles') ?> </dt> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> [<?= implode(', ', $user->roles) ?>] @@ -24,7 +24,7 @@ </div> <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <dt class="text-sm font-medium leading-5 text-gray-500"> - Permissions + <?= lang('User.form.permissions') ?> </dt> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> [<?= implode(', ', $user->permissions) ?>] diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidebar.php similarity index 50% rename from app/Views/admin/_sidenav.php rename to app/Views/admin/_sidebar.php index 5e0b205e38cb873479c344fb2cb6b64875b93d60..d6e1bf49d751fff870c339a4a0a1fd977d690e04 100644 --- a/app/Views/admin/_sidenav.php +++ b/app/Views/admin/_sidebar.php @@ -9,7 +9,19 @@ $navigation = [ 'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']], ]; ?> -<nav class="<?= $class ?>"> +<a href="<?= route_to( + 'admin' +) ?>" class="inline-flex items-center px-4 py-2 mb-2 text-xl"> + <?= svg('logo-castopod', 'h-8 mr-2') ?> + Castopod +</a> +<a href="<?= route_to( + 'home' +) ?>" class="inline-flex items-center px-6 py-2 mb-2 text-sm underline outline-none hover:no-underline focus:shadow-outline"> + <?= lang('AdminNavigation.go_to_website') ?> + <?= icon('external-link', 'ml-2 text-gray-500') ?> +</a> +<nav class="flex flex-col flex-1 overflow-y-auto"> <?php foreach ($navigation as $section => $data): ?> <div class="mb-4"> <button class="inline-flex items-center w-full px-6 py-1 outline-none focus:shadow-outline" type="button"> @@ -30,11 +42,23 @@ $navigation = [ </ul> </div> <?php endforeach; ?> - - <a href="<?= route_to( - 'home' - ) ?>" class="inline-flex items-center px-4 py-1 mt-auto text-sm underline outline-none hover:no-underline focus:shadow-outline"> - <?= lang('AdminNavigation.go_to_website') ?> - <?= icon('external-link', 'ml-2 text-gray-500') ?> - </a> </nav> +<div class="w-full mt-auto border-t" data-toggle="dropdown"> + <button type="button" class="inline-flex items-center w-full px-6 py-2 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> + <?= icon('user', 'text-gray-500 mr-2') ?> + <?= user()->username ?> + <?= icon('caret-right', 'ml-auto') ?> + </button> + <nav class="absolute z-50 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="right-end"> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'my-account' + ) ?>">My Account</a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'change-password' + ) ?>">Change password</a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'logout' + ) ?>">Logout</a> + </nav> +</div> + diff --git a/app/Views/admin/contributor/add.php b/app/Views/admin/contributor/add.php index 2f69a13aca6b3ad22e68b5310e028a06dd4793e0..8d8530d83760215e39622f81eeb56b16dac12a92 100644 --- a/app/Views/admin/contributor/add.php +++ b/app/Views/admin/contributor/add.php @@ -4,6 +4,10 @@ <?= lang('Contributor.add_contributor', [$podcast->title]) ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Contributor.add_contributor', [$podcast->title]) ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> @@ -26,11 +30,12 @@ 'required' => 'required', ]) ?> -<?= form_button([ - 'content' => lang('Contributor.form.submit_add'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Contributor.form.submit_add'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/contributor/edit.php b/app/Views/admin/contributor/edit.php index 3a2c73addaaca85a8e4fbe2c6d85cc777bd1f47e..cc995aeecf730d8b4b727f2797c066e38fa6c232 100644 --- a/app/Views/admin/contributor/edit.php +++ b/app/Views/admin/contributor/edit.php @@ -4,6 +4,10 @@ <?= lang('Contributor.edit_role', [$user->username]) ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Contributor.edit_role', [$user->username]) ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> @@ -19,11 +23,12 @@ 'required' => 'required', ]) ?> -<?= form_button([ - 'content' => lang('Contributor.form.submit_edit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Contributor.form.submit_edit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php index e3c5e9874f16ada7c5bf85cb254367e834e58480..8d55150c8031a0161bc7894ba441d4640efec3b3 100644 --- a/app/Views/admin/contributor/list.php +++ b/app/Views/admin/contributor/list.php @@ -2,47 +2,67 @@ <?= $this->section('title') ?> <?= lang('Contributor.podcast_contributors') ?> -<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'contributor-add', - $podcast->id -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Contributor.add') ?></a> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= lang('Contributor.podcast_contributors') ?> +<?= $this->endSection() ?> + +<?= $this->section('headerRight') ?> +<?= button(lang('Contributor.add'), route_to('contributor-add', $podcast->id), [ + 'variant' => 'primary', + 'iconLeft' => 'add', +]) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> -<table class="table-auto"> - <thead> - <tr> - <th class="px-4 py-2">Username</th> - <th class="px-4 py-2">Role</th> - <th class="px-4 py-2">Actions</th> - </tr> - </thead> - <tbody> - <?php foreach ($podcast->contributors as $contributor): ?> - <tr> - <td class="px-4 py-2 border"><?= $contributor->username ?></td> - <td class="px-4 py-2 border"><?= lang( - 'Contributor.roles.' . $contributor->podcast_role - ) ?></td> - <td class="px-4 py-2 border"> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( - 'contributor-edit', - $podcast->id, - $contributor->id - ) ?>"><?= lang('Contributor.edit') ?></a> - <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( - 'contributor-remove', - $podcast->id, - $contributor->id - ) ?>"><?= lang('Contributor.remove') ?></a> - </td> - </tr> - <?php endforeach; ?> - </tbody> -</table> +<?= data_table( + [ + [ + 'header' => lang('Contributor.list.username'), + 'cell' => function ($contributor) { + return $contributor->username; + }, + ], + [ + 'header' => lang('Contributor.list.role'), + 'cell' => function ($contributor) { + return lang('Contributor.roles.' . $contributor->podcast_role); + }, + ], + [ + 'header' => lang('Common.actions'), + 'cell' => function ($contributor, $podcast) { + return button( + lang('Contributor.edit'), + route_to( + 'contributor-edit', + $podcast->id, + $contributor->id + ), + [ + 'variant' => 'info', + 'size' => 'small', + ], + ['class' => 'mr-2'] + ) . + button( + lang('Contributor.remove'), + route_to( + 'contributor-remove', + $podcast->id, + $contributor->id + ), + ['variant' => 'danger', 'size' => 'small'], + ['class' => 'mr-2'] + ); + }, + ], + ], + $podcast->contributors, + $podcast +) ?> <?= $this->endSection() ?> diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php index 12e34057a6123c7907c4417feff14b1bf7f827fc..214ccaaf7a8fc2b9df7707f5a50056cbe13f289b 100644 --- a/app/Views/admin/dashboard.php +++ b/app/Views/admin/dashboard.php @@ -1,6 +1,14 @@ +<?= helper('components') ?> <?= $this->extend('admin/_layout') ?> <?= $this->section('title') ?> -Welcome to the admin dashboard! +Dashboard <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +Admin dashboard +<?= $this->endSection() ?> + +<?= $this->section('content') ?> +Welcome to the admin area! +<?= $this->endsection() ?> diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php index 54e9832a2d6399c1efc86ef91759c49da92f3640..30bf7fc1e534766bf3a8b9870ddda93a98965f02 100644 --- a/app/Views/admin/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -4,26 +4,44 @@ <?= lang('Episode.create') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Episode.create') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> <?= form_open_multipart(route_to('episode-create', $podcast->id), [ 'method' => 'post', - 'class' => 'flex flex-col max-w-md', + 'class' => 'flex flex-col', ]) ?> <?= csrf_field() ?> -<?= form_label(lang('Episode.form.enclosure'), 'enclosure') ?> -<?= form_input([ - 'id' => 'enclosure', - 'name' => 'enclosure', - 'class' => 'form-input mb-4', - 'required' => 'required', - 'type' => 'file', - 'accept' => '.mp3,.m4a', -]) ?> +<div class="flex w-full mb-6"> + <?= form_input([ + 'id' => 'enclosure', + 'name' => 'enclosure', + 'class' => 'form-enclosure-input', + 'required' => 'required', + 'type' => 'file', + 'accept' => '.mp3,.m4a', + ]) ?> + <label for="enclosure"><?= icon('upload', 'mr-2 text-') ?> + <span><?= lang('Episode.form.enclosure') ?></span></label> +</div> + +<?= form_section( + lang('Episode.form.info_section_title'), + lang('Episode.form.info_section_subtitle') +) ?> -<?= form_label(lang('Episode.form.image'), 'image') ?> +<?= form_label( + lang('Episode.form.image'), + 'image', + [], + lang('Episode.form.image_hint'), + true +) ?> <?= form_input([ 'id' => 'image', 'name' => 'image', @@ -35,7 +53,12 @@ 'Common.forms.image_size_hint' ) ?></small> -<?= form_label(lang('Episode.form.title'), 'title') ?> +<?= form_label( + lang('Episode.form.title'), + 'title', + [], + lang('Episode.form.title_hint') +) ?> <?= form_input([ 'id' => 'title', 'name' => 'title', @@ -45,7 +68,12 @@ 'data-slugify' => 'title', ]) ?> -<?= form_label(lang('Episode.form.slug'), 'slug') ?> +<?= form_label( + lang('Episode.form.slug'), + 'slug', + [], + lang('Episode.form.slug_hint') +) ?> <?= form_input([ 'id' => 'slug', 'name' => 'slug', @@ -55,6 +83,74 @@ 'data-slugify' => 'slug', ]) ?> +<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row"> + <div class="flex flex-col flex-1"> + <?= form_label(lang('Episode.form.season_number'), 'season_number') ?> + <?= form_input([ + 'id' => 'season_number', + 'name' => 'season_number', + 'class' => 'form-input w-full', + 'value' => old('season_number'), + 'type' => 'number', + ]) ?> + </div> + <div class="flex flex-col flex-1"> + <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> + <?= form_input([ + 'id' => 'episode_number', + 'name' => 'episode_number', + 'class' => 'form-input w-full', + 'value' => old('episode_number'), + 'required' => 'required', + 'type' => 'number', + ]) ?> + </div> +</div> + + +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend> + <?= lang('Episode.form.type.label') . + hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + ['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'], + 'full', + old('type') ? old('type') == 'full' : true + ) ?> + <label for="full" class="inline-flex items-center"> + <?= lang('Episode.form.type.full') ?> + </label> + <?= form_radio( + ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'], + 'trailer', + old('type') ? old('type') == 'trailer' : false + ) ?> + <label for="trailer" class="inline-flex items-center"> + <?= lang('Episode.form.type.trailer') ?> + </label> + <?= form_radio( + [ + 'id' => 'bonus', + 'name' => 'type', + 'class' => 'form-radio-btn', + ], + 'bonus', + old('type') ? old('type') == 'bonus' : false + ) ?> + <label for="bonus" class="inline-flex items-center"> + <?= lang('Episode.form.type.bonus') ?> + </label> +<?= form_fieldset_close() ?> + +<?= form_section_close() ?> + + +<?= form_section( + lang('Episode.form.show_notes_section_title'), + lang('Episode.form.show_notes_section_subtitle') +) ?> + <div class="mb-4"> <?= form_label(lang('Episode.form.description'), 'description') ?> <?= form_textarea( @@ -69,6 +165,36 @@ ) ?> </div> +<div class="mb-4"> + <?= form_label( + lang('Episode.form.description_footer'), + 'description_footer', + [], + lang('Episode.form.description_footer_hint') + ) ?> + <?= form_textarea( + [ + 'id' => 'description_footer', + 'name' => 'description_footer', + 'class' => 'form-textarea', + ], + old( + 'description_footer', + $podcast->episode_description_footer ?? '', + false + ), + 'data-editor="markdown"' + ) ?> +</div> + +<?= form_section_close() ?> + + +<?= form_section( + lang('Episode.form.publication_section_title'), + 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"> @@ -99,76 +225,69 @@ </div> <?= form_fieldset_close() ?> -<?= form_label(lang('Episode.form.season_number'), 'season_number') ?> -<?= form_input([ - 'id' => 'season_number', - 'name' => 'season_number', - 'class' => 'form-input mb-4', - 'value' => old('season_number'), - 'type' => 'number', -]) ?> - -<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> -<?= form_input([ - 'id' => 'episode_number', - 'name' => 'episode_number', - 'class' => 'form-input mb-4', - 'value' => old('episode_number'), - 'required' => 'required', - 'type' => 'number', -]) ?> - -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'], - 'yes', - old('explicit', false) +<?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?> + <legend> + <?= lang('Episode.form.parental_advisory.label') . + hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + [ + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : true ) ?> - <span class="ml-2"><?= lang('Episode.form.explicit') ?></span> -</label> - -<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Episode.form.type.label') ?></legend> - <label for="full" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'full', 'name' => 'type', 'class' => 'form-radio'], - 'full', - old('type') ? old('type') == 'full' : true - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.full') ?></span> - </label> - <label for="trailer" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio'], - 'trailer', - old('type') ? old('type') == 'trailer' : false - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span> - </label> - <label for="bonus" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio'], - 'bonus', - old('type') ? old('type') == 'bonus' : false - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span> - </label> + <label for="undefined"><?= lang( + 'Episode.form.parental_advisory.undefined' + ) ?></label> + <?= form_radio( + [ + 'id' => 'clean', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'clean', + old('parental_advisory') ? old('parental_advisory') === 'clean' : false + ) ?> + <label for="clean"><?= lang( + 'Episode.form.parental_advisory.clean' + ) ?></label> + <?= form_radio( + [ + 'id' => 'explicit', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'explicit', + old('parental_advisory') + ? old('parental_advisory') === 'explicit' + : false + ) ?> + <label for="explicit"><?= lang( + 'Episode.form.parental_advisory.explicit' + ) ?></label> <?= form_fieldset_close() ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'], - 'yes', - old('block', false) - ) ?> - <span class="ml-2"><?= lang('Episode.form.block') ?></span> -</label> +<?= form_switch( + lang('Episode.form.block') . + hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'), + ['id' => 'block', 'name' => 'block'], + 'yes', + old('block', false) +) ?> -<?= form_button([ - 'content' => lang('Episode.form.submit_create'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= form_section_close() ?> + +<?= button( + lang('Episode.form.submit_create'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php index 9d0ad5211d9fc8508757e49db45453394e38559b..155013db42611d68874d7ae55ce16b54a49c4456 100644 --- a/app/Views/admin/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -4,25 +4,43 @@ <?= lang('Episode.edit') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Episode.edit') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> -<?= form_open_multipart( - route_to('episode-edit', $episode->podcast->id, $episode->id), - ['method' => 'post', 'class' => 'flex flex-col max-w-md'] -) ?> +<?= form_open_multipart(route_to('episode-edit', $podcast->id, $episode->id), [ + 'method' => 'post', + 'class' => 'flex flex-col', +]) ?> <?= csrf_field() ?> -<?= form_label(lang('Episode.form.enclosure'), 'enclosure') ?> -<?= form_input([ - 'id' => 'enclosure', - 'name' => 'enclosure', - 'class' => 'form-input mb-4', - 'type' => 'file', - 'accept' => '.mp3,.m4a', -]) ?> +<div class="flex w-full mb-6"> + <?= form_input([ + 'id' => 'enclosure', + 'name' => 'enclosure', + 'class' => 'form-enclosure-input', + 'type' => 'file', + 'accept' => '.mp3,.m4a', + ]) ?> + <label for="enclosure"><?= icon('upload', 'mr-2 text-') ?> + <span><?= lang('Episode.form.enclosure') ?></span></label> +</div> -<?= form_label(lang('Episode.form.image'), 'image') ?> +<?= form_section( + lang('Episode.form.info_section_title'), + lang('Episode.form.info_section_subtitle') +) ?> + +<?= form_label( + lang('Episode.form.image'), + 'image', + [], + lang('Episode.form.image_hint'), + true +) ?> <img src="<?= $episode->image->thumbnail_url ?>" alt="<?= $episode->title ?>" @@ -39,7 +57,12 @@ 'Common.forms.image_size_hint' ) ?></small> -<?= form_label(lang('Episode.form.title'), 'title') ?> +<?= form_label( + lang('Episode.form.title'), + 'title', + [], + lang('Episode.form.title_hint') +) ?> <?= form_input([ 'id' => 'title', 'name' => 'title', @@ -49,7 +72,12 @@ 'data-slugify' => 'title', ]) ?> -<?= form_label(lang('Episode.form.slug'), 'slug') ?> +<?= form_label( + lang('Episode.form.slug'), + 'slug', + [], + lang('Episode.form.slug_hint') +) ?> <?= form_input([ 'id' => 'slug', 'name' => 'slug', @@ -59,6 +87,69 @@ 'data-slugify' => 'slug', ]) ?> +<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row"> + <div class="flex flex-col flex-1"> + <?= form_label(lang('Episode.form.season_number'), 'season_number') ?> + <?= form_input([ + 'id' => 'season_number', + 'name' => 'season_number', + 'class' => 'form-input w-full', + 'value' => old('season_number', $episode->season_number), + 'type' => 'number', + ]) ?> + </div> + <div class="flex flex-col flex-1"> + <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> + <?= form_input([ + 'id' => 'episode_number', + 'name' => 'episode_number', + 'class' => 'form-input w-full', + 'value' => old('episode_number', $episode->number), + 'required' => 'required', + 'type' => 'number', + ]) ?> + </div> +</div> + +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend> + <?= lang('Episode.form.type.label') . + hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + ['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'], + 'full', + old('type') ? old('type') === 'full' : $episode->type === 'full' + ) ?> + <label for="full" class="inline-flex items-center"> + <?= lang('Episode.form.type.full') ?> + </label> + <?= form_radio( + ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'], + 'trailer', + old('type') ? old('type') === 'trailer' : $episode->type === 'trailer' + ) ?> + <label for="trailer" class="inline-flex items-center"> + <?= lang('Episode.form.type.trailer') ?> + </label> + <?= form_radio( + ['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio-btn'], + 'bonus', + old('type') ? old('type') === 'bonus' : $episode->type === 'bonus' + ) ?> + <label for="bonus" class="inline-flex items-center"> + <?= lang('Episode.form.type.bonus') ?> + </label> +<?= form_fieldset_close() ?> + +<?= form_section_close() ?> + + +<?= form_section( + lang('Episode.form.show_notes_section_title'), + lang('Episode.form.show_notes_section_subtitle') +) ?> + <div class="mb-4"> <?= form_label(lang('Episode.form.description'), 'description') ?> <?= form_textarea( @@ -73,6 +164,36 @@ ) ?> </div> +<div class="mb-4"> + <?= form_label( + lang('Episode.form.description_footer'), + 'description_footer', + [], + lang('Episode.form.description_footer_hint') + ) ?> + <?= form_textarea( + [ + 'id' => 'description_footer', + 'name' => 'description_footer', + 'class' => 'form-textarea', + ], + old( + 'description_footer', + $podcast->episode_description_footer ?? '', + false + ), + 'data-editor="markdown"' + ) ?> +</div> + +<?= form_section_close() ?> + + +<?= form_section( + lang('Episode.form.publication_section_title'), + 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"> @@ -111,76 +232,76 @@ </div> <?= form_fieldset_close() ?> -<?= form_label(lang('Episode.form.season_number'), 'season_number') ?> -<?= form_input([ - 'id' => 'season_number', - 'name' => 'season_number', - 'class' => 'form-input mb-4', - 'value' => old('season_number', $episode->season_number), - 'type' => 'number', -]) ?> - -<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> -<?= form_input([ - 'id' => 'episode_number', - 'name' => 'episode_number', - 'class' => 'form-input mb-4', - 'value' => old('episode_number', $episode->number), - 'required' => 'required', - 'type' => 'number', -]) ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'], - 'yes', - old('explicit', $episode->explicit) +<?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?> + <legend> + <?= lang('Episode.form.parental_advisory.label') . + hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + [ + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : $episode->parental_advisory === null ) ?> - <span class="ml-2"><?= lang('Episode.form.explicit') ?></span> -</label> - -<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Episode.form.type.label') ?></legend> - <label for="full" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'full', 'name' => 'type', 'class' => 'form-radio'], - 'full', - old('type') ? old('type') == 'full' : $episode->type == 'full' - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.full') ?></span> - </label> - <label for="trailer" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio'], - 'trailer', - old('type') ? old('type') == 'trailer' : $episode->type == 'trailer' - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span> - </label> - <label for="bonus" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio'], - 'bonus', - old('type') ? old('type') == 'bonus' : $episode->type == 'bonus' - ) ?> - <span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span> - </label> + <label for="undefined"><?= lang( + 'Episode.form.parental_advisory.undefined' + ) ?></label> + <?= form_radio( + [ + 'id' => 'clean', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'clean', + old('parental_advisory') + ? old('parental_advisory') === 'clean' + : $episode->parental_advisory === 'clean' + ) ?> + <label for="clean"><?= lang( + 'Episode.form.parental_advisory.clean' + ) ?></label> + <?= form_radio( + [ + 'id' => 'explicit', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'explicit', + old('parental_advisory') + ? old('parental_advisory') === 'explicit' + : $episode->parental_advisory === 'explicit' + ) ?> + <label for="explicit"><?= lang( + 'Episode.form.parental_advisory.explicit' + ) ?></label> <?= form_fieldset_close() ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'], - 'yes', - old('block', $episode->block) - ) ?> - <span class="ml-2"><?= lang('Episode.form.block') ?></span> -</label> +<?= form_switch( + lang('Episode.form.block') . + hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'), + ['id' => 'block', 'name' => 'block'], + 'yes', + old( + 'block', -<?= form_button([ - 'content' => lang('Episode.form.submit_edit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> + $episode->block + ) +) ?> + +<?= form_section_close() ?> + +<?= button( + lang('Episode.form.submit_edit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index c6cfe92da31986c270bb7176863a0c7d039cf3e8..5adf9c8cd9b05f3321954a16b5ca30dad5805bb5 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -1,23 +1,130 @@ <?= $this->extend('admin/_layout') ?> <?= $this->section('title') ?> +<?= lang('Episode.all_podcast_episodes') ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= lang('Episode.all_podcast_episodes') ?> (<?= $pager->getDetails()[ + 'total' + ] ?>) +<?= $this->endSection() ?> -<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>) -<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'episode-create', - $podcast->id -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Episode.create') ?></a> +<?= $this->section('headerRight') ?> +<?= button( + lang('Episode.create'), + route_to('episode-create', $podcast->id), + ['variant' => 'primary', 'iconLeft' => 'add'] +) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> -<?= view('admin/_partials/_episode-list.php', [ - 'episodes' => $podcast->episodes, -]) ?> +<p class="mb-4 text-sm italic text-gray-700"><?= lang('Common.pageInfo', [ + 'currentPage' => $pager->getDetails()['currentPage'], + 'pageCount' => $pager->getDetails()['pageCount'], +]) ?></p> +<div class="flex flex-wrap mb-6"> + <?php if ($episodes): ?> + <?php foreach ($episodes as $episode): ?> + <article class="flex w-full max-w-lg p-4 mx-auto"> + <img + loading="lazy" + src="<?= $episode->image->thumbnail_url ?>" + alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" /> + <div class="flex flex-col flex-1"> + <div class="flex"> + <a class="flex-1 text-sm hover:underline" href="<?= route_to( + 'episode-view', + $podcast->id, + $episode->id + ) ?>"> + <h2 class="inline-flex justify-between w-full font-bold leading-none group"> + <span class="mr-1 group-hover:underline"><?= $episode->title ?></span> + <?php if ( + $episode->season_number && + $episode->number + ): ?> + <abbr class="text-xs font-bold text-gray-600" title="<?= lang( + 'Episode.season_episode', + [ + 'seasonNumber' => + $episode->season_number, + 'episodeNumber' => $episode->number, + ] + ) ?>"><?= lang('Episode.season_episode_abbr', [ + 'seasonNumber' => $episode->season_number, + 'episodeNumber' => $episode->number, +]) ?></abbr> + <?php elseif ( + !$episode->season_number && + $episode->number + ): ?> + <abbr class="text-xs font-bold text-gray-600" title="<?= lang( + 'Episode.number', + [ + 'episodeNumber' => $episode->number, + ] + ) ?>"><?= lang('Episode.number_abbr', [ + 'episodeNumber' => $episode->number, +]) ?></abbr> + <?php endif; ?> + </h2> + </a> + <div class="relative" data-toggle="dropdown"> + <button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> + <?= icon('more') ?> + </button> + <nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="bottom-end" data-popper-offset-x="0" data-popper-offset-y="-24" > + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode-edit', + $podcast->id, + $episode->id + ) ?>"><?= lang('Episode.edit') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode', + $podcast->name, + $episode->slug + ) ?>"><?= lang('Episode.go_to_page') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode-delete', + $podcast->id, + $episode->id + ) ?>"><?= lang('Episode.delete') ?></a> + </nav> + </div> + </div> + <div class="mb-2 text-xs"> + <time + pubdate + datetime="<?= $episode->published_at->toDateTimeString() ?>" + 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, + ]) ?> + </time> + </div> + <audio controls preload="none" class="w-full mt-auto"> + <source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>"> + Your browser does not support the audio tag. + </audio> + </div> + </article> + <?php endforeach; ?> + <?php else: ?> + <p class="italic"><?= lang('Podcast.no_episode') ?></p> + <?php endif; ?> +</div> + +<?= $pager->links() ?> <?= $this->endSection() ?> diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index d3ccaa1e31759e6b33b0823946fda17d2222a385..5ecb2031131198e69bab7af8b640125e43d2a536 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -4,40 +4,46 @@ <?= $episode->title ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= $episode->title ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> -<img - src="<?= $episode->image->medium_url ?>" - alt="Episode cover" - class="object-cover w-40 h-40 mb-6" -/> -<audio controls preload="none" class="mb-12"> - <source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>"> - Your browser does not support the audio tag. -</audio> +<div class="flex flex-wrap"> + <div class="w-full max-w-sm mb-6 md:mr-4"> + <img + src="<?= $episode->image->medium_url ?>" + alt="Episode cover" + class="object-cover w-full" + /> + <audio controls preload="none" class="w-full mb-6"> + <source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>"> + Your browser does not support the audio tag. + </audio> -<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( - 'episode-edit', - $episode->podcast->id, - $episode->id -) ?>"><?= lang('Episode.edit') ?></a> - <a href="<?= route_to( - 'episode', - $episode->podcast->name, - $episode->slug - ) ?>" class="inline-flex px-4 py-2 text-white bg-gray-700 hover:bg-gray-800"><?= lang( - 'Episode.go_to_page' -) ?></a> - <a href="<?= route_to( - 'episode-delete', - $episode->podcast->id, - $episode->id - ) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang( - 'Episode.delete' -) ?></a> + <div class="flex justify-around"> + <?= button( + lang('Episode.edit'), + route_to('episode-edit', $podcast->id, $episode->id), + ['variant' => 'info', 'iconLeft' => 'edit'] + ) ?> + <?= button( + lang('Episode.go_to_page'), + route_to('episode', $podcast->name, $episode->slug), + ['variant' => 'secondary', 'iconLeft' => 'external-link'] + ) ?> + <?= button( + lang('Episode.delete'), + route_to('episode-delete', $podcast->id, $episode->id), + ['variant' => 'danger', 'iconLeft' => 'delete-bin'] + ) ?> + </div> + </div> -<section class="prose"> -<?= $episode->description_html ?> -</section> + <section class="w-full max-w-sm prose"> + <?= $episode->description_html ?> + </section> +</div> <?= $this->endSection() ?> diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php index 285f058e6e0649ddc9179672595260e1e1c34219..9ae867b6a71e4331dac121d95efd2d28e7505f2d 100644 --- a/app/Views/admin/my_account/change_password.php +++ b/app/Views/admin/my_account/change_password.php @@ -31,11 +31,12 @@ 'autocomplete' => 'new-password', ]) ?> -<?= form_button([ - 'content' => lang('User.form.submit_password_change'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('User.form.submit_password_change'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/page/create.php b/app/Views/admin/page/create.php index 817a6c833fe1a35f48dee50b31397292d667b347..1147f9d9c2b354b35681868c18313a0683ab1b31 100644 --- a/app/Views/admin/page/create.php +++ b/app/Views/admin/page/create.php @@ -4,6 +4,10 @@ <?= lang('Page.create') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Page.create') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> @@ -46,11 +50,13 @@ ) ?> </div> -<?= form_button([ - 'content' => lang('Page.form.submit_create'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> + +<?= button( + lang('Page.form.submit_create'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/page/edit.php b/app/Views/admin/page/edit.php index 58b99b5e1221c969e2a039b6dcacff5cb019ae4a..9d23cddff32515291a04f1c0c436a55e488176ef 100644 --- a/app/Views/admin/page/edit.php +++ b/app/Views/admin/page/edit.php @@ -4,6 +4,10 @@ <?= lang('Page.edit') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Page.edit') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> @@ -46,11 +50,12 @@ ) ?> </div> -<?= form_button([ - 'content' => lang('Page.form.submit_edit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Page.form.submit_edit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/page/list.php b/app/Views/admin/page/list.php index fd2a962c7b16ef8027e2093eafbb7f7109700d8f..1db15fd73b28de88bc563caade74b31c64e34512 100644 --- a/app/Views/admin/page/list.php +++ b/app/Views/admin/page/list.php @@ -1,47 +1,62 @@ <?= $this->extend('admin/_layout') ?> <?= $this->section('title') ?> +<?= lang('Page.all_pages') ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> <?= lang('Page.all_pages') ?> (<?= count($pages) ?>) -<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'page-create' -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Page.create') ?></a> +<?= $this->endSection() ?> + +<?= $this->section('headerRight') ?> +<?= button(lang('Page.create'), route_to('page-create'), [ + 'variant' => 'primary', + 'iconLeft' => 'add', +]) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> -<table class="table-auto"> - <thead> - <tr> - <th class="px-4 py-2">Title</th> - <th class="px-4 py-2">Slug</th> - <th class="px-4 py-2">Actions</th> - </tr> - </thead> - <tbody> - <?php foreach ($pages as $page): ?> - <tr> - <td class="px-4 py-2 border"><?= $page->title ?></td> - <td class="px-4 py-2 border"><?= $page->slug ?></td> - <td class="px-4 py-2 border"> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to( - 'page', - $page->slug - ) ?>"><?= lang('Page.go_to_page') ?></a> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to( - 'page-edit', - $page->id - ) ?>"><?= lang('Page.edit') ?></a> - <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( - 'page-delete', - $page->id - ) ?>"><?= lang('Page.delete') ?></a> - </td> - </tr> - <?php endforeach; ?> - </tbody> -</table> +<?= data_table( + [ + [ + 'header' => lang('Page.page'), + 'cell' => function ($page) { + return '<div class="flex flex-col">' . + $page->title . + '<span class="text-sm text-gray-600">/' . + $page->slug . + '</span></div>'; + }, + ], + [ + 'header' => lang('Common.actions'), + 'cell' => function ($page) { + return button( + lang('Page.go_to_page'), + route_to('page', $page->slug), + [ + 'variant' => 'secondary', + 'size' => 'small', + ], + ['class' => 'mr-2'] + ) . + button( + lang('Page.edit'), + route_to('page-edit', $page->id), + ['variant' => 'info', 'size' => 'small'], + ['class' => 'mr-2'] + ) . + button( + lang('Page.delete'), + route_to('page-delete', $page->id), + ['variant' => 'danger', 'size' => 'small'] + ); + }, + ], + ], + $pages +) ?> <?= $this->endSection() ?> diff --git a/app/Views/admin/page/view.php b/app/Views/admin/page/view.php index 992550e4a0c54fef0baa64835af7c63f7bf834bc..7a64d6f13d1e346e43bfc9e80220478d1355469e 100644 --- a/app/Views/admin/page/view.php +++ b/app/Views/admin/page/view.php @@ -2,12 +2,17 @@ <?= $this->section('title') ?> <?= $page->title ?> -<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-teal-500 rounded shadow-xs outline-none hover:bg-teal-600 focus:shadow-outline" href="<?= route_to( - 'page-edit', - $page->id -) ?>"> -<?= icon('edit', 'mr-2') ?> -<?= lang('Page.edit') ?></a> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $page->title ?> +<?= $this->endSection() ?> + +<?= $this->section('headerRight') ?> +<?= button(lang('Page.edit'), route_to('page-edit', $page->id), [ + 'variant' => 'primary', + 'iconLeft' => 'add', +]) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php new file mode 100644 index 0000000000000000000000000000000000000000..23bc1f3ffcc52653f87c27fe7b4a410e5d4941eb --- /dev/null +++ b/app/Views/admin/podcast/_sidebar.php @@ -0,0 +1,94 @@ +<?php +$podcastNavigation = [ + 'dashboard' => [ + 'icon' => 'dashboard', + 'items' => ['podcast-view', 'podcast-edit'], + ], + 'episodes' => [ + 'icon' => 'mic', + 'items' => ['episode-list', 'episode-create'], + ], + 'analytics' => [ + 'icon' => 'line-chart', + 'items' => [], + ], + 'contributors' => [ + 'icon' => 'group', + 'items' => ['contributor-list', 'contributor-add'], + ], + 'settings' => [ + 'icon' => 'settings', + 'items' => ['platforms'], + ], +]; ?> + +<a href="<?= route_to( + 'admin' +) ?>" class="inline-flex items-center px-4 py-2 border-b"> + <?= icon('arrow-left', 'mr-4') ?> + <?= svg('logo-castopod', 'h-8 mr-2') ?> + Castopod +</a> +<div class="flex items-center border-b"> + <img + src="<?= $podcast->image->thumbnail_url ?>" + alt="<?= $podcast->title ?>" + class="object-cover w-16 h-16 mr-2" + /> + <div class="flex flex-col items-start flex-1"> + <span class="font-semibold truncate"><?= $podcast->title ?></span> + <a href="<?= route_to( + 'podcast', + $podcast->name + ) ?>" class="inline-flex items-center text-sm underline outline-none hover:no-underline focus:shadow-outline" + data-toggle="tooltip" data-placement="bottom" title="<?= lang( + 'PodcastNavigation.go_to_page' + ) ?>">@<?= $podcast->name ?> + <?= icon('external-link', 'ml-1 text-gray-500') ?> + </a> + </div> +</div> +<nav class="flex flex-col flex-1 py-6 overflow-y-auto"> + <?php foreach ($podcastNavigation as $section => $data): ?> + <div class="mb-4"> + <button class="inline-flex items-center w-full px-6 py-1 outline-none focus:shadow-outline" type="button"> + <?= icon($data['icon'], 'text-gray-500') ?> + <span class="ml-2"><?= lang( + 'PodcastNavigation.' . $section + ) ?></span> + </button> + <ul> + <?php foreach ($data['items'] as $item): ?> + <?php $isActive = + base_url(route_to($item, $podcast->id)) == current_url(); ?> + <li> + <a class="block py-1 pl-12 pr-2 text-sm text-gray-600 outline-none hover:text-gray-900 focus:shadow-outline <?= $isActive + ? 'font-semibold text-gray-900' + : '' ?>" href="<?= route_to( + $item, + $podcast->id +) ?>"><?= lang('PodcastNavigation.' . $item) ?></a> + </li> + <?php endforeach; ?> + </ul> + </div> + <?php endforeach; ?> +</nav> +<div class="w-full mt-auto border-t" data-toggle="dropdown"> + <button type="button" class="inline-flex items-center w-full px-6 py-2 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> + <?= icon('user', 'text-gray-500 mr-2') ?> + <?= user()->username ?> + <?= icon('caret-right', 'ml-auto') ?> + </button> + <nav class="absolute z-50 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="right-end"> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'my-account' + ) ?>">My Account</a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'change-password' + ) ?>">Change password</a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'logout' + ) ?>">Logout</a> + </nav> +</div> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index 4832413d06d8a196c47963dd2cf141a18d24bca5..138dad7dc965cc950c7cf5d03301a464cf9d18ca 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -4,20 +4,30 @@ <?= lang('Podcast.create') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Podcast.create') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> <?= form_open_multipart(route_to('podcast-create'), [ 'method' => 'post', - 'class' => 'flex flex-col max-w-md', + 'class' => 'flex flex-col', ]) ?> <?= csrf_field() ?> +<?= form_section( + lang('Podcast.form.identity_section_title'), + lang('Podcast.form.identity_section_subtitle') +) ?> + <?= form_label(lang('Podcast.form.image'), 'image') ?> <?= form_input([ 'id' => 'image', 'name' => 'image', 'class' => 'form-input', + 'required' => 'required', 'type' => 'file', 'accept' => '.jpg,.jpeg,.png', @@ -35,7 +45,12 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.name'), 'name') ?> +<?= form_label( + lang('Podcast.form.name'), + 'name', + [], + lang('Podcast.form.name_hint') +) ?> <?= form_input([ 'id' => 'name', 'name' => 'name', @@ -44,6 +59,33 @@ 'required' => 'required', ]) ?> +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend> + <?= lang('Podcast.form.type.label') . + hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + [ + 'id' => 'episodic', + 'name' => 'type', + 'class' => 'form-radio-btn', + ], + 'episodic', + old('type') ? old('type') == 'episodic' : true + ) ?> + <label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label> + <?= form_radio( + [ + 'id' => 'serial', + 'name' => 'type', + 'class' => 'form-radio-btn', + ], + 'serial', + old('type') ? old('type') == 'serial' : false + ) ?> + <label for="serial"><?= lang('Podcast.form.type.serial') ?></label> +<?= form_fieldset_close() ?> + <div class="mb-4"> <?= form_label(lang('Podcast.form.description'), 'description') ?> <?= form_textarea( @@ -58,21 +100,13 @@ ) ?> </div> -<div class="mb-4"> - <?= form_label( - lang('Podcast.form.episode_description_footer'), - 'episode_description_footer' - ) ?> - <?= form_textarea( - [ - 'id' => 'episode_description_footer', - 'name' => 'episode_description_footer', - 'class' => 'form-textarea', - ], - old('episode_description_footer', '', false), - 'data-editor="markdown"' - ) ?> -</div> +<?= form_section_close() ?> + + +<?= form_section( + lang('Podcast.form.classification_section_title'), + lang('Podcast.form.classification_section_subtitle') +) ?> <?= form_label(lang('Podcast.form.language'), 'language') ?> <?= form_dropdown('language', $languageOptions, old('language', $browserLang), [ @@ -88,16 +122,87 @@ 'required' => 'required', ]) ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'], - 'yes', - old('explicit', false) +<?= form_label( + lang('Podcast.form.other_categories'), + + 'other_categories', + [], + '', + true +) ?> +<?= form_multiselect( + 'other_categories[]', + $categoryOptions, + old('other_categories', []), + [ + 'id' => 'other_categories', + 'class' => 'mb-4', + 'required' => 'required', + 'data-max-item-count' => '2', + ] +) ?> + +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend> + <?= lang('Podcast.form.parental_advisory.label') . + hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + [ + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : true ) ?> - <span class="ml-2"><?= lang('Podcast.form.explicit') ?></span> -</label> + <label for="undefined"><?= lang( + 'Podcast.form.parental_advisory.undefined' + ) ?></label> + <?= form_radio( + [ + 'id' => 'clean', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'clean', + old('parental_advisory') ? old('parental_advisory') === 'clean' : false + ) ?> + <label for="clean"><?= lang( + 'Podcast.form.parental_advisory.clean' + ) ?></label> + <?= form_radio( + [ + 'id' => 'explicit', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'explicit', + old('parental_advisory') + ? old('parental_advisory') === 'explicit' + : false + ) ?> + <label for="explicit"><?= lang( + 'Podcast.form.parental_advisory.explicit' + ) ?></label> +<?= form_fieldset_close() ?> + +<?= form_section_close() ?> -<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?> + +<?= form_section( + lang('Podcast.form.author_section_title'), + lang('Podcast.form.author_section_subtitle') +) ?> + +<?= form_label( + lang('Podcast.form.owner_name'), + 'owner_name', + [], + lang('Podcast.form.owner_name_hint') +) ?> <?= form_input([ 'id' => 'owner_name', 'name' => 'owner_name', @@ -106,7 +211,12 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?> +<?= form_label( + lang('Podcast.form.owner_email'), + 'owner_email', + [], + lang('Podcast.form.owner_email_hint') +) ?> <?= form_input([ 'id' => 'owner_email', 'name' => 'owner_email', @@ -116,37 +226,21 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.author'), 'author') ?> +<?= form_label( + lang('Podcast.form.publisher'), + 'publisher', + [], + lang('Podcast.form.publisher_hint'), + true +) ?> <?= form_input([ - 'id' => 'author', - 'name' => 'author', + 'id' => 'publisher', + 'name' => 'publisher', 'class' => 'form-input mb-4', - 'value' => old('author'), + 'value' => old('publisher'), ]) ?> -<?= form_fieldset('', [ - 'class' => 'flex flex-col mb-4', -]) ?> - <legend><?= lang('Podcast.form.type.label') ?></legend> - <label for="episodic" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio'], - 'episodic', - old('type') ? old('type') == 'episodic' : true - ) ?> - <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span> - </label> - <label for="serial" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio'], - 'serial', - old('type') ? old('type') == 'serial' : false - ) ?> - <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span> - </label> -<?= form_fieldset_close() ?> - -<?= form_label(lang('Podcast.form.copyright'), 'copyright') ?> +<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?> <?= form_input([ 'id' => 'copyright', 'name' => 'copyright', @@ -154,42 +248,39 @@ 'value' => old('copyright'), ]) ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'], - 'yes', - old('block', false) - ) ?> - <span class="ml-2"><?= lang('Podcast.form.block') ?></span> -</label> - -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'complete', 'name' => 'complete', 'class' => 'form-checkbox'], - 'yes', - old('complete', false) - ) ?> - <span class="ml-2"><?= lang('Podcast.form.complete') ?></span> -</label> +<?= form_section_close() ?> -<div class="mb-4"> - <?= form_label(lang('Podcast.form.custom_html_head'), 'custom_html_head') ?> - <?= form_textarea( - [ - 'id' => 'custom_html_head', - 'name' => 'custom_html_head', - 'class' => 'form-textarea', - ], - old('custom_html_head', '', false), - 'data-editor="html"' - ) ?> -</div> -<?= form_button([ - 'content' => lang('Podcast.form.submit_create'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= form_section( + lang('Podcast.form.status_section_title'), + lang('Podcast.form.status_section_subtitle') +) ?> + +<?= form_switch( + lang('Podcast.form.block'), + ['id' => 'block', 'name' => 'block'], + 'yes', + old('block', false), + 'mb-2' +) ?> + +<?= form_switch( + lang('Podcast.form.complete'), + ['id' => 'complete', 'name' => 'complete'], + 'yes', + old('complete', false) +) ?> + +<?= form_section_close() ?> + + +<?= button( + lang('Podcast.form.submit_create'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + <?= form_close() ?> diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php index 73271a3f28522b24823bda354255445c3e9e30f8..8b6b1c6c06c200acd84d6518ba5f69382ec955e5 100644 --- a/app/Views/admin/podcast/edit.php +++ b/app/Views/admin/podcast/edit.php @@ -4,15 +4,23 @@ <?= lang('Podcast.edit') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Podcast.edit') ?> +<?= $this->endSection() ?> <?= $this->section('content') ?> <?= form_open_multipart(route_to('podcast-edit', $podcast->id), [ 'method' => 'post', - 'class' => 'flex flex-col max-w-md', + 'class' => 'flex flex-col', ]) ?> <?= csrf_field() ?> +<?= form_section( + lang('Podcast.form.identity_section_title'), + lang('Podcast.form.identity_section_subtitle') +) ?> + <?= form_label(lang('Podcast.form.image'), 'image') ?> <img src="<?= $podcast->image->thumbnail_url ?>" @@ -39,7 +47,12 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.name'), 'name') ?> +<?= form_label( + lang('Podcast.form.name'), + 'name', + [], + lang('Podcast.form.name_hint') +) ?> <?= form_input([ 'id' => 'name', 'name' => 'name', @@ -48,6 +61,24 @@ 'required' => 'required', ]) ?> +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend><?= lang('Podcast.form.type.label') . + hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'], + 'episodic', + old('type') ? old('type') == 'episodic' : $podcast->type == 'episodic' + ) ?> + <label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label> + <?= form_radio( + ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'], + 'serial', + old('type') ? old('type') == 'serial' : $podcast->type == 'serial' + ) ?> + <label for="serial"><?= lang('Podcast.form.type.serial') ?></label> +<?= form_fieldset_close() ?> + <div class="mb-4"> <?= form_label(lang('Podcast.form.description'), 'description') ?> <?= form_textarea( @@ -62,25 +93,13 @@ ) ?> </div> -<div class="mb-4"> - <?= form_label( - lang('Podcast.form.episode_description_footer'), - 'episode_description_footer' - ) ?> - <?= form_textarea( - [ - 'id' => 'episode_description_footer', - 'name' => 'episode_description_footer', - 'class' => 'form-textarea', - ], - old( - 'episode_description_footer', - $podcast->episode_description_footer, - false - ), - 'data-editor="markdown"' - ) ?> -</div> +<?= form_section_close() ?> + + +<?= form_section( + lang('Podcast.form.classification_section_title'), + lang('Podcast.form.classification_section_subtitle') +) ?> <?= form_label(lang('Podcast.form.language'), 'language') ?> <?= form_dropdown( @@ -98,7 +117,7 @@ <?= form_dropdown( 'category', $categoryOptions, - old('category', $podcast->category_id), + old('category', (string) $podcast->category_id), [ 'id' => 'category', 'class' => 'form-select mb-4', @@ -106,16 +125,85 @@ ] ) ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'], - 'yes', - old('explicit', $podcast->explicit) +<?= form_label( + lang('Podcast.form.other_categories'), + 'other_categories', + [], + '', + true +) ?> +<?= form_multiselect( + 'other_categories[]', + $categoryOptions, + old('other_categories', $podcast->other_categories_ids), + [ + 'id' => 'other_categories', + 'class' => 'mb-4', + 'data-max-item-count' => '2', + ] +) ?> + +<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> + <legend><?= lang('Podcast.form.parental_advisory.label') . + hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?> + </legend> + <?= form_radio( + [ + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : $podcast->parental_advisory === null + ) ?> + <label for="undefined"><?= lang( + 'Podcast.form.parental_advisory.undefined' + ) ?></label> + <?= form_radio( + [ + 'id' => 'clean', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'clean', + old('parental_advisory') + ? old('parental_advisory') === 'clean' + : $podcast->parental_advisory === 'clean' + ) ?> + <label for="clean"><?= lang( + 'Podcast.form.parental_advisory.clean' + ) ?></label> + <?= form_radio( + [ + 'id' => 'explicit', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'explicit', + old('parental_advisory') + ? old('parental_advisory') === 'explicit' + : $podcast->parental_advisory === 'explicit' ) ?> - <span class="ml-2"><?= lang('Podcast.form.explicit') ?></span> -</label> + <label for="explicit"><?= lang( + 'Podcast.form.parental_advisory.explicit' + ) ?></label> +<?= form_fieldset_close() ?> + +<?= form_section_close() ?> + +<?= form_section( + lang('Podcast.form.author_section_title'), + lang('Podcast.form.author_section_subtitle') +) ?> -<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?> +<?= form_label( + lang('Podcast.form.owner_name'), + 'owner_name', + [], + lang('Podcast.form.owner_name_hint') +) ?> <?= form_input([ 'id' => 'owner_name', 'name' => 'owner_name', @@ -124,7 +212,12 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?> +<?= form_label( + lang('Podcast.form.owner_email'), + 'owner_email', + [], + lang('Podcast.form.owner_email_hint') +) ?> <?= form_input([ 'id' => 'owner_email', 'name' => 'owner_email', @@ -134,37 +227,21 @@ 'required' => 'required', ]) ?> -<?= form_label(lang('Podcast.form.author'), 'author') ?> +<?= form_label( + lang('Podcast.form.publisher'), + 'publisher', + [], + lang('Podcast.form.publisher_hint'), + true +) ?> <?= form_input([ - 'id' => 'author', - 'name' => 'author', + 'id' => 'publisher', + 'name' => 'publisher', 'class' => 'form-input mb-4', - 'value' => old('author', $podcast->author), + 'value' => old('publisher', $podcast->publisher), ]) ?> -<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Podcast.form.type.label') ?></legend> - <label for="episodic" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio'], - 'episodic', - old('type') - ? old('type') == 'episodic' - : $podcast->type == 'episodic' - ) ?> - <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span> - </label> - <label for="serial" class="inline-flex items-center"> - <?= form_radio( - ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio'], - 'serial', - old('type') ? old('type') == 'serial' : $podcast->type == 'serial' - ) ?> - <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span> - </label> -<?= form_fieldset_close() ?> - -<?= form_label(lang('Podcast.form.copyright'), 'copyright') ?> +<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?> <?= form_input([ 'id' => 'copyright', 'name' => 'copyright', @@ -172,42 +249,37 @@ 'value' => old('copyright', $podcast->copyright), ]) ?> -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'], - 'yes', - old('block', $podcast->block) - ) ?> - <span class="ml-2"><?= lang('Podcast.form.block') ?></span> -</label> - -<label class="inline-flex items-center mb-4"> - <?= form_checkbox( - ['id' => 'complete', 'name' => 'complete', 'class' => 'form-checkbox'], - 'yes', - old('complete', $podcast->complete) - ) ?> - <span class="ml-2"><?= lang('Podcast.form.complete') ?></span> -</label> +<?= form_section_close() ?> -<div class="mb-4"> - <?= form_label(lang('Podcast.form.custom_html_head'), 'custom_html_head') ?> - <?= form_textarea( - [ - 'id' => 'custom_html_head', - 'name' => 'custom_html_head', - 'class' => 'form-textarea', - ], - old('custom_html_head', $podcast->custom_html_head, false), - 'data-editor="html"' - ) ?> -</div> -<?= form_button([ - 'content' => lang('Podcast.form.submit_edit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= form_section( + lang('Podcast.form.status_section_title'), + lang('Podcast.form.status_section_subtitle') +) ?> + +<?= form_switch( + lang('Podcast.form.block'), + ['id' => 'block', 'name' => 'block'], + 'yes', + old('block', $podcast->block), + 'mb-2' +) ?> + +<?= form_switch( + lang('Podcast.form.complete'), + ['id' => 'complete', 'name' => 'complete'], + 'yes', + old('complete', $podcast->complete) +) ?> + +<?= form_section_close() ?> + +<?= button( + lang('Podcast.form.submit_edit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/podcast/import.php b/app/Views/admin/podcast/import.php index 853de8991be5e534a33b25ec554aaa75a70d0ede..6ec7ac586c828f8286dd84c4ccf5532feaf10d47 100644 --- a/app/Views/admin/podcast/import.php +++ b/app/Views/admin/podcast/import.php @@ -4,37 +4,61 @@ <?= lang('Podcast.import') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Podcast.import') ?> +<?= $this->endSection() ?> <?= $this->section('content') ?> -<?= form_open_multipart(route_to('rzqr'), [ +<?= form_open_multipart(route_to('podcast-import'), [ 'method' => 'post', - 'class' => 'flex flex-col max-w-md', + 'class' => 'flex flex-col items-start', ]) ?> <?= csrf_field() ?> -<?= form_label(lang('Podcast.form_import.name'), 'name') ?> -<?= form_input([ - 'id' => 'name', - 'name' => 'name', - 'class' => 'form-input mb-4', - 'value' => old('name'), - 'required' => 'required', -]) ?> +<?= form_section( + lang('PodcastImport.old_podcast_section_title'), + lang('PodcastImport.old_podcast_section_subtitle') +) ?> <?= form_label( - lang('Podcast.form_import.imported_feed_url'), - 'imported_feed_url' + lang('PodcastImport.imported_feed_url'), + 'imported_feed_url', + [], + lang('PodcastImport.imported_feed_url_hint') ) ?> <?= form_input([ 'id' => 'imported_feed_url', 'name' => 'imported_feed_url', - 'class' => 'form-input mb-4', + 'class' => 'form-input', 'value' => old('imported_feed_url'), + 'placeholder' => 'https://...', 'type' => 'url', 'required' => 'required', ]) ?> +<?= form_section_close() ?> + + +<?= form_section( + lang('PodcastImport.new_podcast_section_title'), + lang('PodcastImport.new_podcast_section_subtitle') +) ?> + +<?= form_label( + lang('PodcastImport.name'), + 'name', + [], + lang('PodcastImport.name_hint') +) ?> +<?= form_input([ + 'id' => 'name', + 'name' => 'name', + 'class' => 'form-input mb-4', + 'value' => old('name'), + 'required' => 'required', +]) ?> + <?= form_label(lang('Podcast.form.language'), 'language') ?> <?= form_dropdown('language', $languageOptions, old('language', $browserLang), [ 'id' => 'language', @@ -49,38 +73,50 @@ 'required' => 'required', ]) ?> +<?= form_section_close() ?> + + +<?= form_section( + lang('PodcastImport.advanced_params_section_title'), + lang('PodcastImport.advanced_params_section_subtitle') +) ?> + <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Podcast.form_import.slug_field.label') ?></legend> + <legend><?= lang('PodcastImport.slug_field.label') ?></legend> <label for="link" class="inline-flex items-center"> <?= form_radio( - ['id' => 'link', 'name' => 'slug_field', 'class' => 'form-radio'], + [ + 'id' => 'link', + 'name' => 'slug_field', + 'class' => 'form-radio text-green-500', + ], 'link', old('slug_field') ? old('slug_field') == 'link' : true ) ?> - <span class="ml-2"><?= lang( - 'Podcast.form_import.slug_field.link' - ) ?></span> + <span class="ml-2"><?= lang('PodcastImport.slug_field.link') ?></span> </label> <label for="title" class="inline-flex items-center"> <?= form_radio( - ['id' => 'title', 'name' => 'slug_field', 'class' => 'form-radio'], + [ + 'id' => 'title', + 'name' => 'slug_field', + 'class' => 'form-radio text-green-500', + ], 'title', old('slug_field') ? old('slug_field') == 'title' : false ) ?> - <span class="ml-2"><?= lang( - 'Podcast.form_import.slug_field.title' - ) ?></span> + <span class="ml-2"><?= lang('PodcastImport.slug_field.title') ?></span> </label> <?= form_fieldset_close() ?> <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Podcast.form_import.description_field.label') ?></legend> + <legend><?= lang('PodcastImport.description_field.label') ?></legend> <label for="description" class="inline-flex items-center"> <?= form_radio( [ 'id' => 'description', 'name' => 'description_field', - 'class' => 'form-radio', + 'class' => 'form-radio text-green-500', ], 'description', old('description_field') @@ -88,7 +124,7 @@ : true ) ?> <span class="ml-2"><?= lang( - 'Podcast.form_import.description_field.description' + 'PodcastImport.description_field.description' ) ?></span> </label> <label for="summary" class="inline-flex items-center"> @@ -96,7 +132,7 @@ [ 'id' => 'summary', 'name' => 'description_field', - 'class' => 'form-radio', + 'class' => 'form-radio text-green-500', ], 'summary', old('description_field') @@ -104,7 +140,7 @@ : false ) ?> <span class="ml-2"><?= lang( - 'Podcast.form_import.description_field.summary' + 'PodcastImport.description_field.summary' ) ?></span> </label> <label for="subtitle_summary" class="inline-flex items-center"> @@ -112,7 +148,7 @@ [ 'id' => 'subtitle_summary', 'name' => 'description_field', - 'class' => 'form-radio', + 'class' => 'form-radio text-green-500', ], 'subtitle_summary', old('description_field') @@ -120,7 +156,7 @@ : false ) ?> <span class="ml-2"><?= lang( - 'Podcast.form_import.description_field.subtitle_summary' + 'PodcastImport.description_field.subtitle_summary' ) ?></span> </label> <?= form_fieldset_close() ?> @@ -131,15 +167,21 @@ [ 'id' => 'force_renumber', 'name' => 'force_renumber', - 'class' => 'form-checkbox', + 'class' => 'form-checkbox text-green-500', ], 'yes', old('force_renumber', false) ) ?> - <span class="ml-2"><?= lang('Podcast.form_import.force_renumber') ?></span> + <span class="ml-2"><?= lang('PodcastImport.force_renumber') ?></span> + <?= hint_tooltip(lang('PodcastImport.force_renumber_hint'), 'ml-1') ?> </label> -<?= form_label(lang('Podcast.form_import.season_number'), 'season_number') ?> +<?= form_label( + lang('PodcastImport.season_number'), + 'season_number', + [], + lang('PodcastImport.season_number_hint') +) ?> <?= form_input([ 'id' => 'season_number', 'name' => 'season_number', @@ -148,7 +190,12 @@ 'type' => 'number', ]) ?> -<?= form_label(lang('Podcast.form_import.max_episodes'), 'max_episodes') ?> +<?= form_label( + lang('PodcastImport.max_episodes'), + 'max_episodes', + [], + lang('PodcastImport.max_episodes_hint') +) ?> <?= form_input([ 'id' => 'max_episodes', 'name' => 'max_episodes', @@ -157,11 +204,14 @@ 'type' => 'number', ]) ?> -<?= form_button([ - 'content' => lang('Podcast.form_import.submit_import'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= form_section_close() ?> + +<?= button( + lang('PodcastImport.submit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php new file mode 100644 index 0000000000000000000000000000000000000000..840bfa556986787f29db2d754a0d64598025d9b3 --- /dev/null +++ b/app/Views/admin/podcast/latest_episodes.php @@ -0,0 +1,101 @@ +<section class="flex flex-col"> + <header class="flex justify-between py-2"> + <h1 class="text-xl"><?= lang('Podcast.latest_episodes') ?></h1> + <a href="<?= route_to( + 'episode-list', + $podcast->id + ) ?>" class="inline-flex items-center text-sm underline hover:no-underline"> + <?= lang('Podcast.see_all_episodes') ?> + <?= icon('chevron-right', 'ml-2') ?> + </a> + </header> + <?php if ($episodes): ?> + <div class="flex justify-between gap-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;"> + <img + src="<?= $episode->image->thumbnail_url ?>" + alt="<?= $episode->title ?>" class="object-cover" /> + <div class="flex justify-between p-2"> + <div class="flex flex-col"> + <a href="<?= route_to( + 'episode-view', + $podcast->id, + $episode->id + ) ?>" + class="text-sm font-semibold hover:underline" + ><?= $episode->title ?> + </a> + <div class="text-xs"> + <?php if ( + $episode->season_number && + $episode->number + ): ?> + <abbr class="font-bold text-gray-600" title="<?= lang( + 'Episode.season_episode', + [ + 'seasonNumber' => + $episode->season_number, + 'episodeNumber' => $episode->number, + ] + ) ?>"><?= lang( + 'Episode.season_episode_abbr', + [ + 'seasonNumber' => $episode->season_number, + 'episodeNumber' => $episode->number, + ] +) ?></abbr> + <?php elseif ( + !$episode->season_number && + $episode->number + ): ?> + <abbr class="font-bold text-gray-600" title="<?= lang( + 'Episode.number', + [ + 'episodeNumber' => $episode->number, + ] + ) ?>"><?= lang('Episode.number_abbr', [ + 'episodeNumber' => $episode->number, +]) ?></abbr> + <?php endif; ?> + <span class="mx-1">•</span> + <time + pubdate + datetime="<?= $episode->published_at->toDateTimeString() ?>" + title="<?= $episode->published_at ?>"> + <?= lang('Common.mediumDate', [ + $episode->published_at, + ]) ?> + </time> + </div> + </div> + <div class="relative" data-toggle="dropdown"> + <button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false"> + <?= icon('more') ?> + </button> + <nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="top-end" data-popper-offset-x="0" data-popper-offset-y="-24" > + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode-edit', + $podcast->id, + $episode->id + ) ?>"><?= lang('Episode.edit') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode', + $podcast->name, + $episode->slug + ) ?>"><?= lang('Episode.go_to_page') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'episode-delete', + $podcast->id, + $episode->id + ) ?>"><?= lang('Episode.delete') ?></a> + </nav> + </div> + </div> + </article> + <?php endforeach; ?> + </div> + <?php else: ?> + <p class="italic"><?= lang('Podcast.no_episode') ?></p> + <?php endif; ?> +</section> \ No newline at end of file diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php index cc1097cf326a2a98fc5dde1f94a54e19af49e3c6..a6a00c7237d8d35ac99d730822ceaedecb601700 100644 --- a/app/Views/admin/podcast/list.php +++ b/app/Views/admin/podcast/list.php @@ -1,17 +1,24 @@ <?= $this->extend('admin/_layout') ?> <?= $this->section('title') ?> +<?= lang('Podcast.all_podcasts') ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> <?= lang('Podcast.all_podcasts') ?> (<?= count($podcasts) ?>) -<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'podcast-create' -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Podcast.create') ?></a> -<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'podcast-import' -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Podcast.import') ?></a> +<?= $this->endSection() ?> + +<?= $this->section('headerRight') ?> +<?= button( + lang('Podcast.create'), + route_to('podcast-create'), + ['variant' => 'primary', 'iconLeft' => 'add'], + ['class' => 'mr-2'] +) ?> +<?= button(lang('Podcast.import'), route_to('podcast-import'), [ + 'variant' => 'primary', + 'iconLeft' => 'download', +]) ?> <?= $this->endSection() ?> @@ -20,9 +27,35 @@ <div class="flex flex-wrap"> <?php if (!empty($podcasts)): ?> <?php foreach ($podcasts as $podcast): ?> - <?= view('admin/_partials/_podcast-card', [ - 'podcast' => $podcast, - ]) ?> + <article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow"> + <img + alt="<?= $podcast->title ?>" + src="<?= $podcast->image + ->thumbnail_url ?>" class="object-cover w-full h-40" /> + <div class="p-2"> + <a href="<?= route_to( + 'podcast-view', + $podcast->id + ) ?>" class="hover:underline"> + <h2 class="font-semibold"><?= $podcast->title ?></h2> + </a> + <p class="text-gray-600">@<?= $podcast->name ?></p> + </div> + <footer class="flex items-center justify-end p-2"> + <a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to( + 'podcast-edit', + $podcast->id + ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang( + 'Podcast.edit' +) ?>"><?= icon('edit') ?></a> + <a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to( + 'podcast-view', + $podcast->id + ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang( + 'Podcast.view' +) ?>"><?= icon('eye') ?></a> + </footer> + </article> <?php endforeach; ?> <?php else: ?> <p class="italic"><?= lang('Podcast.no_podcast') ?></p> diff --git a/app/Views/admin/podcast/settings/dashboard.php b/app/Views/admin/podcast/settings/dashboard.php index f7a742fd88b0ef3c90e1819cc2435037b7fd35bb..4b9bfa561ddb702635485147d5c80945542802c5 100644 --- a/app/Views/admin/podcast/settings/dashboard.php +++ b/app/Views/admin/podcast/settings/dashboard.php @@ -4,6 +4,10 @@ <?= lang('Podcast.platforms.title') ?> <?= $this->endSection() ?> -<?= $this->section('content') ?> +<?= $this->section('pageTitle') ?> +<?= lang('Podcast.platforms.title') ?> +<?= $this->endSection() ?> +<?= $this->section('content') ?> +Podcast settings... <?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/settings/platforms.php b/app/Views/admin/podcast/settings/platforms.php index 5137eed9a279cb721c45c6d1a58a9b593369704b..4cbdc8c08a9d055bb36a6e72847bc9ab8bb608f5 100644 --- a/app/Views/admin/podcast/settings/platforms.php +++ b/app/Views/admin/podcast/settings/platforms.php @@ -4,6 +4,10 @@ <?= lang('Platforms.title') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('Platforms.title') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> <?= form_open(route_to('platforms', $podcast->id), [ @@ -88,11 +92,12 @@ <?php endforeach; ?> -<?= form_button([ - 'content' => lang('Platforms.submit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Platforms.submit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/podcast/view.php b/app/Views/admin/podcast/view.php index e1fdba10c0f304fdd2ed90408a51b42672e9202d..e0bb6aefcfaef97380e7800689b25532a6a2fadb 100644 --- a/app/Views/admin/podcast/view.php +++ b/app/Views/admin/podcast/view.php @@ -2,45 +2,29 @@ <?= $this->section('title') ?> <?= $podcast->title ?> -<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-teal-500 rounded shadow-xs outline-none hover:bg-teal-600 focus:shadow-outline" href="<?= route_to( - 'podcast-edit', - $podcast->id -) ?>"> -<?= icon('edit', 'mr-2') ?> -<?= lang('Podcast.edit') ?> -</a> -<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to( - 'episode-create', - $podcast->id -) ?>"> -<?= icon('add', 'mr-2') ?> -<?= lang('Episode.create') ?></a> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('headerRight') ?> +<?= button( + lang('Podcast.edit'), + route_to('podcast-edit', $podcast->id), + ['variant' => 'secondary', 'iconLeft' => 'edit'], + ['class' => 'mr-2'] +) ?> +<?= button(lang('Episode.create'), route_to('episode-create', $podcast->id), [ + 'variant' => 'primary', + 'iconLeft' => 'add', +]) ?> <?= $this->endSection() ?> <?= $this->section('content') ?> - <img - class="w-64 mb-4" - src="<?= $podcast->image->medium_url ?>" - alt="<?= $podcast->title ?>" - /> - <a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to( - 'contributor-list', - $podcast->id - ) ?>"><?= lang('Podcast.see_contributors') ?></a> - <a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to( - 'platforms', - $podcast->id - ) ?>"><?= lang('Platforms.title') ?></a> - <a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to( - 'podcast', - $podcast->name - ) ?>"><?= lang('Podcast.go_to_page') ?></a> - <a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to( - 'podcast-delete', - $podcast->id - ) ?>"><?= lang('Podcast.delete') ?></a> - <?= view('admin/_partials/_episode-list.php', [ - 'episodes' => $podcast->episodes, - ]) ?> +<?= view_cell('\App\Controllers\Admin\Podcast::latestEpisodes', [ + 'limit' => 5, +]) ?> + <?= $this->endSection() ?> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php index 9b860c1bf516f654695f7e8c7af2c357a6cc1c43..b55c0d136e58c31b0289fe5ee17d21bf82483ecf 100644 --- a/app/Views/admin/user/create.php +++ b/app/Views/admin/user/create.php @@ -4,12 +4,14 @@ <?= lang('User.create') ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('User.create') ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> -<?= form_open(route_to('user-create'), [ - 'class' => 'flex flex-col max-w-sm', -]) ?> +<?= form_open(route_to('user-create'), ['class' => 'flex flex-col max-w-sm']) ?> <?= csrf_field() ?> <?= form_label(lang('User.form.email'), 'email') ?> @@ -33,16 +35,18 @@ <?= form_input([ 'id' => 'password', 'name' => 'password', + 'class' => 'form-input mb-4', 'type' => 'password', 'autocomplete' => 'new-password', ]) ?> -<?= form_button([ - 'content' => lang('User.form.submit_create'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('User.form.submit_create'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/user/edit.php b/app/Views/admin/user/edit.php index ef9fc1f287ddb84cae83f36fb434a58e8cebd556..a26931436101b0a1cfa36008e77653635369386d 100644 --- a/app/Views/admin/user/edit.php +++ b/app/Views/admin/user/edit.php @@ -4,6 +4,10 @@ <?= lang('User.edit_roles', ['username' => $user->username]) ?> <?= $this->endSection() ?> +<?= $this->section('pageTitle') ?> +<?= lang('User.edit_roles', ['username' => $user->username]) ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> @@ -15,14 +19,15 @@ <?= form_label(lang('User.form.roles'), 'roles') ?> <?= form_multiselect('roles[]', $roleOptions, $user->roles, [ 'id' => 'roles', - 'class' => 'form-multiselect mb-4', + 'class' => 'mb-4', ]) ?> -<?= form_button([ - 'content' => lang('User.form.submit_edit'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('User.form.submit_edit'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php index f47b3b22202826dbce35ee1872544c5fe85c4f09..1e54610cb0ae15df0183dac4fc19ba1829f9e2ef 100644 --- a/app/Views/admin/user/list.php +++ b/app/Views/admin/user/list.php @@ -1,63 +1,89 @@ <?= $this->extend('admin/_layout') ?> <?= $this->section('title') ?> +<?= lang('User.all_users') ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> <?= lang('User.all_users') ?> (<?= count($users) ?>) <?= $this->endSection() ?> +<?= $this->section('headerRight') ?> +<?= button(lang('User.create'), route_to('user-create'), [ + 'variant' => 'primary', + 'iconLeft' => 'user-add', +]) ?> +<?= $this->endSection() ?> + <?= $this->section('content') ?> -<table class="table-auto"> - <thead> - <tr> - <th class="px-4 py-2">Username</th> - <th class="px-4 py-2">Email</th> - <th class="px-4 py-2">Roles</th> - <th class="px-4 py-2">Banned?</th> - <th class="px-4 py-2">Actions</th> - </tr> - </thead> - <tbody> - <?php foreach ($users as $user): ?> - <tr> - <td class="px-4 py-2 border"><?= $user->username ?></td> - <td class="px-4 py-2 border"><?= $user->email ?></td> - <td class="px-4 py-2 border"> - [<?= implode(', ', $user->roles) ?>] - <a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to( - 'user-edit', - $user->id - ) ?>" data-toggle="tooltip" data-placement="bottom" - title="<?= lang('User.edit_roles', [ - 'username' => $user->username, - ]) ?>"> - <?= icon('edit') ?> - </a> - </td> - <td class="px-4 py-2 border"><?= $user->isBanned() - ? 'Yes' - : 'No' ?></td> - <td class="px-4 py-2 border"> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to( - 'user-force_pass_reset', - $user->id - ) ?>"><?= lang('User.forcePassReset') ?></a> - <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to( - $user->isBanned() ? 'user-unban' : 'user-ban', - $user->id - ) ?>"> - <?= $user->isBanned() - ? lang('User.unban') - : lang('User.ban') ?></a> - <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to( - 'user-delete', - $user->id - ) ?>"><?= lang('User.delete') ?></a> - </td> - </tr> - <?php endforeach; ?> - </tbody> -</table> +<?= data_table( + [ + [ + 'header' => lang('User.list.user'), + 'cell' => function ($user) { + return '<div class="flex flex-col">' . + $user->username . + '<span class="text-sm text-gray-600">' . + $user->email . + '</span></div>'; + }, + ], + [ + 'header' => lang('User.list.roles'), + 'cell' => function ($user) { + return implode(',', $user->roles) . + icon_button( + 'edit', + lang('User.edit_roles', [ + 'username' => $user->username, + ]), + route_to('user-edit', $user->id), + ['variant' => 'info'], + ['class' => 'ml-2'] + ); + }, + ], + [ + 'header' => lang('User.list.banned'), + 'cell' => function ($user) { + return $user->isBanned() + ? lang('Common.yes') + : lang('Common.no'); + }, + ], + [ + 'header' => lang('Common.actions'), + 'cell' => function ($user) { + return button( + lang('User.forcePassReset'), + route_to('user-force_pass_reset', $user->id), + [ + 'variant' => 'secondary', + 'size' => 'small', + ], + ['class' => 'mr-2'] + ) . + button( + lang('User.' . ($user->isBanned() ? 'unban' : 'ban')), + route_to( + $user->isBanned() ? 'user-unban' : 'user-ban', + $user->id + ), + ['variant' => 'warning', 'size' => 'small'], + ['class' => 'mr-2'] + ) . + button( + lang('User.delete'), + route_to('user-delete', $user->id), + ['variant' => 'danger', 'size' => 'small'] + ); + }, + ], + ], + $users +) ?> <?= $this->endSection() ?> diff --git a/app/Views/auth/_layout.php b/app/Views/auth/_layout.php index 6e0a94e2e9013af6a6e20d44a5b7a573772fdb60..04fa70884454bfa84edaaba61c4ccdb9ef23a4e7 100644 --- a/app/Views/auth/_layout.php +++ b/app/Views/auth/_layout.php @@ -24,8 +24,9 @@ </main> <footer class="flex flex-col text-sm"> <?= $this->renderSection('footer') ?> - <p class="py-4 border-t"> - Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative. - </p> + <small class="py-4 text-center border-t"><?= lang('Common.powered_by', [ + 'castopod' => + '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + ]) ?></small> </footer> </body> diff --git a/app/Views/auth/forgot.php b/app/Views/auth/forgot.php index 7a742021a710b867782d3649a8d42ffca47a24e6..376fa93d9cdeada6f6918070720f24499f6d9439 100644 --- a/app/Views/auth/forgot.php +++ b/app/Views/auth/forgot.php @@ -22,11 +22,12 @@ 'required' => 'required', ]) ?> -<?= form_button([ - 'content' => lang('Auth.sendInstructions'), - 'type' => 'submit', - 'class' => 'px-4 py-2 ml-auto border', -]) ?> +<?= button( + lang('Auth.sendInstructions'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php index c231de542318505fbc50a337388008073d4c4c59..4faf178575da0f97f5cd8d951e3e84ef4baf7b1e 100644 --- a/app/Views/auth/login.php +++ b/app/Views/auth/login.php @@ -28,11 +28,13 @@ 'required' => 'required', ]) ?> -<?= form_button([ - 'content' => lang('Auth.loginAction'), - 'class' => 'px-4 py-2 ml-auto border', - 'type' => 'submit', -]) ?> + +<?= button( + lang('Auth.loginAction'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php index aa2a7db6d4c348b555f8450598f003ce993f9632..de58efd46f792b5920bca98397a1f9722ba3745d 100644 --- a/app/Views/auth/register.php +++ b/app/Views/auth/register.php @@ -44,11 +44,12 @@ 'autocomplete' => 'new-password', ]) ?> -<?= form_button([ - 'content' => lang('Auth.register'), - 'class' => 'px-4 py-2 ml-auto border', - 'type' => 'submit', -]) ?> +<?= button( + lang('Auth.register'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/auth/reset.php b/app/Views/auth/reset.php index 8e26e72f2ad4db5b616f636da811f834989db252..a89577aee2795ebe08192e6ea665f3d571685329 100644 --- a/app/Views/auth/reset.php +++ b/app/Views/auth/reset.php @@ -42,11 +42,12 @@ 'autocomplete' => 'new-password', ]) ?> -<?= form_button([ - 'content' => lang('Auth.resetPassword'), - 'class' => 'px-4 py-2 ml-auto border', - 'type' => 'submit', -]) ?> +<?= button( + lang('Auth.resetPassword'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/episode.php b/app/Views/episode.php index dbdd6a2ea31d830a84bd2a4a8e9d4fbb2fb298e3..40076e1787719c1149173fbcbdd0302adcee8a1d 100644 --- a/app/Views/episode.php +++ b/app/Views/episode.php @@ -1,6 +1,6 @@ <?= helper('page') ?> <!DOCTYPE html> -<html lang="<?= $episode->podcast->language ?>"> +<html lang="<?= service('request')->getLocale() ?>"> <head> <meta charset="UTF-8"/> @@ -16,16 +16,16 @@ <div class="container flex items-start px-2 py-2 mx-auto"> <img class="w-12 h-12 mr-2 rounded cover" - src="<?= $episode->podcast->image->thumbnail_url ?>" - alt="<?= $episode->podcast->title ?>" + src="<?= $podcast->image->thumbnail_url ?>" + alt="<?= $podcast->title ?>" /> <a - href="<?= route_to('podcast', $episode->podcast->name) ?>" + href="<?= route_to('podcast', $podcast->name) ?>" class="flex flex-col text-lg leading-tight text-white" title="<?= lang('Episode.back_to_podcast') ?>"> - <?= $episode->podcast->title ?> + <?= $podcast->title ?> <span class="text-sm text-gray-300"> - @<?= $episode->podcast->name ?> + @<?= $podcast->name ?> </span> </a> </div> @@ -34,7 +34,7 @@ <nav class="flex items-center px-2 py-4"> <?php if ($previousEpisode): ?> <a class="flex items-center text-xs leading-snug text-gray-600 hover:text-gray-900" href="<?= $previousEpisode->link ?>" title="<?= $previousEpisode->title ?>"> - <?= icon('arrow-left', 'mr-2') ?> + <?= icon('chevron-left', 'mr-2') ?> <div class="flex flex-col"> <?= $previousEpisode->season_number == $episode->season_number @@ -53,7 +53,7 @@ : lang('Episode.next_season') ?> <span class="w-40 font-semibold truncate"><?= $nextEpisode->title ?></span> </div> - <?= icon('arrow-right', 'ml-2') ?> + <?= icon('chevron-right', 'ml-2') ?> </a> <?php endif; ?> </nav> @@ -67,7 +67,7 @@ <?php if ($episode->season_number): ?> <a class="mr-1 underline hover:no-underline" href="<?= route_to( 'podcast', - $episode->podcast->name + $podcast->name ) . '?season=' . $episode->season_number ?>"> @@ -130,7 +130,7 @@ <div class="container flex flex-col items-center justify-between mx-auto text-sm md:flex-row "> <?= render_page_links('inline-flex mb-4 md:mb-0') ?> <div class="flex flex-col items-end text-xs"> - <p><?= $episode->podcast->copyright ?></p> + <p><?= $podcast->copyright ?></p> <p><?= lang('Common.powered_by', [ 'castopod' => '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', diff --git a/app/Views/install/_layout.php b/app/Views/install/_layout.php index 81f634ff01812a52788976990da0aa8220e7fa4b..20bd2d2a4f94ac751b274b9cf9afba0747c024eb 100644 --- a/app/Views/install/_layout.php +++ b/app/Views/install/_layout.php @@ -21,6 +21,9 @@ <?= $this->renderSection('content') ?> </main> <footer class="container px-2 py-4 mx-auto text-sm text-right border-t"> - Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative. + <small><?= lang('Common.powered_by', [ + 'castopod' => + '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + ]) ?></small> </footer> </body> diff --git a/app/Views/install/env.php b/app/Views/install/env.php index d11b2a7f20fd216990035e36628bd8893449e71c..25db9b5d7b33b621b15cc94659279567ec13e7da 100644 --- a/app/Views/install/env.php +++ b/app/Views/install/env.php @@ -86,11 +86,12 @@ ]) ?> <?= form_fieldset_close() ?> -<?= form_button([ - 'content' => lang('Install.form.submit_install'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Install.form.submit_install'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/install/superadmin.php b/app/Views/install/superadmin.php index d4ddc61f976b77a808f9f59cb56774a78c746173..6b32c32cc355023337f2fe05b58ffd81bce4eb8b 100644 --- a/app/Views/install/superadmin.php +++ b/app/Views/install/superadmin.php @@ -41,11 +41,12 @@ ]) ?> <?= form_fieldset_close() ?> -<?= form_button([ - 'content' => lang('Install.form.submit_create_superadmin'), - 'type' => 'submit', - 'class' => 'self-end px-4 py-2 bg-gray-200', -]) ?> +<?= button( + lang('Install.form.submit_create_superadmin'), + null, + ['variant' => 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> <?= form_close() ?> diff --git a/app/Views/pager/default_full.php b/app/Views/pager/default_full.php new file mode 100644 index 0000000000000000000000000000000000000000..e2a065d0c10a9fd3173d8895a66e410484ce215f --- /dev/null +++ b/app/Views/pager/default_full.php @@ -0,0 +1,67 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + * + * @var \CodeIgniter\Pager\PagerRenderer $pager + */ + +$pager->setSurroundCount(2); ?> + +<nav aria-label="<?= lang('Pager.pageNavigation') ?>"> + <ul class="flex justify-center"> + <?php if ($pager->hasPreviousPage()): ?> + <li> + <a href="<?= $pager->getFirst() ?>" aria-label="<?= lang( + 'Pager.first' +) ?>" class="block px-3 py-2 text-gray-700 hover:bg-gray-200 hover:text-black"> + <span aria-hidden="true"><?= lang('Pager.first') ?></span> + </a> + </li> + <li> + <a href="<?= $pager->getPreviousPage() ?>" aria-label="<?= lang( + 'Pager.previous' +) ?>" class="block px-3 py-2 text-gray-700 hover:bg-gray-200 hover:text-black"> + <span aria-hidden="true"><?= lang( + 'Pager.previous' + ) ?></span> + </a> + </li> + <?php endif; ?> + + <?php foreach ($pager->links() as $link): ?> + <li> + <?php if ($link['active']): ?> + <span class="block px-4 py-2 font-semibold text-white bg-green-500 rounded-full"> + <?= $link['title'] ?> + </span> + <?php else: ?> + <a href="<?= $link[ + 'uri' + ] ?>" class="block px-4 py-2 text-gray-700 rounded-full hover:bg-gray-200 hover:text-black"> + <?= $link['title'] ?> + </a> + <?php endif; ?> + </li> + <?php endforeach; ?> + + <?php if ($pager->hasNextPage()): ?> + <li> + <a href="<?= $pager->getNextPage() ?>" aria-label="<?= lang( + 'Pager.next' +) ?>" class="block px-3 py-2 text-gray-700 hover:bg-gray-200 hover:text-black"> + <span aria-hidden="true"><?= lang('Pager.next') ?></span> + </a> + </li> + <li> + <a href="<?= $pager->getLast() ?>" aria-label="<?= lang( + 'Pager.last' +) ?>" class="block px-3 py-2 text-gray-700 hover:bg-gray-200 hover:text-black"> + <span aria-hidden="true"><?= lang('Pager.last') ?></span> + </a> + </li> + <?php endif; ?> + </ul> +</nav> diff --git a/app/Views/podcast.php b/app/Views/podcast.php index 212be0da39b02a7f1ac09058452fa07cc0a515a1..2c6cb9d6331b5e55175f9864f9828259d9abf6e7 100644 --- a/app/Views/podcast.php +++ b/app/Views/podcast.php @@ -1,7 +1,7 @@ <?= helper('page') ?> <!DOCTYPE html> -<html lang="<?= $podcast->language ?>"> +<html lang="<?= service('request')->getLocale() ?>"> <head> <meta charset="UTF-8"/> @@ -10,7 +10,6 @@ <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"/> - <?= $podcast->custom_html_head ?> </head> <body class="flex flex-col min-h-screen"> @@ -24,10 +23,10 @@ <div class="flex items-center mb-4"> <address> <?= lang('Podcast.by', [ - 'author' => $podcast->author, + 'publisher' => $podcast->publisher, ]) ?> </address> - <?= $podcast->explicit + <?= $podcast->parental_advisory === 'explicit' ? '<span class="px-1 ml-2 text-xs font-semibold leading-tight tracking-wider uppercase border-2 border-gray-700 rounded md:border-white">' . lang('Common.explicit') . '</span>' @@ -48,15 +47,26 @@ </a> <?php endforeach; ?> </div> - <div class="mb-2 opacity-75"> + <div class="mb-2 opacity-75"> <?= $podcast->description_html ?> </div> - <span class="px-2 py-1 text-sm text-gray-700 bg-gray-200 rounded"> + <span class="px-2 py-1 mb-2 mr-2 text-sm text-gray-700 bg-gray-200 rounded"> <?= lang( 'Podcast.category_options.' . $podcast->category->code ) ?> </span> + <?php foreach ( + $podcast->other_categories + as $other_category + ): ?> + <span class="px-2 py-1 mb-2 mr-2 text-sm text-gray-700 bg-gray-200 rounded"> + <?= lang( + 'Podcast.category_options.' . + $other_category->code + ) ?> + </span> + <?php endforeach; ?> </div> </div> </header> diff --git a/composer.json b/composer.json index dc3cf041ac06fd52060e02dc7347e11af1a8ebf6..063237497ac57f8ee0cfdfb4dc3cc6e766afd804 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "myth/auth": "dev-develop", "codeigniter4/codeigniter4": "dev-develop", "league/commonmark": "^1.5", - "vlucas/phpdotenv": "^5.1", + "vlucas/phpdotenv": "^5.2", "league/html-to-markdown": "^4.10" }, "require-dev": { diff --git a/composer.lock b/composer.lock index d3126548ad7da734a97091f1f41a16d15dbe32fe..9e637f246bbbd922a93071b304ed94360402f005 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/codeigniter4/CodeIgniter4.git", - "reference": "9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4" + "reference": "9204aef421921f2c07021dda418ebfc200fe4a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4", - "reference": "9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9204aef421921f2c07021dda418ebfc200fe4a31", + "reference": "9204aef421921f2c07021dda418ebfc200fe4a31", "shasum": "" }, "require": { @@ -51,6 +51,9 @@ "CodeIgniter\\ComposerScripts::postUpdate", "bash admin/setup.sh" ], + "analyze": [ + "phpstan analyze" + ], "test": [ "phpunit" ] @@ -66,7 +69,7 @@ "slack": "https://codeigniterchat.slack.com", "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" }, - "time": "2020-09-07T16:29:38+00:00" + "time": "2020-09-24T17:15:24+00:00" }, { "name": "composer/ca-bundle", @@ -455,16 +458,16 @@ }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "4939c81f63a8a4968c108c440275c94955753b19" + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/4939c81f63a8a4968c108c440275c94955753b19", - "reference": "4939c81f63a8a4968c108c440275c94955753b19", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", "shasum": "" }, "require": { @@ -476,10 +479,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev", - "dev-develop": "1.1.x-dev" - }, "laminas": { "module": "Laminas\\ZendFrameworkBridge" } @@ -509,20 +508,20 @@ "type": "community_bridge" } ], - "time": "2020-08-18T16:34:51+00:00" + "time": "2020-09-14T14:23:00+00:00" }, { "name": "league/commonmark", - "version": "1.5.4", + "version": "1.5.5", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "21819c989e69bab07e933866ad30c7e3f32984ba" + "reference": "45832dfed6007b984c0d40addfac48d403dc6432" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/21819c989e69bab07e933866ad30c7e3f32984ba", - "reference": "21819c989e69bab07e933866ad30c7e3f32984ba", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/45832dfed6007b984c0d40addfac48d403dc6432", + "reference": "45832dfed6007b984c0d40addfac48d403dc6432", "shasum": "" }, "require": { @@ -534,7 +533,7 @@ }, "require-dev": { "cebe/markdown": "~1.0", - "commonmark/commonmark.js": "0.29.1", + "commonmark/commonmark.js": "0.29.2", "erusev/parsedown": "~1.0", "ext-json": "*", "github/gfm": "0.29.0", @@ -604,7 +603,7 @@ "type": "tidelift" } ], - "time": "2020-08-18T01:19:12+00:00" + "time": "2020-09-13T14:44:46+00:00" }, { "name": "league/html-to-markdown", @@ -1247,16 +1246,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.1.0", + "version": "v5.2.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "448c76d7a9e30c341ff5bc367a923af74ae18467" + "reference": "fba64139db67123c7a57072e5f8d3db10d160b66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/448c76d7a9e30c341ff5bc367a923af74ae18467", - "reference": "448c76d7a9e30c341ff5bc367a923af74ae18467", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/fba64139db67123c7a57072e5f8d3db10d160b66", + "reference": "fba64139db67123c7a57072e5f8d3db10d160b66", "shasum": "" }, "require": { @@ -1279,7 +1278,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "5.2-dev" } }, "autoload": { @@ -1319,7 +1318,7 @@ "type": "tidelift" } ], - "time": "2020-07-14T19:26:25+00:00" + "time": "2020-09-14T15:57:31+00:00" }, { "name": "whichbrowser/parser", @@ -1705,16 +1704,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.1", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", - "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -1753,20 +1752,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-08-15T11:14:08+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -1798,7 +1797,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpspec/prophecy", diff --git a/package-lock.json b/package-lock.json index bc926e37449949c85555a03579fc22c40b364d72..ae21fc46fb8b46627eab60f2b188a83ab33ef17e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1386,25 +1386,73 @@ } }, "@commitlint/cli": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-9.1.2.tgz", - "integrity": "sha512-ctRrrPqjZ8r4Vc4FXpPaScEpkPwfvB0Us3NK2SD2AnLwXGMxOLFTabDmNySU1Xc40ud2CmJsaV8lpavvzs8ZZA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-11.0.0.tgz", + "integrity": "sha512-YWZWg1DuqqO5Zjh7vUOeSX76vm0FFyz4y0cpGMFhrhvUi5unc4IVfCXZ6337R9zxuBtmveiRuuhQqnRRer+13g==", "dev": true, "requires": { - "@babel/runtime": "^7.9.6", - "@commitlint/format": "^9.1.2", - "@commitlint/lint": "^9.1.2", - "@commitlint/load": "^9.1.2", - "@commitlint/read": "^9.1.2", + "@babel/runtime": "^7.11.2", + "@commitlint/format": "^11.0.0", + "@commitlint/lint": "^11.0.0", + "@commitlint/load": "^11.0.0", + "@commitlint/read": "^11.0.0", "chalk": "4.1.0", "core-js": "^3.6.1", - "get-stdin": "7.0.0", + "get-stdin": "8.0.0", "lodash": "^4.17.19", "resolve-from": "5.0.0", "resolve-global": "1.0.0", "yargs": "^15.1.0" }, "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@commitlint/execute-rule": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-11.0.0.tgz", + "integrity": "sha512-g01p1g4BmYlZ2+tdotCavrMunnPFPhTzG1ZiLKTCYrooHRbmvqo42ZZn4QMStUEIcn+jfLb6BRZX3JzIwA1ezQ==", + "dev": true + }, + "@commitlint/load": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-11.0.0.tgz", + "integrity": "sha512-t5ZBrtgvgCwPfxmG811FCp39/o3SJ7L+SNsxFL92OR4WQxPcu6c8taD0CG2lzOHGuRyuMxZ7ps3EbngT2WpiCg==", + "dev": true, + "requires": { + "@commitlint/execute-rule": "^11.0.0", + "@commitlint/resolve-extends": "^11.0.0", + "@commitlint/types": "^11.0.0", + "chalk": "4.1.0", + "cosmiconfig": "^7.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0" + } + }, + "@commitlint/resolve-extends": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-11.0.0.tgz", + "integrity": "sha512-WinU6Uv6L7HDGLqn/To13KM1CWvZ09VHZqryqxXa1OY+EvJkfU734CwnOEeNlSCK7FVLrB4kmodLJtL1dkEpXw==", + "dev": true, + "requires": { + "import-fresh": "^3.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -1440,17 +1488,54 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } }, "resolve-from": { "version": "5.0.0", @@ -1459,9 +1544,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -1470,28 +1555,28 @@ } }, "@commitlint/config-conventional": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-9.1.2.tgz", - "integrity": "sha512-2zfnsrBJuCNJEKMEmltYlCUEoQNE4anvEBI/SYEuiB1JYXYaELijobDBpqhUVjh5NEpprNTY16oMZat6ewnxOg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-11.0.0.tgz", + "integrity": "sha512-SNDRsb5gLuDd2PL83yCOQX6pE7gevC79UPFx+GLbLfw6jGnnbO9/tlL76MLD8MOViqGbo7ZicjChO9Gn+7tHhA==", "dev": true, "requires": { - "conventional-changelog-conventionalcommits": "4.3.0" + "conventional-changelog-conventionalcommits": "^4.3.1" } }, "@commitlint/ensure": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-9.1.2.tgz", - "integrity": "sha512-hwQICwpNSTsZgj/1/SdPvYAzhwjwgCJI4vLbT879+Jc+AJ6sj2bUDGw/F89vzgKz1VnaMm4D65bNhoWhG3pdhQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-11.0.0.tgz", + "integrity": "sha512-/T4tjseSwlirKZdnx4AuICMNNlFvRyPQimbZIOYujp9DSO6XRtOy9NrmvWujwHsq9F5Wb80QWi4WMW6HMaENug==", "dev": true, "requires": { - "@commitlint/types": "^9.1.2", + "@commitlint/types": "^11.0.0", "lodash": "^4.17.19" }, "dependencies": { - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", "dev": true } } @@ -1500,28 +1585,43 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-9.1.2.tgz", "integrity": "sha512-NGbeo0KCVYo1yj9vVPFHv6RGFpIF6wcQxpFYUKGIzZVV9Vz1WyiKS689JXa99Dt1aN0cZlEJJLnTNDIgYls0Vg==", - "dev": true + "dev": true, + "optional": true }, "@commitlint/format": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-9.1.2.tgz", - "integrity": "sha512-+ZWTOSGEU6dbn3NRh1q7sY5K5QLiSs7E2uSzuYnWHXcQk8nlTvnE0ibwMCQxdKLaOTZiN57fHM/7M9Re2gsRuw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-11.0.0.tgz", + "integrity": "sha512-bpBLWmG0wfZH/svzqD1hsGTpm79TKJWcf6EXZllh2J/LSSYKxGlv967lpw0hNojme0sZd4a/97R3qA2QHWWSLg==", "dev": true, "requires": { - "@commitlint/types": "^9.1.2", + "@commitlint/types": "^11.0.0", "chalk": "^4.0.0" + }, + "dependencies": { + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + } } }, "@commitlint/is-ignored": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-9.1.2.tgz", - "integrity": "sha512-423W/+Ro+Cc8cg81+t9gds1EscMZNjnGT31nKDvxVxJxXiXQsYYoFEQbU+nfUrRGQsUikEgEJ3ppVGr1linvcQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-11.0.0.tgz", + "integrity": "sha512-VLHOUBN+sOlkYC4tGuzE41yNPO2w09sQnOpfS+pSPnBFkNUUHawEuA44PLHtDvQgVuYrMAmSWFQpWabMoP5/Xg==", "dev": true, "requires": { - "@commitlint/types": "^9.1.2", + "@commitlint/types": "^11.0.0", "semver": "7.3.2" }, "dependencies": { + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + }, "semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", @@ -1531,15 +1631,23 @@ } }, "@commitlint/lint": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-9.1.2.tgz", - "integrity": "sha512-XvggqHZ4XSTKOgzJhCzz52cWRRO57QQnEviwGj0qnD4jdwC+8h2u9LNZwoa2tGAuaNM3nSm//wNK7FRZhgiiFA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-11.0.0.tgz", + "integrity": "sha512-Q8IIqGIHfwKr8ecVZyYh6NtXFmKw4YSEWEr2GJTB/fTZXgaOGtGFZDWOesCZllQ63f1s/oWJYtVv5RAEuwN8BQ==", "dev": true, "requires": { - "@commitlint/is-ignored": "^9.1.2", - "@commitlint/parse": "^9.1.2", - "@commitlint/rules": "^9.1.2", - "@commitlint/types": "^9.1.2" + "@commitlint/is-ignored": "^11.0.0", + "@commitlint/parse": "^11.0.0", + "@commitlint/rules": "^11.0.0", + "@commitlint/types": "^11.0.0" + }, + "dependencies": { + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + } } }, "@commitlint/load": { @@ -1547,6 +1655,7 @@ "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-9.1.2.tgz", "integrity": "sha512-FPL82xBuF7J3EJ57kLVoligQP4BFRwrknooP+vNT787AXmQ/Fddc/iYYwHwy67pNkk5N++/51UyDl/CqiHb6nA==", "dev": true, + "optional": true, "requires": { "@commitlint/execute-rule": "^9.1.2", "@commitlint/resolve-extends": "^9.1.2", @@ -1562,6 +1671,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, + "optional": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -1572,6 +1682,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, + "optional": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1582,6 +1693,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "optional": true, "requires": { "color-name": "~1.1.4" } @@ -1590,13 +1702,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "optional": true }, "cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", "dev": true, + "optional": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.1.0", @@ -1609,13 +1723,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "dev": true, + "optional": true }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, + "optional": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1625,7 +1741,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "dev": true, + "optional": true } } }, @@ -1633,13 +1750,15 @@ "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "dev": true, + "optional": true }, "parse-json": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", "dev": true, + "optional": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -1651,13 +1770,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true + "dev": true, + "optional": true }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, + "optional": true, "requires": { "has-flag": "^4.0.0" } @@ -1665,15 +1786,15 @@ } }, "@commitlint/message": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-9.1.2.tgz", - "integrity": "sha512-ndlx5z7bPVLG347oYJUHuQ41eTcsw+aUYT1ZwQyci0Duy2atpuoeeSw9SuM1PjufzRCpb6ExzFEgGzcCRKAJsg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-11.0.0.tgz", + "integrity": "sha512-01ObK/18JL7PEIE3dBRtoMmU6S3ecPYDTQWWhcO+ErA3Ai0KDYqV5VWWEijdcVafNpdeUNrEMigRkxXHQLbyJA==", "dev": true }, "@commitlint/parse": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-9.1.2.tgz", - "integrity": "sha512-d+/VYbkotctW+lzDpus/R6xTerOqFQkW1myH+3PwnqYSE6JU/uHT4MlZNGJBv8pX9SPlR66t6X9puFobqtezEw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-11.0.0.tgz", + "integrity": "sha512-DekKQAIYWAXIcyAZ6/PDBJylWJ1BROTfDIzr9PMVxZRxBPc1gW2TG8fLgjZfBP5mc0cuthPkVi91KQQKGri/7A==", "dev": true, "requires": { "conventional-changelog-angular": "^5.0.0", @@ -1681,14 +1802,44 @@ } }, "@commitlint/read": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-9.1.2.tgz", - "integrity": "sha512-C2sNBQOqeQXMxpWtRnXYKYB3D9yuybPtQNY/P67A6o8XH/UMHkFaUTyIx1KRgu0IG0yTTItRt46FGnsMWLotvA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-11.0.0.tgz", + "integrity": "sha512-37V0V91GSv0aDzMzJioKpCoZw6l0shk7+tRG8RkW1GfZzUIytdg3XqJmM+IaIYpaop0m6BbZtfq+idzUwJnw7g==", "dev": true, "requires": { - "@commitlint/top-level": "^9.1.2", - "fs-extra": "^8.1.0", + "@commitlint/top-level": "^11.0.0", + "fs-extra": "^9.0.0", "git-raw-commits": "^2.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + } } }, "@commitlint/resolve-extends": { @@ -1696,6 +1847,7 @@ "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-9.1.2.tgz", "integrity": "sha512-HcoL+qFGmWEu9VM4fY0HI+VzF4yHcg3x+9Hx6pYFZ+r2wLbnKs964y0v68oyMO/mS/46MVoLNXZGR8U3adpadg==", "dev": true, + "optional": true, "requires": { "import-fresh": "^3.0.0", "lodash": "^4.17.19", @@ -1708,6 +1860,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, + "optional": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1717,7 +1870,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "dev": true, + "optional": true } } }, @@ -1725,48 +1879,98 @@ "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "dev": true, + "optional": true }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true + "dev": true, + "optional": true } } }, "@commitlint/rules": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-9.1.2.tgz", - "integrity": "sha512-1vecFuzqVqjiT57ocXq1bL8V6GEF1NZs3BR0dQzObaqHftImIxBVII299gasckTkcuxNc8M+7XxZyKxUthukpQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-11.0.0.tgz", + "integrity": "sha512-2hD9y9Ep5ZfoNxDDPkQadd2jJeocrwC4vJ98I0g8pNYn/W8hS9+/FuNpolREHN8PhmexXbkjrwyQrWbuC0DVaA==", "dev": true, "requires": { - "@commitlint/ensure": "^9.1.2", - "@commitlint/message": "^9.1.2", - "@commitlint/to-lines": "^9.1.2", - "@commitlint/types": "^9.1.2" + "@commitlint/ensure": "^11.0.0", + "@commitlint/message": "^11.0.0", + "@commitlint/to-lines": "^11.0.0", + "@commitlint/types": "^11.0.0" + }, + "dependencies": { + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + } } }, "@commitlint/to-lines": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-9.1.2.tgz", - "integrity": "sha512-o4zWcMf9EnzA3MOqx01780SgrKq5hqDJmUBPk30g6an0XcDuDy3OSZHHTJFdzsg4V9FjC4OY44sFeK7GN7NaxQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-11.0.0.tgz", + "integrity": "sha512-TIDTB0Y23jlCNubDROUVokbJk6860idYB5cZkLWcRS9tlb6YSoeLn1NLafPlrhhkkkZzTYnlKYzCVrBNVes1iw==", "dev": true }, "@commitlint/top-level": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-9.1.2.tgz", - "integrity": "sha512-KMPP5xVePcz3B1dKqcZdU4FZBVOkT+bG3ip4RQX2TeCJoomMkTjd0utALs7rpTGLID6BXbwwXepZCZJREjR/Bw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-11.0.0.tgz", + "integrity": "sha512-O0nFU8o+Ws+py5pfMQIuyxOtfR/kwtr5ybqTvR+C2lUPer2x6lnQU+OnfD7hPM+A+COIUZWx10mYQvkR3MmtAA==", "dev": true, "requires": { - "find-up": "^4.0.0" + "find-up": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + } } }, "@commitlint/types": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-9.1.2.tgz", "integrity": "sha512-r3fwVbVH+M8W0qYlBBZFsUwKe6NT5qvz+EmU7sr8VeN1cQ63z+3cfXyTo7WGGEMEgKiT0jboNAK3b1FZp8k9LQ==", - "dev": true + "dev": true, + "optional": true }, "@csstools/convert-colors": { "version": "1.4.0", @@ -1817,12 +2021,6 @@ "resolve-from": "^4.0.0" } }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1880,9 +2078,9 @@ } }, "@popperjs/core": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz", - "integrity": "sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==" + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.3.tgz", + "integrity": "sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==" }, "@prettier/plugin-php": { "version": "0.14.3", @@ -1896,9 +2094,9 @@ } }, "@rollup/plugin-babel": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.2.0.tgz", - "integrity": "sha512-CPABsajaKjINgBQ3it+yMnfVO3ibsrMBxRzbUOUw2cL1hsZJ7aogU8mgglQm3S2hHJgjnAmxPz0Rq7DVdmHsTw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.2.1.tgz", + "integrity": "sha512-Jd7oqFR2dzZJ3NWANDyBjwTtX/lYbZpVcmkHrfQcpvawHs9E4c0nYk5U2mfZ6I/DZcIvy506KZJi54XK/jxH7A==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", @@ -1906,9 +2104,9 @@ } }, "@rollup/plugin-commonjs": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.0.0.tgz", - "integrity": "sha512-8uAdikHqVyrT32w1zB9VhW6uGwGjhKgnDNP4pQJsjdnyF4FgCj6/bmv24c7v2CuKhq32CcyCwRzMPEElaKkn0w==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz", + "integrity": "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==", "dev": true, "requires": { "@rollup/pluginutils": "^3.1.0", @@ -2014,15 +2212,6 @@ "integrity": "sha512-aPgMH+CjQiScLZculoDNOQUrrK2ktkbl3D6uCLYp1jgYRlNDrMONu9nMu8LfwAeetYNpVNeIGx7WzHSu0kvECg==", "dev": true }, - "@types/codemirror": { - "version": "0.0.97", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.97.tgz", - "integrity": "sha512-n5d7o9nWhC49DjfhsxANP7naWSeTzrjXASkUDQh7626sM4zK9XP2EVcHp1IcCf/IPV6c7ORzDUDF3Bkt231VKg==", - "dev": true, - "requires": { - "@types/tern": "*" - } - }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -2158,15 +2347,6 @@ "@types/node": "*" } }, - "@types/tern": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz", - "integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, "@types/unist": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz", @@ -2174,13 +2354,13 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.1.tgz", - "integrity": "sha512-pQZtXupCn11O4AwpYVUX4PDFfmIJl90ZgrEBg0CEcqlwvPiG0uY81fimr1oMFblZnpKAq6prrT9a59pj1x58rw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz", + "integrity": "sha512-RqEcaHuEKnn3oPFislZ6TNzsBLqpZjN93G69SS+laav/I8w/iGMuMq97P0D2/2/kW4SCebHggqhbcCfbDaaX+g==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.0.1", - "@typescript-eslint/scope-manager": "4.0.1", + "@typescript-eslint/experimental-utils": "4.3.0", + "@typescript-eslint/scope-manager": "4.3.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -2197,55 +2377,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.0.1.tgz", - "integrity": "sha512-gAqOjLiHoED79iYTt3F4uSHrYmg/GPz/zGezdB0jAdr6S6gwNiR/j7cTZ8nREKVzMVKLd9G3xbg1sV9GClW3sw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz", + "integrity": "sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.0.1", - "@typescript-eslint/types": "4.0.1", - "@typescript-eslint/typescript-estree": "4.0.1", + "@typescript-eslint/scope-manager": "4.3.0", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/typescript-estree": "4.3.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.0.1.tgz", - "integrity": "sha512-1+qLmXHNAWSQ7RB6fdSQszAiA7JTwzakj5cNYjBTUmpH2cqilxMZEIV+DRKjVZs8NzP3ALmKexB0w/ExjcK9Iw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.3.0.tgz", + "integrity": "sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.0.1", - "@typescript-eslint/types": "4.0.1", - "@typescript-eslint/typescript-estree": "4.0.1", + "@typescript-eslint/scope-manager": "4.3.0", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/typescript-estree": "4.3.0", "debug": "^4.1.1" } }, "@typescript-eslint/scope-manager": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.0.1.tgz", - "integrity": "sha512-u3YEXVJ8jsj7QCJk3om0Y457fy2euEOkkzxIB/LKU3MdyI+FJ2gI0M4aKEaXzwCSfNDiZ13a3lDo5DVozc+XLQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz", + "integrity": "sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==", "dev": true, "requires": { - "@typescript-eslint/types": "4.0.1", - "@typescript-eslint/visitor-keys": "4.0.1" + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0" } }, "@typescript-eslint/types": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.0.1.tgz", - "integrity": "sha512-S+gD3fgbkZYW2rnbjugNMqibm9HpEjqZBZkTiI3PwbbNGWmAcxolWIUwZ0SKeG4Dy2ktpKKaI/6+HGYVH8Qrlg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.3.0.tgz", + "integrity": "sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.0.1.tgz", - "integrity": "sha512-zGzleORFXrRWRJAMLTB2iJD1IZbCPkg4hsI8mGdpYlKaqzvKYSEWVAYh14eauaR+qIoZVWrXgYSXqLtTlxotiw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz", + "integrity": "sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.0.1", - "@typescript-eslint/visitor-keys": "4.0.1", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -2263,12 +2443,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.0.1.tgz", - "integrity": "sha512-yBSqd6FjnTzbg5RUy9J+9kJEyQjTI34JdGMJz+9ttlJzLCnGkBikxw+N5n2VDcc3CesbIEJ0MnZc5uRYnrEnCw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz", + "integrity": "sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.0.1", + "@typescript-eslint/types": "4.3.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -2289,9 +2469,9 @@ "dev": true }, "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, "acorn-node": { @@ -2322,9 +2502,9 @@ } }, "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -2420,6 +2600,12 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -2682,6 +2868,12 @@ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2697,14 +2889,6 @@ "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } } }, "caniuse-api": { @@ -2813,6 +2997,16 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "choices.js": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/choices.js/-/choices.js-9.0.1.tgz", + "integrity": "sha512-JgpeDY0Tmg7tqY6jaW/druSklJSt7W68tXFJIw0GSGWmO37SDAL8o60eICNGbzIODjj02VNNtf5h6TgoHDtCsA==", + "requires": { + "deepmerge": "^4.2.0", + "fuse.js": "^3.4.5", + "redux": "^4.0.4" + } + }, "chokidar": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", @@ -3086,11 +3280,6 @@ } } }, - "codemirror": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", - "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" - }, "collapse-white-space": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", @@ -3286,41 +3475,14 @@ } }, "conventional-changelog-conventionalcommits": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.3.0.tgz", - "integrity": "sha512-oYHydvZKU+bS8LnGqTMlNrrd7769EsuEHKy4fh1oMdvvDi7fem8U+nvfresJ1IDB8K00Mn4LpiA/lR+7Gs6rgg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.4.0.tgz", + "integrity": "sha512-ybvx76jTh08tpaYrYn/yd0uJNLt5yMrb1BphDe4WBredMlvPisvMghfpnJb6RmRNcqXeuhR6LfGZGewbkRm9yA==", "dev": true, "requires": { - "compare-func": "^1.3.1", + "compare-func": "^2.0.0", "lodash": "^4.17.15", "q": "^1.5.1" - }, - "dependencies": { - "compare-func": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.4.tgz", - "integrity": "sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==", - "dev": true, - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^3.0.0" - } - }, - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - } } }, "conventional-commit-types": { @@ -3852,8 +4014,7 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "define-properties": { "version": "1.1.3", @@ -4008,9 +4169,9 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, "requires": { "is-obj": "^2.0.0" @@ -4113,6 +4274,12 @@ "is-symbol": "^1.0.2" } }, + "escalade": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", + "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -4120,9 +4287,9 @@ "dev": true }, "eslint": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz", - "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.10.0.tgz", + "integrity": "sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4133,7 +4300,7 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", - "eslint-scope": "^5.1.0", + "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^1.3.0", "espree": "^7.3.0", @@ -4201,12 +4368,6 @@ "resolve-from": "^4.0.0" } }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4243,9 +4404,9 @@ } }, "eslint-config-prettier": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", - "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz", + "integrity": "sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -4269,12 +4430,12 @@ } }, "eslint-scope": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", - "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "esrecurse": "^4.1.0", + "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, @@ -4855,6 +5016,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "fuse.js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", + "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==" + }, "generic-names": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz", @@ -4883,9 +5049,9 @@ "dev": true }, "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true }, "get-stream": { @@ -5168,15 +5334,15 @@ "dev": true }, "husky": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", - "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz", + "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==", "dev": true, "requires": { "chalk": "^4.0.0", "ci-info": "^2.0.0", "compare-versions": "^3.6.0", - "cosmiconfig": "^6.0.0", + "cosmiconfig": "^7.0.0", "find-versions": "^3.2.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", @@ -5186,16 +5352,16 @@ }, "dependencies": { "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "requires": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" } }, "import-fresh": { @@ -5209,14 +5375,14 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -5734,8 +5900,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.0", @@ -5859,9 +6024,9 @@ } }, "lint-staged": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.3.0.tgz", - "integrity": "sha512-an3VgjHqmJk0TORB/sdQl0CTkRg4E5ybYCXTTCSJ5h9jFwZbcgKIx5oVma5e7wp/uKt17s1QYFmYqT9MGVosGw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.0.tgz", + "integrity": "sha512-uaiX4U5yERUSiIEQc329vhCTDDwUcSvKdRLsNomkYLRzijk3v8V9GWm2Nz0RMVB87VcuzLvtgy6OsjoH++QHIg==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -6143,6 +6308,30 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.forown": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-4.4.0.tgz", + "integrity": "sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", + "dev": true + }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", @@ -6155,6 +6344,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -6322,7 +6517,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -6447,9 +6641,9 @@ } }, "meow": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.0.tgz", - "integrity": "sha512-kq5F0KVteskZ3JdfyQFivJEj2RaA8NFsS4+r9DaMKLcUHpk5OcHS3Q0XkCXONB1mZRPsu/Y/qImKri0nwSEZog==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", + "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", "dev": true, "requires": { "@types/minimist": "^1.2.0", @@ -6546,14 +6740,6 @@ "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" - }, - "dependencies": { - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - } } }, "mixin-deep": { @@ -6648,14 +6834,6 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } } }, "normalize-path": { @@ -7041,57 +7219,6 @@ "dev": true, "requires": { "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } } }, "pkg-up": { @@ -7241,9 +7368,9 @@ } }, "postcss-cli": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-7.1.2.tgz", - "integrity": "sha512-3mlEmN1v2NVuosMWZM2tP8bgZn7rO5PYxRRrXtdSyL5KipcgBDjJ9ct8/LKxImMCJJi3x5nYhCGFJOkGyEqXBQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-8.0.0.tgz", + "integrity": "sha512-WgQIz1tc8htjob2DULE6dTssDzItuBh3UbscdrAlvid7M6X2WBZUrHCaLMtIuFkHFijAnimIq3nkpXV6FdDTSg==", "dev": true, "requires": { "chalk": "^4.0.0", @@ -7252,14 +7379,55 @@ "fs-extra": "^9.0.0", "get-stdin": "^8.0.0", "globby": "^11.0.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "postcss-reporter": "^6.0.0", + "postcss-load-config": "^2.1.1", + "postcss-reporter": "^7.0.0", "pretty-hrtime": "^1.0.3", "read-cache": "^1.0.0", - "yargs": "^15.0.2" + "yargs": "^16.0.0" }, "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.1.tgz", + "integrity": "sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "fs-extra": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", @@ -7272,10 +7440,10 @@ "universalify": "^1.0.0" } }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "jsonfile": { @@ -7288,11 +7456,79 @@ "universalify": "^1.0.0" } }, + "postcss-load-config": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.1.tgz", + "integrity": "sha512-D2ENobdoZsW0+BHy4x1CAkXtbXtYWYRIxL/JbtRBqrRGOPtJ2zoga/bEZWhV/ShWB5saVxJMzbMdSyA/vv4tXw==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, "universalify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.1.tgz", + "integrity": "sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg==", + "dev": true + }, + "yargs": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.0.3.tgz", + "integrity": "sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA==", + "dev": true, + "requires": { + "cliui": "^7.0.0", + "escalade": "^3.0.2", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.1", + "yargs-parser": "^20.0.0" + } + }, + "yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A==", + "dev": true } } }, @@ -8396,37 +8632,18 @@ } }, "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.0.tgz", + "integrity": "sha512-TQ7aIDKgd7FFekFMHLRoDfl0aY3XmIAAhE4Bduyh5GvFi6uYPSVORWY4jkeC7qidFw7YtXwF5ejYQfUHOC73rQ==", "dev": true, "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - } + "colorette": "^1.2.1", + "lodash.difference": "^4.5.0", + "lodash.forown": "^4.4.0", + "lodash.get": "^4.4.2", + "lodash.groupby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "log-symbols": "^4.0.0" } }, "postcss-resolve-nested-selector": { @@ -8555,9 +8772,9 @@ "dev": true }, "prettier": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", - "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", + "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", "dev": true }, "prettier-linter-helpers": { @@ -8729,9 +8946,9 @@ } }, "prosemirror-view": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.15.6.tgz", - "integrity": "sha512-9FBFB+rK5pvvzHsHOacy0T/Jf+OxZSzY8tSlQiur3SZwAVaNVQm+fl23V/6gU2dHBnreGxjYx9jK+F3XPsPCGw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.16.0.tgz", + "integrity": "sha512-iFtStCw2byF0yLc3mm1ezGdFSd6SWM4pnJod+ZaJiU5ju36QdYM4Xwa+qNm/AaI2/MgxpJqi8jsGWOJNkeBQ/Q==", "requires": { "prosemirror-model": "^1.1.0", "prosemirror-state": "^1.0.0", @@ -8817,14 +9034,14 @@ }, "dependencies": { "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -8907,6 +9124,15 @@ } } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", @@ -8922,6 +9148,12 @@ "regenerate": "^1.4.0" } }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, "regenerator-transform": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", @@ -9155,9 +9387,9 @@ } }, "rollup": { - "version": "2.26.10", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.10.tgz", - "integrity": "sha512-dUnjCWOA0h9qNX6qtcHidyatz8FAFZxVxt1dbcGtKdlJkpSxGK3G9+DLCYvtZr9v94D129ij9zUhG+xbRoqepw==", + "version": "2.28.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.28.2.tgz", + "integrity": "sha512-8txbsFBFLmm9Xdt4ByTOGa9Muonmc8MfNjnGAR8U8scJlF1ZW7AgNZa7aqBXaKtlvnYP/ab++fQIq9dB9NWUbg==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -9266,9 +9498,9 @@ } }, "rollup-plugin-terser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.1.tgz", - "integrity": "sha512-HL0dgzSxBYG/Ly9i/E5Sc+PuKKZ0zBzk11VmLCfdUtpqH4yYqkLclPkTqRy85FU9246yetImOClaQ/ufnj08vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -9693,9 +9925,9 @@ } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", "dev": true }, "specificity": { @@ -9962,9 +10194,9 @@ } }, "stylelint": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.7.0.tgz", - "integrity": "sha512-1wStd4zVetnlHO98VjcHQbjSDmvcA39smkZQMct2cf+hom40H0xlQNdzzbswoG/jGBh61/Ue9m7Lu99PY51O6A==", + "version": "13.7.2", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.7.2.tgz", + "integrity": "sha512-mmieorkfmO+ZA6CNDu1ic9qpt4tFvH2QUB7vqXgrMVHe5ENU69q7YDq0YUg/UHLuCsZOWhUAvcMcLzLDIERzSg==", "dev": true, "requires": { "@stylelint/postcss-css-in-js": "^0.37.2", @@ -10064,9 +10296,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001124", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz", - "integrity": "sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA==", + "version": "1.0.30001142", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz", + "integrity": "sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ==", "dev": true }, "chalk": { @@ -10130,12 +10362,6 @@ "to-regex-range": "^5.0.1" } }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -10192,31 +10418,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "meow": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", - "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - } - }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -10381,6 +10582,11 @@ } } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -10413,9 +10619,9 @@ } }, "tailwindcss": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.7.6.tgz", - "integrity": "sha512-focAhU3ciM1/UYBHQVKKzede4zC3y9+IHzU2N/ZF6mbZbhY8S96lOxrO2Y6LMU08+Dbh2xBLmO1bsioLk3Egig==", + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-1.8.10.tgz", + "integrity": "sha512-7QkERG/cWCzsuMqHMwjOaLMVixOGLNBiXsrkssxlE1aWfkxVbGqiuMokR2162xRyaH2mBIHKxmlf1qb3DvIPqw==", "dev": true, "requires": { "@fullhuman/postcss-purgecss": "^2.1.2", @@ -10426,6 +10632,7 @@ "color": "^3.1.2", "detective": "^5.2.0", "fs-extra": "^8.0.0", + "html-tags": "^3.1.0", "lodash": "^4.17.20", "node-emoji": "^1.8.1", "normalize.css": "^8.0.1", @@ -10439,20 +10646,12 @@ "pretty-hrtime": "^1.0.3", "reduce-css-calc": "^2.1.6", "resolve": "^1.14.2" - }, - "dependencies": { - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - } } }, "terser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.0.tgz", - "integrity": "sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.2.tgz", + "integrity": "sha512-H67sydwBz5jCUA32ZRL319ULu+Su1cAoZnnc+lXnenGRYWyLE3Scgkt8mNoAsMx0h5kdo758zdoS0LG9rYZXDQ==", "dev": true, "requires": { "commander": "^2.20.0", @@ -10641,9 +10840,9 @@ } }, "typescript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", - "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", "dev": true }, "uc.micro": { @@ -11093,9 +11292,9 @@ "dev": true }, "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -11108,7 +11307,7 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" + "yargs-parser": "^18.1.2" }, "dependencies": { "ansi-regex": { @@ -11153,14 +11352,6 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } } } } diff --git a/package.json b/package.json index 258cb74f7a27b42df843bd5d9245fdc116d20f59..a131f985cc87b63e168fa6ae0944c43568649c88 100644 --- a/package.json +++ b/package.json @@ -24,56 +24,55 @@ "commit": "git-cz" }, "dependencies": { - "@popperjs/core": "^2.4.4", - "codemirror": "^5.57.0", + "@popperjs/core": "^2.5.3", + "choices.js": "^9.0.1", "prosemirror-example-setup": "^1.1.2", "prosemirror-markdown": "^1.5.0", "prosemirror-state": "^1.3.3", - "prosemirror-view": "^1.15.6" + "prosemirror-view": "^1.16.0" }, "devDependencies": { "@babel/core": "^7.11.6", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/preset-env": "^7.11.5", "@babel/preset-typescript": "^7.10.4", - "@commitlint/cli": "^9.1.2", - "@commitlint/config-conventional": "^9.1.2", + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", "@prettier/plugin-php": "^0.14.3", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-commonjs": "^15.0.0", + "@rollup/plugin-babel": "^5.2.1", + "@rollup/plugin-commonjs": "^15.1.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-multi-entry": "^4.0.0", "@rollup/plugin-node-resolve": "^9.0.0", "@tailwindcss/custom-forms": "^0.2.1", "@tailwindcss/typography": "^0.2.0", - "@types/codemirror": "0.0.97", "@types/prosemirror-markdown": "^1.0.3", "@types/prosemirror-view": "^1.15.1", - "@typescript-eslint/eslint-plugin": "^4.0.1", - "@typescript-eslint/parser": "^4.0.1", + "@typescript-eslint/eslint-plugin": "^4.3.0", + "@typescript-eslint/parser": "^4.3.0", "cross-env": "^7.0.2", "cssnano": "^4.1.10", "cz-conventional-changelog": "^3.3.0", - "eslint": "^7.8.1", - "eslint-config-prettier": "^6.11.0", + "eslint": "^7.10.0", + "eslint-config-prettier": "^6.12.0", "eslint-plugin-prettier": "^3.1.4", - "husky": "^4.2.5", - "lint-staged": "^10.3.0", - "postcss-cli": "^7.1.2", + "husky": "^4.3.0", + "lint-staged": "^10.4.0", + "postcss-cli": "^8.0.0", "postcss-import": "^12.0.1", "postcss-preset-env": "^6.7.0", - "prettier": "2.1.1", + "prettier": "2.1.2", "prettier-plugin-organize-imports": "^1.1.1", - "rollup": "^2.26.10", + "rollup": "^2.28.2", "rollup-plugin-multi-input": "^1.1.1", "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-postcss": "^3.1.8", - "rollup-plugin-terser": "^7.0.1", - "stylelint": "^13.7.0", + "rollup-plugin-terser": "^7.0.2", + "stylelint": "^13.7.2", "stylelint-config-standard": "^20.0.0", "svgo": "^1.3.2", - "tailwindcss": "^1.7.6", - "typescript": "^4.0.2" + "tailwindcss": "^1.8.10", + "typescript": "^4.0.3" }, "husky": { "hooks": { diff --git a/tailwind.config.js b/tailwind.config.js index 12a2371c5cf51dab339fecb7d6dad8438d9bb2e6..d4f1a1cf50e548c6d242a791ca18da817943acc8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,10 +1,12 @@ /* eslint-disable */ module.exports = { - purge: ["./app/Views/**/*.php", "./app/Views/**/*.ts"], - theme: { - extend: {}, - }, + purge: [ + "./app/Views/**/*.php", + "./app/Views/**/*.ts", + "/app/Helpers/**/*.php", + ], + theme: {}, variants: { textDecoration: ["responsive", "hover", "focus", "group-hover"], }, @@ -12,4 +14,8 @@ module.exports = { require("@tailwindcss/custom-forms"), require("@tailwindcss/typography"), ], + future: { + removeDeprecatedGapUtilities: true, + purgeLayersByDefault: true, + }, };