From b05d177f1b7f44fef043ac5eb41f07133a2cf52d Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Tue, 21 Sep 2021 15:51:04 +0000
Subject: [PATCH] fix: update MarkdownEditor component + restyle Button and
 other components

---
 app/Helpers/components_helper.php             |  8 ++--
 app/Language/en/Podcast.php                   |  4 +-
 app/Models/PersonModel.php                    |  2 +-
 app/Models/PodcastModel.php                   | 20 +++++++-
 .../js/modules/markdown-write-preview.ts      | 40 +++++++++++++---
 app/Views/Components/Button.php               | 22 ++++-----
 app/Views/Components/Forms/FormComponent.php  |  5 --
 app/Views/Components/Forms/MarkdownEditor.php | 46 ++++++++++---------
 app/Views/Components/Forms/Section.php        |  2 +-
 .../Admin/Controllers/PodcastController.php   |  1 -
 .../Controllers/PodcastImportController.php   |  4 +-
 modules/Admin/Language/en/Podcast.php         |  4 +-
 themes/cp_admin/_layout.php                   |  2 +-
 themes/cp_admin/contributor/list.php          |  6 +--
 themes/cp_admin/episode/list.php              |  2 +-
 themes/cp_admin/episode/persons.php           |  3 +-
 themes/cp_admin/fediverse/blocked_actors.php  |  2 +-
 themes/cp_admin/fediverse/blocked_domains.php |  2 +-
 themes/cp_admin/page/create.php               |  4 +-
 themes/cp_admin/page/edit.php                 | 12 ++---
 themes/cp_admin/page/list.php                 |  2 +-
 themes/cp_admin/page/view.php                 |  2 +-
 themes/cp_admin/person/create.php             |  2 +-
 themes/cp_admin/podcast/list.php              |  4 +-
 themes/cp_admin/podcast/persons.php           | 30 ++++++------
 themes/cp_admin/podcast/platforms.php         |  8 ++--
 themes/cp_admin/podcast/view.php              |  4 +-
 themes/cp_admin/user/create.php               |  2 +-
 themes/cp_admin/user/list.php                 |  2 +-
 themes/cp_auth/_layout.php                    | 14 +++---
 themes/cp_auth/_message_block.php             | 20 ++++++++
 themes/cp_auth/forgot.php                     |  2 +-
 themes/cp_auth/login.php                      |  2 +-
 themes/cp_auth/register.php                   |  3 +-
 themes/cp_auth/reset.php                      |  3 +-
 35 files changed, 176 insertions(+), 115 deletions(-)
 create mode 100644 themes/cp_auth/_message_block.php

diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php
index a858da80fc..f77d66ad92 100644
--- a/app/Helpers/components_helper.php
+++ b/app/Helpers/components_helper.php
@@ -57,14 +57,14 @@ if (! function_exists('data_table')) {
             '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">',
+                '<thead class="text-xs font-semibold text-left text-gray-500 uppercase">',
 
             '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-50 hover:bg-pine-50">',
-            'row_alt_start' => '<tr class="hover:bg-pine-50">',
+            'row_start' => '<tr class="border-t hover:bg-pine-50">',
+            'row_alt_start' => '<tr class="border-t hover:bg-pine-50">',
         ];
 
         $table->setTemplate($template);
@@ -89,7 +89,7 @@ if (! function_exists('data_table')) {
             return lang('Common.no_data');
         }
 
-        return '<div class="overflow-x-auto bg-white rounded-lg shadow ' . $class . '" >' .
+        return '<div class="overflow-x-auto bg-white rounded-lg border-3 border-pine-100 ' . $class . '" >' .
             $table->generate() .
             '</div>';
     }
diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php
index ec83a05e73..0683cb2d65 100644
--- a/app/Language/en/Podcast.php
+++ b/app/Language/en/Podcast.php
@@ -11,8 +11,8 @@ declare(strict_types=1);
 return [
     'all_podcasts' => 'All podcasts',
     'no_podcast' => 'No podcast found!',
-    'create' => 'Create a podcast',
-    'import' => 'Import a podcast',
+    'create' => 'Create podcast',
+    'import' => 'Import podcast',
     'new_episode' => 'New Episode',
     'feed' => 'RSS',
     'view' => 'View podcast',
diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php
index 41cc30a26c..36fba45ca1 100644
--- a/app/Models/PersonModel.php
+++ b/app/Models/PersonModel.php
@@ -62,7 +62,7 @@ class PersonModel extends Model
     protected $validationRules = [
         'full_name' => 'required',
         'unique_name' =>
-            'required|regex_match[/^[a-z0-9\-]{1,191}$/]|is_unique[persons.unique_name,id,{id}]',
+            'required|regex_match[/^[a-z0-9\-]{1,32}$/]|is_unique[persons.unique_name,id,{id}]',
         'created_by' => 'required',
         'updated_by' => 'required',
     ];
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 8022db3a69..5e18549101 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -10,6 +10,7 @@ declare(strict_types=1);
 
 namespace App\Models;
 
+use App\Entities\Actor;
 use App\Entities\Podcast;
 use CodeIgniter\Database\Query;
 use CodeIgniter\HTTP\URI;
@@ -88,7 +89,7 @@ class PodcastModel extends Model
     protected $validationRules = [
         'title' => 'required',
         'handle' =>
-            'required|regex_match[/^[a-zA-Z0-9\_]{1,191}$/]|is_unique[podcasts.handle,id,{id}]',
+            'required|regex_match[/^[a-zA-Z0-9\_]{1,32}$/]|is_unique[podcasts.handle,id,{id}]',
         'description_markdown' => 'required',
         'image_path' => 'required',
         'language_code' => 'required',
@@ -102,7 +103,7 @@ class PodcastModel extends Model
     /**
      * @var string[]
      */
-    protected $beforeInsert = ['createPodcastActor'];
+    protected $beforeInsert = ['setPodcastGUID', 'createPodcastActor'];
 
     /**
      * @var string[]
@@ -489,4 +490,19 @@ class PodcastModel extends Model
 
         return $data;
     }
+
+    /**
+     * @param mixed[] $data
+     *
+     * @return mixed[]
+     */
+    protected function setPodcastGUID(array $data): array
+    {
+        if (! array_key_exists('guid', $data['data']) || $data['data']['guid'] === null) {
+            helper('misc');
+            $data['data']['guid'] = podcast_uuid(url_to('podcast_feed', $data['data']['handle']));
+        }
+
+        return $data;
+    }
 }
diff --git a/app/Resources/js/modules/markdown-write-preview.ts b/app/Resources/js/modules/markdown-write-preview.ts
index 7c8947c3e7..7c10359bf4 100644
--- a/app/Resources/js/modules/markdown-write-preview.ts
+++ b/app/Resources/js/modules/markdown-write-preview.ts
@@ -1,4 +1,4 @@
-import { html, LitElement, TemplateResult } from "lit";
+import { css, html, LitElement, TemplateResult } from "lit";
 import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
 import { MarkdownPreview } from "./markdown-preview";
 
@@ -28,20 +28,48 @@ export class MarkdownWritePreview extends LitElement {
     ) as MarkdownPreview;
   }
 
+  firstUpdated(): void {
+    this.write();
+  }
+
   write(): void {
     this._markdownPreview.hide();
-    this._write[0].classList.add("font-semibold");
-    this._preview[0].classList.remove("font-semibold");
+    this._write[0].classList.add("active");
+    this._preview[0].classList.remove("active");
   }
 
   preview(): void {
     this._markdownPreview.show();
-    this._preview[0].classList.add("font-semibold");
-    this._write[0].classList.remove("font-semibold");
+    this._preview[0].classList.add("active");
+    this._write[0].classList.remove("active");
   }
 
+  static styles = css`
+    ::slotted(button) {
+      opacity: 0.5;
+    }
+
+    ::slotted(button.active) {
+      position: relative;
+      opacity: 1;
+    }
+
+    ::slotted(button.active)::after {
+      content: "";
+      position: absolute;
+      bottom: -2px;
+      left: 0;
+      right: 0;
+      width: 80%;
+      height: 4px;
+      margin: 0 auto;
+      background-color: #009486;
+      border-radius: 9999px;
+    }
+  `;
+
   render(): TemplateResult<1> {
-    return html`<slot name="write" @click="${this.write}"></slot>
+    return html`<slot name="write" class="active" @click="${this.write}"></slot>
       <slot name="preview" @click="${this.preview}"></slot>`;
   }
 }
diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php
index 34473df475..dc2080fccc 100644
--- a/app/Views/Components/Button.php
+++ b/app/Views/Components/Button.php
@@ -28,29 +28,29 @@ class Button extends Component
     public function render(): string
     {
         $baseClass =
-            'inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring';
+            'flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring-2 focus:ring-pine-500 focus:ring-offset-2 focus:ring-offset-pine-100';
 
         $variantClass = [
             'default' => 'text-black bg-gray-300 hover:bg-gray-400',
             'primary' => 'text-white bg-pine-500 hover:bg-pine-800',
-            'secondary' => 'text-white bg-gray-700 hover:bg-gray-800',
+            'secondary' => 'border-2 border-pine-500 text-pine-500 bg-pine-100 hover:border-pine-800 hover:text-pine-800',
             'accent' => 'text-white bg-rose-600 hover:bg-rose-800',
-            'success' => 'text-white bg-green-600 hover:bg-green-700',
+            'success' => 'text-white bg-pine-500 hover:bg-pine-800',
             'danger' => 'text-white bg-red-600 hover:bg-red-700',
             'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
             'info' => 'text-white bg-blue-500 hover:bg-blue-600',
         ];
 
         $sizeClass = [
-            'small' => 'text-xs md:text-sm',
-            'base' => 'text-sm md:text-base',
-            'large' => 'text-lg md:text-xl',
+            'small' => 'text-xs leading-6',
+            'base' => 'text-sm leading-5',
+            'large' => 'text-base leading-6',
         ];
 
         $basePaddings = [
-            'small' => 'px-2 md:px-3 md:py-1',
-            'base' => 'px-3 py-1 md:px-4 md:py-2',
-            'large' => 'px-3 py-2 md:px-5',
+            'small' => 'px-2 py-1',
+            'base' => 'px-3 py-2',
+            'large' => 'px-4 py-2',
         ];
 
         $squaredPaddings = [
@@ -76,11 +76,11 @@ class Button extends Component
         }
 
         if ($this->iconLeft !== '') {
-            $this->slot = '<Icon glyph="' . $this->iconLeft . '" class="mr-2" />' . $this->slot;
+            $this->slot = '<Icon glyph="' . $this->iconLeft . '" class="mr-2 opacity-75" />' . $this->slot;
         }
 
         if ($this->iconRight !== '') {
-            $this->slot .= '<Icon glyph="' . $this->iconRight . '" class="ml-2" />';
+            $this->slot .= '<Icon glyph="' . $this->iconRight . '" class="ml-2 opacity-75" />';
         }
 
         unset($this->attributes['slot']);
diff --git a/app/Views/Components/Forms/FormComponent.php b/app/Views/Components/Forms/FormComponent.php
index 818f1ff5af..251187efba 100644
--- a/app/Views/Components/Forms/FormComponent.php
+++ b/app/Views/Components/Forms/FormComponent.php
@@ -31,11 +31,6 @@ class FormComponent extends Component
     public function setRequired(string $value): void
     {
         $this->required = $value === 'true';
-        if ($this->required) {
-            $this->attributes['required'] = 'required';
-        } else {
-            unset($this->attributes['required']);
-        }
     }
 
     public function setReadonly(string $value): void
diff --git a/app/Views/Components/Forms/MarkdownEditor.php b/app/Views/Components/Forms/MarkdownEditor.php
index 4a729729c0..1c064ae573 100644
--- a/app/Views/Components/Forms/MarkdownEditor.php
+++ b/app/Views/Components/Forms/MarkdownEditor.php
@@ -36,31 +36,33 @@ class MarkdownEditor extends FormComponent
 
         return <<<HTML
             <div class="{$editorClass}">
-                <header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-black">
-                    <markdown-write-preview for="{$this->id}" class="relative inline-flex h-8">
-                        <button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">{$translations['write']}</button>
-                        <button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">{$translations['preview']}</button>
-                    </markdown-write-preview>
-                    <markdown-toolbar for=" {$this->id} " class="flex gap-4 px-2 py-1">
-                        <div class="inline-flex text-2xl gap-x-1">
-                            <md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['heading']}</md-header>
-                            <md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['bold']}</md-bold>
-                            <md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['italic']}</md-italic>
-                        </div>
-                        <div class="inline-flex text-2xl gap-x-1">
-                            <md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['list-unordered']}</md-unordered-list>
-                            <md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['list-ordered']}</md-ordered-list>
-                        </div>
-                        <div class="inline-flex text-2xl gap-x-1">
-                            <md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['quote']}</md-quote>
-                            <md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['link']}</md-link>
-                            <md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['image-add']}</md-image>
-                        </div>
-                    </markdown-toolbar>
+                <header class="px-2">
+                    <div class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-300">
+                        <markdown-write-preview for="{$this->id}" class="relative inline-flex h-8">
+                            <button type="button" slot="write" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">{$translations['write']}</button>
+                            <button type="button" slot="preview" class="px-2 font-semibold focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">{$translations['preview']}</button>
+                        </markdown-write-preview>
+                        <markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">
+                            <div class="inline-flex text-2xl gap-x-1">
+                                <md-header class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['heading']}</md-header>
+                                <md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['bold']}</md-bold>
+                                <md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['italic']}</md-italic>
+                            </div>
+                            <div class="inline-flex text-2xl gap-x-1">
+                                <md-unordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['list-unordered']}</md-unordered-list>
+                                <md-ordered-list class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['list-ordered']}</md-ordered-list>
+                            </div>
+                            <div class="inline-flex text-2xl gap-x-1">
+                                <md-quote class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['quote']}</md-quote>
+                                <md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['link']}</md-link>
+                                <md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">{$icons['image-add']}</md-image>
+                            </div>
+                        </markdown-toolbar>
+                    </div>
                 </header>
                 <div class="relative">
                     {$textarea}
-                    <markdown-preview for=" {$this->id} " class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white" />
+                    <markdown-preview for="{$this->id}" class="absolute top-0 left-0 hidden w-full h-full p-2 overflow-y-auto prose bg-gray-50" showClass="bg-white" />
                 </div>
                 <footer class="flex px-2 py-1 bg-gray-100 border-t">
                     <a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-gray-500 hover:text-gray-700" target="_blank" rel="noopener noreferrer">{$icons['markdown']}{$translations['help']}</a>
diff --git a/app/Views/Components/Forms/Section.php b/app/Views/Components/Forms/Section.php
index 7458f6436b..b7aed14b16 100644
--- a/app/Views/Components/Forms/Section.php
+++ b/app/Views/Components/Forms/Section.php
@@ -19,7 +19,7 @@ class Section extends Component
         $subtitle = $this->subtitle === null ? '' : '<p class="text-sm text-gray-600 clear-left ' . $this->subtitleClass . '">' . $this->subtitle . '</p>';
 
         return <<<HTML
-            <fieldset class="w-full max-w-xl p-8 bg-white border-2 border-black rounded-xl {$this->class}">
+            <fieldset class="w-full max-w-xl p-8 bg-white border-3 border-pine-100 rounded-xl {$this->class}">
                 <Heading tagName="legend" class="float-left">{$this->title}</Heading>
                 {$subtitle}
                 <div class="flex flex-col gap-4 py-4 clear-left">{$this->slot}</div>
diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php
index 6999c7efbe..dfdfc57be3 100644
--- a/modules/Admin/Controllers/PodcastController.php
+++ b/modules/Admin/Controllers/PodcastController.php
@@ -192,7 +192,6 @@ class PodcastController extends BaseController
         }
 
         $podcast = new Podcast([
-            'guid' => podcast_uuid(url_to('podcast_feed', $this->request->getPost('handle'))),
             'title' => $this->request->getPost('title'),
             'handle' => $this->request->getPost('handle'),
             'description_markdown' => $this->request->getPost('description'),
diff --git a/modules/Admin/Controllers/PodcastImportController.php b/modules/Admin/Controllers/PodcastImportController.php
index 4d750c4477..044f25a66a 100644
--- a/modules/Admin/Controllers/PodcastImportController.php
+++ b/modules/Admin/Controllers/PodcastImportController.php
@@ -128,12 +128,10 @@ class PodcastImportController extends BaseController
                     $nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
                 );
             }
+            $guid = null;
             if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
                 $guid = (string) $nsPodcast->guid;
-            } else {
-                $guid = podcast_uuid(url_to('podcast_feed', $this->request->getPost('handle')));
             }
-
             $podcast = new Podcast([
                 'guid' => $guid,
                 'handle' => $this->request->getPost('handle'),
diff --git a/modules/Admin/Language/en/Podcast.php b/modules/Admin/Language/en/Podcast.php
index cd8d087250..2a107c52d1 100644
--- a/modules/Admin/Language/en/Podcast.php
+++ b/modules/Admin/Language/en/Podcast.php
@@ -11,8 +11,8 @@ declare(strict_types=1);
 return [
     'all_podcasts' => 'All podcasts',
     'no_podcast' => 'No podcast found!',
-    'create' => 'Create a podcast',
-    'import' => 'Import a podcast',
+    'create' => 'Create podcast',
+    'import' => 'Import podcast',
     'new_episode' => 'New Episode',
     'feed' => 'RSS',
     'view' => 'View podcast',
diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php
index e77eaf7bd3..efb1eb76c5 100644
--- a/themes/cp_admin/_layout.php
+++ b/themes/cp_admin/_layout.php
@@ -102,4 +102,4 @@
         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:ring"
         style="transform: translateX(0px);"><?= icon('menu') ?></button>
-</body>
+</body>
\ No newline at end of file
diff --git a/themes/cp_admin/contributor/list.php b/themes/cp_admin/contributor/list.php
index f6e3a6ea99..ea6c4750c3 100644
--- a/themes/cp_admin/contributor/list.php
+++ b/themes/cp_admin/contributor/list.php
@@ -9,7 +9,7 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="accent" iconLeft="add"><?= lang('Contributor.add') ?></Button>
+<Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="primary" iconLeft="add"><?= lang('Contributor.add') ?></Button>
 <?= $this->endSection() ?>
 
 
@@ -32,8 +32,8 @@
         [
             'header' => lang('Common.actions'),
             'cell' => function ($contributor, $podcast) {
-                return '<Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="info" size="small">' . lang('Contributor.edit') . '</Button>' .
-                '<Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" size="small">' . lang('Contributor.remove') . '</Button>';
+                return '<Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="secondary" iconLeft="edit" size="small">' . lang('Contributor.edit') . '</Button>' .
+                '<Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" iconLeft="delete-bin" size="small">' . lang('Contributor.remove') . '</Button>';
             },
         ],
     ],
diff --git a/themes/cp_admin/episode/list.php b/themes/cp_admin/episode/list.php
index ec7b2c4dbd..7693633821 100644
--- a/themes/cp_admin/episode/list.php
+++ b/themes/cp_admin/episode/list.php
@@ -9,7 +9,7 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="accent" iconLeft="add"><?= lang('Episode.create') ?></Button>
+<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add"><?= lang('Episode.create') ?></Button>
 <?= $this->endSection() ?>
 
 
diff --git a/themes/cp_admin/episode/persons.php b/themes/cp_admin/episode/persons.php
index 2b64fe3c81..a903f0ce1f 100644
--- a/themes/cp_admin/episode/persons.php
+++ b/themes/cp_admin/episode/persons.php
@@ -55,9 +55,10 @@
         ],
     ],
     $episode->persons,
+    'max-w-xl'
 ) ?>
 
-<form action="<?= route_to('episode-person-edit', $episode->id) ?>" method="POST" class="mt-6">
+<form action="<?= route_to('episode-persons-manage', $episode->id) ?>" method="POST" class="mt-6">
 <?= csrf_field() ?>
 
 <Forms.Section
diff --git a/themes/cp_admin/fediverse/blocked_actors.php b/themes/cp_admin/fediverse/blocked_actors.php
index 754cacce3a..2d4bf514c6 100644
--- a/themes/cp_admin/fediverse/blocked_actors.php
+++ b/themes/cp_admin/fediverse/blocked_actors.php
@@ -12,7 +12,7 @@
 <?= $this->section('content') ?>
 
 <form action="<?= route_to('fediverse-attempt-block-actor') ?>" method="POST" class="flex flex-col max-w-md">
-    <Forms.Field name="handle" label="<?= lang('Fediverse.block_lists_form.handle') ?>" hint="<?= lang('Fediverse.block_lists_form.handle_hint') ?>" />
+    <Forms.Field name="handle" label="<?= lang('Fediverse.block_lists_form.handle') ?>" hint="<?= lang('Fediverse.block_lists_form.handle_hint') ?>" required="true" />
     <Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
 </form>
 
diff --git a/themes/cp_admin/fediverse/blocked_domains.php b/themes/cp_admin/fediverse/blocked_domains.php
index 65d10ea994..3ff9ae87cd 100644
--- a/themes/cp_admin/fediverse/blocked_domains.php
+++ b/themes/cp_admin/fediverse/blocked_domains.php
@@ -12,7 +12,7 @@
 <?= $this->section('content') ?>
 
 <form action="<?= route_to('fediverse-attempt-block-domain') ?>" method="POST" class="flex flex-col max-w-md">
-    <Forms.Field name="domain" label="<?= lang('Fediverse.block_lists_form.domain') ?>" />
+    <Forms.Field name="domain" label="<?= lang('Fediverse.block_lists_form.domain') ?>" required="true" />
     <Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
 </form>
 
diff --git a/themes/cp_admin/page/create.php b/themes/cp_admin/page/create.php
index 0a13133b9d..50c6351b73 100644
--- a/themes/cp_admin/page/create.php
+++ b/themes/cp_admin/page/create.php
@@ -21,10 +21,10 @@
     data-slugify="title"
     class="max-w-sm" />
 
-<div>
+<div class="flex flex-col max-w-sm">
     <Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
     <permalink-edit class="inline-flex items-center text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
-        <span slot="domain"><?= base_url('pages') . '/' ?></span>
+        <span slot="domain" class="flex-shrink-0"><?= base_url('pages') . '/' ?></span>
         <Forms.Input name="slug" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
     </permalink-edit>
 </div>
diff --git a/themes/cp_admin/page/edit.php b/themes/cp_admin/page/edit.php
index 14ac283eec..a8957e20b9 100644
--- a/themes/cp_admin/page/edit.php
+++ b/themes/cp_admin/page/edit.php
@@ -11,23 +11,23 @@
 
 <?= $this->section('content') ?>
 
-<form action="<?= route_to('page-edit', $page->id) ?>" method="POST" class="flex flex-col max-w-3xl">
+<form action="<?= route_to('page-edit', $page->id) ?>" method="POST" class="flex flex-col max-w-3xl gap-y-4">
 <?= csrf_field() ?>
 
-
 <Forms.Field
     name="title"
     label="<?= lang('Page.form.title') ?>"
     required="true"
     data-slugify="title"
     value="<?= $page->title ?>"
-    slot="slug-input" />
+    slot="slug-input"
+    class="max-w-sm" />
 
-<div>
+<div class="flex flex-col max-w-sm">
     <Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
     <permalink-edit class="inline-flex items-center text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>">
-        <span slot="domain"><?= base_url('pages') . '/' ?></span>
-        <Forms.Input name="slug" value="<?= $episode->slug ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= $page->slug ?>"/>
+        <span slot="domain" class="flex-shrink-0"><?= base_url('pages') . '/' ?></span>
+        <Forms.Input name="slug" value="<?= $page->slug ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= $page->slug ?>"/>
     </permalink-edit>
 </div>
 
diff --git a/themes/cp_admin/page/list.php b/themes/cp_admin/page/list.php
index 8925a76997..37ba1ad3cb 100644
--- a/themes/cp_admin/page/list.php
+++ b/themes/cp_admin/page/list.php
@@ -9,7 +9,7 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('page-create') ?>" variant="accent" iconLeft="add"><?= lang('Page.create') ?></Button>
+<Button uri="<?= route_to('page-create') ?>" variant="primary" iconLeft="add"><?= lang('Page.create') ?></Button>
 <?= $this->endSection() ?>
 
 
diff --git a/themes/cp_admin/page/view.php b/themes/cp_admin/page/view.php
index 97d8bd3523..fde36892c7 100644
--- a/themes/cp_admin/page/view.php
+++ b/themes/cp_admin/page/view.php
@@ -9,7 +9,7 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button variant="accent" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add"><?= lang('Page.edit') ?></Button>
+<Button variant="primary" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add"><?= lang('Page.edit') ?></Button>
 <?= $this->endSection() ?>
 
 <?= $this->section('content') ?>
diff --git a/themes/cp_admin/person/create.php b/themes/cp_admin/person/create.php
index dc634870c5..2b1425737b 100644
--- a/themes/cp_admin/person/create.php
+++ b/themes/cp_admin/person/create.php
@@ -11,7 +11,7 @@
 
 <?= $this->section('content') ?>
 
-<form action="<?= route_to('person-create') ?>" method="POST" class="flex flex-col max-w-sm" enctype="multipart/form-data">
+<form action="<?= route_to('person-create') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
 <?= csrf_field() ?>
 
 <Forms.Field
diff --git a/themes/cp_admin/podcast/list.php b/themes/cp_admin/podcast/list.php
index 637b40bcdc..4d1738e637 100644
--- a/themes/cp_admin/podcast/list.php
+++ b/themes/cp_admin/podcast/list.php
@@ -9,8 +9,8 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('podcast-create') ?>" variant="accent" iconLeft="add"><?= lang('Podcast.create') ?></Button>
-<Button uri="<?= route_to('podcast-import') ?>" variant="primary" iconLeft="download"><?= lang('Podcast.import') ?></Button>
+<Button uri="<?= route_to('podcast-import') ?>" variant="secondary" iconLeft="download"><?= lang('Podcast.import') ?></Button>
+<Button uri="<?= route_to('podcast-create') ?>" variant="primary" iconLeft="add"><?= lang('Podcast.create') ?></Button>
 <?= $this->endSection() ?>
 
 
diff --git a/themes/cp_admin/podcast/persons.php b/themes/cp_admin/podcast/persons.php
index 2b2f9c3a34..f5e2e58a4e 100644
--- a/themes/cp_admin/podcast/persons.php
+++ b/themes/cp_admin/podcast/persons.php
@@ -50,31 +50,31 @@
         [
             'header' => lang('Common.actions'),
             'cell' => function ($person): string {
-                return '<Button uri="' . route_to('podcast-person-remove', $person->podcast_id, $person->id) . '" variant="danger" size="small">' . lang('Person.podcast_form.remove') . '</Button>';
+                return '<Button uri="' . route_to('podcast-person-remove', $person->podcast_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin">' . lang('Person.podcast_form.remove') . '</Button>';
             },
         ],
     ],
     $podcast->persons,
+    'max-w-xl'
 ) ?>
 
-<form action="<?= route_to('podcast-person-edit', $podcast->id) ?>" method="POST" class="mt-6">
-<?= csrf_field() ?>
+<form action="<?= route_to('podcast-persons-manage', $podcast->id) ?>" method="POST" class="mt-6">
+    <?= csrf_field() ?>
 
-<Forms.Section
-    title="<?= lang('Person.podcast_form.add_section_title') ?>"
-    subtitle="<?= lang('Person.podcast_form.add_section_subtitle') ?>"
->
+    <Forms.Section
+        title="<?= lang('Person.podcast_form.add_section_title') ?>"
+        subtitle="<?= lang('Person.podcast_form.add_section_subtitle') ?>"
+    >
 
-<Forms.Label for="persons" hint="<?= lang('Person.podcast_form.persons_hint') ?>"><?= lang('Person.podcast_form.persons') ?></Forms.Label>
-<Forms.MultiSelect id="persons" name="persons[]" class="mb-4" required="required" options="<?= esc(json_encode($personOptions)) ?>" selected="<?= esc(json_encode(old('persons', []))) ?>"/>
+    <Forms.Label for="persons" hint="<?= lang('Person.podcast_form.persons_hint') ?>"><?= lang('Person.podcast_form.persons') ?></Forms.Label>
+    <Forms.MultiSelect id="persons" name="persons[]" class="mb-4" required="required" options="<?= esc(json_encode($personOptions)) ?>" selected="<?= esc(json_encode(old('persons', []))) ?>"/>
 
-<Forms.Label for="roles" hint="<?= lang('Person.podcast_form.roles_hint') ?>" isOptional="true"><?= lang('Person.podcast_form.roles') ?></Forms.Label>
-<Forms.MultiSelect id="roles" name="roles[]" class="mb-4" options="<?= esc(json_encode($taxonomyOptions)) ?>" selected="<?= esc(json_encode(old('roles', []))) ?>"/>
+    <Forms.Label for="roles" hint="<?= lang('Person.podcast_form.roles_hint') ?>" isOptional="true"><?= lang('Person.podcast_form.roles') ?></Forms.Label>
+    <Forms.MultiSelect id="roles" name="roles[]" class="mb-4" options="<?= esc(json_encode($taxonomyOptions)) ?>" selected="<?= esc(json_encode(old('roles', []))) ?>"/>
 
-<Button variant="primary" class="self-end" type="submit"><?= lang('Person.podcast_form.submit_add') ?></Button>
-
-</Forms.Section>
+    <Button variant="primary" class="self-end" type="submit"><?= lang('Person.podcast_form.submit_add') ?></Button>
 
+    </Forms.Section>
 </form>
 
-<?= $this->endSection() ?>
+<?= $this->endSection() ?>
\ No newline at end of file
diff --git a/themes/cp_admin/podcast/platforms.php b/themes/cp_admin/podcast/platforms.php
index 88313a735e..2b4cad9ae3 100644
--- a/themes/cp_admin/podcast/platforms.php
+++ b/themes/cp_admin/podcast/platforms.php
@@ -8,9 +8,13 @@
 <?= lang('Platforms.title') ?>
 <?= $this->endSection() ?>
 
+<?= $this->section('headerRight') ?>
+<Button form="platforms-form" variant="primary" type="submit" class="self-end"><?= lang('Platforms.submit') ?></Button>
+<?= $this->endSection() ?>
+
 <?= $this->section('content') ?>
 
-<form action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="flex flex-col max-w-md">
+<form id="platforms-form" action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="flex flex-col max-w-md">
 <?= csrf_field() ?>
 
 <?php foreach ($platforms as $platform): ?>
@@ -102,8 +106,6 @@
 
 <?php endforeach; ?>
 
-<Button variant="primary" type="submit" class="self-end"><?= lang('Platforms.submit') ?></Button>
-
 </form>
 
 <?= $this->endSection() ?>
diff --git a/themes/cp_admin/podcast/view.php b/themes/cp_admin/podcast/view.php
index a9b141e395..0595700821 100644
--- a/themes/cp_admin/podcast/view.php
+++ b/themes/cp_admin/podcast/view.php
@@ -9,8 +9,8 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('podcast-edit', $podcast->id) ?>" variant="primary" iconLeft="edit"><?= lang('Podcast.edit') ?></Button>
-<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="accent" iconLeft="add"><?= lang('Episode.create') ?></Button>
+<Button uri="<?= route_to('podcast-edit', $podcast->id) ?>" variant="secondary" iconLeft="edit"><?= lang('Podcast.edit') ?></Button>
+<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add"><?= lang('Episode.create') ?></Button>
 <?= $this->endSection() ?>
 
 <?= $this->section('content') ?>
diff --git a/themes/cp_admin/user/create.php b/themes/cp_admin/user/create.php
index 6f68d2e9d7..d81377df4e 100644
--- a/themes/cp_admin/user/create.php
+++ b/themes/cp_admin/user/create.php
@@ -11,7 +11,7 @@
 
 <?= $this->section('content') ?>
 
-<form action="<?= route_to('user-create') ?>" method="POST" class="flex flex-col max-w-sm">
+<form action="<?= route_to('user-create') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4">
 <?= csrf_field() ?>
 
 <Forms.Field
diff --git a/themes/cp_admin/user/list.php b/themes/cp_admin/user/list.php
index e8d46852c4..af2aac8eb2 100644
--- a/themes/cp_admin/user/list.php
+++ b/themes/cp_admin/user/list.php
@@ -9,7 +9,7 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('headerRight') ?>
-<Button uri="<?= route_to('user-create') ?>" variant="accent" iconLeft="user-add"><?= lang('User.create') ?></Button>
+<Button uri="<?= route_to('user-create') ?>" variant="primary" iconLeft="user-add"><?= lang('User.create') ?></Button>
 <?= $this->endSection() ?>
 
 
diff --git a/themes/cp_auth/_layout.php b/themes/cp_auth/_layout.php
index f2cbc7b04b..30fca7a6e1 100644
--- a/themes/cp_auth/_layout.php
+++ b/themes/cp_auth/_layout.php
@@ -12,24 +12,24 @@
         ->asset('styles/index.css', 'css') ?>
 </head>
 
-<body class="flex flex-col items-center justify-center min-h-screen mx-auto bg-gray-100">
+<body class="flex flex-col items-center justify-center min-h-screen mx-auto bg-pine-50">
 	<header class="mb-4">
 		<a href="<?= route_to(
             'home',
-        ) ?>" class="inline-flex items-baseline text-4xl font-bold font-display text-pine-700">
+        ) ?>" class="inline-flex items-baseline text-4xl font-bold font-display text-pine-500">
 			<?= 'castopod' . svg('castopod-logo', 'h-8 ml-2') ?>
 		</a>
 	</header>
-	<main class="w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg shadow">
-		<h1 class="mb-6 text-2xl font-bold text-center font-display"><?= $this->renderSection(
+	<main class="flex flex-col w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg border-3 border-pine-100 gap-y-4">
+		<Heading tagName="h1" size="large" class="self-center"><?= $this->renderSection(
             'title',
-        ) ?></h1>
-		<!-- view('_message_block') -->
+        ) ?></Heading>
+		<?= view('_message_block') ?>
 		<?= $this->renderSection('content') ?>
 	</main>
 	<footer class="flex flex-col text-sm">
 		<?= $this->renderSection('footer') ?>
-		<small class="py-4 text-center border-t"><?= lang('Common.powered_by', [
+		<small class="py-4 text-center border-t-2 border-pine-100"><?= lang('Common.powered_by', [
 		    'castopod' =>
 		        '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
 		]) ?></small>
diff --git a/themes/cp_auth/_message_block.php b/themes/cp_auth/_message_block.php
new file mode 100644
index 0000000000..3794ba39ac
--- /dev/null
+++ b/themes/cp_auth/_message_block.php
@@ -0,0 +1,20 @@
+<?php declare(strict_types=1);
+
+if (session()->has('message')): ?>
+    <Alert variant="success"><?= session('message') ?></Alert>
+<?php endif; ?>
+
+<?php if (session()->has('error')): ?>
+    <Alert variant="danger"><?= session('error') ?></Alert>
+<?php endif; ?>
+
+<?php if (session()->has('errors')): ?>
+    <Alert variant="danger">
+        <ul>
+            <?php foreach (session('errors') as $error): ?>
+                <li><?= $error ?></li>
+            <?php endforeach; ?>
+        </ul>
+    </Alert>
+<?php endif;
+?>
diff --git a/themes/cp_auth/forgot.php b/themes/cp_auth/forgot.php
index 5491648d7d..3895eb9703 100644
--- a/themes/cp_auth/forgot.php
+++ b/themes/cp_auth/forgot.php
@@ -10,7 +10,7 @@
 
 <p class="mb-4 text-gray-600"><?= lang('Auth.enterEmailForInstructions') ?></p>
 
-<form action="<?= route_to('forgot') ?>" method="POST" class="flex flex-col">
+<form action="<?= route_to('forgot') ?>" method="POST" class="flex flex-col w-full gap-y-4">
     <?= csrf_field() ?>
 
     <Forms.Field
diff --git a/themes/cp_auth/login.php b/themes/cp_auth/login.php
index d70c88dba8..8f7d124781 100644
--- a/themes/cp_auth/login.php
+++ b/themes/cp_auth/login.php
@@ -8,7 +8,7 @@
 
 <?= $this->section('content') ?>
 
-<form actions="<?= route_to('login') ?>" method="POST" class="flex flex-col">
+<form actions="<?= route_to('login') ?>" method="POST" class="flex flex-col w-full gap-y-4">
     <?= csrf_field() ?>
 
     <Forms.Field
diff --git a/themes/cp_auth/register.php b/themes/cp_auth/register.php
index 2be3657e6b..292f25ba3e 100644
--- a/themes/cp_auth/register.php
+++ b/themes/cp_auth/register.php
@@ -8,7 +8,7 @@
 
 <?= $this->section('content') ?>
 
-<form action="<?= route_to('register') ?>" method="POST" class="flex flex-col">
+<form action="<?= route_to('register') ?>" method="POST" class="flex flex-col w-full gap-y-4">
 <?= csrf_field() ?>
 
 <Forms.Field
@@ -26,6 +26,7 @@
 <Forms.Field
     name="password"
     label="<?= lang('Auth.password') ?>"
+    type="password"
     required="true"
     autocomplete="new-password" />
 
diff --git a/themes/cp_auth/reset.php b/themes/cp_auth/reset.php
index 09c597e6ec..a711430fdd 100644
--- a/themes/cp_auth/reset.php
+++ b/themes/cp_auth/reset.php
@@ -5,12 +5,11 @@
     <?= lang('Auth.resetYourPassword') ?>
 <?= $this->endSection() ?>
 
-
 <?= $this->section('content') ?>
 
 <p class="mb-4"><?= lang('Auth.enterCodeEmailPassword') ?></p>
 
-<form action="<?= route_to('reset-password') ?>" method="POST" class="flex flex-col">
+<form action="<?= route_to('reset-password') ?>" method="POST" class="flex flex-col w-full">
 <?= csrf_field() ?>
 
 <Forms.Field
-- 
GitLab