diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 1bc7dd61589f8134c67e3afb7434bb7ad63c751b..9436638053425efc6f1bf2a919fc19c9a3148547 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -46,6 +46,7 @@ class Autoload extends AutoloadConfig 'Config' => APPPATH . 'Config', 'ActivityPub' => APPPATH . 'Libraries/ActivityPub', 'Analytics' => APPPATH . 'Libraries/Analytics', + 'ViewComponents' => APPPATH . 'Libraries/ViewComponents', ]; /** @@ -84,5 +85,5 @@ class Autoload extends AutoloadConfig * ``` * @var array<int, string> */ - public $files = []; + public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php']; } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 649114ce6a4345bec2e722fa93bb469d869885ee..a15db5112d1e28acd63d3931418a418b430903ab 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -110,7 +110,9 @@ if (! function_exists('button')) { CODE_SAMPLE; } } + // ------------------------------------------------------------------------ + if (! function_exists('icon_button')) { /** * Icon Button component @@ -145,6 +147,7 @@ if (! function_exists('icon_button')) { } } // ------------------------------------------------------------------------ + if (! function_exists('hint_tooltip')) { /** * Hint component @@ -167,7 +170,9 @@ if (! function_exists('hint_tooltip')) { return $tooltip . '">' . icon('question') . '</span>'; } } + // ------------------------------------------------------------------------ + if (! function_exists('data_table')) { /** * Data table component @@ -223,7 +228,9 @@ if (! function_exists('data_table')) { '</div>'; } } + // ------------------------------------------------------------------------ + if (! function_exists('publication_pill')) { /** * Publication pill component @@ -250,7 +257,9 @@ if (! function_exists('publication_pill')) { '</span>'; } } + // ------------------------------------------------------------------------ + if (! function_exists('publication_button')) { /** * Publication button component @@ -508,27 +517,5 @@ if (! function_exists('relative_time')) { CODE_SAMPLE; } } -// ------------------------------------------------------------------------ -if (! function_exists('xml_editor')) { - /** - * XML Editor field - * - * @param array<string, mixed> $customData - * @param array<string, mixed> $extra - */ - function xml_editor(array $customData = [], string $value = '', array $extra = []): string - { - $defaultData = [ - 'slot' => 'textarea', - 'rows' => 5, - ]; - $data = array_merge($defaultData, $customData); - $textarea = form_textarea($data, $value, $extra); - - return <<<CODE_SAMPLE - <xml-editor>{$textarea}</time-ago> - CODE_SAMPLE; - } -} // ------------------------------------------------------------------------ diff --git a/app/Helpers/form_helper.php b/app/Helpers/form_helper.php index 49eb58026921d8e88cfcd5be52e0fc4b8cc35520..3ff660bd2c91f2d1a2a1d3b210b6887fd8422363 100644 --- a/app/Helpers/form_helper.php +++ b/app/Helpers/form_helper.php @@ -141,38 +141,12 @@ if (! function_exists('form_label')) { //-------------------------------------------------------------------- -if (! function_exists('form_multiselect')) { - /** - * Multi-select menu - * - * @param array<string, string> $options - * @param string[] $selected - * @param array<string, string> $customExtra - */ - function form_multiselect( - string $name = '', - array $options = [], - array $selected = [], - array $customExtra = [] - ): string { - $defaultExtra = [ - 'data-class' => $customExtra['class'], - 'multiple' => 'multiple', - ]; - $extra = array_merge($defaultExtra, $customExtra); - - return form_dropdown($name, $options, $selected, $extra); - } -} - -//-------------------------------------------------------------------- - if (! function_exists('form_dropdown')) { /** * Drop-down Menu (based on html select tag) * * @param array<string, mixed> $options - * @param string[] $selected + * @param array<string|int> $selected * @param array<string, mixed> $customExtra */ function form_dropdown( @@ -236,81 +210,3 @@ if (! function_exists('form_dropdown')) { return $form . "</select>\n"; } } - -//-------------------------------------------------------------------- - -if (! function_exists('form_editor')) { - /** - * Markdown editor - * - * @param array<string, mixed> $data - * @param array<string, mixed>|string $extra - */ - function form_markdown_editor(array $data = [], string $value = '', string | array $extra = ''): string - { - $editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600'; - if (array_key_exists('class', $data) && $data['class'] !== '') { - $editorClass .= ' ' . $data['class']; - unset($data['class']); - } - - $data['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full'; - - return '<div class="' . $editorClass . '">' . - '<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' . - '<markdown-write-preview for="' . $data['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">' . lang( - 'Common.forms.editor.write' - ) . '</button>' . - '<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang( - 'Common.forms.editor.preview' - ) . '</button>' . - '</markdown-write-preview>' . - '<markdown-toolbar for="' . $data['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">' . icon( - 'heading' - ) . '</md-header>' . - '<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( - 'bold' - ) . '</md-bold>' . - '<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( - '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">' . icon( - '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">' . icon( - '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">' . icon( - 'quote' - ) . '</md-quote>' . - '<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( - 'link' - ) . '</md-link>' . - '<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( - 'image-add' - ) . '</md-image>' . - '</div>' . - '</markdown-toolbar>' . - '</header>' . - '<div class="relative">' . - form_textarea($data, $value, $extra) . - '<markdown-preview for="' . $data['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>' . - '</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">' . icon( - 'markdown', - 'mr-1 text-lg text-gray-400' - ) . lang('Common.forms.editor.help') . '</a>' . - '</footer>' . - '</div>'; - } -} - -// ------------------------------------------------------------------------ diff --git a/app/Libraries/ViewComponents/Component.php b/app/Libraries/ViewComponents/Component.php new file mode 100644 index 0000000000000000000000000000000000000000..57873df5c96734c7b329935136a047980abe049b --- /dev/null +++ b/app/Libraries/ViewComponents/Component.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents; + +class Component implements ComponentInterface +{ + /** + * @var array<string, string> + */ + protected array $attributes = [ + 'class' => '', + ]; + + /** + * @param array<string, mixed> $properties + * @param array<string, string> $attributes + */ + public function __construct( + protected array $properties, + array $attributes + ) { + // overwrite default properties if set + foreach ($properties as $key => $value) { + $this->{$key} = $value; + } + + $this->attributes = array_merge($this->attributes, $attributes); + } + + public function render(): string + { + return static::class . ': RENDER METHOD NOT IMPLEMENTED'; + } +} diff --git a/app/Libraries/ViewComponents/ComponentInterface.php b/app/Libraries/ViewComponents/ComponentInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..397bee874eca19ff0c0361ea55fa9f827bc59cea --- /dev/null +++ b/app/Libraries/ViewComponents/ComponentInterface.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents; + +interface ComponentInterface +{ + public function render(): string; +} diff --git a/app/Libraries/ViewComponents/ComponentLoader.php b/app/Libraries/ViewComponents/ComponentLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..9ec75d7b875fdc62bbb44c8afb36ab1ff107a80b --- /dev/null +++ b/app/Libraries/ViewComponents/ComponentLoader.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents; + +use ViewComponents\Config\ViewComponents; +use ViewComponents\Exceptions\ComponentNotFoundException; + +class ComponentLoader +{ + protected ViewComponents $config; + + protected string $name; + + /** + * @var array<string, mixed> + */ + protected array $properties = []; + + /** + * @var array<string, string> + */ + protected array $attributes = []; + + public function __construct() + { + $this->config = config('ViewComponents'); + } + + public function __get(string $property): mixed + { + if (property_exists($this, $property)) { + return $this->{$property}; + } + } + + // @phpstan-ignore-next-line + public function __set(string $property, mixed $value) + { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + + return $this; + } + + /** + * @throws ComponentNotFoundException + */ + public function load(): string + { + // first, check if there exists a component class to load in class components path + if (file_exists("{$this->config->classComponentsPath}/{$this->name}.php")) { + return $this->loadComponentClass(); + } + + // check for the existence of a view file if no component class has been found + // component view files are camel case + $camelCaseName = strtolower(preg_replace('~(?<!^)(?<!\/)[A-Z]~', '_$0', $this->name) ?? ''); + + if (file_exists("{$this->config->componentsViewPath}/{$camelCaseName}.php")) { + return $this->loadComponentView($camelCaseName); + } + + throw new ComponentNotFoundException("Could not find component \"{$this->name}\""); + } + + private function loadComponentClass(): string + { + $classComponentsNamespace = $this->config->classComponentsNamespace; + + $namespacedName = str_replace('/', '\\', $this->name); + $componentClassNamespace = "{$classComponentsNamespace}\\{$namespacedName}"; + + $component = new $componentClassNamespace($this->properties, $this->attributes); + return $component->render(); + } + + private function loadComponentView(string $name): string + { + $viewData = [...$this->properties, ...$this->attributes]; + + return view("components/{$name}", $viewData); + } +} diff --git a/app/Libraries/ViewComponents/Config/Services.php b/app/Libraries/ViewComponents/Config/Services.php new file mode 100644 index 0000000000000000000000000000000000000000..1a503731080dac94b736df727d6c214875def1ac --- /dev/null +++ b/app/Libraries/ViewComponents/Config/Services.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents\Config; + +use CodeIgniter\Config\BaseService; +use ViewComponents\ComponentLoader; + +/** + * Services Configuration file. + * + * Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow + * the core of the framework to be swapped out easily without affecting the usage within the rest of your application. + * + * This file holds any application-specific services, or service overrides that you might need. An example has been + * included with the general method format you should use for your service methods. For more examples, see the core + * Services file at system/Config/Services.php. + */ +class Services extends BaseService +{ + public static function viewcomponents(bool $getShared = true): ComponentLoader + { + if ($getShared) { + return self::getSharedInstance('viewcomponents'); + } + + return new ComponentLoader(); + } +} diff --git a/app/Libraries/ViewComponents/Config/ViewComponents.php b/app/Libraries/ViewComponents/Config/ViewComponents.php new file mode 100644 index 0000000000000000000000000000000000000000..2131991288949d71a6d982774b1667a3a4d49a4b --- /dev/null +++ b/app/Libraries/ViewComponents/Config/ViewComponents.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents\Config; + +use CodeIgniter\Config\BaseConfig; + +class ViewComponents extends BaseConfig +{ + public string $classComponentsNamespace = APP_NAMESPACE . '\View\Components'; + + public string $classComponentsPath = APPPATH . 'View/Components'; + + public string $componentsViewPath = APPPATH . 'Views/components'; +} diff --git a/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php b/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php new file mode 100644 index 0000000000000000000000000000000000000000..5b7aaa76a5785d96a192eaaa617cd4f0e4e3457a --- /dev/null +++ b/app/Libraries/ViewComponents/Exceptions/ComponentNotFoundException.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace ViewComponents\Exceptions; + +use CodeIgniter\Exceptions\ExceptionInterface; +use RuntimeException; + +class ComponentNotFoundException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/app/Libraries/ViewComponents/Helpers/view_components_helper.php b/app/Libraries/ViewComponents/Helpers/view_components_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..d6a9625d891691c51ca5e7d295f3180e8121d243 --- /dev/null +++ b/app/Libraries/ViewComponents/Helpers/view_components_helper.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +if (! function_exists('component')) { + /** + * Loads the specified class or view file component in the parameters + * + * @param array<string, array<string, mixed>> $properties + * @param array<string, array<string, mixed>> $attributes + */ + function component(string $name, array $properties = [], array $attributes = []): string + { + $componentLoader = service('viewcomponents'); + + $componentLoader->name = $name; + $componentLoader->properties = $properties; + $componentLoader->attributes = $attributes; + + return $componentLoader->load(); + } +} diff --git a/app/Resources/js/modules/xml-editor.ts b/app/Resources/js/modules/xml-editor.ts index 6f521d5b1acabd4a8de35043338510a61b7f9b2f..81a8b254e240ff5b261f36e5899d6ebea8f31679 100644 --- a/app/Resources/js/modules/xml-editor.ts +++ b/app/Resources/js/modules/xml-editor.ts @@ -60,6 +60,10 @@ export class XMLEditor extends LitElement { border: 1px solid #6b7280; background-color: #ffffff; } + .cm-editor.cm-focused { + outline: 2px solid transparent; + box-shadow: 0 0 0 1px #2563eb; + } `; render(): TemplateResult<1> { diff --git a/app/View/Components/Button.php b/app/View/Components/Button.php new file mode 100644 index 0000000000000000000000000000000000000000..ac4416b2377a7e0cc498ff973eff9276c73fef12 --- /dev/null +++ b/app/View/Components/Button.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components; + +use ViewComponents\Component; + +class Button extends Component +{ + protected string $label = ''; + + protected string $uri = ''; + + protected string $variant = 'default'; + + protected string $size = 'base'; + + protected string $iconLeft = ''; + + protected string $iconRight = ''; + + protected bool $isSquared = false; + + public function render(): string + { + $baseClass = + 'inline-flex items-center font-semibold shadow-xs rounded-full focus:outline-none focus:ring'; + + $variantClass = [ + 'default' => 'text-black bg-gray-300 hover:bg-gray-400', + 'primary' => 'text-white bg-pine-700 hover:bg-pine-800', + 'secondary' => 'text-white bg-gray-700 hover:bg-gray-800', + 'accent' => 'text-white bg-rose-600 hover:bg-rose-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-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', + ]; + + $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', + ]; + + $squaredPaddings = [ + 'small' => 'p-1', + 'base' => 'p-2', + 'large' => 'p-3', + ]; + + $buttonClass = + $baseClass . + ' ' . + ($this->isSquared + ? $squaredPaddings[$this->size] + : $basePaddings[$this->size]) . + ' ' . + $sizeClass[$this->size] . + ' ' . + $variantClass[$this->variant]; + + if (array_key_exists('class', $this->attributes)) { + $buttonClass .= ' ' . $this->attributes['class']; + unset($this->attributes['class']); + } + + if ($this->iconLeft !== '') { + $this->label = icon($this->iconLeft, 'mr-2') . $this->label; + } + + if ($this->iconRight !== '') { + $this->label .= icon($this->iconRight, 'ml-2'); + } + + if ($this->uri !== '') { + return anchor($this->uri, $this->label, array_merge([ + 'class' => $buttonClass, + ], $this->attributes)); + } + + $defaultButtonAttributes = [ + 'type' => 'button', + ]; + $attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes)); + + return <<<CODE_SAMPLE + <button class="{$buttonClass}" {$attributes}>{$this->label}</button> + CODE_SAMPLE; + } +} diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php new file mode 100644 index 0000000000000000000000000000000000000000..3dda5d57bf9f5d2e24b49afba757c6f490425a3f --- /dev/null +++ b/app/View/Components/Forms/Input.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class Input extends Component +{ + public function render(): string + { + return ''; + } +} diff --git a/app/View/Components/Forms/Label.php b/app/View/Components/Forms/Label.php new file mode 100644 index 0000000000000000000000000000000000000000..ea43a51303012eedd47398d97882818dfcaecd40 --- /dev/null +++ b/app/View/Components/Forms/Label.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class Label extends Component +{ + /** + * @var array<string, string> + */ + protected array $attributes = [ + 'for' => '', + 'name' => '', + 'value' => '', + 'class' => '', + ]; + + protected string $text = ''; + + protected string $hint = ''; + + protected bool $isOptional = false; + + public function render(): string + { + $labelClass = $this->attributes['class']; + unset($this->attributes['class']); + + $attributes = stringify_attributes($this->attributes); + $optionalText = $this->isOptional ? '<small class="ml-1 lowercase">(' . + lang('Common.optional') . + ')</small>' : ''; + $hint = $this->hint !== '' ? hint_tooltip($this->hint, 'ml-1') : ''; + + return <<<CODE_SAMPLE + <label class="{$labelClass}" {$attributes}>{$this->text}{$optionalText}{$hint}</label> + CODE_SAMPLE; + } +} diff --git a/app/View/Components/Forms/MarkdownEditor.php b/app/View/Components/Forms/MarkdownEditor.php new file mode 100644 index 0000000000000000000000000000000000000000..48bb716540da431048a52e268ea33bbdce4f2047 --- /dev/null +++ b/app/View/Components/Forms/MarkdownEditor.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class MarkdownEditor extends Component +{ + protected string $content = ''; + + public function render(): string + { + $editorClass = 'w-full flex flex-col bg-white border border-gray-500 focus-within:ring-1 focus-within:ring-blue-600'; + if ($this->attributes['class'] !== '') { + $editorClass .= ' ' . $this->attributes['class']; + unset($this->attributes['class']); + } + + $this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full'; + + return '<div class="' . $editorClass . '">' . + '<header class="sticky top-0 z-20 flex flex-wrap justify-between bg-white border-b border-gray-500">' . + '<markdown-write-preview for="' . $this->attributes['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">' . lang( + 'Common.forms.editor.write' + ) . '</button>' . + '<button type="button" slot="preview" class="px-2 focus:outline-none focus:ring-inset focus:ring-2 focus:ring-pine-600">' . lang( + 'Common.forms.editor.preview' + ) . '</button>' . + '</markdown-write-preview>' . + '<markdown-toolbar for="' . $this->attributes['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">' . icon( + 'heading' + ) . '</md-header>' . + '<md-bold class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( + 'bold' + ) . '</md-bold>' . + '<md-italic class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( + '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">' . icon( + '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">' . icon( + '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">' . icon( + 'quote' + ) . '</md-quote>' . + '<md-link class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( + 'link' + ) . '</md-link>' . + '<md-image class="opacity-50 hover:opacity-100 focus:outline-none focus:ring-2 focus:opacity-100 focus:ring-pine-600">' . icon( + 'image-add' + ) . '</md-image>' . + '</div>' . + '</markdown-toolbar>' . + '</header>' . + '<div class="relative">' . + form_textarea($this->attributes, $this->content) . + '<markdown-preview for="' . $this->attributes['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>' . + '</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">' . icon( + 'markdown', + 'mr-1 text-lg text-gray-400' + ) . lang('Common.forms.editor.help') . '</a>' . + '</footer>' . + '</div>'; + } +} diff --git a/app/View/Components/Forms/MultiSelect.php b/app/View/Components/Forms/MultiSelect.php new file mode 100644 index 0000000000000000000000000000000000000000..ea9374b63ac851135aed6f150b64a7b161f8dbf0 --- /dev/null +++ b/app/View/Components/Forms/MultiSelect.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class MultiSelect extends Component +{ + /** + * @var array<string, string> + */ + protected array $options = []; + + /** + * @var string[] + */ + protected array $selected = []; + + public function render(): string + { + $defaultAttributes = [ + 'data-class' => $this->attributes['class'], + 'multiple' => 'multiple', + ]; + $extra = array_merge($defaultAttributes, $this->attributes); + + return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra); + } +} diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php new file mode 100644 index 0000000000000000000000000000000000000000..ea9374b63ac851135aed6f150b64a7b161f8dbf0 --- /dev/null +++ b/app/View/Components/Forms/Select.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class MultiSelect extends Component +{ + /** + * @var array<string, string> + */ + protected array $options = []; + + /** + * @var string[] + */ + protected array $selected = []; + + public function render(): string + { + $defaultAttributes = [ + 'data-class' => $this->attributes['class'], + 'multiple' => 'multiple', + ]; + $extra = array_merge($defaultAttributes, $this->attributes); + + return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra); + } +} diff --git a/app/View/Components/Forms/Toggler.php b/app/View/Components/Forms/Toggler.php new file mode 100644 index 0000000000000000000000000000000000000000..f3421bfc17bd77fbfe9e7ed8cafa4a5e4a2fb836 --- /dev/null +++ b/app/View/Components/Forms/Toggler.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +/** + * Form Checkbox Switch + * + * Abstracts form_label to stylize it as a switch toggle + */ +class Toggler extends Component +{ + /** + * @var array<string, string> + */ + protected array $attributes = [ + 'id' => '', + 'name' => '', + 'value' => '', + 'class' => '', + ]; + + protected string $label = ''; + + protected string $hint = ''; + + protected bool $checked = false; + + public function render(): string + { + $wrapperClass = $this->attributes['class']; + unset($this->attributes['class']); + + $this->attributes['class'] = 'form-switch'; + + $checkbox = form_checkbox($this->attributes, $this->attributes['value'], $this->checked); + $hint = $this->hint !== '' ? hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1') : ''; + return <<<CODE_SAMPLE + <label class="relative inline-flex items-center {$wrapperClass}"> + {$checkbox} + <span class="form-switch-slider"></span> + <span class="ml-2">{$this->label}{$hint}</span> + </label> + CODE_SAMPLE; + } +} diff --git a/app/View/Components/Forms/XMLEditor.php b/app/View/Components/Forms/XMLEditor.php new file mode 100644 index 0000000000000000000000000000000000000000..ef3406e47e60431ce815ecefdea9dfcf83327541 --- /dev/null +++ b/app/View/Components/Forms/XMLEditor.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components\Forms; + +use ViewComponents\Component; + +class XMLEditor extends Component +{ + protected string $content = ''; + + /** + * @var array<string, string> + */ + protected array $attributes = [ + 'slot' => 'textarea', + 'rows' => '5', + 'class' => 'textarea', + ]; + + public function render(): string + { + $textarea = form_textarea($this->attributes, $this->content); + + return <<<CODE_SAMPLE + <xml-editor>{$textarea}</time-ago> + CODE_SAMPLE; + } +} diff --git a/app/View/Components/Icon.php b/app/View/Components/Icon.php new file mode 100644 index 0000000000000000000000000000000000000000..b60051af445b307527c1a2962354e6663317aeaf --- /dev/null +++ b/app/View/Components/Icon.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace App\View\Components; + +use ViewComponents\Component; + +class Icon extends Component +{ + public string $glyph = ''; + + public function render(): string + { + $svgContents = file_get_contents('assets/icons/' . $this->glyph . '.svg'); + + if ($svgContents) { + if ($this->attributes['class'] !== '') { + $svgContents = str_replace('<svg', '<svg class="' . $this->attributes['class'] . '"', $svgContents); + } + + return $svgContents; + } + + return 'â–¡'; + } +} diff --git a/app/Views/admin/contributor/add.php b/app/Views/admin/contributor/add.php index 5c982f2d821c9572b61349085cd90851b4fb2fc1..fbe53d9adf56957ab94c42eb7b6fb89ebef8b35c 100644 --- a/app/Views/admin/contributor/add.php +++ b/app/Views/admin/contributor/add.php @@ -16,7 +16,7 @@ ]) ?> <?= csrf_field() ?> -<?= form_label(lang('Contributor.form.user'), 'user') ?> +<?= component('Forms/Label', ['text' => lang('Contributor.form.user')], ['for' => 'user']) ?> <?= form_dropdown('user', $userOptions, [old('user', '')], [ 'id' => 'user', 'class' => 'form-select mb-4', @@ -24,7 +24,7 @@ 'placeholder' => lang('Contributor.form.user_placeholder') ]) ?> -<?= form_label(lang('Contributor.form.role'), 'role') ?> +<?= component('Forms/Label', ['text' => lang('Contributor.form.role')], ['for' => 'role']) ?> <?= form_dropdown('role', $roleOptions, [old('role', '')], [ 'id' => 'role', 'class' => 'form-select mb-4', diff --git a/app/Views/admin/contributor/edit.php b/app/Views/admin/contributor/edit.php index a0be336a9523c32d851b4622b2f7932e70e3b525..221e9fb6b36186b417fd0c10309b6a01d627ad51 100644 --- a/app/Views/admin/contributor/edit.php +++ b/app/Views/admin/contributor/edit.php @@ -16,7 +16,7 @@ ]) ?> <?= csrf_field() ?> -<?= form_label(lang('Contributor.form.role'), 'role') ?> +<?= component('Forms/Label', ['text' => lang('Contributor.form.role')], ['for' => 'role']) ?> <?= form_dropdown('role', $roleOptions, [old('role', $contributorGroupId)], [ 'id' => 'role', 'class' => 'form-select mb-4', diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php index 738b62272b7d874280d2464512576b92de856837..c5b885f347191690041f59eaa71ef2fcc7932b85 100644 --- a/app/Views/admin/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -28,11 +28,10 @@ lang('Episode.form.info_section_subtitle'), ) ?> -<?= form_label( - lang('Episode.form.audio_file'), - 'audio_file', - [], - lang('Episode.form.audio_file_hint'), +<?= component( + 'Forms/Label', + ['text' => lang('Episode.form.audio_file'), 'hint' => lang('Episode.form.audio_file_hint')], + ['for' => 'audio_file'], ) ?> <?= form_input([ 'id' => 'audio_file', @@ -43,12 +42,14 @@ 'accept' => '.mp3,.m4a', ]) ?> -<?= form_label( - lang('Episode.form.image'), - 'image', - [], - lang('Episode.form.image_hint'), - true, +<?= component( + 'Forms/Label', + [ + 'text' => lang('Episode.form.image'), + 'hint' => lang('Episode.form.image_hint'), + 'isOptional' => true + ], + ['for' => 'image'], ) ?> <?= form_input([ 'id' => 'image', @@ -61,11 +62,11 @@ 'Common.forms.image_size_hint', ) ?></small> -<?= form_label( - lang('Episode.form.title'), - 'title', - [], - lang('Episode.form.title_hint'), +<?= component( + 'Forms/Label', + ['text' => lang('Episode.form.title'), + 'hint' => lang('Episode.form.title_hint')], + ['for' => 'title'], ) ?> <?= form_input([ 'id' => 'title', @@ -76,10 +77,10 @@ 'data-slugify' => 'title', ]) ?> -<?= form_label( - lang('Episode.form.permalink'), - 'slug', - [], +<?= component( + 'Forms/Label', + ['text' => lang('Episode.form.permalink')], + ['for' => 'slug'] ) ?> <permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>"> <span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span> @@ -96,7 +97,7 @@ <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') ?> + <?= component('Forms/Label', ['text' => lang('Episode.form.season_number')], ['for' => 'season_number']) ?> <?= form_input([ 'id' => 'season_number', 'name' => 'season_number', @@ -106,7 +107,7 @@ ]) ?> </div> <div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> + <?= component('Forms/Label', ['text' => lang('Episode.form.episode_number')], ['for' => 'episode_number']) ?> <?= form_input([ 'id' => 'episode_number', 'name' => 'episode_number', @@ -203,36 +204,45 @@ ) ?> <div class="mb-4"> - <?= form_label(lang('Episode.form.description'), 'description') ?> - <?= form_markdown_editor( + <?= component('Forms/Label', ['text' => lang('Episode.form.description')], ['for' => 'description']) ?> + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('description', '', false), + ], [ 'id' => 'description', 'name' => 'description', 'required' => 'required', ], - old('description', '', false), ) ?> </div> <div class="mb-4"> - <?= form_label( - lang('Episode.form.description_footer'), - 'description_footer', - [], - lang('Episode.form.description_footer_hint'), - true + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.description_footer'), + 'hint' => lang('Episode.form.description_footer_hint'), + 'isOptional' => true + ], + [ + 'for' => 'description_footer' + ], ) ?> - <?= form_markdown_editor( + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old( + 'description_footer', + $podcast->episode_description_footer_markdown ?? '', + false, + ), + ], [ 'id' => 'description_footer', 'name' => 'description_footer', 'rows' => 6 ], - old( - 'description_footer', - $podcast->episode_description_footer_markdown ?? '', - false, - ), ) ?> </div> @@ -243,12 +253,13 @@ lang('Episode.form.location_section_subtitle'), ) ?> -<?= form_label( - lang('Episode.form.location_name'), - 'location_name', - [], - lang('Episode.form.location_name_hint'), - true, +<?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.location_name'), + 'hint' => lang('Episode.form.location_name_hint'), + 'isOptional' => true + ], + ['for' => 'location_name'], ) ?> <?= form_input([ 'id' => 'location_name', @@ -290,12 +301,12 @@ <div class="py-2 tab-panels"> <section id="transcript-file-upload" class="flex items-center tab-panel"> - <?= form_label( - lang('Episode.form.transcript_file'), - 'transcript_file', - ['class' => 'sr-only'], - lang('Episode.form.transcript_file'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.transcript_file'), + 'isOptional' => true + ], + ['for' => 'transcript_file', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'transcript_file', @@ -306,12 +317,12 @@ ]) ?> </section> <section id="transcript-file-remote-url" class="tab-panel"> - <?= form_label( - lang('Episode.form.transcript_file_remote_url'), - 'transcript_file_remote_url', - ['class' => 'sr-only'], - lang('Episode.form.transcript_file_remote_url'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.transcript_file_remote_url'), + 'isOptional' => true + ], + ['for' => 'transcript_file_remote_url', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'transcript_file_remote_url', @@ -353,12 +364,12 @@ <div class="py-2 tab-panels"> <section id="chapters-file-upload" class="flex items-center tab-panel"> - <?= form_label( - lang('Episode.form.chapters_file'), - 'chapters_file', - ['class' => 'sr-only'], - lang('Episode.form.chapters_file'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.chapters_file'), + 'isOptional' => true + ], + ['for' => 'chapters_file', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'chapters_file', @@ -369,12 +380,15 @@ ]) ?> </section> <section id="chapters-file-remote-url" class="tab-panel"> - <?= form_label( - lang('Episode.form.chapters_file_remote_url'), - 'chapters_file_remote_url', - ['class' => 'sr-only'], - lang('Episode.form.chapters_file_remote_url'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.chapters_file_remote_url'), + 'isOptional' => true + ], + [ + 'for' => 'chapters_file_remote_url', + 'class' => 'sr-only' + ], ) ?> <?= form_input([ 'id' => 'chapters_file_remote_url', @@ -395,27 +409,37 @@ lang('Episode.form.advanced_section_title'), lang('Episode.form.advanced_section_subtitle'), ) ?> -<?= form_label( - lang('Episode.form.custom_rss'), - 'custom_rss', - [], - lang('Episode.form.custom_rss_hint'), - true, +<?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.custom_rss'), + 'hint' => lang('Episode.form.custom_rss_hint'), + 'isOptional' => true + ], + ['for' => 'custom_rss'] +) ?> +<?= component('Forms/XMLEditor', + [ + 'content' => old('custom_rss', '') + ], + [ + 'id' => 'custom_rss', + 'name' => 'custom_rss', + ] ) ?> -<?= xml_editor([ - 'id' => 'custom_rss', - 'name' => 'custom_rss', - 'class' => 'form-textarea', - 'value' => old('custom_rss'), -]) ?> <?= form_section_close() ?> -<?= form_switch( - lang('Episode.form.block') . - hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'), - ['id' => 'block', 'name' => 'block'], - 'yes', - old('block', false), +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Episode.form.block'), + 'hint' => lang('Episode.form.block_hint') + ], + [ + 'id' => 'block', + 'name' => 'block', + 'value' => 'yes', + 'checked' => old('block', false), + ] ) ?> <?= button( diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php index 1fd1e87d2cfc709ee97819f418dd39997095757a..863d8cfaaa28a267e23fc5b36a856f738076d1b7 100644 --- a/app/Views/admin/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -18,8 +18,8 @@ <?= csrf_field() ?> <div class="inline-flex w-full p-2 mb-4 text-sm font-semibold text-yellow-800 bg-red-100 border border-red-300 rounded" role="alert"> - <?= icon('alert', 'mr-2 text-lg flex-shrink-0') . - lang('Episode.form.warning') ?> + <?= icon('alert', 'mr-2 text-lg flex-shrink-0') . + lang('Episode.form.warning') ?> </div> <?= form_section( @@ -35,11 +35,11 @@ />', ) ?> -<?= form_label( - lang('Episode.form.audio_file'), - 'audio_file', - [], - lang('Episode.form.audio_file_hint'), +<?= component( + 'Forms/Label', + ['text' => + lang('Episode.form.audio_file'), 'hint' => lang('Episode.form.audio_file_hint'),], + ['for' => 'audio_file'], ) ?> <?= form_input([ 'id' => 'audio_file', @@ -49,12 +49,12 @@ 'accept' => '.mp3,.m4a', ]) ?> -<?= form_label( - lang('Episode.form.image'), - 'image', - [], - lang('Episode.form.image_hint'), - true, +<?= component( + 'Forms/Label', + ['text' => + lang('Episode.form.image'), 'hint' => lang('Episode.form.image_hint'), 'isOptional' => true,], + ['for' => + 'image',] ) ?> <?= form_input([ @@ -68,11 +68,12 @@ 'Common.forms.image_size_hint', ) ?></small> -<?= form_label( - lang('Episode.form.title'), - 'title', - [], - lang('Episode.form.title_hint'), +<?= component( + 'Forms/Label', + ['text' => + lang('Episode.form.title'), 'hint' => lang('Episode.form.title_hint'),], + ['for' => + 'title',] ) ?> <?= form_input([ 'id' => 'title', @@ -83,27 +84,29 @@ 'data-slugify' => 'title', ]) ?> -<?= form_label( - lang('Episode.form.permalink'), - 'slug', - [], +<?= component( + 'Forms/Label', + [ + 'text' => lang('Episode.form.permalink') + ], + ['for' => 'slug',] ) ?> <permalink-edit class="inline-flex items-center mb-4 text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>"> - <span slot="domain"><?= base_url('/@'. $podcast->handle . '/episodes' ) . '/' ?></span> + <span slot="domain"><?= base_url('/@' . $podcast->handle . '/episodes') . '/' ?></span> <?= form_input([ - 'id' => 'slug', - 'name' => 'slug', - 'class' => 'form-input flex-1 w-0 text-xs', - 'value' => old('slug', $episode->slug), - 'required' => 'required', - 'data-slugify' => 'slug', - 'slot' => 'slug-input' + 'id' => 'slug', + 'name' => 'slug', + 'class' => 'form-input flex-1 w-0 text-xs', + 'value' => old('slug', $episode->slug), + 'required' => 'required', + 'data-slugify' => 'slug', + 'slot' => 'slug-input' ]) ?> </permalink-edit> <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') ?> + <?= component('Forms/Label', ['text' => lang('Episode.form.season_number')], ['for' => 'season_number']) ?> <?= form_input([ 'id' => 'season_number', 'name' => 'season_number', @@ -113,7 +116,7 @@ ]) ?> </div> <div class="flex flex-col flex-1"> - <?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?> + <?= component('Forms/Label', ['text' => lang('Episode.form.episode_number')], ['for' => 'episode_number']) ?> <?= form_input([ 'id' => 'episode_number', 'name' => 'episode_number', @@ -125,83 +128,83 @@ </div> <?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?> - <legend> +<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> +</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_fieldset('', ['class' => 'mb-6']) ?> - <legend> +<legend> <?= lang('Episode.form.parental_advisory.label') . hint_tooltip(lang('Episode.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' - : $episode->parental_advisory === null, - ) ?> - <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> +</legend> +<?= form_radio( + [ + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', + ], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : $episode->parental_advisory === null, +) ?> +<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() ?> <?= form_section_close() ?> @@ -213,36 +216,43 @@ ) ?> <div class="mb-4"> - <?= form_label(lang('Episode.form.description'), 'description') ?> - <?= form_markdown_editor( + <?= component('Forms/Label', ['text' => lang('Episode.form.description')], ['for' => 'description']) ?> + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('description', $episode->description_markdown, false), + ], [ 'id' => 'description', 'name' => 'description', 'required' => 'required', ], - old('description', $episode->description_markdown, false), ) ?> </div> <div class="mb-4"> - <?= form_label( - lang('Episode.form.description_footer'), - 'description_footer', - [], - lang('Episode.form.description_footer_hint'), - true + <?= component('Forms/Label', + [ + 'text' => lang('Episode.form.description_footer'), + 'hint' => lang('Episode.form.description_footer_hint'), + 'isOptional' => true + ], + ['for' => 'description_footer'], ) ?> - <?= form_markdown_editor( + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old( + 'description_footer', + $podcast->episode_description_footer_markdown ?? '', + false, + ), + ], [ 'id' => 'description_footer', 'name' => 'description_footer', 'rows' => 6 ], - old( - 'description_footer', - $podcast->episode_description_footer_markdown ?? '', - false, - ), ) ?> </div> @@ -253,12 +263,13 @@ lang('Episode.form.location_section_subtitle'), ) ?> -<?= form_label( - lang('Episode.form.location_name'), - 'location_name', - [], - lang('Episode.form.location_name_hint'), - true, +<?= component('Forms/Label', + [ + 'text' => lang('Episode.form.location_name'), + 'hint' => lang('Episode.form.location_name_hint'), + 'isOptional' => true + ], + ['for' => 'location_name'] ) ?> <?= form_input([ 'id' => 'location_name', @@ -273,34 +284,34 @@ lang('Episode.form.additional_files_section_title'), lang('Episode.form.additional_files_section_subtitle', [ 'podcastNamespaceLink' => - '“<a href="https://github.com/Podcastindex-org/podcast-namespace" target="_blank" rel="noreferrer noopener" style="text-decoration: underline;">podcast namespace</a>â€', + '“<a href="https://github.com/Podcastindex-org/podcast-namespace" target="_blank" rel="noreferrer noopener" style="text-decoration: underline;">podcast namespace</a>â€', ]), ) ?> <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Episode.form.transcript') . - '<small class="ml-1 lowercase">(' . - lang('Common.optional') . - ')</small>' . - hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend> - <div class="mb-4 form-input-tabs"> - <input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= $episode->transcript_file_remote_url - ? '' - : 'checked' ?> /> - <label for="transcript-file-upload-choice"><?= lang( - 'Common.forms.upload_file', - ) ?></label> - - <input type="radio" name="transcript-choice" id="transcript-file-remote-url-choice" aria-controls="transcript-file-remote-url-choice" value="remote-url" <?= $episode->transcript_file_remote_url - ? 'checked' - : '' ?> /> - <label for="transcript-file-remote-url-choice"><?= lang( - 'Common.forms.remote_url', - ) ?></label> - - <div class="py-2 tab-panels"> - <section id="transcript-file-upload" class="flex items-center tab-panel"> - <?php if ($episode->transcript_file): ?> +<legend><?= lang('Episode.form.transcript') . + '<small class="ml-1 lowercase">(' . + lang('Common.optional') . + ')</small>' . + hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend> +<div class="mb-4 form-input-tabs"> + <input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= $episode->transcript_file_remote_url + ? '' + : 'checked' ?> /> + <label for="transcript-file-upload-choice"><?= lang( + 'Common.forms.upload_file', + ) ?></label> + + <input type="radio" name="transcript-choice" id="transcript-file-remote-url-choice" aria-controls="transcript-file-remote-url-choice" value="remote-url" <?= $episode->transcript_file_remote_url + ? 'checked' + : '' ?> /> + <label for="transcript-file-remote-url-choice"><?= lang( + 'Common.forms.remote_url', + ) ?></label> + + <div class="py-2 tab-panels"> + <section id="transcript-file-upload" class="flex items-center tab-panel"> + <?php if ($episode->transcript_file) : ?> <div class="flex justify-between"> <?= anchor( $episode->transcript_file_url, @@ -321,7 +332,7 @@ icon('delete-bin', 'mx-auto'), [ 'class' => - 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', + 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', 'data-toggle' => 'tooltip', 'data-placement' => 'bottom', 'title' => lang( @@ -331,12 +342,12 @@ ) ?> </div> <?php endif; ?> - <?= form_label( - lang('Episode.form.transcript_file'), - 'transcript_file', - ['class' => 'sr-only'], - lang('Episode.form.transcript_file'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.transcript_file'), + 'isOptional' => true + ], + ['for' => 'transcript_file', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'transcript_file', @@ -345,14 +356,14 @@ 'type' => 'file', 'accept' => '.txt,.html,.srt,.json', ]) ?> - </section> - <section id="transcript-file-remote-url" class="tab-panel"> - <?= form_label( - lang('Episode.form.transcript_file_remote_url'), - 'transcript_file_remote_url', - ['class' => 'sr-only'], - lang('Episode.form.transcript_file_remote_url'), - true, + </section> + <section id="transcript-file-remote-url" class="tab-panel"> + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.transcript_file_remote_url'), + 'isOptional' => true + ], + ['for' => 'transcript_file_remote_url', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'transcript_file_remote_url', @@ -365,35 +376,35 @@ $episode->transcript_file_remote_url, ), ]) ?> - </section> - </div> + </section> </div> +</div> <?= form_fieldset_close() ?> <?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?> - <legend><?= lang('Episode.form.chapters') . - '<small class="ml-1 lowercase">(' . - lang('Common.optional') . - ')</small>' . - hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend> - <div class="mb-4 form-input-tabs"> - <input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= $episode->chapters_file_remote_url - ? '' - : 'checked' ?> /> - <label for="chapters-file-upload-choice"><?= lang( - 'Common.forms.upload_file', - ) ?></label> - - <input type="radio" name="chapters-choice" id="chapters-file-remote-url-choice" aria-controls="chapters-file-remote-url-choice" value="remote-url" <?= $episode->chapters_file_remote_url - ? 'checked' - : '' ?> /> - <label for="chapters-file-remote-url-choice"><?= lang( - 'Common.forms.remote_url', - ) ?></label> - - <div class="py-2 tab-panels"> - <section id="chapters-file-upload" class="flex items-center tab-panel"> - <?php if ($episode->chapters_file): ?> +<legend><?= lang('Episode.form.chapters') . + '<small class="ml-1 lowercase">(' . + lang('Common.optional') . + ')</small>' . + hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend> +<div class="mb-4 form-input-tabs"> + <input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= $episode->chapters_file_remote_url + ? '' + : 'checked' ?> /> + <label for="chapters-file-upload-choice"><?= lang( + 'Common.forms.upload_file', + ) ?></label> + + <input type="radio" name="chapters-choice" id="chapters-file-remote-url-choice" aria-controls="chapters-file-remote-url-choice" value="remote-url" <?= $episode->chapters_file_remote_url + ? 'checked' + : '' ?> /> + <label for="chapters-file-remote-url-choice"><?= lang( + 'Common.forms.remote_url', + ) ?></label> + + <div class="py-2 tab-panels"> + <section id="chapters-file-upload" class="flex items-center tab-panel"> + <?php if ($episode->chapters_file) : ?> <div class="flex justify-between"> <?= anchor( $episode->chapters_file_url, @@ -413,7 +424,7 @@ icon('delete-bin', 'mx-auto'), [ 'class' => - 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', + 'p-1 bg-red-200 rounded-full text-red-700 hover:text-red-900', 'data-toggle' => 'tooltip', 'data-placement' => 'bottom', 'title' => lang( @@ -423,12 +434,12 @@ ) ?> </div> <?php endif; ?> - <?= form_label( - lang('Episode.form.chapters_file'), - 'chapters_file', - ['class' => 'sr-only'], - lang('Episode.form.chapters_file'), - true, + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.chapters_file'), + 'isOptional' => true + ], + ['for' => 'chapters_file', 'class' => 'sr-only'], ) ?> <?= form_input([ 'id' => 'chapters_file', @@ -437,14 +448,17 @@ 'type' => 'file', 'accept' => '.json', ]) ?> - </section> - <section id="chapters-file-remote-url" class="tab-panel"> - <?= form_label( - lang('Episode.form.chapters_file_remote_url'), - 'chapters_file_remote_url', - ['class' => 'sr-only'], - lang('Episode.form.chapters_file_remote_url'), - true, + </section> + <section id="chapters-file-remote-url" class="tab-panel"> + <?= component( 'Forms/Label', + [ + 'text' => lang('Episode.form.chapters_file_remote_url'), + 'isOptional' => true + ], + [ + 'for' => 'chapters_file_remote_url', + 'class' => 'sr-only' + ], ) ?> <?= form_input([ 'id' => 'chapters_file_remote_url', @@ -457,9 +471,9 @@ $episode->chapters_file_remote_url, ), ]) ?> - </section> - </div> + </section> </div> +</div> <?= form_fieldset_close() ?> <?= form_section_close() ?> @@ -468,27 +482,39 @@ lang('Episode.form.advanced_section_title'), lang('Episode.form.advanced_section_subtitle'), ) ?> -<?= form_label( - lang('Episode.form.custom_rss'), - 'custom_rss', - [], - lang('Episode.form.custom_rss_hint'), - true, +<?= component('Forms/Label', + [ + 'text' => lang('Episode.form.custom_rss'), + 'hint' => lang('Episode.form.custom_rss_hint'), + 'isOptional' => true, + ], + [ + 'for' => 'custom_rss', + ] +) ?> +<?= component('Forms/XMLEditor', + [ + 'content' => old('custom_rss', $episode->custom_rss_string) + ], + [ + 'id' => 'custom_rss', + 'name' => 'custom_rss', + ] ) ?> -<?= xml_editor([ - 'id' => 'custom_rss', - 'name' => 'custom_rss', - 'class' => 'form-textarea', - 'value' => old('custom_rss', $episode->custom_rss_string), -]) ?> <?= form_section_close() ?> -<?= form_switch( - lang('Episode.form.block') . - hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'), - ['id' => 'block', 'name' => 'block'], - 'yes', - old('block', $episode->is_blocked), +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Episode.form.block'), + 'hint' => lang('Episode.form.block_hint') + ], + [ + 'id' => 'block', + 'name' => 'block', + 'value' => 'yes', + 'checked' => old('block', $episode->is_blocked), + ] ) ?> <?= button( diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php index b15060b77c09f7652308abeaa4c58d4563610afe..4cadb490c6220314a781ab3f818369c0f84cc744 100644 --- a/app/Views/admin/episode/embeddable_player.php +++ b/app/Views/admin/episode/embeddable_player.php @@ -10,7 +10,7 @@ <?= $this->section('content') ?> -<?= form_label(lang('Episode.embeddable_player.label'), 'label') ?> +<p><?= lang('Episode.embeddable_player.label') ?></p> <div class="flex w-full mt-6 mb-6"> <?php foreach ($themes as $themeKey => $theme): ?> diff --git a/app/Views/admin/episode/persons.php b/app/Views/admin/episode/persons.php index 293129f52476e9bd7ddce9950a429e0d219f1f9c..d2e1dc910f46b7dc29fcbdbf95af889eb5f4acd4 100644 --- a/app/Views/admin/episode/persons.php +++ b/app/Views/admin/episode/persons.php @@ -60,8 +60,8 @@ ($person->information_url === null ? '' : "<a href=\"{$person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" . - $person->information_url . - '</a>') . + $person->information_url . + '</a>') . '</div></div>'; }, ], @@ -95,40 +95,36 @@ lang('Person.episode_form.add_section_subtitle'), ) ?> -<?= form_label( - lang('Person.episode_form.persons'), - 'persons', - [], - lang('Person.episode_form.persons_hint'), +<?= component( + 'Forms/Label', + ['text' => lang('Person.episode_form.persons'), 'hint' => lang('Person.episode_form.persons_hint')], + ['for' => 'persons'], ) ?> -<?= form_multiselect('persons[]', $personOptions, old('persons', []), [ +<?= component('Forms/MultiSelect', ['options' => $personOptions, 'selected' => old('persons', [])], [ 'id' => 'persons', - 'class' => 'form-select mb-4', + 'name' => 'persons[]', + 'class' => 'mb-4', 'required' => 'required', ]) ?> -<?= form_label( - lang('Person.episode_form.roles'), - 'roles', - [], - lang('Person.episode_form.roles_hint'), - true, +<?= component( + 'Forms/Label', + ['text' => lang('Person.episode_form.roles'), 'hint' => lang('Person.episode_form.roles_hint'), 'isOptional' => true], + ['for' => 'roles'], ) ?> -<?= form_multiselect( - 'roles[]', - $taxonomyOptions, - old('roles', []), - ['id' => 'roles', 'class' => 'form-select mb-4'], -) ?> - - +<?= component('Forms/MultiSelect', ['options' => $taxonomyOptions, 'selected' => old('roles', [])], [ + 'id' => 'roles', + 'name' => 'roles[]', + 'class' => 'mb-4', +]) ?> + <?= form_section_close() ?> <?= button( lang('Person.episode_form.submit_add'), '', ['variant' => 'primary'], ['type' => 'submit', 'class' => 'self-end'], -) ?> +) ?> <?= form_close() ?> <?= $this->endSection() ?> diff --git a/app/Views/admin/episode/publish_edit.php b/app/Views/admin/episode/publish_edit.php index b83914a027714227bf140781488ef4723dd0b87d..8e18531bfc33600d0283d5b326a90144edfe2122 100644 --- a/app/Views/admin/episode/publish_edit.php +++ b/app/Views/admin/episode/publish_edit.php @@ -8,7 +8,6 @@ <?= lang('Episode.publish_edit') ?> <?= $this->endSection() ?> - <?= $this->section('content') ?> <?= anchor( @@ -33,14 +32,11 @@ <small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.post_hint') ?></small> <div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl"> <div class="flex px-4 py-3"> - <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor - ->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> + <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> <div class="flex flex-col min-w-0"> <p class="flex items-baseline min-w-0"> - <span class="mr-2 font-semibold truncate"><?= $podcast->actor - ->display_name ?></span> - <span class="text-sm text-gray-500 truncate">@<?= $podcast - ->actor->username ?></span> + <span class="mr-2 font-semibold truncate"><?= $podcast->actor->display_name ?></span> + <span class="text-sm text-gray-500 truncate">@<?= $podcast->actor->username ?></span> </p> <?= relative_time($post->published_at, 'text-xs text-gray-500') ?> </div> @@ -85,17 +81,17 @@ </div> <footer class="flex justify-around px-6 py-3"> <span class="inline-flex items-center"><?= icon( - 'chat', - 'text-xl mr-1 text-gray-400', - ) . '0' ?></span> + 'chat', + 'text-xl mr-1 text-gray-400', + ) . '0' ?></span> <span class="inline-flex items-center"><?= icon( - 'repeat', - 'text-xl mr-1 text-gray-400', - ) . '0' ?></span> + 'repeat', + 'text-xl mr-1 text-gray-400', + ) . '0' ?></span> <span class="inline-flex items-center"><?= icon( - 'heart', - 'text-xl mr-1 text-gray-400', - ) . '0' ?></span> + 'heart', + 'text-xl mr-1 text-gray-400', + ) . '0' ?></span> </footer> </div> @@ -151,10 +147,10 @@ 'data-input' => '', ]) ?> <button class="p-3 border border-l-0 border-gray-500 bg-pine-100 focus:outline-none rounded-r-md hover:bg-pine-200 focus:ring" type="button" aria-label="<?= lang( - 'Episode.publish_form.scheduled_publication_date_clear', - ) ?>" title="<?= lang( - 'Episode.publish_form.scheduled_publication_date_clear', - ) ?>" data-clear=""><?= icon('close') ?></button> + 'Episode.publish_form.scheduled_publication_date_clear', + ) ?>" title="<?= lang( + 'Episode.publish_form.scheduled_publication_date_clear', + ) ?>" data-clear=""><?= icon('close') ?></button> </div> </div> </div> diff --git a/app/Views/admin/page/create.php b/app/Views/admin/page/create.php index 087b08056960310711a1cade47fc773bb2d300f4..711f7fdf9ad04fe03586e9c99176ec8bfd0f4ccb 100644 --- a/app/Views/admin/page/create.php +++ b/app/Views/admin/page/create.php @@ -46,14 +46,17 @@ <div class="mb-4"> <?= form_label(lang('Page.form.content'), 'content') ?> - <?= form_markdown_editor( + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('content', '', false) + ], [ 'id' => 'content', 'name' => 'content', 'required' => 'required', + 'rows' => 20 ], - old('content', '', false), - ['rows' => '20'] ) ?> </div> diff --git a/app/Views/admin/page/edit.php b/app/Views/admin/page/edit.php index 4f74d2c3e9f944625cb2b3ddb94ec56df3cb3f9f..0b2902d8d8a52a29087bf4d373491c287eb740c8 100644 --- a/app/Views/admin/page/edit.php +++ b/app/Views/admin/page/edit.php @@ -46,13 +46,16 @@ <div class="mb-4"> <?= form_label(lang('Page.form.content'), 'content') ?> - <?= form_markdown_editor( + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('content', $page->content_markdown, false), + ], [ 'id' => 'content', 'name' => 'content', 'required' => 'required', ], - old('content', $page->content_markdown, false), ) ?> </div> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index 3a5a9b94f85150e7a606ea44cf4dcc83ddaedaeb..5bdbbf8ad9693c1135020e5f8822a1084ee9582d 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -82,13 +82,16 @@ <div class="mb-4"> <?= form_label(lang('Podcast.form.description'), 'description') ?> - <?= form_markdown_editor( + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('description', '', false), + ], [ 'id' => 'description', 'name' => 'description', 'required' => 'required', ], - old('description', '', false), ) ?> </div> @@ -122,16 +125,12 @@ '', true, ) ?> -<?= form_multiselect( - 'other_categories[]', - $categoryOptions, - [old('other_categories', '')], - [ - 'id' => 'other_categories', - 'class' => 'mb-4', - 'data-max-item-count' => '2', - ], -) ?> +<?= component('Forms/MultiSelect', ['options' => $categoryOptions, 'selected' => old('other_categories', [])], [ + 'id' => 'other_categories', + 'name' => 'other_categories[]', + 'class' => 'mb-4', + 'data-max-item-count' => '2', +]) ?> <?= form_fieldset('', ['class' => 'mb-4']) ?> <legend> @@ -340,12 +339,15 @@ lang('Podcast.form.custom_rss_hint'), true, ) ?> -<?= xml_editor([ - 'id' => 'custom_rss', - 'name' => 'custom_rss', - 'class' => 'form-textarea', - 'value' => old('custom_rss'), -]) ?> +<?= component('Forms/XMLEditor', + [ + 'content' => old('custom_rss', '') + ], + [ + 'id' => 'custom_rss', + 'name' => 'custom_rss', + ] +) ?> <?= form_section_close() ?> <?= form_section( @@ -353,28 +355,46 @@ lang('Podcast.form.status_section_subtitle'), ) ?> -<?= form_switch( - lang('Podcast.form.block'), - ['id' => 'block', 'name' => 'block'], - 'yes', - old('block', false), - 'mb-2', +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.lock'), + 'hint' => lang('Podcast.form.lock_hint'), + ], + [ + 'id' => 'lock', + 'name' => 'lock', + 'value' => 'yes', + 'checked' => old('complete', true), + 'class' => 'mb-2' + ] ) ?> -<?= form_switch( - lang('Podcast.form.complete'), - ['id' => 'complete', 'name' => 'complete'], - 'yes', - old('complete', false), - 'mb-2', +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.block'), + ], + [ + 'id' => 'block', + 'name' => 'block', + 'value' => 'yes', + 'checked' => old('block', false), + 'class' => 'mb-2' + ] ) ?> -<?= form_switch( - lang('Podcast.form.lock') . - hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1'), - ['id' => 'lock', 'name' => 'lock'], - 'yes', - old('lock', true), +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.complete'), + ], + [ + 'id' => 'complete', + 'name' => 'complete', + 'value' => 'yes', + 'checked' => old('complete', false), + ] ) ?> <?= form_section_close() ?> diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php index a24413ae3596fb7fbf4a615390367122eca1babb..8523209c955c020c10525808fbe311072b0971e4 100644 --- a/app/Views/admin/podcast/edit.php +++ b/app/Views/admin/podcast/edit.php @@ -8,12 +8,14 @@ <?= lang('Podcast.edit') ?> <?= $this->endSection() ?> + <?= $this->section('content') ?> <?= form_open_multipart(route_to('podcast-edit', $podcast->id), [ 'method' => 'post', 'class' => 'flex flex-col', ]) ?> + <?= csrf_field() ?> <?= form_section( @@ -22,11 +24,8 @@ ) ?> <?= form_label(lang('Podcast.form.image'), 'image') ?> -<img - src="<?= $podcast->image->thumbnail_url ?>" - alt="<?= $podcast->title ?>" - class="object-cover w-32 h-32" -/> + +<img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="object-cover w-32 h-32" /> <?= form_input([ 'id' => 'image', 'name' => 'image', @@ -34,11 +33,14 @@ 'type' => 'file', 'accept' => '.jpg,.jpeg,.png', ]) ?> + + <small class="mb-4 text-gray-600"><?= lang( - 'Common.forms.image_size_hint', -) ?></small> + 'Common.forms.image_size_hint', + ) ?></small> <?= form_label(lang('Podcast.form.title'), 'title') ?> + <?= form_input([ 'id' => 'title', 'name' => 'title', @@ -46,46 +48,52 @@ 'value' => old('title', $podcast->title), 'required' => 'required', ]) ?> + <span class="mb-4 text-sm"><?= $podcast->link ?></span> <?= form_fieldset('', ['class' => 'mb-4']) ?> - <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> + +<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_markdown_editor([ + <?= component( + 'Forms/MarkdownEditor', + [ + 'content' => old('description', $podcast->description_markdown, false) + ], + [ 'id' => 'description', 'name' => 'description', 'required' => 'required', ], - old('description', $podcast->description_markdown, false) ) ?> </div> <?= form_section_close() ?> - <?= form_section( - lang('Podcast.form.classification_section_title'), - lang('Podcast.form.classification_section_subtitle'), +lang('Podcast.form.classification_section_title'), +lang('Podcast.form.classification_section_subtitle'), ) ?> <?= form_label(lang('Podcast.form.language'), 'language') ?> + <?= form_dropdown( 'language', $languageOptions, @@ -101,7 +109,7 @@ <?= form_dropdown( 'category', $categoryOptions, - [old('category', (string) $podcast->category_id)], + [old('category', $podcast->category_id)], [ 'id' => 'category', 'class' => 'form-select mb-4', @@ -116,65 +124,63 @@ '', true, ) ?> -<?= form_multiselect( - 'other_categories[]', - $categoryOptions, - old('other_categories', $podcast->other_categories_ids), + +<?= component('Forms/MultiSelect', ['options' => $categoryOptions, 'selected' => old('other_categories', $podcast->other_categories_ids)], [ + 'id' => 'other_categories', + 'name' => 'other_categories[]', + 'class' => 'mb-4', + 'data-max-item-count' => '2', +]) ?> + +<?= form_fieldset('', ['class' => 'mb-4']) ?> +<legend><?= lang('Podcast.form.parental_advisory.label') . + hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?></legend> +<?= form_radio( [ - 'id' => 'other_categories', - 'class' => 'mb-4', - 'data-max-item-count' => '2', - ], + 'id' => 'undefined', + 'name' => 'parental_advisory', + 'class' => 'form-radio-btn', +], + 'undefined', + old('parental_advisory') + ? old('parental_advisory') === 'undefined' + : $podcast->parental_advisory === null, ) ?> -<?= form_fieldset('', ['class' => 'mb-4']) ?> - <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', - ) ?> - <label for="explicit"><?= lang( - 'Podcast.form.parental_advisory.explicit', - ) ?></label> -<?= form_fieldset_close() ?> +<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', +) ?> +<label for="explicit"><?= lang( + 'Podcast.form.parental_advisory.explicit', + ) ?></label> +<?= form_fieldset_close() ?> <?= form_section_close() ?> <?= form_section( @@ -188,6 +194,7 @@ [], lang('Podcast.form.owner_name_hint'), ) ?> + <?= form_input([ 'id' => 'owner_name', 'name' => 'owner_name', @@ -202,6 +209,7 @@ [], lang('Podcast.form.owner_email_hint'), ) ?> + <?= form_input([ 'id' => 'owner_email', 'name' => 'owner_email', @@ -218,6 +226,7 @@ lang('Podcast.form.publisher_hint'), true, ) ?> + <?= form_input([ 'id' => 'publisher', 'name' => 'publisher', @@ -226,6 +235,7 @@ ]) ?> <?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?> + <?= form_input([ 'id' => 'copyright', 'name' => 'copyright', @@ -247,12 +257,14 @@ lang('Podcast.form.location_name_hint'), true, ) ?> + <?= form_input([ 'id' => 'location_name', 'name' => 'location_name', 'class' => 'form-input mb-4', 'value' => old('location_name', $podcast->location_name), ]) ?> + <?= form_section_close() ?> <?= form_section( @@ -267,6 +279,7 @@ lang('Podcast.form.payment_pointer_hint'), true, ) ?> + <?= form_input([ 'id' => 'payment_pointer', 'name' => 'payment_pointer', @@ -285,41 +298,41 @@ true, ) ?> <?= form_input([ - 'id' => 'partner_id', - 'name' => 'partner_id', - 'class' => 'form-input w-full', - 'value' => old('partner_id', $podcast->partner_id), - ]) ?> + 'id' => 'partner_id', + 'name' => 'partner_id', + 'class' => 'form-input w-full', + 'value' => old('partner_id', $podcast->partner_id), +]) ?> </div> <div class="flex flex-col flex-1"> <?= form_label( - lang('Podcast.form.partner_link_url'), - 'partner_link_url', - [], - lang('Podcast.form.partner_link_url_hint'), - true, - ) ?> + lang('Podcast.form.partner_link_url'), + 'partner_link_url', + [], + lang('Podcast.form.partner_link_url_hint'), + true, +) ?> <?= form_input([ - 'id' => 'partner_link_url', - 'name' => 'partner_link_url', - 'class' => 'form-input w-full', - 'value' => old('partner_link_url', $podcast->partner_link_url), - ]) ?> + 'id' => 'partner_link_url', + 'name' => 'partner_link_url', + 'class' => 'form-input w-full', + 'value' => old('partner_link_url', $podcast->partner_link_url), +]) ?> </div> <div class="flex flex-col flex-1"> <?= form_label( - lang('Podcast.form.partner_image_url'), - 'partner_image_url', - [], - lang('Podcast.form.partner_image_url_hint'), - true, - ) ?> - <?= form_input([ - 'id' => 'partner_image_url', - 'name' => 'partner_image_url', - 'class' => 'form-input w-full', - 'value' => old('partner_image_url', $podcast->partner_image_url), - ]) ?> + lang('Podcast.form.partner_image_url'), + 'partner_image_url', + [], + lang('Podcast.form.partner_image_url_hint'), + true, +) ?> + <?= form_input([ + 'id' => 'partner_image_url', + 'name' => 'partner_image_url', + 'class' => 'form-input w-full', + 'value' => old('partner_image_url', $podcast->partner_image_url), +]) ?> </div> </div> <?= form_section_close() ?> @@ -328,6 +341,7 @@ lang('Podcast.form.advanced_section_title'), lang('Podcast.form.advanced_section_subtitle'), ) ?> + <?= form_label( lang('Podcast.form.custom_rss'), 'custom_rss', @@ -335,12 +349,17 @@ lang('Podcast.form.custom_rss_hint'), true, ) ?> -<?= xml_editor([ - 'id' => 'custom_rss', - 'name' => 'custom_rss', - 'class' => 'form-textarea', - 'value' => old('custom_rss', $podcast->custom_rss_string), -]) ?> + +<?= component('Forms/XMLEditor', + [ + 'content' => old('custom_rss', $podcast->custom_rss_string) + ], + [ + 'id' => 'custom_rss', + 'name' => 'custom_rss', + ] +) ?> + <?= form_section_close() ?> <?= form_section( @@ -348,40 +367,62 @@ lang('Podcast.form.status_section_subtitle'), ) ?> -<?= form_switch( - lang('Podcast.form.block'), - ['id' => 'block', 'name' => 'block'], - 'yes', - old('block', $podcast->is_blocked), - 'mb-2', +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.lock'), + 'hint' => lang('Podcast.form.lock_hint'), + ], + [ + 'id' => 'lock', + 'name' => 'lock', + 'value' => 'yes', + 'checked' => old('complete', $podcast->is_locked), + 'class' => 'mb-2' + ] ) ?> -<?= form_switch( - lang('Podcast.form.complete'), - ['id' => 'complete', 'name' => 'complete'], - 'yes', - old('complete', $podcast->is_completed), - 'mb-2', +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.block'), + ], + [ + 'id' => 'block', + 'name' => 'block', + 'value' => 'yes', + 'checked' => old('block', $podcast->is_blocked), + 'class' => 'mb-2' + ] ) ?> -<?= form_switch( - lang('Podcast.form.lock') . - hint_tooltip(lang('Podcast.form.lock_hint'), 'ml-1'), - ['id' => 'lock', 'name' => 'lock'], - 'yes', - old('lock', $podcast->is_locked), +<?= component( + 'Forms/Toggler', + [ + 'label' => lang('Podcast.form.complete'), + ], + [ + 'id' => 'complete', + 'name' => 'complete', + 'value' => 'yes', + 'checked' => old('complete', $podcast->is_completed), + ] ) ?> <?= form_section_close() ?> -<?= button( - lang('Podcast.form.submit_edit'), - '', - ['variant' => 'primary'], - ['type' => 'submit', 'class' => 'self-end'], +<?= component( + 'Button', + [ + 'label' => lang('Podcast.form.submit_edit'), + 'variant' => 'primary', + ], + [ + 'type' => 'submit', + 'class' => 'self-end' + ] ) ?> <?= form_close() ?> - <?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/persons.php b/app/Views/admin/podcast/persons.php index 506cedfe41f5250f8ad1d36aa3c957399e0ef927..f9360ec0f64b5b9fc0b1caffd29d2e2f571e8759 100644 --- a/app/Views/admin/podcast/persons.php +++ b/app/Views/admin/podcast/persons.php @@ -99,10 +99,12 @@ [], lang('Person.podcast_form.persons_hint'), ) ?> -<?= form_multiselect('persons[]', $personOptions, old('persons', []), [ +<?= component('Forms/MultiSelect', ['options' => $personOptions, 'selected' => old('persons', [])], [ 'id' => 'persons', - 'class' => 'form-select mb-4', + 'name' => 'persons[]', + 'class' => 'mb-4', 'required' => 'required', + 'data-max-item-count' => '2', ]) ?> <?= form_label( @@ -112,9 +114,10 @@ lang('Person.podcast_form.roles_hint'), true, ) ?> -<?= form_multiselect('roles[]', $taxonomyOptions, old('roles', []), [ +<?= component('Forms/MultiSelect', ['options' => $taxonomyOptions, 'selected' => old('roles', [])], [ 'id' => 'roles', - 'class' => 'form-select mb-4', + 'name' => 'roles[]', + 'class' => 'mb-4', ]) ?> <?= form_section_close() ?> diff --git a/app/Views/admin/podcast/platforms.php b/app/Views/admin/podcast/platforms.php index 6cd06182d0830f8d1dde91881c81f858e26a1384..77b46d34933f86a5a6f0b7d6c13db9ff0e47ab54 100644 --- a/app/Views/admin/podcast/platforms.php +++ b/app/Views/admin/podcast/platforms.php @@ -103,34 +103,39 @@ 'type' => 'text', 'placeholder' => lang("Platforms.description.{$platform->type}"), ]) ?> - <?= form_switch( - lang('Platforms.visible'), + <?= component( + 'Forms/Toggler', + [ + 'label' => lang('Platforms.visible'), + ], [ 'id' => $platform->slug . '_visible', 'name' => 'platforms[' . $platform->slug . '][visible]', - ], - 'yes', - old( - $platform->slug . '_visible', - $platform->is_visible ? $platform->is_visible : false, - ), - 'text-sm mb-1', + 'value' => 'yes', + 'checked' => old( + $platform->slug . '_visible', + $platform->is_visible ? $platform->is_visible : false, + ), + 'class' => 'text-sm mb-1' + ] ) ?> - <?= form_switch( - lang('Platforms.on_embeddable_player'), + <?= component( + 'Forms/Toggler', [ - 'id' => $platform->slug . '_on_embeddable_player', - 'name' => - 'platforms[' . $platform->slug . '][on_embeddable_player]', + 'label' => lang('Platforms.on_embeddable_player'), ], - 'yes', - old( - $platform->slug . '_on_embeddable_player', - $platform->is_on_embeddable_player - ? $platform->is_on_embeddable_player - : false, - ), - 'text-sm', + [ + 'id' => $platform->slug . '_on_embeddable_player', + 'name' => 'platforms[' . $platform->slug . '][on_embeddable_player]', + 'value' => 'yes', + 'checked' => old( + $platform->slug . '_on_embeddable_player', + $platform->is_on_embeddable_player + ? $platform->is_on_embeddable_player + : false, + ), + 'class' => 'text-sm' + ] ) ?> </div> </div> diff --git a/app/Views/admin/user/edit.php b/app/Views/admin/user/edit.php index 4fac32d99a5ae2f4054d7c2129f30e69923f2fc9..d57f80842b26a06f485969d5eae82efa20aa8937 100644 --- a/app/Views/admin/user/edit.php +++ b/app/Views/admin/user/edit.php @@ -17,8 +17,9 @@ <?= csrf_field() ?> <?= form_label(lang('User.form.roles'), 'roles') ?> -<?= form_multiselect('roles[]', $roleOptions, $user->roles, [ +<?= component('Forms/MultiSelect', ['options' => $roleOptions, 'selected' => $user->roles], [ 'id' => 'roles', + 'name' => 'roles[]', 'class' => 'mb-4', ]) ?> diff --git a/app/Views/components/.gitkeep b/app/Views/components/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/Views/podcast/_partials/comment_with_replies_authenticated.php b/app/Views/podcast/_partials/comment_with_replies_authenticated.php index 567137b395fa1360171861a672803bede206db95..52c0b4b3d0c4b05c1d1a744b6ba26ed0cb89514a 100644 --- a/app/Views/podcast/_partials/comment_with_replies_authenticated.php +++ b/app/Views/podcast/_partials/comment_with_replies_authenticated.php @@ -1,4 +1,4 @@ -<?= $this->include('podcast/_partials/comment_card_authenticated') ?> +<?= $this->include('podcast/_partials/comment_card_authenticated') ?> <div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl"> <?= form_open( route_to('comment-attempt-reply', $podcast->id, $episode->id, $comment->id), diff --git a/app/Views/podcast/activity.php b/app/Views/podcast/activity.php index 11959f914c656def98bed2baacdbba1353d06584..a6d91f94dcd8eb3c18dba99752b73912d76137fc 100644 --- a/app/Views/podcast/activity.php +++ b/app/Views/podcast/activity.php @@ -38,7 +38,6 @@ 'Podcast.episodes', ) ?></a> </nav> - <section class="max-w-2xl px-6 py-8 mx-auto space-y-8"> <?php foreach ($posts as $post): ?> <?php if ($post->reblog_of_id !== null): ?> diff --git a/ecs.php b/ecs.php index 8a67127a37fcc233dd7dd7515f7c91e34a43ef39..1ef585e7341ba94a6a04bae1e29823783cdf178e 100644 --- a/ecs.php +++ b/ecs.php @@ -1,6 +1,7 @@ <?php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symplify\CodingStandard\Fixer\Naming\StandardizeHereNowDocKeywordFixer; use Symplify\EasyCodingStandard\ValueObject\Option; use Symplify\EasyCodingStandard\ValueObject\Set\SetList; @@ -20,6 +21,10 @@ return static function (ContainerConfigurator $containerConfigurator): void { // skip specific generated files __DIR__ . '/app/Language/*/PersonsTaxonomy.php', + + StandardizeHereNowDocKeywordFixer::class => [ + __DIR__ . '/app/Views/Components', + ] ]); $containerConfigurator->import(SetList::PSR_12); diff --git a/phpstan.neon b/phpstan.neon index 36d4a64106d2c442a5eeda6a913567b1707140aa..ac3d788d6c1e01d5ba3babfe8c1d772b8287fa3a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -35,3 +35,4 @@ parameters: - app/Helpers - app/Libraries/ActivityPub/Helpers - app/Libraries/Analytics/Helpers + - app/Libraries/ViewComponents/Helpers diff --git a/rector.php b/rector.php index e3ba7ecf788ef14c9c93210e86e1ed2d7f177549..f3057c667927285936e47bc45a3ec15fc9e2ef21 100644 --- a/rector.php +++ b/rector.php @@ -65,7 +65,7 @@ return static function (ContainerConfigurator $containerConfigurator): void { ], ]); - // Path to phpstan with extensions, that PHPSTan in Rector uses to determine types + // Path to phpstan with extensions, that PHPStan in Rector uses to determine types $parameters->set( Option::PHPSTAN_FOR_RECTOR_PATH, __DIR__ . '/phpstan.neon', diff --git a/tailwind.config.js b/tailwind.config.js index 36fd2211a020f3aa3baf1cb909563f21d9b50e51..4b4b984edd939611f4e8d17b7c654d9a988bb19c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,6 +4,7 @@ module.exports = { mode: "jit", purge: [ "./app/Views/**/*.php", + "./app/View/Components/**/*.php", "./app/Helpers/*.php", "./app/Resources/**/*.ts", ],