feat: integrate stylized form components and update podcast edit page

This commit is contained in:
Yassine Doghri 2021-09-10 16:02:25 +00:00
parent 23bdc6f8e3
commit 6536729546
23 changed files with 558 additions and 486 deletions

View File

@ -22,11 +22,12 @@ class Component implements ComponentInterface
*/
public function __construct(array $attributes)
{
helper('viewcomponents');
if ($attributes !== []) {
$this->hydrate($attributes);
}
// overwrite default attributes if set
$this->attributes = array_merge($this->attributes, $attributes);
}

View File

@ -109,7 +109,7 @@ class ComponentRenderer
private function renderPairedTags(string $output): string
{
$pattern = '/<\s*(?<name>[A-Z][A-Za-z0-9\.]*?)(?<attributes>[\s\S\=\'\"]*)>(?<slot>.*)<\/\s*\1\s*>/uUsm';
ini_set('pcre.backtrack_limit', '-1');
/*
$matches[0] = full tags matched and all of its content
$matches[name] = pascal cased tag name

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
if (! function_exists('flatten_attributes')) {
/**
* Stringify attributes for use in HTML tags.
*
* Helper function used to convert a string, array, or object of attributes to a string.
*
* @param mixed $attributes string, array, object
*/
function flatten_attributes($attributes, bool $js = false): string
{
$atts = '';
if ($attributes === null) {
return $atts;
}
if (is_string($attributes)) {
return ' ' . $attributes;
}
$attributes = (array) $attributes;
foreach ($attributes as $key => $val) {
$atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . $val . '"';
}
return rtrim($atts, ',');
}
}

View File

@ -57,12 +57,17 @@ export class XMLEditor extends LitElement {
static styles = css`
.cm-wrap {
border: 1px solid #6b7280;
border-radius: 0.5rem;
overflow: hidden;
border: 3px solid #000000;
background-color: #ffffff;
}
.cm-editor.cm-focused {
outline: 2px solid transparent;
box-shadow: 0 0 0 1px #2563eb;
box-shadow: 0 0 0 2px #e7f9e4, 0 0 0 calc(4px) #009486;
}
.cm-gutters {
background-color: #ffffff !important;
}
`;

View File

@ -1,5 +1,5 @@
.breadcrumb {
@apply inline-flex flex-wrap px-1 py-2 text-sm;
@apply inline-flex flex-wrap px-1 text-sm;
}
.breadcrumb-item + .breadcrumb-item::before {

View File

@ -138,8 +138,9 @@
}
.choices__inner {
@apply p-2 bg-white border border-gray-700;
@apply p-2 bg-white border-black rounded-lg border-3;
box-shadow: 2px 2px 0 black;
display: inline-block;
vertical-align: top;
width: 100%;
@ -158,11 +159,11 @@
}
.is-open .choices__inner {
border-radius: 0;
@apply rounded-b-none;
}
.is-flipped.is-open .choices__inner {
border-radius: 0;
@apply rounded-t-none rounded-b-lg border-b-3;
}
.choices__list {
@ -172,9 +173,7 @@
}
.choices__list--single {
@apply pr-4;
display: inline-block;
width: 100%;
@apply inline-block w-full pr-4;
}
[dir="rtl"] .choices__list--single {
@ -191,7 +190,7 @@
}
.choices__list--multiple .choices__item {
@apply inline-block px-2 py-1 mb-1 mr-1 text-sm text-white align-middle bg-pine-600;
@apply inline-block px-2 py-1 mb-1 mr-1 text-sm text-white align-middle rounded bg-pine-500;
word-break: break-all;
box-sizing: border-box;
@ -216,12 +215,11 @@
}
.choices__list--dropdown {
@apply z-50 border-2 border-black shadow-lg;
visibility: hidden;
z-index: 1;
position: absolute;
width: 100%;
background-color: #ffffff;
border: 1px solid #dddddd;
top: 100%;
margin-top: -1px;
overflow: hidden;
@ -234,10 +232,11 @@
}
.is-open .choices__list--dropdown {
border-color: #b7b7b7;
@apply border-t-0 rounded-b-lg;
}
.is-flipped .choices__list--dropdown {
@apply border-b-0 rounded-t-lg rounded-b-none border-t-3;
top: auto;
bottom: 100%;
margin-top: 0;

View File

@ -1,26 +1,17 @@
@layer components {
.form-radio-btn {
@apply absolute opacity-0;
@apply absolute mt-3 ml-3 border-black border-3 text-pine-500 focus:ring-2 focus:ring-pine-800;
}
.form-radio-btn:focus + label {
@apply ring;
@apply ring ring-pine-100;
}
.form-radio-btn + label {
@apply inline-block px-2 py-1 text-sm text-black bg-white border rounded cursor-pointer;
&:hover {
@apply bg-pine-100;
}
@apply inline-block py-2 pl-8 pr-2 text-sm font-semibold text-gray-500 bg-white border-black rounded-lg cursor-pointer border-3;
}
.form-radio-btn:checked + label {
@apply text-white bg-pine-600;
&::before {
@apply mr-2 text-pine-200;
content: "✓";
}
@apply text-black border-pine-500;
}
}

View File

@ -3,26 +3,37 @@
@apply absolute w-0 h-0 opacity-0;
&:checked + .form-switch-slider {
@apply bg-pine-600;
@apply bg-pine-500;
}
&:focus + .form-switch-slider {
@apply ring;
@apply ring ring-offset-2 ring-pine-500 ring-offset-pine-100;
}
&:checked + .form-switch-slider::before {
@apply transform translate-x-5;
@apply transform translate-x-8;
}
&:checked + .form-switch-slider::after {
@apply transform translate-x-1;
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23ffffff'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z'/%3E%3C/svg%3E%0A");
}
}
.form-switch-slider {
@apply relative inset-0 flex-shrink-0 w-10 h-5 transition duration-200 bg-gray-400 rounded-full cursor-pointer;
@apply relative inset-0 flex-shrink-0 w-[72px] h-10 transition duration-200 bg-gray-400 border-black rounded-full cursor-pointer border-3;
&::before {
@apply absolute w-4 h-4 transition duration-200 bg-white rounded-full ring-1 ring-black ring-opacity-5;
@apply absolute z-10 w-[28px] h-[28px] transition duration-200 bg-white rounded-full ring-1 ring-black ring-opacity-5 shadow;
content: "";
left: 2px;
bottom: 2px;
left: 3px;
bottom: 3px;
}
&::after {
@apply absolute w-6 h-6 transition duration-150 transform translate-x-8 top-1 left-1;
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'/%3E%3C/svg%3E%0A");
}
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class Field extends FormComponent
{
protected string $as = 'Input';
protected string $label = '';
protected ?string $helperText = null;
protected ?string $hintText = null;
public function render(): string
{
$helperText = $this->helperText === null ? '' : '<Forms.Helper>' . $this->helperText . '</Forms.Helper>';
$labelAttributes = [
'for' => $this->id,
'isOptional' => $this->required ? 'false' : 'true',
];
if ($this->hintText) {
$labelAttributes['hint'] = $this->hintText;
}
$labelAttributes = stringify_attributes($labelAttributes);
// remove field specific attributes to inject the rest to Form Component
$fieldComponentAttributes = $this->attributes;
unset($fieldComponentAttributes['as']);
unset($fieldComponentAttributes['label']);
unset($fieldComponentAttributes['class']);
unset($fieldComponentAttributes['helperText']);
unset($fieldComponentAttributes['hintText']);
$fieldComponentAttributes = flatten_attributes($fieldComponentAttributes);
return <<<HTML
<div class="flex flex-col {$this->class}">
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
<Forms.{$this->as} {$fieldComponentAttributes} class="mb-1"/>
{$helperText}
</div>
HTML;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
use ViewComponents\Component;
class FormComponent extends Component
{
protected ?string $id = null;
protected string $name = '';
protected string $value = '';
protected bool $required = false;
public function __construct($attributes)
{
parent::__construct($attributes);
if ($this->id === null) {
$this->id = $this->name;
}
}
public function setRequired(string $value): void
{
$this->required = $value === 'true';
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class Helper extends FormComponent
{
/**
* @var "default"|"error"
*/
protected string $type = 'default';
public function render(): string
{
$class = 'text-gray-600';
return <<<HTML
<small class="{$class} {$this->class}">{$this->slot}</small>
HTML;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class Input extends FormComponent
{
protected string $type = 'text';
public function render(): string
{
$class = 'px-3 py-2 rounded-lg border-3 focus:ring-2 focus:ring-pine-500 focus:ring-offset-2 focus:ring-offset-pine-100 ' . $this->class;
if (session()->has('errors')) {
$error = session('errors')[$this->name];
if ($error) {
$class .= ' border-red';
}
} else {
$class .= ' border-black focus:border-black';
}
$data = [
'id' => $this->id,
'name' => $this->name,
'class' => $class,
'type' => $this->type,
];
if ($this->required) {
$data['required'] = 'required';
}
return form_input($data, old($this->name, $this->value));
}
}

View File

@ -8,16 +8,9 @@ use ViewComponents\Component;
class Label extends Component
{
/**
* @var array<string, string>
*/
protected array $attributes = [
'for' => '',
'name' => '',
'class' => '',
];
protected ?string $for = null;
protected string $hint = '';
protected ?string $hint = null;
protected bool $isOptional = false;
@ -28,14 +21,14 @@ class Label extends Component
public function render(): string
{
$labelClass = $this->attributes['class'];
$labelClass = 'text-sm ' . $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') : '';
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
return <<<HTML
<label class="{$labelClass}" {$attributes}>{$this->slot}{$optionalText}{$hint}</label>

View File

@ -4,73 +4,68 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use ViewComponents\Component;
class MarkdownEditor extends Component
class MarkdownEditor extends FormComponent
{
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']);
}
$editorClass = 'w-full flex flex-col bg-white border-3 border-black rounded-lg overflow-hidden focus-within:ring-2 focus-within:ring-offset-2 focus-withing:ring-offset-pine-100 focus-within:ring-pine-500 ' . $this->class;
$this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
$this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
$this->attributes['rows'] = 6;
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->slot) .
'<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>';
$textarea = form_textarea($this->attributes, old($this->name, $this->value, false));
$icons = [
'heading' => icon('heading'),
'bold' => icon('bold'),
'italic' => icon('italic'),
'list-unordered' => icon('list-unordered'),
'list-ordered' => icon('list-ordered'),
'quote' => icon('quote'),
'link' => icon('link'),
'image-add' => icon('image-add'),
'markdown' => icon(
'markdown',
'mr-1 text-lg text-gray-400'
),
];
$translations = [
'write' => lang('Common.forms.editor.write'),
'preview' => lang('Common.forms.editor.preview'),
'help' => lang('Common.forms.editor.help'),
];
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>
<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" />
</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>
</footer>
</div>
HTML;
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
/**
* Form Checkbox Switch
*
* Abstracts form_label to stylize it as a switch toggle
*/
class RadioButton extends FormComponent
{
protected bool $isChecked = false;
public function setIsChecked(string $value): void
{
$this->isChecked = $value === 'true';
}
public function render(): string
{
$radioInput = form_radio(
[
'id' => $this->value,
'name' => $this->name,
'class' => 'form-radio-btn',
],
$this->value,
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
);
return <<<HTML
<div>
{$radioInput}
<label for="{$this->value}">{$this->slot}</label>
</div>
HTML;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Views\Components\Forms;
class Section extends FormComponent
{
protected string $title = '';
protected ?string $subtitle = null;
public function render(): string
{
$subtitle = $this->subtitle === null ? '' : '<p class="text-sm text-gray-600 clear-left">' . $this->subtitle . '</p>';
return <<<HTML
<fieldset class="w-full max-w-xl p-8 bg-white border-2 border-black 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>
</fieldset>
HTML;
}
}

View File

@ -4,28 +4,28 @@ declare(strict_types=1);
namespace App\Views\Components\Forms;
use ViewComponents\Component;
class MultiSelect extends Component
class Select extends FormComponent
{
/**
* @var array<string, string>
*/
protected array $options = [];
/**
* @var string[]
*/
protected array $selected = [];
protected string $selected;
public function setOptions(string $value): void
{
// dd(json_decode(html_entity_decode(html_entity_decode($value)), true));
$this->options = json_decode(html_entity_decode($value), true);
}
public function render(): string
{
$defaultAttributes = [
'data-class' => $this->attributes['class'],
'multiple' => 'multiple',
'data-class' => 'border-3 rounded-lg ' . $this->class,
];
$extra = array_merge($defaultAttributes, $this->attributes);
return form_dropdown($this->attributes['name'], $this->options, $this->selected, $extra);
return form_dropdown($this->name, $this->options, $this->selected !== '' ? [$this->selected] : [], $extra);
}
}

View File

@ -4,12 +4,11 @@ declare(strict_types=1);
namespace App\Views\Components;
use Exception;
use ViewComponents\Component;
class Heading extends Component
{
protected string $level = '';
protected string $tagName = 'div';
/**
* @var "small"|"base"|"large"
@ -18,21 +17,16 @@ class Heading extends Component
public function render(): string
{
if ($this->level === '') {
throw new Exception('level property must be set for Heading component.');
}
$sizeClasses = [
'small' => 'tracking-wide text-base',
'base' => 'text-xl',
'large' => 'text-3xl',
];
$class = 'relative z-10 font-bold text-pine-800 font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-pine-100 before:-z-10 ' . $sizeClasses[$this->size];
$level = $this->level;
$class = $this->class . ' relative z-10 font-bold text-pine-800 font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-pine-100 before:-z-10 ' . $sizeClasses[$this->size];
return <<<HTML
<h{$level} class="{$class}">{$this->slot}</h{$level}>
<{$this->tagName} class="{$class}">{$this->slot}</{$this->tagName}>
HTML;
}
}

View File

@ -33,3 +33,4 @@ parameters:
paths:
- app/Helpers
- app/Common.php
- app/Libraries/ViewComponents/Helpers

View File

@ -51,6 +51,12 @@ module.exports = {
zIndex: {
"-10": "-10",
},
borderWidth: {
3: "3px",
},
ringWidth: {
3: "3px",
},
},
},
variants: {},

View File

@ -79,21 +79,17 @@
]) ?>
</footer>
</aside>
<main class="holy-grail__main">
<header class="bg-white">
<div class="container flex flex-wrap items-end justify-between px-2 py-10 mx-auto md:px-12 gap-y-6 gap-x-6">
<div class="flex flex-col">
<?= render_breadcrumb('text-gray-800 text-xs') ?>
<main class="relative holy-grail__main">
<header class="flex-col py-10 bg-white">
<div class="container mx-auto">
<?= render_breadcrumb('text-gray-800 text-xs') ?>
<div class="flex justify-between">
<div class="flex flex-wrap items-center">
<Heading level="1" size="large"><?= $this->renderSection(
'pageTitle',
) ?></Heading>
<Heading tagName="h1" size="large"><?= $this->renderSection('pageTitle') ?></Heading>
<?= $this->renderSection('headerLeft') ?>
</div>
<div class="flex gap-1"><?= $this->renderSection('headerRight') ?></div>
</div>
<div class="flex flex-wrap"><?= $this->renderSection(
'headerRight',
) ?></div>
</div>
</header>
<div class="container px-2 py-8 mx-auto md:px-12">

View File

@ -12,373 +12,216 @@
<?= lang('Podcast.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<Button variant="primary" type="submit" form="podcast-edit-form"><?= lang('Podcast.form.submit_edit') ?></Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open_multipart((string) route_to('podcast-edit', $podcast->id), [
'id' => 'podcast-edit-form',
'method' => 'post',
'class' => 'flex flex-col',
]) ?>
<?= csrf_field() ?>
<?= form_section(
lang('Podcast.form.identity_section_title'),
lang('Podcast.form.identity_section_subtitle'),
) ?>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.identity_section_title') ?>"
subtitle="<?= lang('Podcast.form.identity_section_subtitle') ?>" >
<?= form_label(lang('Podcast.form.image'), 'image') ?>
<img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="object-cover w-32 h-32" />
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<Forms.Field
name="image"
label="<?= lang('Podcast.form.image') ?>"
helperText="<?= lang('Common.forms.image_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
name="title"
label="<?= lang('Podcast.form.title') ?>"
helperText="<?= $podcast->link ?>"
value="<?= $podcast->title ?>"
required="true" />
<small class="mb-4 text-gray-600"><?= lang(
'Common.forms.image_size_hint',
) ?></small>
<Forms.Field
as="MarkdownEditor"
name="description"
label="<?= lang('Podcast.form.description') ?>"
value="<?= $podcast->title ?>"
required="true" />
<?= form_label(lang('Podcast.form.title'), 'title') ?>
<?= form_input([
'id' => 'title',
'name' => 'title',
'class' => 'form-input mb-1',
'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>
<?= form_fieldset_close() ?>
<div class="mb-4">
<Forms.Label for="description"><?= lang('Podcast.form.description') ?></Forms.Label>
<Forms.MarkdownEditor id="description" name="description" required="required"><?= old('description', $podcast->description_markdown, false) ?></Forms.MarkdownEditor>
</div>
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.classification_section_title'),
lang('Podcast.form.classification_section_subtitle'),
) ?>
<?= form_label(lang('Podcast.form.language'), 'language') ?>
<?= form_dropdown(
'language',
$languageOptions,
[old('language', $podcast->language_code)],
[
'id' => 'language',
'class' => 'form-select mb-4',
'required' => 'required',
],
) ?>
<?= form_label(lang('Podcast.form.category'), 'category') ?>
<?= form_dropdown(
'category',
$categoryOptions,
[old('category', $podcast->category_id)],
[
'id' => 'category',
'class' => 'form-select mb-4',
'required' => 'required',
],
) ?>
<?= form_label(
lang('Podcast.form.other_categories'),
'other_categories',
[],
'',
true,
) ?>
<Forms.MultiSelect
id="other_categories"
name="other_categories[]"
class="mb-4"
data-max-item-count="2"
selected="<?= json_encode(old('other_categories', $podcast->other_categories_ids)) ?>"
options="<?= htmlspecialchars(json_encode($categoryOptions)) ?>" />
<?= 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() ?>
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.author_section_title'),
lang('Podcast.form.author_section_subtitle'),
) ?>
<?= form_label(
lang('Podcast.form.owner_name'),
'owner_name',
[],
lang('Podcast.form.owner_name_hint'),
) ?>
<?= form_input([
'id' => 'owner_name',
'name' => 'owner_name',
'class' => 'form-input mb-4',
'value' => old('owner_name', $podcast->owner_name),
'required' => 'required',
]) ?>
<?= form_label(
lang('Podcast.form.owner_email'),
'owner_email',
[],
lang('Podcast.form.owner_email_hint'),
) ?>
<?= form_input([
'id' => 'owner_email',
'name' => 'owner_email',
'class' => 'form-input mb-4',
'value' => old('owner_email', $podcast->owner_email),
'type' => 'email',
'required' => 'required',
]) ?>
<?= form_label(
lang('Podcast.form.publisher'),
'publisher',
[],
lang('Podcast.form.publisher_hint'),
true,
) ?>
<?= form_input([
'id' => 'publisher',
'name' => 'publisher',
'class' => 'form-input mb-4',
'value' => old('publisher', $podcast->publisher),
]) ?>
<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?>
<?= form_input([
'id' => 'copyright',
'name' => 'copyright',
'class' => 'form-input mb-4',
'value' => old('copyright', $podcast->copyright),
]) ?>
<?= form_section_close() ?>
<?= form_section(
lang('Podcast.form.location_section_title'),
lang('Podcast.form.location_section_subtitle'),
) ?>
<?= form_label(
lang('Podcast.form.location_name'),
'location_name',
[],
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(
lang('Podcast.form.monetization_section_title'),
lang('Podcast.form.monetization_section_subtitle'),
) ?>
<?= form_label(
lang('Podcast.form.payment_pointer'),
'payment_pointer',
[],
lang('Podcast.form.payment_pointer_hint'),
true,
) ?>
<?= form_input([
'id' => 'payment_pointer',
'name' => 'payment_pointer',
'class' => 'form-input mb-4',
'value' => old('payment_pointer', $podcast->payment_pointer),
]) ?>
<?= form_label(lang('Podcast.form.partnership')) ?>
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-shrink w-32">
<?= form_label(
lang('Podcast.form.partner_id'),
'partner_id',
[],
lang('Podcast.form.partner_id_hint'),
true,
) ?>
<?= form_input([
'id' => 'partner_id',
'name' => 'partner_id',
'class' => 'form-input w-full',
'value' => old('partner_id', $podcast->partner_id),
]) ?>
<fieldset>
<legend><?= lang('Podcast.form.type.label') .
hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?></legend>
<div class="flex gap-2">
<Forms.RadioButton
value="episodic"
name="type"
isChecked="<?= $podcast->type === 'episodic' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.episodic') ?></Forms.RadioButton>
<Forms.RadioButton
value="serial"
name="type"
isChecked="<?= $podcast->type === 'serial' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.serial') ?></Forms.RadioButton>
</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,
) ?>
<?= form_input([
'id' => 'partner_link_url',
'name' => 'partner_link_url',
'class' => 'form-input w-full',
'value' => old('partner_link_url', $podcast->partner_link_url),
]) ?>
</fieldset>
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.classification_section_title') ?>"
subtitle="<?= lang('Podcast.form.classification_section_subtitle') ?>" >
<Forms.Field
as="Select"
name="language"
label="<?= lang('Podcast.form.language') ?>"
selected="<?= $podcast->language_code ?>"
required="true"
options="<?= esc(json_encode($languageOptions)) ?>" />
<Forms.Field
as="Select"
name="category"
label="<?= lang('Podcast.form.category') ?>"
selected="<?= $podcast->category_id ?>"
required="true"
options="<?= esc(json_encode($categoryOptions)) ?>" />
<Forms.Field
as="MultiSelect"
name="other_categories[]"
label="<?= lang('Podcast.form.other_categories') ?>"
selected="<?= json_encode(old('other_categories', $podcast->other_categories_ids)) ?>"
data-max-item-count="2"
options="<?= esc(json_encode($categoryOptions)) ?>" />
<fieldset class="mb-4">
<legend><?= lang('Podcast.form.parental_advisory.label') .
hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?></legend>
<div class="flex gap-2">
<Forms.RadioButton
value="undefined"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.undefined') ?></Forms.RadioButton>
<Forms.RadioButton
value="clean"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.clean', ) ?></Forms.RadioButton>
<Forms.RadioButton
value="explicit"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.explicit', ) ?></Forms.RadioButton>
</div>
</fieldset>
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.author_section_title') ?>"
subtitle="<?= lang('Podcast.form.author_section_subtitle') ?>" >
<Forms.Field
name="owner_name"
label="<?= lang('Podcast.form.owner_name') ?>"
value="<?= $podcast->owner_name ?>"
hintText="<?= lang('Podcast.form.owner_name_hint') ?>"
required="true" />
<Forms.Field
name="owner_email"
type="email"
label="<?= lang('Podcast.form.owner_email') ?>"
value="<?= $podcast->owner_email ?>"
hintText="<?= lang('Podcast.form.owner_email_hint') ?>"
required="true" />
<Forms.Field
name="publisher"
label="<?= lang('Podcast.form.publisher') ?>"
value="<?= $podcast->publisher ?>"
hintText="<?= lang('Podcast.form.publisher_hint') ?>" />
<Forms.Field
name="copyright"
label="<?= lang('Podcast.form.copyright') ?>"
value="<?= $podcast->copyright ?>" />
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
<Forms.Field
name="location_name"
label="<?= lang('Podcast.form.location_name') ?>"
value="<?= $podcast->location_name ?>"
hintText="<?= lang('Podcast.form.location_name_hint') ?>" />
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.monetization_section_title') ?>"
subtitle="<?= lang('Podcast.form.monetization_section_subtitle') ?>" >
<Forms.Field
name="payment_pointer"
label="<?= lang('Podcast.form.payment_pointer') ?>"
value="<?= $podcast->payment_pointer ?>"
hintText="<?= lang('Podcast.form.payment_pointer_hint') ?>" />
<fieldset class="flex flex-col items-start p-4 bg-gray-100 rounded">
<Heading tagName="legend" class="float-left" size="small"><?= lang('Podcast.form.partnership') ?></Heading>
<div class="flex flex-col w-full clear-left gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-shrink w-32">
<Forms.Label for="partner_id" hint="<?= lang('Podcast.form.partner_id_hint') ?>" isOptional="true"><?= lang('Podcast.form.partner_id') ?></Forms.Label>
<Forms.Input name="partner_id" value="<?= $podcast->partner_id ?>" />
</div>
<div class="flex flex-col flex-1">
<Forms.Label for="partner_link_url" hint="<?= lang('Podcast.form.partner_link_url_hint') ?>" isOptional="true"><?= lang('Podcast.form.partner_link_url') ?></Forms.Label>
<Forms.Input name="partner_link_url" value="<?= $podcast->partner_link_url ?>" />
</div>
</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),
]) ?>
<div class="flex flex-col w-full mt-2">
<Forms.Label for="partner_image_url" hint="<?= lang('Podcast.form.partner_image_url_hint') ?>" isOptional="true"><?= lang('Podcast.form.partner_image_url') ?></Forms.Label>
<Forms.Input name="partner_image_url" value="<?= $podcast->partner_image_url ?>" />
</div>
</div>
<?= form_section_close() ?>
</fieldset>
</Forms.Section>
<?= form_section(
lang('Podcast.form.advanced_section_title'),
lang('Podcast.form.advanced_section_subtitle'),
) ?>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.advanced_section_title') ?>"
subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" >
<?= form_label(
lang('Podcast.form.custom_rss'),
'custom_rss',
[],
lang('Podcast.form.custom_rss_hint'),
true,
) ?>
<Forms.XMLEditor id="custom_rss" name="custom_rss"><?= old('custom_rss', $podcast->custom_rss_string, false) ?></Forms.XMLEditor>
<Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= lang('Podcast.form.custom_rss') ?>"
value="<?= $podcast->custom_rss_string ?>"
hintText="<?= lang('Podcast.form.custom_rss_hint') ?>" />
<?= form_section_close() ?>
</Forms.Section>
<?= form_section(
lang('Podcast.form.status_section_title'),
lang('Podcast.form.status_section_subtitle'),
) ?>
<Forms.Toggler class="mb-2" id="lock" name="lock" value="yes" checked="<?= old('complete', $podcast->is_locked) ?>" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" id="block" name="block" value="yes" checked="<?= old('complete', $podcast->is_blocked) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler id="complete" name="complete" value="yes" checked="<?= old('complete', $podcast->is_completed) ?>">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
<?= form_section_close() ?>
<Button variant="primary" type="submit" class="self-end">
<?= lang('Podcast.form.submit_edit') ?>
</Button>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.status_section_title') ?>"
subtitle="<?= lang('Podcast.form.status_section_subtitle') ?>" >
<Forms.Toggler class="mb-2" id="lock" name="lock" value="yes" checked="<?= old('complete', $podcast->is_locked) ?>" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" id="block" name="block" value="yes" checked="<?= old('complete', $podcast->is_blocked) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler id="complete" name="complete" value="yes" checked="<?= old('complete', $podcast->is_completed) ?>">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
</Forms.Section>
<?= form_close() ?>

View File

@ -1,6 +1,6 @@
<section class="flex flex-col">
<header class="flex justify-between py-2">
<Heading level="2"><?= lang('Podcast.latest_episodes') ?></Heading>
<Heading tagName="h2"><?= lang('Podcast.latest_episodes') ?></Heading>
<a href="<?= route_to(
'episode-list',
$podcast->id,