From 2d44b457a02205d2e7da258d7029b8bc5da39533 Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> Date: Fri, 2 Oct 2020 15:38:16 +0000 Subject: [PATCH] feat: enhance admin ui with responsive design and ux improvements - add podcast sidebar navigation - add podcast dashboard with latest episodes - add pagination to podcast episodes - add components helper to reuse ui components (button, data_table, etc.) - enhance podcast and episode forms by splitting them into form sections - add hint tooltips to podcast and episode forms - transform radio inputs as buttons for better ux - replace explicit field by parental_advisory - replace author field by publisher - add podcasts_categories table to set multiple categories - use choices.js to enhance multiselect fields - update Language files - update js dependencies to latest versions closes #31, #9 --- DEPENDENCIES.md | 2 +- app/Config/Pager.php | 2 +- app/Controllers/Admin/BaseController.php | 2 +- app/Controllers/Admin/Contributor.php | 2 +- app/Controllers/Admin/Episode.php | 67 +- app/Controllers/Admin/Myaccount.php | 3 +- app/Controllers/Admin/Page.php | 4 +- app/Controllers/Admin/Podcast.php | 110 +- app/Controllers/Admin/User.php | 8 +- app/Controllers/Auth.php | 8 + app/Controllers/BaseController.php | 2 +- app/Controllers/Episode.php | 1 + .../2020-05-30-101500_add_podcasts.php | 15 +- .../2020-06-05-170000_add_episodes.php | 9 +- ...0-09-29-150000_add_podcasts_categories.php | 42 + app/Entities/Episode.php | 2 +- app/Entities/Podcast.php | 44 +- app/Entities/User.php | 6 + app/Helpers/breadcrumb_helper.php | 9 +- app/Helpers/components_helper.php | 258 ++++ app/Helpers/form_helper.php | 187 +++ app/Helpers/rss_helper.php | 84 +- app/Helpers/svg_helper.php | 5 +- app/Language/en/AdminNavigation.php | 8 +- app/Language/en/Common.php | 13 + app/Language/en/Contributor.php | 4 + app/Language/en/Episode.php | 76 +- app/Language/en/Page.php | 1 + app/Language/en/Pager.php | 19 + app/Language/en/Podcast.php | 118 +- app/Language/en/PodcastImport.php | 43 + app/Language/en/PodcastNavigation.php | 23 + app/Language/en/User.php | 8 +- app/Libraries/Breadcrumb.php | 6 +- app/Models/CategoryModel.php | 68 ++ app/Models/EpisodeModel.php | 3 +- app/Models/PodcastModel.php | 5 +- app/Views/_assets/admin.ts | 8 +- app/Views/_assets/icons/arrow-left.svg | 2 +- app/Views/_assets/icons/caret-right.svg | 6 + app/Views/_assets/icons/chevron-left.svg | 6 + .../{arrow-right.svg => chevron-right.svg} | 0 app/Views/_assets/icons/download.svg | 6 + app/Views/_assets/icons/line-chart.svg | 6 + app/Views/_assets/icons/menu.svg | 6 + app/Views/_assets/icons/question.svg | 6 + app/Views/_assets/icons/settings.svg | 6 + app/Views/_assets/icons/user-add.svg | 6 + app/Views/_assets/icons/user.svg | 6 + app/Views/_assets/modules/EnclosureInput.ts | 24 + app/Views/_assets/modules/HTMLEditor.ts | 19 - app/Views/_assets/modules/MarkdownEditor.ts | 16 +- app/Views/_assets/modules/MultiSelect.ts | 40 + app/Views/_assets/modules/SidebarToggler.ts | 62 + app/Views/_assets/modules/Tooltip.ts | 8 +- app/Views/_assets/styles/breadcrumb.css | 2 +- app/Views/_assets/styles/enclosureInput.css | 16 + app/Views/_assets/styles/index.css | 4 + app/Views/_assets/styles/layout.css | 27 +- app/Views/_assets/styles/multiSelect.css | 180 +++ app/Views/_assets/styles/radioBtn.css | 24 + app/Views/_assets/styles/switch.css | 26 + app/Views/_layout.php | 10 +- app/Views/admin/_header.php | 28 - app/Views/admin/_layout.php | 57 +- app/Views/admin/_partials/_episode-card.php | 44 - app/Views/admin/_partials/_episode-list.php | 11 - app/Views/admin/_partials/_podcast-card.php | 29 - app/Views/admin/_partials/_user_info.php | 8 +- .../admin/{_sidenav.php => _sidebar.php} | 40 +- app/Views/admin/contributor/add.php | 15 +- app/Views/admin/contributor/edit.php | 15 +- app/Views/admin/contributor/list.php | 94 +- app/Views/admin/dashboard.php | 10 +- app/Views/admin/episode/create.php | 277 +++-- app/Views/admin/episode/edit.php | 281 +++-- app/Views/admin/episode/list.php | 127 +- app/Views/admin/episode/view.php | 68 +- .../admin/my_account/change_password.php | 11 +- app/Views/admin/page/create.php | 16 +- app/Views/admin/page/edit.php | 15 +- app/Views/admin/page/list.php | 87 +- app/Views/admin/page/view.php | 17 +- app/Views/admin/podcast/_sidebar.php | 94 ++ app/Views/admin/podcast/create.php | 265 ++-- app/Views/admin/podcast/edit.php | 256 ++-- app/Views/admin/podcast/import.php | 126 +- app/Views/admin/podcast/latest_episodes.php | 101 ++ app/Views/admin/podcast/list.php | 59 +- .../admin/podcast/settings/dashboard.php | 6 +- .../admin/podcast/settings/platforms.php | 15 +- app/Views/admin/podcast/view.php | 58 +- app/Views/admin/user/create.php | 20 +- app/Views/admin/user/edit.php | 17 +- app/Views/admin/user/list.php | 128 +- app/Views/auth/_layout.php | 7 +- app/Views/auth/forgot.php | 11 +- app/Views/auth/login.php | 12 +- app/Views/auth/register.php | 11 +- app/Views/auth/reset.php | 11 +- app/Views/episode.php | 20 +- app/Views/install/_layout.php | 5 +- app/Views/install/env.php | 11 +- app/Views/install/superadmin.php | 11 +- app/Views/pager/default_full.php | 67 ++ app/Views/podcast.php | 22 +- composer.json | 2 +- composer.lock | 69 +- package-lock.json | 1061 ++++++++++------- package.json | 41 +- tailwind.config.js | 14 +- 111 files changed, 3933 insertions(+), 1626 deletions(-) create mode 100644 app/Database/Migrations/2020-09-29-150000_add_podcasts_categories.php create mode 100644 app/Helpers/components_helper.php create mode 100644 app/Helpers/form_helper.php create mode 100644 app/Language/en/Pager.php create mode 100644 app/Language/en/PodcastImport.php create mode 100644 app/Language/en/PodcastNavigation.php create mode 100644 app/Views/_assets/icons/caret-right.svg create mode 100644 app/Views/_assets/icons/chevron-left.svg rename app/Views/_assets/icons/{arrow-right.svg => chevron-right.svg} (100%) create mode 100644 app/Views/_assets/icons/download.svg create mode 100644 app/Views/_assets/icons/line-chart.svg create mode 100644 app/Views/_assets/icons/menu.svg create mode 100644 app/Views/_assets/icons/question.svg create mode 100644 app/Views/_assets/icons/settings.svg create mode 100644 app/Views/_assets/icons/user-add.svg create mode 100644 app/Views/_assets/icons/user.svg create mode 100644 app/Views/_assets/modules/EnclosureInput.ts delete mode 100644 app/Views/_assets/modules/HTMLEditor.ts create mode 100644 app/Views/_assets/modules/MultiSelect.ts create mode 100644 app/Views/_assets/modules/SidebarToggler.ts create mode 100644 app/Views/_assets/styles/enclosureInput.css create mode 100644 app/Views/_assets/styles/multiSelect.css create mode 100644 app/Views/_assets/styles/radioBtn.css create mode 100644 app/Views/_assets/styles/switch.css delete mode 100644 app/Views/admin/_header.php delete mode 100644 app/Views/admin/_partials/_episode-card.php delete mode 100644 app/Views/admin/_partials/_episode-list.php delete mode 100644 app/Views/admin/_partials/_podcast-card.php rename app/Views/admin/{_sidenav.php => _sidebar.php} (50%) create mode 100644 app/Views/admin/podcast/_sidebar.php create mode 100644 app/Views/admin/podcast/latest_episodes.php create mode 100644 app/Views/pager/default_full.php diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index fed484c99b..df0c8a6e8d 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 50a4a5f97e..699ab90782 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 92f4849ab4..5d7de7d273 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 693746d003..01e66ee695 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 39ce2e0c0b..5e25bd00c1 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 7a058af079..50f2fb4534 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 384b72bf21..f2ce56db4b 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 ab0dde47df..794bcef384 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 63874156ca..be174529ae 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 aaac73ccc0..1a36daf16f 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 ab5eef7da6..2f5bdcff29 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 a0e0dfa2cb..8b3409014e 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 4c024439dc..a95e4db108 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 e965fd8e6e..24b2f02fc5 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 0000000000..4139b15011 --- /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 9c7311231a..7bbda5e53f 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 0a8a273ba0..c0dd2b7def 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 ed6bc92029..6a3e7a1fcf 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 52022860be..03d9c86b3a 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 0000000000..5b26da6df1 --- /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 0000000000..5be15eecfa --- /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 68da0f2ae1..532b9bcb57 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 a3b353d7ab..921284c9d6 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 9b398f577d..212863f5d1 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 ac242f8f74..7e90eb891b 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 46b95f0416..540dc57113 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 d2563a115f..d6a01ac2d0 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 e0f5f032ee..c15145b1ef 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 0000000000..d18f15edbf --- /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 2c573d7282..8a86df06c9 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 0000000000..6b86eb1693 --- /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 0000000000..05fe31f198 --- /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 2e221aaf30..3c84d274bb 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 43cb2f951a..816f61eb3e 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 af52ff04e7..6764f6ed04 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 9eb734d830..d72f3f0865 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 16f50a264b..2df7234921 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 2f39055678..7967215c18 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 6d82f7ba31..d10d02b504 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 0000000000..346cb156b8 --- /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 0000000000..6d82f7ba31 --- /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 0000000000..42702f57db --- /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 0000000000..c3080e57c7 --- /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 0000000000..666764dc34 --- /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 0000000000..984376ae7d --- /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 0000000000..8ab66f65d2 --- /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 0000000000..ab808608ff --- /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 0000000000..9e64bb5632 --- /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 0000000000..ef6b95e1bd --- /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 7e67b65fe2..0000000000 --- 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 cb38bca969..604e948ea3 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 0000000000..87ef62be34 --- /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 0000000000..f9176f4f65 --- /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 7f91ec0b85..b8d2ab2de0 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 f2cb91620f..0a89fe55b8 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 0000000000..44ea532923 --- /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 f12f46bb99..d68082f6bf 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 bed5c1eb0c..b613b7473d 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 0000000000..83fe9c76a7 --- /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 0000000000..7e6045d6f4 --- /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 0000000000..be0cbe0f35 --- /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 5bd01391ee..c2ce491826 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 274f52dc63..0000000000 --- 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 f87c44f151..8cd3c3f166 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 4ff75d35ce..0000000000 --- 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 d396e43be9..0000000000 --- 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 00d7f44116..0000000000 --- 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 576311f197..e116b04e5e 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 5e0b205e38..d6e1bf49d7 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 2f69a13aca..8d8530d837 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 3a2c73adda..cc995aeecf 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 e3c5e9874f..8d55150c80 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 12e34057a6..214ccaaf7a 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 54e9832a2d..30bf7fc1e5 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 9d0ad5211d..155013db42 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 c6cfe92da3..5adf9c8cd9 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 d3ccaa1e31..5ecb203113 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 285f058e6e..9ae867b6a7 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 817a6c833f..1147f9d9c2 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 58b99b5e12..9d23cddff3 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 fd2a962c7b..1db15fd73b 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 992550e4a0..7a64d6f13d 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 0000000000..23bc1f3ffc --- /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 4832413d06..138dad7dc9 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 73271a3f28..8b6b1c6c06 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 853de8991b..6ec7ac586c 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 0000000000..840bfa5569 --- /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 cc1097cf32..a6a00c7237 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 f7a742fd88..4b9bfa561d 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 5137eed9a2..4cbdc8c08a 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 e1fdba10c0..e0bb6aefcf 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 9b860c1bf5..b55c0d136e 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 ef9fc1f287..a269314361 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 f47b3b2220..1e54610cb0 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 6e0a94e2e9..04fa708844 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 7a742021a7..376fa93d9c 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 c231de5423..4faf178575 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 aa2a7db6d4..de58efd46f 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 8e26e72f2a..a89577aee2 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 dbdd6a2ea3..40076e1787 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 81f634ff01..20bd2d2a4f 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 d11b2a7f20..25db9b5d7b 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 d4ddc61f97..6b32c32cc3 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 0000000000..e2a065d0c1 --- /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 212be0da39..2c6cb9d633 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 dc3cf041ac..063237497a 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 d3126548ad..9e637f246b 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 bc926e3744..ae21fc46fb 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 258cb74f7a..a131f985cc 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 12a2371c5c..d4f1a1cf50 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, + }, }; -- GitLab