Commit 65367295 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: integrate stylized form components and update podcast edit page

parent 23bdc6f8
......@@ -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);
}
......
......@@ -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
......
<?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, ',');
}
}
......@@ -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;
}
`;
......
.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 {
......
......@@ -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;
......
@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;
}
}
......@@ -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");
}
}
}
<?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;
}
}
<?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';
}
}
<?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;
}
}
<?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));
}
}
......@@ -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>
......
......@@ -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 focus:ring-0 w-full h-full';
$this->attributes['rows'] = 6;
$this->attributes['class'] = 'border-none outline-none focus:border-none focus:outline-none w-full h-full';
$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 '<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>';
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;
}
}
<?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;
}
}
<?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>';