mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-03 16:02:02 +00:00
feat(plugins): add aside with plugin metadata next to plugin's readme
- enhance plugin card ui - refactor components to be more consistent - invert toggler label for better UX - edit view components regex
This commit is contained in:
parent
e6bfdfc390
commit
dfb7888aeb
@ -31,6 +31,6 @@ if (! function_exists('replace_breadcrumb_params')) {
|
||||
function replace_breadcrumb_params(array $newParams): void
|
||||
{
|
||||
$breadcrumb = Services::breadcrumb();
|
||||
$breadcrumb->replaceParams(esc($newParams));
|
||||
$breadcrumb->replaceParams($newParams);
|
||||
}
|
||||
}
|
||||
|
@ -16,31 +16,6 @@ use CodeIgniter\View\Table;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('hint_tooltip')) {
|
||||
/**
|
||||
* Hint component
|
||||
*
|
||||
* Used to produce tooltip with a question mark icon for hint texts
|
||||
*
|
||||
* @param string $hintText The hint text
|
||||
*/
|
||||
function hint_tooltip(string $hintText = '', string $class = ''): string
|
||||
{
|
||||
$tooltip =
|
||||
'<span data-tooltip="bottom" tabindex="0" title="' .
|
||||
esc($hintText) .
|
||||
'" class="inline-block align-middle opacity-75 focus:ring-accent';
|
||||
|
||||
if ($class !== '') {
|
||||
$tooltip .= ' ' . $class;
|
||||
}
|
||||
|
||||
return $tooltip . '">' . icon('question-fill') . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('data_table')) {
|
||||
/**
|
||||
* Data table component
|
||||
@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) {
|
||||
*/
|
||||
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
||||
{
|
||||
$class = match ($publicationStatus) {
|
||||
'published' => 'text-pine-500 border-pine-500 bg-pine-50',
|
||||
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
|
||||
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50',
|
||||
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||
default => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||
$variant = match ($publicationStatus) {
|
||||
'published' => 'success',
|
||||
'scheduled' => 'warning',
|
||||
'with_podcast' => 'info',
|
||||
'not_published' => 'default',
|
||||
default => 'default',
|
||||
};
|
||||
|
||||
$title = match ($publicationStatus) {
|
||||
@ -130,16 +105,12 @@ if (! function_exists('publication_pill')) {
|
||||
|
||||
$label = lang('Episode.publication_status.' . $publicationStatus);
|
||||
|
||||
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' .
|
||||
$class .
|
||||
' ' .
|
||||
$customClass .
|
||||
'">' .
|
||||
$label .
|
||||
($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||
// @icon('error-warning-fill')
|
||||
return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
|
||||
'">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||
'class' => 'flex-shrink-0 ml-1 text-lg',
|
||||
]) : '') .
|
||||
'</span>';
|
||||
'</x-Pill>';
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +153,7 @@ if (! function_exists('publication_button')) {
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</Button>
|
||||
<x-Button variant="{$variant}" uri="{$route}" iconLeft="{$iconLeft}" >{$label}</x-Button>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@ -356,7 +327,7 @@ if (! function_exists('location_link')) {
|
||||
'class' => 'mr-2 flex-shrink-0',
|
||||
]) . '<span class="truncate">' . esc($location->name) . '</span>',
|
||||
[
|
||||
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' .
|
||||
'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' .
|
||||
($class === '' ? '' : " {$class}"),
|
||||
'target' => '_blank',
|
||||
'rel' => 'noreferrer noopener',
|
||||
|
@ -20,30 +20,30 @@ if (! function_exists('render_page_links')) {
|
||||
{
|
||||
$pages = (new PageModel())->findAll();
|
||||
$links = anchor(route_to('home'), lang('Common.home'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
if ($podcastHandle !== null) {
|
||||
$links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
}
|
||||
|
||||
$links .= anchor(route_to('credits'), lang('Person.credits'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
$links .= anchor(route_to('map'), lang('Page.map.title'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
foreach ($pages as $page) {
|
||||
$links .= anchor($page->link, esc($page->title), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
]);
|
||||
}
|
||||
|
||||
// if set in .env, add legal notice link at the end of page links
|
||||
if (config('App')->legalNoticeURL !== null) {
|
||||
$links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [
|
||||
'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent',
|
||||
'class' => 'px-2 py-1 underline hover:no-underline',
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]);
|
||||
|
@ -32,12 +32,18 @@ class Breadcrumb
|
||||
$uri = '';
|
||||
foreach (current_url(true)->getSegments() as $segment) {
|
||||
$uri .= '/' . $segment;
|
||||
$this->links[] = [
|
||||
$link = [
|
||||
'text' => is_numeric($segment)
|
||||
? $segment
|
||||
: lang('Breadcrumb.' . $segment),
|
||||
'href' => base_url($uri),
|
||||
];
|
||||
|
||||
if (is_numeric($segment)) {
|
||||
$this->links[] = $link;
|
||||
} else {
|
||||
$this->links[$segment] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,20 +52,19 @@ class Breadcrumb
|
||||
*
|
||||
* Given a breadcrumb with numeric params, this function replaces them with the values provided in $newParams
|
||||
*
|
||||
* Example with `Home / podcasts / 1 / episodes / 1`
|
||||
* Example with `Home / podcasts / 1 / episodes / 1 / foo`
|
||||
*
|
||||
* $newParams = [ 0 => 'foo', 1 => 'bar' ] replaceParams($newParams);
|
||||
* $newParams = [ 0 => 'bar', 1 => 'baz', 'foo' => 'I Pity The Foo' ] replaceParams($newParams);
|
||||
*
|
||||
* The breadcrumb is now `Home / podcasts / foo / episodes / bar`
|
||||
* The breadcrumb is now `Home / podcasts / foo / episodes / bar / I Pity The Foo`
|
||||
*
|
||||
* @param string[] $newParams
|
||||
*/
|
||||
public function replaceParams(array $newParams): void
|
||||
{
|
||||
foreach ($this->links as $key => $link) {
|
||||
if (is_numeric($link['text'])) {
|
||||
$this->links[$key]['text'] = $newParams[0];
|
||||
array_shift($newParams);
|
||||
foreach ($newParams as $key => $newValue) {
|
||||
if (array_key_exists($key, $this->links)) {
|
||||
$this->links[$key]['text'] = $newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,26 +4,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace ViewComponents;
|
||||
|
||||
class Component implements ComponentInterface
|
||||
abstract class Component implements ComponentInterface
|
||||
{
|
||||
protected string $slot = '';
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected array $props = [];
|
||||
|
||||
protected string $class = '';
|
||||
/**
|
||||
* @var array<string, string|'boolean'|'array'|'number'>
|
||||
*/
|
||||
protected array $casts = [];
|
||||
|
||||
protected ?string $slot = null;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $attributes = [
|
||||
'class' => '',
|
||||
];
|
||||
protected array $attributes = [];
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
helper('viewcomponents');
|
||||
|
||||
// overwrite default attributes if set
|
||||
$this->attributes = [...$this->attributes, ...$attributes];
|
||||
|
||||
@ -42,9 +46,39 @@ class Component implements ComponentInterface
|
||||
if (is_callable([$this, $method])) {
|
||||
$this->{$method}($value);
|
||||
} else {
|
||||
if (array_key_exists($name, $this->casts)) {
|
||||
$value = match ($this->casts[$name]) {
|
||||
'boolean' => $value === 'true',
|
||||
'number' => (int) $value,
|
||||
'array' => json_decode(htmlspecialchars_decode($value), true),
|
||||
default => $value
|
||||
};
|
||||
}
|
||||
|
||||
$this->{$name} = $value;
|
||||
}
|
||||
|
||||
// remove from attributes
|
||||
if (in_array($name, $this->props, true)) {
|
||||
unset($this->attributes[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
}
|
||||
|
||||
public function mergeClass(string $class): void
|
||||
{
|
||||
if (! array_key_exists('class', $this->attributes)) {
|
||||
$this->attributes['class'] = $class;
|
||||
} else {
|
||||
$this->attributes['class'] .= ' ' . $class;
|
||||
}
|
||||
}
|
||||
|
||||
public function getStringifiedAttributes(): string
|
||||
{
|
||||
return stringify_attributes($this->attributes);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
|
@ -43,38 +43,38 @@ class ComponentRenderer
|
||||
private function renderSelfClosingTags(string $output): string
|
||||
{
|
||||
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
|
||||
// Should match any Component tags <Component />
|
||||
// Should match any Component tags <x-Component />
|
||||
$pattern = "/
|
||||
<
|
||||
\s*
|
||||
(?<name>[A-Z][A-Za-z0-9\.]*?)
|
||||
\s*
|
||||
\\s*
|
||||
x[-\\:](?<name>[\\w\\-\\:\\.]*)
|
||||
\\s*
|
||||
(?<attributes>
|
||||
(?:
|
||||
\s+
|
||||
\\s+
|
||||
(?:
|
||||
(?:
|
||||
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
|
||||
\\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
|
||||
)
|
||||
|
|
||||
(?:
|
||||
[\w\-:.@]+
|
||||
[\\w\\-:.@]+
|
||||
(
|
||||
=
|
||||
(?:
|
||||
\\\"[^\\\"]*\\\"
|
||||
|
|
||||
\'[^\']*\'
|
||||
\\'[^\\']*\\'
|
||||
|
|
||||
[^\'\\\"=<>]+
|
||||
[^\\'\\\"=<>]+
|
||||
)
|
||||
)?
|
||||
)
|
||||
)
|
||||
)*
|
||||
\s*
|
||||
\\s*
|
||||
)
|
||||
\/>
|
||||
\\/>
|
||||
/x";
|
||||
|
||||
/*
|
||||
@ -96,8 +96,9 @@ class ComponentRenderer
|
||||
|
||||
private function renderPairedTags(string $output): string
|
||||
{
|
||||
$pattern = '/<\s*(?<name>[A-Z][A-Za-z0-9\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*\1\s*>/uUsm';
|
||||
ini_set('pcre.backtrack_limit', '-1');
|
||||
// ini_set('pcre.backtrack_limit', '-1');
|
||||
$pattern = '/<\s*x[-\:](?<name>[\w\-\:\.]*?)(?<attributes>(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?<slot>.*)<\/\s*x-\1\s*>/uiUsm';
|
||||
|
||||
/*
|
||||
$matches[0] = full tags matched and all of its content
|
||||
$matches[name] = pascal cased tag name
|
||||
@ -167,8 +168,6 @@ class ComponentRenderer
|
||||
(
|
||||
\"[^\"]+\"
|
||||
|
|
||||
\'[^\']+\'
|
||||
|
|
||||
\\\'[^\\\']+\\\'
|
||||
|
|
||||
[^\s>]+
|
||||
|
@ -1,33 +0,0 @@
|
||||
<?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(mixed $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, ',');
|
||||
}
|
||||
}
|
@ -15,10 +15,6 @@
|
||||
&:hover {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply ring-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
|
@ -9,10 +9,6 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.choices:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.choices:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -327,10 +323,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.choices__button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.choices__input {
|
||||
@apply mb-1 align-middle bg-elevated;
|
||||
|
||||
|
@ -5,6 +5,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ring-accent {
|
||||
@apply outline-none ring-2 ring-offset-2;
|
||||
|
||||
/* FIXME: why doesn't ring-accent-base work? */
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity));
|
||||
--tw-ring-offset-color: hsl(var(--color-background-base));
|
||||
}
|
||||
|
||||
.rounded-conditional-b-xl {
|
||||
border-bottom-right-radius: max(
|
||||
0px,
|
||||
|
@ -34,7 +34,7 @@ Read more component (basic unstyled component)
|
||||
|
||||
/* Don't forget focus and hover styles for accessibility! */
|
||||
.read-more__checkbox:focus ~ .read-more__label {
|
||||
@apply ring;
|
||||
@apply ring-accent;
|
||||
}
|
||||
|
||||
.read-more__checkbox:hover ~ .read-more__label {
|
||||
|
@ -8,9 +8,15 @@ use ViewComponents\Component;
|
||||
|
||||
class Alert extends Component
|
||||
{
|
||||
protected ?string $glyph = null;
|
||||
protected array $props = ['glyph', 'title'];
|
||||
|
||||
protected ?string $title = null;
|
||||
protected string $glyph = '';
|
||||
|
||||
protected ?string $title = '';
|
||||
|
||||
protected array $attributes = [
|
||||
'role' => 'alert',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var 'default'|'success'|'danger'|'warning'
|
||||
@ -19,7 +25,7 @@ class Alert extends Component
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$variants = [
|
||||
$variantData = match ($this->variant) {
|
||||
'success' => [
|
||||
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'glyph' => 'check-fill', // @icon('check-fill')
|
||||
@ -32,30 +38,21 @@ class Alert extends Component
|
||||
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
'glyph' => 'alert-fill', // @icon('alert-fill')
|
||||
],
|
||||
'default' => [
|
||||
default => [
|
||||
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
|
||||
'glyph' => 'error-warning-fill', // @icon('error-warning-fill')
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
if (! array_key_exists($this->variant, $variants)) {
|
||||
$this->variant = 'default';
|
||||
}
|
||||
|
||||
$glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [
|
||||
$glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [
|
||||
'class' => 'flex-shrink-0 mr-2 text-lg',
|
||||
]);
|
||||
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
||||
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
unset($this->attributes['variant']);
|
||||
unset($this->attributes['class']);
|
||||
unset($this->attributes['glyph']);
|
||||
$attributes = stringify_attributes($this->attributes);
|
||||
$title = $this->title === '' ? '' : '<div class="font-semibold">' . $this->title . '</div>';
|
||||
$this->mergeClass('inline-flex w-full p-2 text-sm border rounded ');
|
||||
$this->mergeClass($variantData['class']);
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$class}" role="alert" {$attributes}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
|
||||
<div {$this->getStringifiedAttributes()}>{$glyph}<div>{$title}<p>{$this->slot}</p></div></div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,20 @@ use ViewComponents\Component;
|
||||
|
||||
class Button extends Component
|
||||
{
|
||||
protected array $props = ['uri', 'variant', 'size', 'iconLeft', 'iconRight', 'isSquared', 'isExternal'];
|
||||
|
||||
protected array $casts = [
|
||||
'isSquared' => 'boolean',
|
||||
'isExternal' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $uri = '';
|
||||
|
||||
protected string $variant = 'default';
|
||||
|
||||
/**
|
||||
* @var 'small'|'base'|'large'
|
||||
*/
|
||||
protected string $size = 'base';
|
||||
|
||||
protected string $iconLeft = '';
|
||||
@ -20,65 +30,54 @@ class Button extends Component
|
||||
|
||||
protected bool $isSquared = false;
|
||||
|
||||
public function setIsSquared(string $value): void
|
||||
{
|
||||
$this->isSquared = $value === 'true';
|
||||
}
|
||||
protected bool $isExternal = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$baseClass =
|
||||
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full';
|
||||
$this->mergeClass('gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full');
|
||||
|
||||
$variantClass = [
|
||||
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
|
||||
$variantClass = match ($this->variant) {
|
||||
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
|
||||
'secondary' => 'shadow-sm ring-2 ring-accent ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
|
||||
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
|
||||
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
|
||||
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
|
||||
'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
|
||||
'success' => 'shadow-sm ring-2 ring-pine-700 ring-inset text-pine-700 hover:ring-pine-800 hover:text-pine-800',
|
||||
'danger' => 'shadow-sm ring-2 ring-red-700 ring-inset text-red-700 hover:ring-red-800 hover:text-red-800',
|
||||
'warning' => 'shadow-sm ring-2 ring-yellow-700 ring-inset text-yellow-700 hover:ring-yellow-800 hover:text-yellow-800',
|
||||
'info' => 'shadow-sm ring-2 ring-blue-700 ring-inset text-blue-700 hover:ring-blue-800 hover:text-blue-800',
|
||||
'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
|
||||
];
|
||||
default => 'shadow-sm text-black bg-gray-100 hover:bg-gray-300',
|
||||
};
|
||||
|
||||
$sizeClass = [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'text-xs leading-6',
|
||||
'base' => 'text-sm leading-5',
|
||||
'large' => 'text-base leading-6',
|
||||
];
|
||||
default => 'text-sm leading-5',
|
||||
};
|
||||
|
||||
$iconSize = [
|
||||
$iconSizeClass = match ($this->size) {
|
||||
'small' => 'text-sm',
|
||||
'base' => 'text-lg',
|
||||
'large' => 'text-2xl',
|
||||
];
|
||||
default => 'text-lg',
|
||||
};
|
||||
|
||||
$basePaddings = [
|
||||
$basePaddings = match ($this->size) {
|
||||
'small' => 'px-3 py-1',
|
||||
'base' => 'px-3 py-2',
|
||||
'large' => 'px-4 py-2',
|
||||
];
|
||||
default => 'px-3 py-2',
|
||||
};
|
||||
|
||||
$squaredPaddings = [
|
||||
$squaredPaddings = match ($this->size) {
|
||||
'small' => 'p-1',
|
||||
'base' => 'p-2',
|
||||
'large' => 'p-3',
|
||||
];
|
||||
default => 'p-2',
|
||||
};
|
||||
|
||||
$buttonClass =
|
||||
$baseClass .
|
||||
' ' .
|
||||
($this->isSquared
|
||||
? $squaredPaddings[$this->size]
|
||||
: $basePaddings[$this->size]) .
|
||||
' ' .
|
||||
$sizeClass[$this->size] .
|
||||
' ' .
|
||||
$variantClass[$this->variant];
|
||||
$this->mergeClass($variantClass);
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
if (array_key_exists('class', $this->attributes)) {
|
||||
$buttonClass .= ' ' . $this->attributes['class'];
|
||||
unset($this->attributes['class']);
|
||||
if ($this->isSquared) {
|
||||
$this->mergeClass($squaredPaddings);
|
||||
} else {
|
||||
$this->mergeClass($basePaddings);
|
||||
}
|
||||
|
||||
if ($this->iconLeft !== '' || $this->iconRight !== '') {
|
||||
@ -87,41 +86,30 @@ class Button extends Component
|
||||
|
||||
if ($this->iconLeft !== '') {
|
||||
$this->slot = icon($this->iconLeft, [
|
||||
'class' => 'opacity-75 ' . $iconSize[$this->size],
|
||||
'class' => 'opacity-75 ' . $iconSizeClass,
|
||||
]) . $this->slot;
|
||||
}
|
||||
|
||||
if ($this->iconRight !== '') {
|
||||
$this->slot .= icon($this->iconRight, [
|
||||
'class' => 'opacity-75 ' . $iconSize[$this->size],
|
||||
'class' => 'opacity-75 ' . $iconSizeClass,
|
||||
]);
|
||||
}
|
||||
|
||||
unset($this->attributes['slot']);
|
||||
unset($this->attributes['variant']);
|
||||
unset($this->attributes['size']);
|
||||
unset($this->attributes['iconLeft']);
|
||||
unset($this->attributes['iconRight']);
|
||||
unset($this->attributes['isSquared']);
|
||||
unset($this->attributes['uri']);
|
||||
unset($this->attributes['label']);
|
||||
|
||||
if ($this->uri !== '') {
|
||||
$tagName = 'a';
|
||||
$defaultButtonAttributes = [
|
||||
'href' => $this->uri,
|
||||
];
|
||||
$this->attributes['href'] = $this->uri;
|
||||
if ($this->isExternal) {
|
||||
$this->attributes['target'] = '_blank';
|
||||
$this->attributes['rel'] = 'noopener noreferrer';
|
||||
}
|
||||
} else {
|
||||
$tagName = 'button';
|
||||
$defaultButtonAttributes = [
|
||||
'type' => 'button',
|
||||
];
|
||||
$this->attributes['type'] ??= 'button';
|
||||
}
|
||||
|
||||
$attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes));
|
||||
|
||||
return <<<HTML
|
||||
<{$tagName} class="{$buttonClass}" {$attributes}>{$this->slot}</{$tagName}>
|
||||
<{$tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$tagName}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ use ViewComponents\Component;
|
||||
|
||||
class ChartsComponent extends Component
|
||||
{
|
||||
protected string $title = '';
|
||||
protected string $title;
|
||||
|
||||
protected string $subtitle = '';
|
||||
|
||||
protected string $dataUrl = '';
|
||||
protected string $dataUrl;
|
||||
|
||||
protected string $type = '';
|
||||
protected string $type;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
@ -23,8 +23,10 @@ class ChartsComponent extends Component
|
||||
$subtitleBlock = '<p class="px-6 -mt-4 text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
}
|
||||
|
||||
$this->mergeClass('bg-elevated border-3 rounded-xl border-subtle');
|
||||
|
||||
return <<<HTML
|
||||
<div class="bg-elevated border-3 rounded-xl border-subtle {$this->class}">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<h2 class="px-6 py-4 text-xl">{$this->title}</h2>
|
||||
{$subtitleBlock}
|
||||
<div class="w-full h-[500px]" data-chart-type="{$this->type}" data-chart-url="{$this->dataUrl}"></div>
|
||||
|
@ -8,7 +8,9 @@ use ViewComponents\Component;
|
||||
|
||||
class DashboardCard extends Component
|
||||
{
|
||||
protected ?string $href = null;
|
||||
protected array $props = ['href', 'glyph', 'title', 'subtitle'];
|
||||
|
||||
protected string $href = '';
|
||||
|
||||
protected string $glyph;
|
||||
|
||||
@ -27,11 +29,11 @@ class DashboardCard extends Component
|
||||
'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base',
|
||||
]);
|
||||
|
||||
if ($this->href !== null && $this->href !== '') {
|
||||
if ($this->href !== '') {
|
||||
$chevronRight = icon('arrow-right-s-fill');
|
||||
$viewLang = lang('Common.view');
|
||||
return <<<HTML
|
||||
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated focus:ring-accent rounded-xl border-3 border-subtle group">
|
||||
<a href="{$this->href}" class="flex items-center justify-between w-full gap-4 p-4 lg:max-w-sm lg:flex-col xl:flex-row bg-elevated rounded-xl border-3 border-subtle group">
|
||||
<div class="flex items-start">{$glyph}<div class="flex flex-col ml-2"><div class="flex items-center"><span class="text-xs font-semibold leading-loose tracking-wider uppercase">{$this->title}</span><div class="inline-flex items-center ml-4 transition -translate-x-full group-hover:translate-x-0 group-focus:translate-x-0"><span class="-ml-2 text-xs lowercase transition opacity-0 group-hover:opacity-100 group-focus:opacity-100">{$viewLang}</span>{$chevronRight}</div></div><p class="text-xs">{$this->subtitle}</p></div></div>
|
||||
<div class="text-5xl font-bold">{$this->slot}</div>
|
||||
</a>
|
||||
|
@ -9,17 +9,25 @@ use ViewComponents\Component;
|
||||
|
||||
class DropdownMenu extends Component
|
||||
{
|
||||
public string $id = '';
|
||||
protected array $props = ['id', 'labelledby', 'placement', 'offsetX', 'offsetY', 'items'];
|
||||
|
||||
public string $labelledby;
|
||||
protected array $casts = [
|
||||
'offsetX' => 'number',
|
||||
'offsetY' => 'number',
|
||||
'items' => 'array',
|
||||
];
|
||||
|
||||
public string $placement = 'bottom-end';
|
||||
protected string $id;
|
||||
|
||||
public string $offsetX = '0';
|
||||
protected string $labelledby;
|
||||
|
||||
public string $offsetY = '0';
|
||||
protected string $placement = 'bottom-end';
|
||||
|
||||
public array $items = [];
|
||||
protected int $offsetX = 0;
|
||||
|
||||
protected int $offsetY = 0;
|
||||
|
||||
protected array $items = [];
|
||||
|
||||
public function setItems(string $value): void
|
||||
{
|
||||
@ -37,7 +45,7 @@ class DropdownMenu extends Component
|
||||
switch ($item['type']) {
|
||||
case 'link':
|
||||
$menuItems .= anchor($item['uri'], $item['title'], [
|
||||
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''),
|
||||
]);
|
||||
break;
|
||||
case 'html':
|
||||
@ -51,14 +59,16 @@ class DropdownMenu extends Component
|
||||
}
|
||||
}
|
||||
|
||||
$this->mergeClass('absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3');
|
||||
$this->attributes['id'] = $this->id;
|
||||
$this->attributes['aria-labelledby'] = $this->labelledby;
|
||||
$this->attributes['data-dropdown'] = 'menu';
|
||||
$this->attributes['data-dropdown-placement'] = $this->placement;
|
||||
$this->attributes['data-dropdown-offset-x'] = $this->offsetX;
|
||||
$this->attributes['data-dropdown-offset-y'] = $this->offsetY;
|
||||
|
||||
return <<<HTML
|
||||
<nav id="{$this->id}"
|
||||
class="absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3"
|
||||
aria-labelledby="{$this->labelledby}"
|
||||
data-dropdown="menu"
|
||||
data-dropdown-placement="{$this->placement}"
|
||||
data-dropdown-offset-x="{$this->offsetX}"
|
||||
data-dropdown-offset-y="{$this->offsetY}">{$menuItems}</nav>
|
||||
<nav {$this->getStringifiedAttributes()}>{$menuItems}</nav>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -4,35 +4,41 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
|
||||
class Checkbox extends FormComponent
|
||||
{
|
||||
protected ?string $hint = null;
|
||||
protected array $props = ['hint', 'isChecked'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$attributes = [
|
||||
'id' => $this->value,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 focus:ring-accent w-6 h-6',
|
||||
];
|
||||
|
||||
$checkboxInput = form_checkbox(
|
||||
$attributes,
|
||||
[
|
||||
'id' => $this->value,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6',
|
||||
],
|
||||
'yes',
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
);
|
||||
|
||||
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
$this->mergeClass('inline-flex items-center');
|
||||
|
||||
return <<<HTML
|
||||
<label class="inline-flex items-center {$this->class}">{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
|
||||
<label {$this->getStringifiedAttributes()}>{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,14 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class ColorRadioButton extends FormComponent
|
||||
{
|
||||
protected array $props = ['isChecked'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
protected string $style = '';
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$data = [
|
||||
@ -23,7 +22,7 @@ class ColorRadioButton extends FormComponent
|
||||
'class' => 'color-radio-btn',
|
||||
];
|
||||
|
||||
if ($this->required) {
|
||||
if ($this->isRequired) {
|
||||
$data['required'] = 'required';
|
||||
}
|
||||
|
||||
@ -34,7 +33,7 @@ class ColorRadioButton extends FormComponent
|
||||
);
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$this->class}" style="{$this->style}">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
{$radioInput}
|
||||
<label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
|
||||
</div>
|
||||
|
@ -6,21 +6,28 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class DatetimePicker extends FormComponent
|
||||
{
|
||||
protected array $attributes = [
|
||||
'data-picker' => 'datetime',
|
||||
];
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['class'] = 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0';
|
||||
$this->attributes['data-input'] = '';
|
||||
$dateInput = form_input($this->attributes, old($this->name, $this->value));
|
||||
$dateInput = form_input([
|
||||
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
|
||||
'data-input' => '',
|
||||
], old($this->name, $this->value));
|
||||
|
||||
$clearLabel = lang(
|
||||
'Episode.publish_form.scheduled_publication_date_clear',
|
||||
);
|
||||
$closeIcon = icon('close-fill');
|
||||
|
||||
$this->mergeClass('flex border-3 rounded-lg border-contrast focus-within:ring-accent');
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex border-3 rounded-lg border-contrast focus-within:ring-accent {$this->class}" data-picker="datetime">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
{$dateInput}
|
||||
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset focus:ring-accent" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
|
||||
<button class="p-3 bg-elevated hover:bg-base rounded-r-md focus:ring-inset" type="button" aria-label="{$clearLabel}" title="{$clearLabel}" data-clear="">
|
||||
{$closeIcon}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -4,49 +4,76 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class Field extends FormComponent
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Field extends Component
|
||||
{
|
||||
protected array $props = [
|
||||
'name',
|
||||
'label',
|
||||
'isRequired',
|
||||
'isReadonly',
|
||||
'as',
|
||||
'helper',
|
||||
'hint',
|
||||
];
|
||||
|
||||
protected array $casts = [
|
||||
'isRequired' => 'boolean',
|
||||
'isReadonly' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $name;
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected bool $isRequired = false;
|
||||
|
||||
protected bool $isReadonly = false;
|
||||
|
||||
protected string $as = 'Input';
|
||||
|
||||
protected string $label = '';
|
||||
protected string $helper = '';
|
||||
|
||||
protected ?string $helper = null;
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected string $hint = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$helperText = '';
|
||||
if ($this->helper !== null) {
|
||||
$helperId = $this->id . 'Help';
|
||||
$helperText = '<Forms.Helper id="' . $helperId . '">' . $this->helper . '</Forms.Helper>';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = (new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
]))->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
$labelAttributes = [
|
||||
'for' => $this->id,
|
||||
'isOptional' => $this->required ? 'false' : 'true',
|
||||
'for' => $this->name,
|
||||
'isOptional' => $this->isRequired ? 'false' : 'true',
|
||||
'class' => '-mb-1',
|
||||
'slot' => $this->label,
|
||||
];
|
||||
if ($this->hint) {
|
||||
if ($this->hint !== '') {
|
||||
$labelAttributes['hint'] = $this->hint;
|
||||
}
|
||||
$labelAttributes = stringify_attributes($labelAttributes);
|
||||
$label = new Label($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['helper']);
|
||||
unset($fieldComponentAttributes['hint']);
|
||||
$this->mergeClass('flex flex-col');
|
||||
$fieldClass = $this->attributes['class'];
|
||||
|
||||
unset($this->attributes['class']);
|
||||
|
||||
$this->attributes['name'] = $this->name;
|
||||
$this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false';
|
||||
$this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false';
|
||||
$element = __NAMESPACE__ . '\\' . $this->as;
|
||||
$fieldElement = new $element($fieldComponentAttributes);
|
||||
$fieldElement = new $element($this->attributes);
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex flex-col {$this->class}">
|
||||
<Forms.Label {$labelAttributes}>{$this->label}</Forms.Label>
|
||||
<div class="{$fieldClass}">
|
||||
{$label->render()}
|
||||
{$helperText}
|
||||
<div class="w-full mt-1">
|
||||
{$fieldElement->render()}
|
||||
|
@ -6,28 +6,55 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
use ViewComponents\Component;
|
||||
|
||||
class FormComponent extends Component
|
||||
abstract class FormComponent extends Component
|
||||
{
|
||||
protected ?string $id = null;
|
||||
protected array $props = [
|
||||
'id',
|
||||
'name',
|
||||
'value',
|
||||
'isRequired',
|
||||
'isReadonly',
|
||||
];
|
||||
|
||||
protected string $name = '';
|
||||
protected array $casts = [
|
||||
'isRequired' => 'boolean',
|
||||
'isReadonly' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $id;
|
||||
|
||||
protected string $name;
|
||||
|
||||
protected string $value = '';
|
||||
|
||||
protected bool $required = false;
|
||||
protected bool $isRequired = false;
|
||||
|
||||
protected bool $readonly = false;
|
||||
protected bool $isReadonly = false;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
$parentVars = get_class_vars(self::class);
|
||||
$this->casts = [...$parentVars['casts'], ...$this->casts];
|
||||
$this->props = [...$parentVars['props'], $this->props];
|
||||
|
||||
parent::__construct($attributes);
|
||||
|
||||
if ($this->id === null) {
|
||||
if (! isset($this->id)) {
|
||||
$this->id = $this->name;
|
||||
$this->attributes['id'] = $this->id;
|
||||
}
|
||||
|
||||
$this->attributes['id'] = $this->id;
|
||||
$this->attributes['name'] = $this->name;
|
||||
|
||||
if ($this->isRequired) {
|
||||
$this->attributes['required'] = 'required';
|
||||
}
|
||||
|
||||
if ($this->isReadonly) {
|
||||
$this->attributes['readonly'] = 'readonly';
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,23 +62,4 @@ class FormComponent extends Component
|
||||
{
|
||||
$this->value = htmlspecialchars_decode($value, ENT_QUOTES);
|
||||
}
|
||||
|
||||
public function setRequired(string $value): void
|
||||
{
|
||||
$this->required = $value === 'true';
|
||||
unset($this->attributes['required']);
|
||||
if ($this->required) {
|
||||
$this->attributes['required'] = 'required';
|
||||
}
|
||||
}
|
||||
|
||||
public function setReadonly(string $value): void
|
||||
{
|
||||
$this->readonly = $value === 'true';
|
||||
if ($this->readonly) {
|
||||
$this->attributes['readonly'] = 'readonly';
|
||||
} else {
|
||||
unset($this->attributes['readonly']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class Helper extends FormComponent
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Helper extends Component
|
||||
{
|
||||
/**
|
||||
* @var 'default'|'error'
|
||||
*/
|
||||
protected string $type = 'default';
|
||||
// TODO: add type with error and show errors inline
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$class = 'text-skin-muted';
|
||||
$this->mergeClass('text-skin-muted');
|
||||
|
||||
return <<<HTML
|
||||
<small id="{$this->id}" class="{$class} {$this->class}">{$this->slot}</small>
|
||||
<small {$this->getStringifiedAttributes()}>{$this->slot}</small>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -6,26 +6,29 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class Input extends FormComponent
|
||||
{
|
||||
protected array $props = ['type'];
|
||||
|
||||
protected string $type = 'text';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$baseClass = 'w-full border-contrast rounded-lg focus:border-contrast border-3 focus:ring-accent focus-within:ring-accent ' . $this->class;
|
||||
|
||||
$this->attributes['class'] = $baseClass;
|
||||
$this->mergeClass('w-full border-contrast rounded-lg focus:border-contrast border-3 focus-within:ring-accent');
|
||||
|
||||
if ($this->type === 'file') {
|
||||
$this->attributes['class'] .= ' file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer';
|
||||
$this->mergeClass('file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer');
|
||||
} else {
|
||||
$this->attributes['class'] .= ' px-3 py-2';
|
||||
$this->mergeClass('px-3 py-2');
|
||||
}
|
||||
|
||||
if ($this->readonly) {
|
||||
$this->attributes['class'] .= ' bg-base';
|
||||
if ($this->isReadonly) {
|
||||
$this->mergeClass('bg-base');
|
||||
} else {
|
||||
$this->attributes['class'] .= ' bg-elevated';
|
||||
$this->mergeClass('bg-elevated');
|
||||
}
|
||||
|
||||
$this->attributes['type'] = $this->type;
|
||||
$this->attributes['value'] = $this->value;
|
||||
|
||||
return form_input($this->attributes, old($this->name, $this->value));
|
||||
}
|
||||
}
|
||||
|
@ -4,39 +4,38 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Label extends Component
|
||||
{
|
||||
protected ?string $for = null;
|
||||
protected array $props = ['for', 'hint', 'isOptional'];
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected array $casts = [
|
||||
'isOptional' => 'boolean',
|
||||
];
|
||||
|
||||
protected string $for;
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $isOptional = false;
|
||||
|
||||
public function setIsOptional(string $value): void
|
||||
{
|
||||
$this->isOptional = $value === 'true';
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$labelClass = 'text-sm font-semibold ' . $this->attributes['class'];
|
||||
unset($this->attributes['class']);
|
||||
$this->mergeClass('text-sm font-semibold');
|
||||
|
||||
$optionalText = $this->isOptional ? '<small class="ml-1 font-normal lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' : '';
|
||||
$hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
|
||||
unset($this->attributes['isOptional']);
|
||||
unset($this->attributes['hint']);
|
||||
unset($this->attributes['slot']);
|
||||
|
||||
$attributes = stringify_attributes($this->attributes);
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
return <<<HTML
|
||||
<label class="{$labelClass}" {$attributes}>{$this->slot}{$optionalText}{$hint}</label>
|
||||
<label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class MarkdownEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['disallowList'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
@ -18,18 +20,20 @@ class MarkdownEditor extends FormComponent
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$editorClass = 'w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent ' . $this->class;
|
||||
$this->mergeClass('w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent');
|
||||
$wrapperClass = $this->attributes['class'];
|
||||
|
||||
$this->attributes['class'] = 'bg-elevated border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
|
||||
$this->attributes['rows'] = 6;
|
||||
|
||||
$textarea = form_textarea($this->attributes, old($this->name, $this->value));
|
||||
$markdownIcon = icon(
|
||||
'markdown-fill',
|
||||
[
|
||||
'class' => 'mr-1 text-lg opacity-40',
|
||||
]
|
||||
$textarea = form_textarea(
|
||||
$this->attributes,
|
||||
old($this->name, $this->value)
|
||||
);
|
||||
$markdownIcon = icon('markdown-fill', [
|
||||
'class' => 'mr-1 text-lg opacity-40',
|
||||
]);
|
||||
|
||||
$translations = [
|
||||
'write' => lang('Common.forms.editor.write'),
|
||||
'preview' => lang('Common.forms.editor.preview'),
|
||||
@ -85,19 +89,19 @@ class MarkdownEditor extends FormComponent
|
||||
$toolbarContent .= '<div class="inline-flex text-2xl gap-x-1">';
|
||||
foreach ($buttonsGroup as $button) {
|
||||
if (! in_array($button['name'], $this->disallowList, true)) {
|
||||
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
|
||||
$toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
|
||||
}
|
||||
}
|
||||
$toolbarContent .= '</div>';
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$editorClass}">
|
||||
<div class="{$wrapperClass}">
|
||||
<header class="px-2">
|
||||
<div class="sticky top-0 z-20 flex flex-wrap justify-between border-b border-gray-300 bg-elevated">
|
||||
<markdown-write-preview for="{$this->id}" class="relative inline-flex h-8">
|
||||
<button type="button" slot="write" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['write']}</button>
|
||||
<button type="button" slot="preview" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['preview']}</button>
|
||||
<button type="button" slot="write" class="px-2 font-semibold">{$translations['write']}</button>
|
||||
<button type="button" slot="preview" class="px-2 font-semibold">{$translations['preview']}</button>
|
||||
</markdown-write-preview>
|
||||
<markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">{$toolbarContent}</markdown-toolbar>
|
||||
</div>
|
||||
|
@ -6,6 +6,13 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class MultiSelect extends FormComponent
|
||||
{
|
||||
protected array $props = ['options', 'selected'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
'selected' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
@ -16,18 +23,10 @@ class MultiSelect extends FormComponent
|
||||
*/
|
||||
protected array $selected = [];
|
||||
|
||||
public function setOptions(string $value): void
|
||||
{
|
||||
$this->options = json_decode(htmlspecialchars_decode($value), true);
|
||||
}
|
||||
|
||||
public function setSelected(string $selected): void
|
||||
{
|
||||
$this->selected = json_decode(htmlspecialchars_decode($selected), true);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg');
|
||||
|
||||
$defaultAttributes = [
|
||||
'data-class' => $this->attributes['class'],
|
||||
'multiple' => 'multiple',
|
||||
@ -37,8 +36,7 @@ class MultiSelect extends FormComponent
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
$this->attributes['class'] .= ' w-full bg-elevated border-3 border-contrast rounded-lg';
|
||||
$extra = array_merge($defaultAttributes, $this->attributes);
|
||||
$extra = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
return form_dropdown($this->name, $this->options, $this->selected, $extra);
|
||||
}
|
||||
|
@ -6,12 +6,13 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class Radio extends FormComponent
|
||||
{
|
||||
protected bool $isChecked = false;
|
||||
protected array $props = ['isChecked'];
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
@ -19,14 +20,16 @@ class Radio extends FormComponent
|
||||
[
|
||||
'id' => $this->value,
|
||||
'name' => $this->name,
|
||||
'class' => 'text-accent-base bg-elevated border-contrast border-3 focus:ring-accent w-6 h-6',
|
||||
'class' => 'text-accent-base bg-elevated border-contrast border-3 w-6 h-6',
|
||||
],
|
||||
$this->value,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
);
|
||||
|
||||
$this->mergeClass('inline-flex items-center');
|
||||
|
||||
return <<<HTML
|
||||
<label class="inline-flex items-center {$this->class}">{$radioInput}<span class="ml-2">{$this->slot}</span></label>
|
||||
<label {$this->getStringifiedAttributes()}>{$radioInput}<span class="ml-2">{$this->slot}</span></label>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
|
||||
class RadioButton extends FormComponent
|
||||
{
|
||||
protected array $props = ['isChecked', 'hint'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
protected ?string $hint = null;
|
||||
|
||||
public function setIsChecked(string $value): void
|
||||
{
|
||||
$this->isChecked = $value === 'true';
|
||||
}
|
||||
protected string $hint = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
@ -23,7 +26,7 @@ class RadioButton extends FormComponent
|
||||
'class' => 'form-radio-btn bg-elevated',
|
||||
];
|
||||
|
||||
if ($this->required) {
|
||||
if ($this->isRequired) {
|
||||
$data['required'] = 'required';
|
||||
}
|
||||
|
||||
@ -33,10 +36,13 @@ class RadioButton extends FormComponent
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
);
|
||||
|
||||
$hint = $this->hint ? hint_tooltip($this->hint, 'ml-1 text-base') : '';
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1 text-base',
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
return <<<HTML
|
||||
<div class="{$this->class}">
|
||||
<div {$this->getStringifiedAttributes()}">
|
||||
{$radioInput}
|
||||
<label for="{$this->value}">{$this->slot}{$hint}</label>
|
||||
</div>
|
||||
|
@ -8,17 +8,21 @@ use ViewComponents\Component;
|
||||
|
||||
class Section extends Component
|
||||
{
|
||||
protected string $title = '';
|
||||
protected array $props = ['title', 'subtitle'];
|
||||
|
||||
protected ?string $subtitle = null;
|
||||
protected string $title;
|
||||
|
||||
protected string $subtitle = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$subtitle = $this->subtitle === null ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
$subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
|
||||
$this->mergeClass('w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
|
||||
|
||||
return <<<HTML
|
||||
<fieldset class="w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl {$this->class}">
|
||||
<Heading tagName="legend" class="float-left">{$this->title}</Heading>
|
||||
<fieldset {$this->getStringifiedAttributes()}>
|
||||
<x-Heading tagName="legend" class="float-left">{$this->title}</x-Heading>
|
||||
{$subtitle}
|
||||
<div class="flex flex-col w-0 min-w-full gap-4 py-4">{$this->slot}</div>
|
||||
</fieldset>
|
||||
|
@ -6,6 +6,12 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class Select extends FormComponent
|
||||
{
|
||||
protected array $props = ['options', 'selected'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
@ -13,26 +19,17 @@ class Select extends FormComponent
|
||||
|
||||
protected string $selected = '';
|
||||
|
||||
public function setOptions(string $value): void
|
||||
{
|
||||
$this->options = json_decode(htmlspecialchars_decode($value), true);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full focus:border-contrast border-3 rounded-lg bg-elevated border-contrast');
|
||||
$defaultAttributes = [
|
||||
'class' => 'w-full focus:border-contrast focus:ring-accent border-3 rounded-lg bg-elevated border-contrast ' . $this->class,
|
||||
'data-class' => $this->class,
|
||||
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
|
||||
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
|
||||
'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'),
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
unset($this->attributes['name']);
|
||||
unset($this->attributes['options']);
|
||||
unset($this->attributes['selected']);
|
||||
$extra = [...$this->attributes, ...$defaultAttributes];
|
||||
$extra = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra);
|
||||
}
|
||||
|
@ -15,9 +15,9 @@ class Textarea extends FormComponent
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
unset($this->attributes['value']);
|
||||
$this->mergeClass('bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent');
|
||||
|
||||
$this->attributes['class'] = 'bg-elevated w-full focus:border-contrast focus:ring-accent rounded-lg border-3 border-contrast ' . $this->class;
|
||||
$this->attributes['id'] = $this->id;
|
||||
|
||||
$textarea = form_textarea(
|
||||
$this->attributes,
|
||||
|
@ -4,45 +4,48 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
|
||||
class Toggler extends FormComponent
|
||||
{
|
||||
protected array $props = ['size', 'hint', 'isChecked'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var 'base'|'small
|
||||
*/
|
||||
protected string $size = 'base';
|
||||
|
||||
protected string $label = '';
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $checked = false;
|
||||
|
||||
public function setChecked(string $value): void
|
||||
{
|
||||
$this->checked = $value === 'true';
|
||||
}
|
||||
protected bool $isChecked = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
unset($this->attributes['checked']);
|
||||
|
||||
$wrapperClass = $this->class;
|
||||
unset($this->attributes['class']);
|
||||
|
||||
$sizeClass = [
|
||||
'base' => 'form-switch-slider',
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'form-switch-slider form-switch-slider--small',
|
||||
];
|
||||
default => 'form-switch-slider',
|
||||
};
|
||||
|
||||
$this->attributes['class'] = 'form-switch';
|
||||
$this->mergeClass('relative justify-between inline-flex items-center gap-x-2');
|
||||
|
||||
$checkbox = form_checkbox([
|
||||
'class' => 'form-switch',
|
||||
], 'yes', old($this->name) === 'yes' ? true : $this->isChecked);
|
||||
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
$checkbox = form_checkbox($this->attributes, $this->value, old($this->name) === 'yes' ? true : $this->checked);
|
||||
$hint = $this->hint === '' ? '' : hint_tooltip($this->hint, 'ml-1');
|
||||
return <<<HTML
|
||||
<label class="relative inline-flex items-center {$wrapperClass}">
|
||||
<label {$this->getStringifiedAttributes()}>
|
||||
<span class="">{$this->slot}{$hint}</span>
|
||||
{$checkbox}
|
||||
<span class="{$sizeClass[$this->size]}"></span>
|
||||
<span class="ml-2">{$this->slot}{$hint}</span>
|
||||
<span class="{$sizeClass}"></span>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class XMLEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['content'];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
|
@ -8,6 +8,8 @@ use ViewComponents\Component;
|
||||
|
||||
class Heading extends Component
|
||||
{
|
||||
protected array $props = ['tagName', 'size'];
|
||||
|
||||
protected string $tagName = 'div';
|
||||
|
||||
/**
|
||||
@ -17,16 +19,17 @@ class Heading extends Component
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$sizeClasses = [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'tracking-wide text-base',
|
||||
'base' => 'text-xl',
|
||||
'large' => 'text-3xl',
|
||||
];
|
||||
default => 'text-xl',
|
||||
};
|
||||
|
||||
$class = $this->class . ' relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] ' . $sizeClasses[$this->size];
|
||||
$this->mergeClass('relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10]');
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
return <<<HTML
|
||||
<{$this->tagName} class="{$class}">{$this->slot}</{$this->tagName}>
|
||||
<{$this->tagName} {$this->getStringifiedAttributes()}>{$this->slot}</{$this->tagName}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
28
app/Views/Components/Hint.php
Normal file
28
app/Views/Components/Hint.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Hint extends Component
|
||||
{
|
||||
protected array $attributes = [
|
||||
'data-tooltip' => 'bottom',
|
||||
'tabindex' => '0',
|
||||
];
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->attributes['title'] = $this->slot;
|
||||
|
||||
$this->mergeClass('inline-block align-middle opacity-75');
|
||||
|
||||
$icon = icon('question-fill');
|
||||
|
||||
return <<<HTML
|
||||
<span {$this->getStringifiedAttributes()}>{$icon}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -6,7 +6,9 @@ namespace App\Views\Components;
|
||||
|
||||
class IconButton extends Button
|
||||
{
|
||||
public string $glyph = '';
|
||||
public string $glyph;
|
||||
|
||||
protected array $props = ['glyph'];
|
||||
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
@ -16,18 +18,18 @@ class IconButton extends Button
|
||||
'data-tooltip' => 'bottom',
|
||||
];
|
||||
|
||||
$glyphSize = [
|
||||
'small' => 'text-sm',
|
||||
'base' => 'text-lg',
|
||||
'large' => 'text-2xl',
|
||||
];
|
||||
|
||||
$allAttributes = [...$attributes, ...$iconButtonAttributes];
|
||||
|
||||
parent::__construct($allAttributes);
|
||||
|
||||
$glyphSizeClass = match ($this->size) {
|
||||
'small' => 'text-sm',
|
||||
'large' => 'text-2xl',
|
||||
default => 'text-lg',
|
||||
};
|
||||
|
||||
$this->slot = icon($this->glyph, [
|
||||
'class' => $glyphSize[$this->size],
|
||||
'class' => $glyphSizeClass,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -15,29 +15,44 @@ class Pill extends Component
|
||||
|
||||
public string $variant = 'default';
|
||||
|
||||
public ?string $icon = null;
|
||||
public string $icon = '';
|
||||
|
||||
public ?string $iconClass = '';
|
||||
public string $iconClass = '';
|
||||
|
||||
protected ?string $hint = null;
|
||||
protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint'];
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$variantClasses = [
|
||||
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
$variantClass = match ($this->variant) {
|
||||
'primary' => 'text-accent-contrast bg-accent-base border-accent-base',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'danger' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
];
|
||||
default => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
};
|
||||
|
||||
$icon = $this->icon ? icon($this->icon, [
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'text-xs tracking-wide',
|
||||
default => 'text-sm',
|
||||
};
|
||||
|
||||
$icon = $this->icon !== '' ? icon($this->icon, [
|
||||
'class' => $this->iconClass,
|
||||
]) : '';
|
||||
$hint = $this->hint ? 'data-tooltip="bottom" title="' . $this->hint . '"' : '';
|
||||
|
||||
if ($this->hint !== '') {
|
||||
$this->attributes['data-tooltip'] = 'bottom';
|
||||
$this->attributes['title'] = $this->hint;
|
||||
}
|
||||
|
||||
$this->mergeClass('inline-flex lowercase items-center gap-x-1 px-1 font-semibold border rounded');
|
||||
$this->mergeClass($variantClass);
|
||||
$this->mergeClass($sizeClass);
|
||||
|
||||
return <<<HTML
|
||||
<span class="inline-flex items-center gap-x-1 px-1 font-semibold text-sm border rounded {$variantClasses[$this->variant]} {$this->class}" {$hint}>{$icon}{$this->slot}</span>
|
||||
<span {$this->getStringifiedAttributes()}>{$icon}{$this->slot}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,23 @@ use ViewComponents\Component;
|
||||
|
||||
class ReadMore extends Component
|
||||
{
|
||||
public string $id;
|
||||
protected array $props = ['id'];
|
||||
|
||||
protected string $id;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$readMoreLabel = lang('Common.read_more');
|
||||
$readLessLabel = lang('Common.read_less');
|
||||
|
||||
$this->mergeClass('read-more');
|
||||
$this->attributes['style'] = '--line-clamp: 3';
|
||||
|
||||
return <<<HTML
|
||||
<div class="read-more {$this->class}" style="--line-clamp: 3">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<input id="read-more-checkbox_{$this->id}" type="checkbox" class="read-more__checkbox" aria-hidden="true">
|
||||
<div class="mb-2 read-more__text">{$this->slot}</div>
|
||||
<label for="read-more-checkbox_{$this->id}" class="read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
|
||||
<div class="read-more__text">{$this->slot}</div>
|
||||
<label for="read-more-checkbox_{$this->id}" class="mt-2 read-more__label" data-read-more="{$readMoreLabel}" data-read-less="{$readLessLabel}" aria-hidden="true"></label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
@ -12,11 +12,15 @@ class SeeMore extends Component
|
||||
{
|
||||
$seeMoreLabel = lang('Common.see_more');
|
||||
$seeLessLabel = lang('Common.see_less');
|
||||
|
||||
$this->mergeClass('see-more');
|
||||
$this->attributes['styles'] = '--content-height: 10rem';
|
||||
|
||||
return <<<HTML
|
||||
<div class="see-more" style="--content-height: 10rem">
|
||||
<div {$this->getStringifiedAttributes()}>
|
||||
<input id="see-more-checkbox" type="checkbox" class="see-more__checkbox" aria-hidden="true">
|
||||
<div class="mb-2 see-more__content {$this->class}"><div class="see-more_content-fade"></div>{$this->slot}</div>
|
||||
<label for="see-more-checkbox" class="see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
|
||||
<div class="see-more__content"><div class="see-more_content-fade"></div>{$this->slot}</div>
|
||||
<label for="see-more-checkbox" class="mt-2 see-more__label" data-see-more="{$seeMoreLabel}" data-see-less="{$seeLessLabel}" aria-hidden="true"></label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
if (session()->has('message')): ?>
|
||||
<Alert variant="success" class="mb-4"><?= esc(session('message')) ?></Alert>
|
||||
<Alert variant="success" class="mb-4"><?= session('message') ?></Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('error')): ?>
|
||||
<Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></Alert>
|
||||
<Alert variant="danger" class="mb-4"><?= session('error') ?></Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('errors')): ?>
|
||||
<Alert variant="danger" class="mb-4">
|
||||
<ul>
|
||||
<?php foreach (session('errors') as $error): ?>
|
||||
<li><?= esc($error) ?></li>
|
||||
<li><?= $error ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</Alert>
|
||||
|
@ -23,7 +23,7 @@
|
||||
You do not have sufficient permissions to access that page.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -23,7 +23,7 @@
|
||||
<?= lang('Errors.sorryCannotFind') ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<h2 class="font-mono font-semibold"><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h2>
|
||||
<p class="font-mono"><?= nl2br(esc($exception->getMessage())) ?><br/><span class="pl-4">at <span class="select-all bg-elevated"><?= nl2br(esc($exception->getFile())) ?>:<?= esc($exception->getLine()) ?></span></span></p>
|
||||
<p id="error-stack-trace" class="hidden"><?= nl2br(esc($exception)) ?></p>
|
||||
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base focus:ring-accent">
|
||||
<clipboard-copy for="error-stack-trace" class="items-center self-end px-3 py-1 mt-2 font-semibold leading-8 transition-all rounded-full shadow group text-accent-contrast hover:bg-accent-hover bg-accent-base">
|
||||
<span class="inline-flex items-center copy-base"><?= icon('file-copy-fill', [
|
||||
'class' => 'mr-2',
|
||||
]) ?>Copy stack trace</span>
|
||||
@ -41,11 +41,11 @@
|
||||
<div class="flex flex-col justify-center w-full gap-6 py-12 border-t-2 md:flex-row border-subtle">
|
||||
<div class="w-full max-w-md mx-auto md:mx-0">
|
||||
<h2 class="text-xl font-semibold font-display">Found a bug?</h2>
|
||||
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline focus:ring-accent decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
|
||||
<p>You can help get it fixed by <a href="https://castopod.org/new-issue_bug" target="_blank" rel="noopener noreferrer" class="underline decoration-3 hover:no-underline decoration-accent">creating an issue on the Castopod issue tracker</a>. Please check that the issue does not already exist beforehand.</p>
|
||||
</div>
|
||||
<div class="w-full max-w-md mx-auto md:mx-0">
|
||||
<h2 class="text-xl font-semibold font-display">Not sure what's happening?</h2>
|
||||
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline focus:ring-accent decoration-accent">Castopod community chat</a>!</p>
|
||||
<p>You can ask for help in the <a href="https://castopod.org/chat" target="_blank" rel="noopener noreferrer" class="underline decoration-2 hover:no-underline decoration-accent">Castopod community chat</a>!</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
@ -41,6 +41,9 @@ class PluginController extends BaseController
|
||||
$plugins = service('plugins');
|
||||
|
||||
$vendorPlugins = $plugins->getVendorPlugins($vendor);
|
||||
replace_breadcrumb_params([
|
||||
$vendor => $vendor,
|
||||
]);
|
||||
return view('plugins/installed', [
|
||||
'total' => count($vendorPlugins),
|
||||
'plugins' => $vendorPlugins,
|
||||
@ -59,6 +62,10 @@ class PluginController extends BaseController
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
replace_breadcrumb_params([
|
||||
$vendor => $vendor,
|
||||
$package => $package,
|
||||
]);
|
||||
return view('plugins/view', [
|
||||
'plugin' => $plugin,
|
||||
]);
|
||||
@ -76,6 +83,10 @@ class PluginController extends BaseController
|
||||
}
|
||||
|
||||
helper('form');
|
||||
replace_breadcrumb_params([
|
||||
$vendor => $vendor,
|
||||
$package => $package,
|
||||
]);
|
||||
return view('plugins/settings_general', [
|
||||
'plugin' => $plugin,
|
||||
]);
|
||||
@ -122,7 +133,9 @@ class PluginController extends BaseController
|
||||
|
||||
helper('form');
|
||||
replace_breadcrumb_params([
|
||||
0 => $podcast->handle,
|
||||
0 => $podcast->handle,
|
||||
$vendor => $vendor,
|
||||
$package => $package,
|
||||
]);
|
||||
return view('plugins/settings_podcast', [
|
||||
'podcast' => $podcast,
|
||||
@ -171,8 +184,10 @@ class PluginController extends BaseController
|
||||
|
||||
helper('form');
|
||||
replace_breadcrumb_params([
|
||||
0 => $episode->podcast->handle,
|
||||
1 => $episode->title,
|
||||
0 => $episode->podcast->handle,
|
||||
1 => $episode->title,
|
||||
$vendor => $vendor,
|
||||
$package => $package,
|
||||
]);
|
||||
return view('plugins/settings_episode', [
|
||||
'podcast' => $episode->podcast,
|
||||
|
@ -17,6 +17,8 @@ use League\CommonMark\MarkdownConverter;
|
||||
use Modules\Plugins\ExternalImageProcessor;
|
||||
use Modules\Plugins\ExternalLinkProcessor;
|
||||
use Modules\Plugins\Manifest\Manifest;
|
||||
use Modules\Plugins\Manifest\Person;
|
||||
use Modules\Plugins\Manifest\Repository;
|
||||
use Modules\Plugins\Manifest\Settings;
|
||||
use Modules\Plugins\Manifest\SettingsField;
|
||||
use RuntimeException;
|
||||
@ -35,7 +37,7 @@ abstract class BasePlugin implements PluginInterface
|
||||
|
||||
protected Manifest $manifest;
|
||||
|
||||
protected string $readmeHTML;
|
||||
protected ?string $readmeHTML;
|
||||
|
||||
public function __construct(
|
||||
protected string $vendor,
|
||||
@ -121,6 +123,27 @@ abstract class BasePlugin implements PluginInterface
|
||||
return $this->manifest->homepage;
|
||||
}
|
||||
|
||||
final public function getRepository(): ?Repository
|
||||
{
|
||||
return $this->manifest->repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
final public function getKeywords(): array
|
||||
{
|
||||
return $this->manifest->keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Person[]
|
||||
*/
|
||||
final public function getAuthors(): array
|
||||
{
|
||||
return $this->manifest->authors;
|
||||
}
|
||||
|
||||
final public function getIconSrc(): string
|
||||
{
|
||||
return $this->iconSrc;
|
||||
@ -194,11 +217,16 @@ abstract class BasePlugin implements PluginInterface
|
||||
return $description;
|
||||
}
|
||||
|
||||
final public function getReadmeHTML(): string
|
||||
final public function getReadmeHTML(): ?string
|
||||
{
|
||||
return $this->readmeHTML;
|
||||
}
|
||||
|
||||
final public function getLicense(): string
|
||||
{
|
||||
return $this->manifest->license ?? 'UNLICENSED';
|
||||
}
|
||||
|
||||
final protected function getOption(string $option): mixed
|
||||
{
|
||||
return get_plugin_option($this->key, $option);
|
||||
@ -238,7 +266,7 @@ abstract class BasePlugin implements PluginInterface
|
||||
$environment = new Environment([
|
||||
'html_input' => 'escape',
|
||||
'allow_unsafe_links' => false,
|
||||
'host' => 'hello',
|
||||
'host' => (new URI(base_url()))->getHost(),
|
||||
]);
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
|
||||
@ -247,11 +275,15 @@ abstract class BasePlugin implements PluginInterface
|
||||
|
||||
$environment->addEventListener(
|
||||
DocumentParsedEvent::class,
|
||||
[new ExternalLinkProcessor($environment), 'onDocumentParsed']
|
||||
static function (DocumentParsedEvent $event): void {
|
||||
(new ExternalLinkProcessor())->onDocumentParsed($event);
|
||||
}
|
||||
);
|
||||
$environment->addEventListener(
|
||||
DocumentParsedEvent::class,
|
||||
[new ExternalImageProcessor($environment), 'onDocumentParsed']
|
||||
static function (DocumentParsedEvent $event): void {
|
||||
(new ExternalImageProcessor())->onDocumentParsed($event);
|
||||
}
|
||||
);
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
|
@ -35,6 +35,8 @@ class Plugins
|
||||
|
||||
protected static int $installedCount = 0;
|
||||
|
||||
protected static int $activeCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
helper('plugins');
|
||||
@ -63,6 +65,21 @@ class Plugins
|
||||
return array_slice(static::$plugins, (($page - 1) * $perPage), $perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<BasePlugin>
|
||||
*/
|
||||
public function getActivePlugins(): array
|
||||
{
|
||||
$activePlugins = [];
|
||||
foreach (static::$plugins as $plugin) {
|
||||
if ($plugin->isActive()) {
|
||||
$activePlugins[] = $plugin;
|
||||
}
|
||||
}
|
||||
|
||||
return $activePlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<BasePlugin>
|
||||
*/
|
||||
@ -177,6 +194,11 @@ class Plugins
|
||||
return static::$installedCount;
|
||||
}
|
||||
|
||||
public function getActiveCount(): int
|
||||
{
|
||||
return static::$activeCount;
|
||||
}
|
||||
|
||||
public function uninstall(BasePlugin $plugin): bool
|
||||
{
|
||||
// remove all settings data
|
||||
@ -235,6 +257,10 @@ class Plugins
|
||||
static::$plugins[] = $plugin;
|
||||
static::$pluginsByVendor[$vendor][] = $plugin;
|
||||
++static::$installedCount;
|
||||
|
||||
if ($plugin->isActive()) {
|
||||
++static::$activeCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,19 +5,11 @@ declare(strict_types=1);
|
||||
namespace Modules\Plugins;
|
||||
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
|
||||
|
||||
class ExternalImageProcessor
|
||||
{
|
||||
private EnvironmentInterface $environment;
|
||||
|
||||
public function __construct(EnvironmentInterface $environment)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
public function onDocumentParsed(DocumentParsedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
@ -47,7 +39,6 @@ class ExternalImageProcessor
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
// TODO: load from environment's config
|
||||
// return $host != $this->environment->getConfiguration()->get('host');
|
||||
return $host !== (new URI(base_url()))->getHost();
|
||||
}
|
||||
}
|
||||
|
@ -5,19 +5,11 @@ declare(strict_types=1);
|
||||
namespace Modules\Plugins;
|
||||
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
|
||||
class ExternalLinkProcessor
|
||||
{
|
||||
private EnvironmentInterface $environment;
|
||||
|
||||
public function __construct(EnvironmentInterface $environment)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
public function onDocumentParsed(DocumentParsedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
@ -48,7 +40,6 @@ class ExternalLinkProcessor
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
// TODO: load from environment's config
|
||||
// return $host != $this->environment->getConfiguration()->get('host');
|
||||
return $host !== (new URI(base_url()))->getHost();
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,19 @@ declare(strict_types=1);
|
||||
*/
|
||||
|
||||
return [
|
||||
'installed' => 'Installed plugins ({count})',
|
||||
'installed' => 'Plugins installed',
|
||||
'about' => 'About',
|
||||
'website' => 'Website',
|
||||
'settings' => '{pluginName} settings',
|
||||
'repository' => 'Code repository',
|
||||
'authors' => 'Authors',
|
||||
'author_email' => 'Email {authorName}',
|
||||
'author_homepage' => '{authorName} homepage',
|
||||
'settings' => 'Settings',
|
||||
'view' => 'View',
|
||||
'activate' => 'Activate',
|
||||
'deactivate' => 'Deactivate',
|
||||
'active' => 'Active',
|
||||
'inactive' => 'Inactive',
|
||||
'uninstall' => 'Uninstall',
|
||||
'keywords' => [
|
||||
'podcasting20' => 'Podcasting 2.0',
|
||||
|
@ -10,14 +10,15 @@ use CodeIgniter\HTTP\URI;
|
||||
* @property string $name
|
||||
* @property string $version
|
||||
* @property ?string $description
|
||||
* @property ?Author $author
|
||||
* @property Author[] $authors
|
||||
* @property Person[] $authors
|
||||
* @property Person[] $contributors
|
||||
* @property ?URI $homepage
|
||||
* @property ?string $license
|
||||
* @property bool $private
|
||||
* @property list<string> $keywords
|
||||
* @property list<string> $hooks
|
||||
* @property ?Settings $settings
|
||||
* @property ?Repository $repository
|
||||
*/
|
||||
class Manifest extends ManifestObject
|
||||
{
|
||||
@ -25,27 +26,27 @@ class Manifest extends ManifestObject
|
||||
* @var array<string,string>
|
||||
*/
|
||||
protected const VALIDATION_RULES = [
|
||||
'name' => 'required|max_length[32]',
|
||||
'name' => 'required|max_length[128]',
|
||||
'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]',
|
||||
'description' => 'permit_empty|max_length[128]',
|
||||
'author' => 'permit_empty',
|
||||
'description' => 'permit_empty|max_length[256]',
|
||||
'authors' => 'permit_empty|is_list',
|
||||
'homepage' => 'permit_empty|valid_url_strict',
|
||||
'license' => 'permit_empty|string',
|
||||
'private' => 'permit_empty|is_boolean',
|
||||
'keywords.*' => 'permit_empty',
|
||||
'hooks.*' => 'permit_empty|in_list[channelTag,itemTag,siteHead]',
|
||||
'settings' => 'permit_empty',
|
||||
'settings' => 'permit_empty|is_list',
|
||||
'repository' => 'permit_empty|is_list',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string,array{string}|string>
|
||||
*/
|
||||
protected const CASTS = [
|
||||
'author' => Author::class,
|
||||
'authors' => [Author::class],
|
||||
'homepage' => URI::class,
|
||||
'settings' => Settings::class,
|
||||
'authors' => [Person::class],
|
||||
'homepage' => URI::class,
|
||||
'settings' => Settings::class,
|
||||
'repository' => Repository::class,
|
||||
];
|
||||
|
||||
protected string $name;
|
||||
@ -54,10 +55,8 @@ class Manifest extends ManifestObject
|
||||
|
||||
protected ?string $description = null;
|
||||
|
||||
protected ?Author $author = null;
|
||||
|
||||
/**
|
||||
* @var Author[]
|
||||
* @var Person[]
|
||||
*/
|
||||
protected array $authors = [];
|
||||
|
||||
@ -78,4 +77,6 @@ class Manifest extends ManifestObject
|
||||
protected array $hooks = [];
|
||||
|
||||
protected ?Settings $settings = null;
|
||||
|
||||
protected ?Repository $repository = null;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ abstract class ManifestObject
|
||||
|
||||
public function __get(string $name): mixed
|
||||
{
|
||||
if (isset($this->{$name})) {
|
||||
if (property_exists($this, $name)) {
|
||||
return $this->{$name};
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ use Exception;
|
||||
* @property ?string $email
|
||||
* @property ?URI $url
|
||||
*/
|
||||
class Author extends ManifestObject
|
||||
class Person extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'name' => 'required',
|
34
modules/Plugins/Manifest/Repository.php
Normal file
34
modules/Plugins/Manifest/Repository.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Manifest;
|
||||
|
||||
use CodeIgniter\HTTP\URI;
|
||||
|
||||
/**
|
||||
* @property string $type
|
||||
* @property ?URI $url
|
||||
* @property ?string $directory
|
||||
*/
|
||||
class Repository extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'type' => 'required|in_list[git]',
|
||||
'url' => 'required|valid_url_strict',
|
||||
'directory' => 'permit_empty',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string,array{string}|string>
|
||||
*/
|
||||
protected const CASTS = [
|
||||
'url' => URI::class,
|
||||
];
|
||||
|
||||
protected string $type;
|
||||
|
||||
protected URI $url;
|
||||
|
||||
protected ?string $directory = null;
|
||||
}
|
@ -29,9 +29,9 @@ class SettingsField extends ManifestObject
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected ?string $hint = '';
|
||||
protected string $hint = '';
|
||||
|
||||
protected ?string $helper = '';
|
||||
protected string $helper = '';
|
||||
|
||||
protected bool $optional = false;
|
||||
}
|
||||
|
@ -21,9 +21,6 @@
|
||||
"description": "This helps people discover your plugin as it's listed in repositories",
|
||||
"type": "string"
|
||||
},
|
||||
"author": {
|
||||
"$ref": "#/$defs/person"
|
||||
},
|
||||
"authors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -139,6 +139,9 @@ module.exports = {
|
||||
textDecoration: "none",
|
||||
},
|
||||
},
|
||||
input: {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
sm: {
|
||||
|
@ -36,15 +36,15 @@ $isEpisodeArea = isset($podcast) && isset($episode);
|
||||
<div class="flex flex-col justify-end w-full -mt-4 sticky-header-inner bg-elevated">
|
||||
<?= render_breadcrumb('text-xs items-center flex') ?>
|
||||
<div class="flex justify-between py-1">
|
||||
<div class="flex flex-wrap items-center truncate">
|
||||
<div class="flex flex-wrap items-center truncate gap-x-2">
|
||||
<?php if (($isEpisodeArea && $episode->is_premium) || ($isPodcastArea && $podcast->is_premium)): ?>
|
||||
<div class="inline-flex items-center">
|
||||
<?php // @icon('exchange-dollar-fill')?>
|
||||
<IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></IconButton>
|
||||
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
|
||||
<x-IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></x-IconButton>
|
||||
<x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
|
||||
<x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading>
|
||||
<?php endif; ?>
|
||||
<?= $this->renderSection('headerLeft') ?>
|
||||
</div>
|
||||
|
@ -1,33 +1,33 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
if (session()->has('message')): ?>
|
||||
<Alert variant="success" class="mb-4"><?= esc(session('message')) ?></Alert>
|
||||
<x-Alert variant="success" class="mb-4"><?= esc(session('message')) ?></x-Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('error')): ?>
|
||||
<Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></Alert>
|
||||
<x-Alert variant="danger" class="mb-4"><?= esc(session('error')) ?></x-Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('errors')): ?>
|
||||
<Alert variant="danger" class="mb-4">
|
||||
<x-Alert variant="danger" class="mb-4">
|
||||
<ul>
|
||||
<?php foreach (session('errors') as $error): ?>
|
||||
<li><?= esc($error) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</Alert>
|
||||
</x-Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('warning')): ?>
|
||||
<Alert variant="warning" class="mb-4"><?= esc(session('warning')) ?></Alert>
|
||||
<x-Alert variant="warning" class="mb-4"><?= esc(session('warning')) ?></x-Alert>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (session()->has('warnings')): ?>
|
||||
<Alert variant="warning" class="mb-4">
|
||||
<x-Alert variant="warning" class="mb-4">
|
||||
<ul>
|
||||
<?php foreach (session('warnings') as $warning): ?>
|
||||
<li><?= esc($warning) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</Alert>
|
||||
</x-Alert>
|
||||
<?php endif; ?>
|
||||
|
@ -15,7 +15,7 @@ $isEpisodeArea = isset($podcast) && isset($episode);
|
||||
<?php endif; ?>
|
||||
<footer class="px-2 py-2 mx-auto text-xs text-right">
|
||||
<?= lang('Common.powered_by', [
|
||||
'castopod' => '<a class="inline-flex font-semibold hover:underline focus:ring-accent" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social:castopod', [
|
||||
'castopod' => '<a class="inline-flex font-semibold hover:underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod' . icon('social:castopod', [
|
||||
'class' => 'ml-1 text-lg',
|
||||
]) . '</a> ' .
|
||||
CP_VERSION,
|
||||
|
@ -5,18 +5,18 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
|
||||
<header class="sticky top-0 z-[60] flex items-center h-10 text-white border-b col-span-full bg-navigation border-navigation">
|
||||
<button type="button"
|
||||
data-sidebar-toggler="toggler"
|
||||
class="h-full pr-1 text-xl md:hidden focus:ring-accent focus:ring-inset" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
|
||||
class="h-full pr-1 text-xl md:hidden" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
|
||||
<div class="inline-flex items-center h-full">
|
||||
<a href="<?= route_to(
|
||||
'admin',
|
||||
) ?>" class="inline-flex items-center h-full px-2 border-r border-navigation focus:ring-inset focus:ring-accent">
|
||||
) ?>" class="inline-flex items-center h-full px-2 border-r border-navigation">
|
||||
<?= (isset($podcast) ? icon('arrow-left-line', [
|
||||
'class' => 'mr-2',
|
||||
]) : '') . svg('castopod-logo-base', 'h-6') ?>
|
||||
</a>
|
||||
<a href="<?= route_to(
|
||||
'home',
|
||||
) ?>" class="inline-flex items-center h-full px-2 text-sm font-semibold sm:px-6 hover:underline focus:ring-inset focus:ring-accent" title="<?= lang('Navigation.go_to_website') ?>">
|
||||
) ?>" class="inline-flex items-center h-full px-2 text-sm font-semibold sm:px-6 hover:underline" title="<?= lang('Navigation.go_to_website') ?>">
|
||||
<span class="hidden sm:block"><?= lang('Navigation.go_to_website') ?></span>
|
||||
<?= icon('external-link-fill', [
|
||||
'class' => 'sm:ml-1 text-xl sm:text-base sm:opacity-60',
|
||||
@ -24,7 +24,7 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="inline-flex items-center h-full ml-auto">
|
||||
<button type="button" class="relative h-full px-2 focus:ring-accent focus:ring-inset" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Notifications.title') ?>" data-tooltip="bottom">
|
||||
<button type="button" class="relative h-full px-2" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Notifications.title') ?>" data-tooltip="bottom">
|
||||
<?= icon('notification-2-fill', [
|
||||
'class' => 'text-2xl opacity-80',
|
||||
]) ?>
|
||||
@ -74,11 +74,11 @@ if ($userPodcasts !== []) {
|
||||
];
|
||||
}
|
||||
?>
|
||||
<DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom-end"/>
|
||||
<x-DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom-end"/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center h-full px-3 text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
|
||||
class="inline-flex items-center h-full px-3 text-sm font-semibold gap-x-2"
|
||||
id="my-account-dropdown"
|
||||
data-dropdown="button"
|
||||
data-dropdown-target="my-account-dropdown-menu"
|
||||
@ -154,5 +154,5 @@ if ($userPodcasts !== []) {
|
||||
], $menuItems);
|
||||
}
|
||||
?>
|
||||
<DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" />
|
||||
<x-DropdownMenu id="my-account-dropdown-menu" labelledby="my-account-dropdown" items="<?= esc(json_encode($menuItems)) ?>" />
|
||||
</header>
|
@ -15,18 +15,18 @@
|
||||
}
|
||||
?>
|
||||
<details <?= $isSectionActive ? 'open="open"' : '' ?> class="<?= $isSectionActive ? 'bg-navigation-active' : '' ?> [&[open]>summary::after]:rotate-90">
|
||||
<summary class="inline-flex items-center w-full px-4 py-2 font-semibold focus:ring-accent focus:ring-inset after:w-5 after:h-5 after:transition-transform after:content-chevronRightIcon after:ml-2 after:opacity-60 after:text-white">
|
||||
<summary class="inline-flex items-center w-full h-12 px-4 py-2 font-semibold after:w-5 after:h-5 after:transition-transform after:content-chevronRightIcon after:ml-2 after:opacity-60 after:text-white">
|
||||
<div class="inline-flex items-center mr-auto">
|
||||
<?= icon($data['icon'], [
|
||||
'class' => 'opacity-60 text-2xl mr-4',
|
||||
]) ?>
|
||||
<?= lang($langKey . '.' . $section) ?>
|
||||
<?php if (array_key_exists('count', $data)): ?>
|
||||
<a href="<?= route_to($data['count-route'], $podcastId ?? null, $episodeId ?? null) ?>" class="px-2 ml-2 text-xs font-normal rounded-full focus:ring-accent <?= $isSectionActive ? 'bg-navigation' : 'bg-navigation-active' ?>"><?= $data['count'] ?></a>
|
||||
<a href="<?= route_to($data['count-route'], $podcastId ?? null, $episodeId ?? null) ?>" class="px-2 ml-2 text-xs font-normal rounded-full <?= $isSectionActive ? 'bg-navigation' : 'bg-navigation-active' ?>"><?= $data['count'] ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if(array_key_exists('add-cta', $data)): ?>
|
||||
<a href="<?= route_to($data['add-cta'], $podcastId ?? null, $episodeId ?? null) ?>" class="p-2 rounded-full shadow bg-accent-base focus:ring-accent" title="<?= lang($langKey . '.' . $data['add-cta']) ?>" data-tooltip="bottom">
|
||||
<a href="<?= route_to($data['add-cta'], $podcastId ?? null, $episodeId ?? null) ?>" class="p-2 rounded-full shadow bg-accent-base" title="<?= lang($langKey . '.' . $data['add-cta']) ?>" data-tooltip="bottom">
|
||||
<?= icon('add-fill') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
@ -34,7 +34,8 @@
|
||||
<ul class="flex flex-col pb-4">
|
||||
<?php foreach ($data['items'] as $key => $item):
|
||||
$isActive = $item === $activeItem;
|
||||
$label = array_key_exists('items-labels', $data) ? $data['items-labels'][$key] : lang($langKey . '.' . $item);
|
||||
|
||||
$label = (array_key_exists('items-labels', $data) && array_key_exists($item, $data['items-labels'])) ? $data['items-labels'][$item] : lang($langKey . '.' . $item);
|
||||
$href = str_starts_with($item, '/') ? $item : route_to($item, $podcastId ?? null, $episodeId ?? null);
|
||||
|
||||
$isAllowed = true;
|
||||
@ -48,11 +49,11 @@
|
||||
?>
|
||||
<li class="inline-flex">
|
||||
<?php if ($isAllowed): ?>
|
||||
<a class="relative w-full py-3 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active focus:ring-inset focus:ring-accent<?= $isActive
|
||||
<a class="relative w-full py-3 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active<?= $isActive
|
||||
? ' before:opacity-100 font-semibold inline-flex items-center'
|
||||
: ' hover:before:opacity-60 focus:before:opacity-60' ?>" href="<?= $href ?>"><?= $label ?></a>
|
||||
<?php else: ?>
|
||||
<span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active focus:ring-inset focus:ring-accent"><?= $label ?></span>
|
||||
<span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active"><?= $label ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
@ -22,12 +22,15 @@ $navigation = [
|
||||
'count-route' => 'podcast-list',
|
||||
],
|
||||
'plugins' => [
|
||||
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
|
||||
'items' => ['plugins-installed'],
|
||||
'icon' => 'puzzle-fill', // @icon('puzzle-fill')
|
||||
'items' => ['plugins-installed'],
|
||||
'items-labels' => [
|
||||
'plugins-installed' => lang('Navigation.plugins-installed') . ' (' . service('plugins')->getInstalledCount() . ')',
|
||||
],
|
||||
'items-permissions' => [
|
||||
'plugins-installed' => 'plugins.manage',
|
||||
],
|
||||
'count' => service('plugins')->getInstalledCount(),
|
||||
'count' => service('plugins')->getActiveCount(),
|
||||
'count-route' => 'plugins-installed',
|
||||
],
|
||||
'persons' => [
|
||||
@ -82,12 +85,20 @@ $navigation = [
|
||||
],
|
||||
];
|
||||
|
||||
foreach (plugins()->getActivePlugins() as $plugin) {
|
||||
$route = route_to('plugins-view', $plugin->getKey());
|
||||
$navigation['plugins']['items'][] = $route;
|
||||
$navigation['plugins']['items-labels'][$route] = $plugin->getName();
|
||||
$navigation['plugins']['items-permissions'][$route] = 'plugins.manage';
|
||||
}
|
||||
|
||||
if (auth()->user()->can('podcasts.view')) {
|
||||
$navigation['podcasts']['count'] = (new PodcastModel())->countAllResults();
|
||||
} else {
|
||||
$navigation['podcasts']['count'] = count(get_user_podcasts(auth()->user()));
|
||||
} ?>
|
||||
|
||||
|
||||
<?= view('_partials/_nav_menu', [
|
||||
'navigation' => $navigation,
|
||||
'langKey' => 'Navigation',
|
||||
|
@ -14,24 +14,24 @@
|
||||
<form method="POST" action="<?= route_to('contributor-add', $podcast->id) ?>" class="flex flex-col max-w-sm gap-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="user"
|
||||
label="<?= esc(lang('Contributor.form.user')) ?>"
|
||||
options="<?= esc(json_encode($contributorOptions)) ?>"
|
||||
placeholder="<?= lang('Contributor.form.user_placeholder') ?>"
|
||||
required="true" />
|
||||
isRequired="true" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="role"
|
||||
label="<?= esc(lang('Contributor.form.role')) ?>"
|
||||
options="<?= esc(json_encode($roleOptions)) ?>"
|
||||
placeholder="<?= lang('Contributor.form.role_placeholder') ?>"
|
||||
selected="<?= setting('AuthGroups.defaultPodcastGroup') ?>"
|
||||
required="true" />
|
||||
isRequired="true" />
|
||||
|
||||
<Button type="submit" class="self-end" variant="primary"><?= lang('Contributor.form.submit_add') ?></Button>
|
||||
<x-Button type="submit" class="self-end" variant="primary"><?= lang('Contributor.form.submit_add') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -17,19 +17,19 @@
|
||||
<form action="<?= route_to('contributor-delete', $podcast->id, $contributor->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Alert variant="danger" class="font-semibold"><?= lang('Contributor.delete_form.disclaimer', [
|
||||
<x-Alert variant="danger" class="font-semibold"><?= lang('Contributor.delete_form.disclaimer', [
|
||||
'contributor' => $contributor->username,
|
||||
'podcastTitle' => $podcast->title,
|
||||
]) ?></Alert>
|
||||
]) ?></x-Alert>
|
||||
|
||||
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Contributor.delete_form.understand', [
|
||||
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Contributor.delete_form.understand', [
|
||||
'contributor' => $contributor->username,
|
||||
'podcastTitle' => $podcast->title,
|
||||
]) ?></Forms.Checkbox>
|
||||
]) ?></x-Forms.Checkbox>
|
||||
|
||||
<div class="self-end mt-4">
|
||||
<Button uri="<?= route_to('contributor-view', $podcast->id, $contributor->id) ?>"><?= lang('Common.cancel') ?></Button>
|
||||
<Button type="submit" variant="danger"><?= lang('Contributor.delete_form.submit') ?></Button>
|
||||
<x-Button uri="<?= route_to('contributor-view', $podcast->id, $contributor->id) ?>"><?= lang('Common.cancel') ?></x-Button>
|
||||
<x-Button type="submit" variant="danger"><?= lang('Contributor.delete_form.submit') ?></x-Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -14,16 +14,16 @@
|
||||
<form method="POST" action="<?= route_to('contributor-edit', $podcast->id, $contributor->id) ?>" class="flex flex-col max-w-sm gap-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="role"
|
||||
label="<?= esc(lang('Contributor.form.role')) ?>"
|
||||
options="<?= esc(json_encode($roleOptions)) ?>"
|
||||
selected="<?= $contributorGroup ?>"
|
||||
placeholder="<?= lang('Contributor.form.role_placeholder') ?>"
|
||||
required="true" />
|
||||
isRequired="true" />
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Contributor.form.submit_edit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Contributor.form.submit_edit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Contributor.add') ?></Button>
|
||||
<x-Button uri="<?= route_to('contributor-add', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Contributor.add') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
$role = get_group_info(get_podcast_group($contributor, $podcast->id), $podcast->id)['title'];
|
||||
|
||||
if ($podcast->created_by === $contributor->id) {
|
||||
$role = '<div class="inline-flex items-center"><span class="mr-2 focus:ring-accent" tabindex="0" data-tooltip="bottom" title="' . lang('Auth.podcast_groups.owner.title') . '">' . icon('shield-user-fill') . '</span>' . $role . '</div>';
|
||||
$role = '<div class="inline-flex items-center"><span class="mr-2" tabindex="0" data-tooltip="bottom" title="' . lang('Auth.podcast_groups.owner.title') . '">' . icon('shield-user-fill') . '</span>' . $role . '</div>';
|
||||
}
|
||||
|
||||
return $role;
|
||||
@ -41,8 +41,8 @@
|
||||
'cell' => function ($contributor, $podcast) {
|
||||
// @icon('pencil-fill')
|
||||
// @icon('delete-bin-fill')
|
||||
return '<Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="secondary" iconLeft="pencil-fill" size="small">' . lang('Contributor.edit') . '</Button>' .
|
||||
'<Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" iconLeft="delete-bin-fill" size="small">' . lang('Contributor.remove') . '</Button>';
|
||||
return '<x-Button uri="' . route_to('contributor-edit', $podcast->id, $contributor->id) . '" variant="secondary" iconLeft="pencil-fill" size="small">' . lang('Contributor.edit') . '</x-Button>' .
|
||||
'<x-Button uri="' . route_to('contributor-remove', $podcast->id, $contributor->id) . '" variant="danger" iconLeft="delete-bin-fill" size="small">' . lang('Contributor.remove') . '</x-Button>';
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -13,27 +13,27 @@
|
||||
|
||||
<div class="flex flex-col items-stretch gap-4 lg:flex-row">
|
||||
<?php // @icon('mic-fill')?>
|
||||
<DashboardCard href="<?= $onlyPodcastId === null ? route_to('podcast-list') : route_to('podcast-view', $onlyPodcastId) ?>" glyph="mic-fill" title="<?= lang('Dashboard.podcasts.title') ?>" subtitle="<?= $podcastsData['last_published_at'] ? esc(lang('Dashboard.podcasts.last_published', [
|
||||
<x-DashboardCard href="<?= $onlyPodcastId === null ? route_to('podcast-list') : route_to('podcast-view', $onlyPodcastId) ?>" glyph="mic-fill" title="<?= lang('Dashboard.podcasts.title') ?>" subtitle="<?= $podcastsData['last_published_at'] ? esc(lang('Dashboard.podcasts.last_published', [
|
||||
'lastPublicationDate' => local_date($podcastsData['last_published_at']),
|
||||
], null, false)) : lang('Dashboard.podcasts.not_found') ?>"><?= $podcastsData['number_of_podcasts'] ?></DashboardCard>
|
||||
], null, false)) : lang('Dashboard.podcasts.not_found') ?>"><?= $podcastsData['number_of_podcasts'] ?></x-DashboardCard>
|
||||
<?php // @icon('play-fill')?>
|
||||
<DashboardCard href="<?= $onlyPodcastId === null ? '' : route_to('episode-list', $onlyPodcastId) ?>" glyph="play-fill" title="<?= lang('Dashboard.episodes.title') ?>" subtitle="<?= $episodesData['last_published_at'] ? esc(lang('Dashboard.episodes.last_published', [
|
||||
<x-DashboardCard href="<?= $onlyPodcastId === null ? '' : route_to('episode-list', $onlyPodcastId) ?>" glyph="play-fill" title="<?= lang('Dashboard.episodes.title') ?>" subtitle="<?= $episodesData['last_published_at'] ? esc(lang('Dashboard.episodes.last_published', [
|
||||
'lastPublicationDate' => local_date($episodesData['last_published_at']),
|
||||
], null, false)) : lang('Dashboard.episodes.not_found') ?>"><?= $episodesData['number_of_episodes'] ?></DashboardCard>
|
||||
], null, false)) : lang('Dashboard.episodes.not_found') ?>"><?= $episodesData['number_of_episodes'] ?></x-DashboardCard>
|
||||
<?php // @icon('database-2-fill')?>
|
||||
<DashboardCard glyph="database-2-fill" title="<?= lang('Dashboard.storage.title') ?>" subtitle="<?= lang('Dashboard.storage.subtitle', [
|
||||
<x-DashboardCard glyph="database-2-fill" title="<?= lang('Dashboard.storage.title') ?>" subtitle="<?= lang('Dashboard.storage.subtitle', [
|
||||
'totalUploaded' => $storageData['total_uploaded'],
|
||||
'totalStorage' => $storageData['limit'],
|
||||
]) ?>"><?= $storageData['percentage'] ?>%</DashboardCard>
|
||||
]) ?>"><?= $storageData['percentage'] ?>%</x-DashboardCard>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 mt-4 lg:grid-cols-2">
|
||||
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_storage_by_month') ?>" dataUrl="<?= route_to(
|
||||
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.total_storage_by_month') ?>" dataUrl="<?= route_to(
|
||||
'analytics-data-instance',
|
||||
'Podcast',
|
||||
'TotalStorageByMonth',
|
||||
) ?>" />
|
||||
<Charts.XY class="col-span-1" title="<?= lang('Charts.total_bandwidth_by_month') ?>" subtitle="<?= $bandwidthLimit !== null ? lang('Charts.total_bandwidth_by_month_limit', [
|
||||
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.total_bandwidth_by_month') ?>" subtitle="<?= $bandwidthLimit !== null ? lang('Charts.total_bandwidth_by_month_limit', [
|
||||
'totalBandwidth' => $bandwidthLimit,
|
||||
]) : '' ?>" dataUrl="<?= route_to(
|
||||
'analytics-data-instance',
|
||||
|
@ -19,7 +19,7 @@
|
||||
<span class="font-semibold leading-tight line-clamp-2"><?= esc($episode->title) ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<button class="absolute top-0 right-0 z-10 p-2 mt-2 mr-2 text-white transition -translate-y-12 rounded-full opacity-0 focus:ring-accent focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $episode->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||
<button class="absolute top-0 right-0 z-10 p-2 mt-2 mr-2 text-white transition -translate-y-12 rounded-full opacity-0 focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $episode->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||
<?php $items = [
|
||||
[
|
||||
'type' => 'link',
|
||||
@ -75,5 +75,5 @@ if ($episode->published_at === null) {
|
||||
HTML),
|
||||
];
|
||||
} ?>
|
||||
<DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labelledby="more-dropdown-<?= $episode->id ?>" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
|
||||
<x-DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labelledby="more-dropdown-<?= $episode->id ?>" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
|
||||
</article>
|
@ -35,13 +35,13 @@ $episodeNavigation = [
|
||||
foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {
|
||||
$route = route_to('plugins-episode-settings', $podcast->id, $episode->id, $plugin->getKey());
|
||||
$episodeNavigation['plugins']['items'][] = $route;
|
||||
$episodeNavigation['plugins']['items-labels'][] = $plugin->getName();
|
||||
$episodeNavigation['plugins']['items-labels'][$route] = $plugin->getName();
|
||||
$episodeNavigation['plugins']['items-permissions'][$route] = 'episodes.edit';
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex items-center px-4 py-2 focus:ring-inset focus:ring-accent">
|
||||
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex items-center px-4 py-2">
|
||||
<?= icon('arrow-left-line', [
|
||||
'class' => 'mr-2',
|
||||
]) ?>
|
||||
@ -71,7 +71,7 @@ foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {
|
||||
'episode',
|
||||
esc($podcast->handle),
|
||||
esc($episode->slug),
|
||||
) ?>" class="inline-flex items-center text-xs hover:underline focus:ring-accent"><?= lang(
|
||||
) ?>" class="inline-flex items-center text-xs hover:underline"><?= lang(
|
||||
'EpisodeNavigation.go_to_page',
|
||||
) ?>
|
||||
<?= icon('external-link-fill', [
|
||||
|
@ -15,20 +15,20 @@
|
||||
<?= csrf_field() ?>
|
||||
|
||||
|
||||
<Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
|
||||
<x-Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="audio_file"
|
||||
label="<?= esc(lang('Episode.form.audio_file')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.audio_file_hint')) ?>"
|
||||
helper="<?= esc(lang('Common.size_limit', [formatBytes(file_upload_max_size(), true)])) ?>"
|
||||
type="file"
|
||||
accept=".mp3,.m4a"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-max-size="<?= file_upload_max_size() ?>"
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="cover"
|
||||
label="<?= esc(lang('Episode.form.cover')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.cover_hint')) ?>"
|
||||
@ -36,30 +36,30 @@
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('Episode.form.title')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.title_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title" />
|
||||
|
||||
<div>
|
||||
<Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></Forms.Label>
|
||||
<x-Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></x-Forms.Label>
|
||||
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= url_to('podcast-episodes', $podcast->handle) ?>">
|
||||
<span slot="domain"><?= '…/' . esc($podcast->at_handle) . '/' ?></span>
|
||||
<Forms.Input name="slug" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
<x-Forms.Input name="slug" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
</permalink-edit>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-x-2 gap-y-4 md:flex-row">
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
class="flex-1 w-full"
|
||||
name="season_number"
|
||||
label="<?= esc(lang('Episode.form.season_number')) ?>"
|
||||
type="number"
|
||||
value="<?= $currentSeasonNumber ?>"
|
||||
/>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
class="flex-1 w-full"
|
||||
name="episode_number"
|
||||
label="<?= esc(lang('Episode.form.episode_number')) ?>"
|
||||
@ -71,59 +71,56 @@
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<Forms.RadioButton
|
||||
<x-Forms.RadioButton
|
||||
value="full"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
|
||||
isChecked="true" ><?= lang('Episode.form.type.full') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="true" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="trailer"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="bonus"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></Forms.RadioButton>
|
||||
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend>
|
||||
<?= lang('Episode.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Episode.form.parental_advisory.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<Forms.RadioButton
|
||||
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
name="parental_advisory"
|
||||
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></Forms.RadioButton>
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.show_notes_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.show_notes_section_subtitle') ?>">
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="description"
|
||||
label="<?= esc(lang('Episode.form.description')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
disallowList="header,quote" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="description_footer"
|
||||
label="<?= esc(lang('Episode.form.description_footer')) ?>"
|
||||
@ -131,32 +128,31 @@
|
||||
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
|
||||
disallowList="header,quote" />
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section title="<?= lang('Episode.form.premium_title') ?>">
|
||||
<Forms.Toggler class="mt-2" name="premium" value="yes" checked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>">
|
||||
<?= lang('Episode.form.premium') ?></Forms.Toggler>
|
||||
</Forms.Section>
|
||||
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>">
|
||||
<x-Forms.Toggler class="mt-2" name="premium" isChecked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>">
|
||||
<?= lang('Episode.form.premium') ?></x-Forms.Toggler>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.location_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.location_section_subtitle') ?>"
|
||||
>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="location_name"
|
||||
label="<?= esc(lang('Episode.form.location_name')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.location_name_hint')) ?>" />
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.additional_files_section_title') ?>">
|
||||
|
||||
<fieldset class="flex flex-col">
|
||||
<legend><?= lang('Episode.form.transcript') .
|
||||
'<small class="ml-1 lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' .
|
||||
hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
|
||||
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.transcript_hint') ?></x-Hint></legend>
|
||||
<div class="form-input-tabs">
|
||||
<input type="radio" name="transcript-choice" id="transcript-file-upload-choice" aria-controls="transcript-file-upload-choice" value="upload-file" <?= old('transcript-choice') !== 'remote-file' ? 'checked' : '' ?> />
|
||||
<label for="transcript-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
|
||||
@ -166,12 +162,12 @@
|
||||
|
||||
<div class="py-2 tab-panels">
|
||||
<section id="transcript-file-upload" class="flex items-center tab-panel">
|
||||
<Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
|
||||
<x-Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
|
||||
</section>
|
||||
<section id="transcript-file-remote-url" class="tab-panel">
|
||||
<Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" />
|
||||
<x-Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,8 +178,7 @@
|
||||
<legend><?= lang('Episode.form.chapters') .
|
||||
'<small class="ml-1 lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' .
|
||||
hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
|
||||
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.chapters_hint') ?></x-Hint></legend>
|
||||
<div class="form-input-tabs">
|
||||
<input type="radio" name="chapters-choice" id="chapters-file-upload-choice" aria-controls="chapters-file-upload-choice" value="upload-file" <?= old('chapters-choice') !== 'remote-file' ? 'checked' : '' ?> />
|
||||
<label for="chapters-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
|
||||
@ -193,35 +188,35 @@
|
||||
|
||||
<div class="py-2 tab-panels">
|
||||
<section id="chapters-file-upload" class="flex items-center tab-panel">
|
||||
<Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
|
||||
<x-Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
|
||||
</section>
|
||||
<section id="chapters-file-remote-url" class="tab-panel">
|
||||
<Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" />
|
||||
<x-Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.advanced_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
|
||||
>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="XMLEditor"
|
||||
name="custom_rss"
|
||||
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.custom_rss_hint')) ?>"
|
||||
/>
|
||||
|
||||
<Forms.Toggler name="block" value="yes" checked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></Forms.Toggler>
|
||||
<x-Forms.Toggler name="block" isChecked="false" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
|
||||
<Button class="self-end" variant="primary" type="submit"><?= lang('Episode.form.submit_create') ?></Button>
|
||||
<x-Button class="self-end" variant="primary" type="submit"><?= lang('Episode.form.submit_create') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -13,13 +13,13 @@
|
||||
<form action="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Alert variant="danger" class="font-semibold"><?= lang('Episode.delete_form.disclaimer') ?></Alert>
|
||||
<x-Alert variant="danger" class="font-semibold"><?= lang('Episode.delete_form.disclaimer') ?></x-Alert>
|
||||
|
||||
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Episode.delete_form.understand') ?></Forms.Checkbox>
|
||||
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Episode.delete_form.understand') ?></x-Forms.Checkbox>
|
||||
|
||||
<div class="self-end mt-4">
|
||||
<Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></Button>
|
||||
<Button type="submit" variant="danger"><?= lang('Episode.delete_form.submit') ?></Button>
|
||||
<x-Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></x-Button>
|
||||
<x-Button type="submit" variant="danger"><?= lang('Episode.delete_form.submit') ?></x-Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<Button variant="primary" type="submit" form="episode-edit-form"><?= lang('Episode.form.submit_edit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" form="episode-edit-form"><?= lang('Episode.form.submit_edit') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
@ -19,9 +19,9 @@
|
||||
<?= csrf_field() ?>
|
||||
|
||||
|
||||
<Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
|
||||
<x-Forms.Section title="<?= lang('Episode.form.info_section_title') ?>" >
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="audio_file"
|
||||
label="<?= esc(lang('Episode.form.audio_file')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.audio_file_hint')) ?>"
|
||||
@ -31,7 +31,7 @@
|
||||
data-max-size="<?= file_upload_max_size() ?>"
|
||||
data-max-size-error="<?= lang('Episode.form.file_size_error', [formatBytes(file_upload_max_size(), true)]) ?>" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="cover"
|
||||
label="<?= esc(lang('Episode.form.cover')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.cover_hint')) ?>"
|
||||
@ -39,31 +39,31 @@
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('Episode.form.title')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.title_hint')) ?>"
|
||||
value="<?= esc($episode->title) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title" />
|
||||
|
||||
<div>
|
||||
<Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></Forms.Label>
|
||||
<x-Forms.Label for="slug"><?= lang('Episode.form.permalink') ?></x-Forms.Label>
|
||||
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= url_to('podcast-episodes', esc($podcast->handle)) ?>">
|
||||
<span slot="domain"><?= '…/' . esc($podcast->handle) . '/' ?></span>
|
||||
<Forms.Input name="slug" value="<?= esc($episode->slug) ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
<x-Forms.Input name="slug" value="<?= esc($episode->slug) ?>" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
</permalink-edit>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-x-2 gap-y-4 md:flex-row">
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
class="flex-1 w-full"
|
||||
name="season_number"
|
||||
label="<?= esc(lang('Episode.form.season_number')) ?>"
|
||||
type="number"
|
||||
value="<?= $episode->season_number ?>"
|
||||
/>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
class="flex-1 w-full"
|
||||
name="episode_number"
|
||||
label="<?= esc(lang('Episode.form.episode_number')) ?>"
|
||||
@ -75,57 +75,54 @@
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<Forms.RadioButton
|
||||
<x-Forms.RadioButton
|
||||
value="full"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="trailer"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="bonus"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></Forms.RadioButton>
|
||||
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend>
|
||||
<?= lang('Episode.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Episode.form.parental_advisory.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<Forms.RadioButton
|
||||
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></Forms.RadioButton>
|
||||
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.show_notes_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.show_notes_section_subtitle') ?>">
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="description"
|
||||
label="<?= esc(lang('Episode.form.description')) ?>"
|
||||
value="<?= esc($episode->description_markdown) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
disallowList="header,quote" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="description_footer"
|
||||
label="<?= esc(lang('Episode.form.description_footer')) ?>"
|
||||
@ -133,33 +130,32 @@
|
||||
value="<?= esc($podcast->episode_description_footer_markdown) ?? '' ?>"
|
||||
disallowList="header,quote" />
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section title="<?= lang('Episode.form.premium_title') ?>" >
|
||||
<Forms.Toggler class="mt-2" name="premium" value="yes" checked="<?= $episode->is_premium ? 'true' : 'false' ?>">
|
||||
<?= lang('Episode.form.premium') ?></Forms.Toggler>
|
||||
</Forms.Section>
|
||||
<x-Forms.Section title="<?= lang('Episode.form.premium_title') ?>" >
|
||||
<x-Forms.Toggler class="mt-2" name="premium" isChecked="<?= $episode->is_premium ? 'true' : 'false' ?>">
|
||||
<?= lang('Episode.form.premium') ?></x-Forms.Toggler>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.location_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.location_section_subtitle') ?>"
|
||||
>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="location_name"
|
||||
label="<?= esc(lang('Episode.form.location_name')) ?>"
|
||||
hint="<?= esc(lang('Episode.form.location_name_hint')) ?>"
|
||||
value="<?= esc($episode->location_name) ?>" />
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.additional_files_section_title') ?>">
|
||||
|
||||
<fieldset class="flex flex-col">
|
||||
<legend><?= lang('Episode.form.transcript') .
|
||||
'<small class="ml-1 lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' .
|
||||
hint_tooltip(lang('Episode.form.transcript_hint'), 'ml-1') ?></legend>
|
||||
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.transcript_hint') ?></x-Hint></legend>
|
||||
<div class="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_remote_url ? '' : 'checked' ?> />
|
||||
<label for="transcript-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
|
||||
@ -191,7 +187,7 @@
|
||||
'class' => 'mx-auto',
|
||||
]),
|
||||
[
|
||||
'class' => 'p-1 text-sm bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
|
||||
'class' => 'p-1 text-sm bg-red-100 rounded-full text-red-700 hover:text-red-900',
|
||||
'data-tooltip' => 'bottom',
|
||||
'title' => lang(
|
||||
'Episode.form.transcript_file_delete',
|
||||
@ -200,12 +196,12 @@
|
||||
) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
|
||||
<x-Forms.Label class="sr-only" for="transcript_file" isOptional="true"><?= lang('Episode.form.transcript_file') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" name="transcript_file" type="file" accept=".srt,.vtt" />
|
||||
</section>
|
||||
<section id="transcript-file-remote-url" class="tab-panel">
|
||||
<Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" value="<?= esc($episode->transcript_remote_url) ?>" />
|
||||
<x-Forms.Label class="sr-only" for="transcript_remote_url" isOptional="true"><?= lang('Episode.form.transcript_remote_url') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" placeholder="https://…" name="transcript_remote_url" value="<?= esc($episode->transcript_remote_url) ?>" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,8 +212,7 @@
|
||||
<legend><?= lang('Episode.form.chapters') .
|
||||
'<small class="ml-1 lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>' .
|
||||
hint_tooltip(lang('Episode.form.chapters_hint'), 'ml-1') ?></legend>
|
||||
')</small>' ?><x-Hint class="ml-1"><?= lang('Episode.form.chapters_hint') ?></x-Hint></legend>
|
||||
<div class="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_remote_url ? '' : 'checked' ?> />
|
||||
<label for="chapters-file-upload-choice"><?= lang('Common.forms.upload_file') ?></label>
|
||||
@ -249,7 +244,7 @@
|
||||
'class' => 'mx-auto',
|
||||
]),
|
||||
[
|
||||
'class' => 'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900 focus:ring-accent',
|
||||
'class' => 'text-sm p-1 bg-red-100 rounded-full text-red-700 hover:text-red-900',
|
||||
'data-tooltip' => 'bottom',
|
||||
'title' => lang(
|
||||
'Episode.form.chapters_file_delete',
|
||||
@ -258,23 +253,23 @@
|
||||
) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
|
||||
<x-Forms.Label class="sr-only" for="chapters_file" isOptional="true"><?= lang('Episode.form.chapters_file') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" name="chapters_file" type="file" accept=".json" />
|
||||
</section>
|
||||
<section id="chapters-file-remote-url" class="tab-panel">
|
||||
<Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></Forms.Label>
|
||||
<Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" value="<?= esc($episode->chapters_remote_url) ?>" />
|
||||
<x-Forms.Label class="sr-only" for="chapters_remote_url" isOptional="true"><?= lang('Episode.form.chapters_remote_url') ?></x-Forms.Label>
|
||||
<x-Forms.Input class="w-full" placeholder="https://…" name="chapters_remote_url" value="<?= esc($episode->chapters_remote_url) ?>" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Episode.form.advanced_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.form.advanced_section_subtitle') ?>"
|
||||
>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="XMLEditor"
|
||||
name="custom_rss"
|
||||
label="<?= esc(lang('Episode.form.custom_rss')) ?>"
|
||||
@ -282,18 +277,18 @@
|
||||
content="<?= esc($episode->custom_rss_string) ?>"
|
||||
/>
|
||||
|
||||
<Forms.Toggler id="block" name="block" value="yes" checked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></Forms.Toggler>
|
||||
<x-Forms.Toggler id="block" name="block" isChecked="<?= $episode->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Episode.form.block_hint')) ?>"><?= lang('Episode.form.block') ?></x-Forms.Toggler>
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
</form>
|
||||
|
||||
<?php if ($episode->published_at === null): ?>
|
||||
<?php // @icon('delete-bin-fill')?>
|
||||
<Button class="mt-8" variant="danger" uri="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" iconLeft="delete-bin-fill"><?= lang('Episode.delete') ?></Button>
|
||||
<x-Button class="mt-8" variant="danger" uri="<?= route_to('episode-delete', $podcast->id, $episode->id) ?>" iconLeft="delete-bin-fill"><?= lang('Episode.delete') ?></x-Button>
|
||||
<?php else: ?>
|
||||
<?php // @icon('forbid-fill')?>
|
||||
<Button class="mt-8" variant="disabled" iconLeft="forbid-fill" data-tooltip="right" title="<?= lang('Episode.messages.unpublishBeforeDeleteTip') ?>"><?= lang('Episode.delete') ?></Button>
|
||||
<x-Button class="mt-8" variant="disabled" iconLeft="forbid-fill" data-tooltip="right" title="<?= lang('Episode.messages.unpublishBeforeDeleteTip') ?>"><?= lang('Episode.delete') ?></x-Button>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
|
@ -33,15 +33,15 @@ $embedHeight = config('Embed')->height;
|
||||
<iframe name="embed" id="embed" class="w-full max-w-xl mt-6 h-28" frameborder="0" scrolling="no" style="width: 100%; overflow: hidden;" src="<?= $episode->embed_url ?>"></iframe>
|
||||
|
||||
<div class="flex items-center mt-8 gap-x-2">
|
||||
<Forms.Textarea readonly="true" class="w-full max-w-xl" name="iframe" rows="2" value="<?= esc("<iframe width=\"100%\" height=\"{$embedHeight}\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: {$embedHeight}px; overflow: hidden;\" src=\"{$episode->embed_url}\"></iframe>") ?>" />
|
||||
<x-Forms.Textarea isReadonly="true" class="w-full max-w-xl" name="iframe" rows="2" value="<?= esc("<iframe width=\"100%\" height=\"{$embedHeight}\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: {$embedHeight}px; overflow: hidden;\" src=\"{$episode->embed_url}\"></iframe>") ?>" />
|
||||
<?php // @icon('file-copy-fill')?>
|
||||
<IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="iframe"><?= lang('Episode.embed.clipboard_iframe') ?></IconButton>
|
||||
<x-IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="iframe"><?= lang('Episode.embed.clipboard_iframe') ?></x-IconButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-4 gap-x-2">
|
||||
<Forms.Input readonly="true" class="w-full max-w-xl" name="url" value="<?= esc($episode->embed_url) ?>" />
|
||||
<x-Forms.Input isReadonly="true" class="w-full max-w-xl" name="url" value="<?= esc($episode->embed_url) ?>" />
|
||||
<?php // @icon('file-copy-fill')?>
|
||||
<IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="url"><?= lang('Episode.embed.clipboard_url') ?></IconButton>
|
||||
<x-IconButton glyph="file-copy-fill" data-type="clipboard-copy" data-clipboard-target="url"><?= lang('Episode.embed.clipboard_url') ?></x-IconButton>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Episode.create') ?></Button>
|
||||
<x-Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Episode.create') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
@ -28,16 +28,16 @@
|
||||
</p>
|
||||
<form class="relative flex">
|
||||
<div class="relative">
|
||||
<Forms.Input name="q" placeholder="<?= lang('Episode.list.search.placeholder') ?>" value="<?= esc($query) ?>" class="<?= $query ? 'pr-8' : '' ?>" />
|
||||
<x-Forms.Input name="q" placeholder="<?= lang('Episode.list.search.placeholder') ?>" value="<?= esc($query) ?>" class="<?= $query ? 'pr-8' : '' ?>" />
|
||||
<?php if ($query): ?>
|
||||
<a href="<?= route_to('episode-list', $podcast->id) ?>" class="absolute inset-y-0 right-0 inline-flex items-center justify-center px-2 opacity-75 focus:ring-accent hover:opacity-100 focus:opacity-100" title="<?= lang('Episode.list.search.clear') ?>" data-tooltip="bottom"><?= icon('close-fill', [
|
||||
<a href="<?= route_to('episode-list', $podcast->id) ?>" class="absolute inset-y-0 right-0 inline-flex items-center justify-center px-2 opacity-75 hover:opacity-100 focus:opacity-100" title="<?= lang('Episode.list.search.clear') ?>" data-tooltip="bottom"><?= icon('close-fill', [
|
||||
'class' => 'text-lg',
|
||||
]) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<Button type="submit" variant="secondary" class="px-3 ml-2 rounded-lg shadow-md" title="<?= lang('Episode.list.search.submit') ?>" data-tooltip="bottom" isSquared="true"><?= icon('search-fill', [
|
||||
<x-Button type="submit" variant="secondary" class="px-3 ml-2 rounded-lg shadow-md" title="<?= lang('Episode.list.search.submit') ?>" data-tooltip="bottom" isSquared="true"><?= icon('search-fill', [
|
||||
'class' => 'text-xl',
|
||||
]) ?></Button>
|
||||
]) ?></x-Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -161,10 +161,10 @@ data_table(
|
||||
HTML),
|
||||
];
|
||||
}
|
||||
return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
icon('more-2-fill') .
|
||||
'</button>' .
|
||||
'<DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
|
||||
'<x-DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labelledby="more-dropdown-' . $episode->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button variant="primary" uri="<?= route_to('person-create') ?>" iconLeft="add-fill"><?= lang('Person.create') ?></Button>
|
||||
<x-Button variant="primary" uri="<?= route_to('person-create') ?>" iconLeft="add-fill"><?= lang('Person.create') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
@ -18,12 +18,12 @@
|
||||
<form action="<?= route_to('episode-persons-manage', $podcast->id, $episode->id) ?>" method="POST" class="max-w-xl">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('Person.episode_form.add_section_title') ?>"
|
||||
subtitle="<?= lang('Person.episode_form.add_section_subtitle') ?>"
|
||||
>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
id="persons"
|
||||
name="persons[]"
|
||||
@ -31,10 +31,10 @@
|
||||
hint="<?= esc(lang('Person.episode_form.persons_hint')) ?>"
|
||||
options="<?= esc(json_encode($personOptions)) ?>"
|
||||
selected="<?= esc(json_encode(old('persons', []))) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
id="roles"
|
||||
name="roles[]"
|
||||
@ -44,9 +44,9 @@
|
||||
selected="<?= esc(json_encode(old('roles', []))) ?>"
|
||||
/>
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Person.episode_form.submit_add') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Person.episode_form.submit_add') ?></x-Button>
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
</form>
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($person): string {
|
||||
// @icon('delete-bin-fill')
|
||||
return '<Button uri="' . route_to('episode-person-remove', $person->podcast_id, $person->episode_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.episode_form.remove') . '</Button>';
|
||||
return '<x-Button uri="' . route_to('episode-person-remove', $person->podcast_id, $person->episode_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.episode_form.remove') . '</x-Button>';
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -16,7 +16,7 @@
|
||||
'class' => 'mr-2 text-lg',
|
||||
]) . lang('Episode.publish_form.back_to_episode_dashboard'),
|
||||
[
|
||||
'class' => 'inline-flex items-center font-semibold mr-4 text-sm focus:ring-accent',
|
||||
'class' => 'inline-flex items-center font-semibold mr-4 text-sm',
|
||||
],
|
||||
) ?>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 mb-2">
|
||||
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
|
||||
<x-Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
|
||||
</div>
|
||||
<div class="flex border-y">
|
||||
<img src="<?= $episode->cover
|
||||
@ -82,14 +82,14 @@
|
||||
<legend class="text-lg font-semibold"><?= lang(
|
||||
'Episode.publish_form.publication_date',
|
||||
) ?></legend>
|
||||
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
|
||||
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></x-Forms.Radio>
|
||||
<div class="inline-flex flex-wrap items-center radio-toggler">
|
||||
<input
|
||||
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
|
||||
class="w-6 h-6 border-contrast text-accent-base border-3"
|
||||
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') && old('publication_method') === 'schedule' ? 'checked' : '' ?> />
|
||||
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
|
||||
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
|
||||
<div class="w-full mt-2 radio-toggler-element">
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="DatetimePicker"
|
||||
name="scheduled_publication_date"
|
||||
label="<?= esc(lang('Episode.publish_form.scheduled_publication_date')) ?>"
|
||||
@ -101,11 +101,11 @@
|
||||
</fieldset>
|
||||
<?php endif ?>
|
||||
|
||||
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>
|
||||
<x-Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></x-Alert>
|
||||
|
||||
<div class="flex items-center justify-between w-full mt-4">
|
||||
<Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></Button>
|
||||
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit') ?>"><?= lang('Episode.publish_form.submit') ?></Button>
|
||||
<x-Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></x-Button>
|
||||
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit') ?>"><?= lang('Episode.publish_form.submit') ?></x-Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -24,16 +24,16 @@
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="client_timezone" value="UTC" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="DatetimePicker"
|
||||
name="new_publication_date"
|
||||
label="<?= esc(lang('Episode.publish_date_edit_form.new_publication_date')) ?>"
|
||||
hint="<?= esc(lang('Episode.publish_date_edit_form.new_publication_date_hint')) ?>"
|
||||
value="<?= $episode->published_at ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<Button variant="primary" type="submit" class="mt-4"><?= lang('Episode.publish_date_edit_form.submit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="mt-4"><?= lang('Episode.publish_date_edit_form.submit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 mb-2">
|
||||
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= esc($post->message) ?>" rows="2" />
|
||||
<x-Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= esc($post->message) ?>" rows="2" />
|
||||
</div>
|
||||
<div class="flex border-y">
|
||||
<img src="<?= $episode->cover
|
||||
@ -86,14 +86,14 @@
|
||||
<legend class="text-lg font-semibold"><?= lang(
|
||||
'Episode.publish_form.publication_date',
|
||||
) ?></legend>
|
||||
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
|
||||
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></x-Forms.Radio>
|
||||
<div class="inline-flex flex-wrap items-center radio-toggler">
|
||||
<input
|
||||
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
|
||||
class="w-6 h-6 border-contrast text-accent-base border-3"
|
||||
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') ? old('publication_method') === 'schedule' : 'checked' ?> />
|
||||
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
|
||||
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
|
||||
<div class="w-full mt-2 radio-toggler-element">
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="DatetimePicker"
|
||||
name="scheduled_publication_date"
|
||||
label="<?= esc(lang('Episode.publish_form.scheduled_publication_date')) ?>"
|
||||
@ -105,11 +105,11 @@
|
||||
</fieldset>
|
||||
<?php endif ?>
|
||||
|
||||
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>
|
||||
<x-Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></x-Alert>
|
||||
|
||||
<div class="flex items-center justify-between w-full mt-4">
|
||||
<Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></Button>
|
||||
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit_edit') ?>"><?= lang('Episode.publish_form.submit_edit') ?></Button>
|
||||
<x-Button uri="<?= route_to('episode-publish-cancel', $podcast->id, $episode->id) ?>" variant="danger"><?= lang('Episode.publish_form.cancel_publication') ?></x-Button>
|
||||
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Episode.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Episode.publish_form.submit_edit') ?>"><?= lang('Episode.publish_form.submit_edit') ?></x-Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('soundbites-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Soundbite.create') ?></Button>
|
||||
<x-Button uri="<?= route_to('soundbites-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Soundbite.create') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
@ -26,10 +26,10 @@
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($soundbite): string {
|
||||
return '<button id="more-dropdown-' . $soundbite->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $soundbite->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
return '<button id="more-dropdown-' . $soundbite->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $soundbite->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
icon('more-2-fill') .
|
||||
'</button>' .
|
||||
'<DropdownMenu id="more-dropdown-' . $soundbite->id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([
|
||||
'<x-DropdownMenu id="more-dropdown-' . $soundbite->id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([
|
||||
[
|
||||
'type' => 'link',
|
||||
'title' => lang('Soundbite.delete'),
|
||||
|
@ -14,10 +14,10 @@
|
||||
<form id="soundbites-form" action="<?= route_to('episode-soundbites-edit', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('Soundbite.form.soundbite_title')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
class="max-w-sm"
|
||||
/>
|
||||
<audio-clipper start-time="<?= old('start_time', 0) ?>" audio-duration="<?= $episode->audio->duration ?>" duration="<?= old('duration', $episode->audio->duration >= 60 ? 60 : $episode->audio->duration) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>" class="mt-8">
|
||||
@ -29,7 +29,7 @@
|
||||
</audio-clipper>
|
||||
|
||||
<?php // @icon('arrow-right-fill')?>
|
||||
<Button variant="primary" type="submit" class="self-end mt-4" iconRight="arrow-right-fill"><?= lang('Soundbite.form.submit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end mt-4" iconRight="arrow-right-fill"><?= lang('Soundbite.form.submit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -13,13 +13,13 @@
|
||||
<form action="<?= route_to('episode-unpublish', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col max-w-lg mx-auto">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Alert variant="danger" class="font-semibold"><?= lang('Episode.unpublish_form.disclaimer') ?></Alert>
|
||||
<x-Alert variant="danger" class="font-semibold"><?= lang('Episode.unpublish_form.disclaimer') ?></x-Alert>
|
||||
|
||||
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Episode.unpublish_form.understand') ?></Forms.Checkbox>
|
||||
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Episode.unpublish_form.understand') ?></x-Forms.Checkbox>
|
||||
|
||||
<div class="self-end mt-4">
|
||||
<Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></Button>
|
||||
<Button type="submit" variant="danger"><?= lang('Episode.unpublish_form.submit') ?></Button>
|
||||
<x-Button uri="<?= route_to('episode-view', $podcast->id, $episode->id) ?>"><?= lang('Common.cancel') ?></x-Button>
|
||||
<x-Button type="submit" variant="danger"><?= lang('Episode.unpublish_form.submit') ?></x-Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
@ -16,7 +16,7 @@ use CodeIgniter\I18n\Time;
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('VideoClip.create') ?></Button>
|
||||
<x-Button uri="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('VideoClip.create') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
@ -52,7 +52,7 @@ use CodeIgniter\I18n\Time;
|
||||
'passed' => '',
|
||||
];
|
||||
|
||||
return '<Pill variant="' . $pillVariantMap[$videoClip->status] . '" icon="' . $pillIconMap[$videoClip->status] . '" iconClass="' . $pillIconClassMap[$videoClip->status] . '" hint="' . lang('VideoClip.list.status.' . $videoClip->status . '_hint') . '">' . lang('VideoClip.list.status.' . $videoClip->status) . '</Pill>';
|
||||
return '<x-Pill variant="' . $pillVariantMap[$videoClip->status] . '" icon="' . $pillIconMap[$videoClip->status] . '" iconClass="' . $pillIconClassMap[$videoClip->status] . '" hint="' . lang('VideoClip.list.status.' . $videoClip->status . '_hint') . '">' . lang('VideoClip.list.status.' . $videoClip->status) . '</x-Pill>';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -63,7 +63,7 @@ use CodeIgniter\I18n\Time;
|
||||
'portrait' => 'aspect-[9/16]',
|
||||
'squared' => 'aspect-square',
|
||||
];
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . lang('VideoClip.format.' . $videoClip->format) . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '">' . icon('play-fill') . '</span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . esc($videoClip->title) . '</span><span class="ml-1 text-sm">by ' . esc($videoClip->user->username) . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . lang('VideoClip.format.' . $videoClip->format) . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '">' . icon('play-fill') . '</span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . esc($videoClip->title) . '</span><span class="ml-1 text-sm">by ' . esc($videoClip->user->username) . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -98,14 +98,14 @@ use CodeIgniter\I18n\Time;
|
||||
helper('misc');
|
||||
$filename = 'clip-' . slugify($videoClip->title) . "-{$videoClip->start_time}-{$videoClip->end_time}";
|
||||
// @icon('import-fill')
|
||||
$downloadButton = '<IconButton glyph="import-fill" uri="' . $videoClip->media->file_url . '" download="' . $filename . '">' . lang('VideoClip.download_clip') . '</IconButton>';
|
||||
$downloadButton = '<x-IconButton glyph="import-fill" uri="' . $videoClip->media->file_url . '" download="' . $filename . '">' . lang('VideoClip.download_clip') . '</x-IconButton>';
|
||||
}
|
||||
|
||||
return '<div class="inline-flex items-center gap-x-2">' . $downloadButton .
|
||||
'<button id="more-dropdown-' . $videoClip->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $videoClip->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
'<button id="more-dropdown-' . $videoClip->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $videoClip->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
icon('more-2-fill') .
|
||||
'</button>' .
|
||||
'<DropdownMenu id="more-dropdown-' . $videoClip->id . '-menu" labelledby="more-dropdown-' . $videoClip->id . '" offsetY="-24" items="' . esc(json_encode([
|
||||
'<x-DropdownMenu id="more-dropdown-' . $videoClip->id . '-menu" labelledby="more-dropdown-' . $videoClip->id . '" offsetY="-24" items="' . esc(json_encode([
|
||||
[
|
||||
'type' => 'link',
|
||||
'title' => lang('VideoClip.go_to_page'),
|
||||
|
@ -31,48 +31,48 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end w-full max-w-xl xl:max-w-sm 2xl:max-w-xl gap-y-4">
|
||||
<Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
|
||||
<Forms.Field
|
||||
<x-Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('VideoClip.form.clip_title')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
/>
|
||||
<fieldset class="flex flex-wrap gap-x-1 gap-y-2">
|
||||
<legend><?= lang('VideoClip.form.format.label') ?></legend>
|
||||
<Forms.RadioButton
|
||||
<x-Forms.RadioButton
|
||||
value="landscape"
|
||||
name="format"
|
||||
isChecked="true"
|
||||
required="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isRequired="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="portrait"
|
||||
name="format"
|
||||
required="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.portrait_hint')) ?>"><?= lang('VideoClip.format.portrait') ?></Forms.RadioButton>
|
||||
<Forms.RadioButton
|
||||
isRequired="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.portrait_hint')) ?>"><?= lang('VideoClip.format.portrait') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="squared"
|
||||
name="format"
|
||||
required="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.squared_hint')) ?>"><?= lang('VideoClip.format.squared') ?></Forms.RadioButton>
|
||||
isRequired="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.squared_hint')) ?>"><?= lang('VideoClip.format.squared') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend><?= lang('VideoClip.form.theme') ?></legend>
|
||||
<div class="grid gap-x-4 gap-y-2 grid-cols-colorButtons">
|
||||
<?php foreach (config('MediaClipper')->themes as $themeName => $colors): ?>
|
||||
<Forms.ColorRadioButton
|
||||
<x-Forms.ColorRadioButton
|
||||
class="mx-auto"
|
||||
value="<?= esc($themeName) ?>"
|
||||
name="theme"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
|
||||
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
|
||||
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</fieldset>
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
<?php // @icon('arrow-right-fill')?>
|
||||
<Button variant="primary" type="submit" iconRight="arrow-right-fill" class="self-end"><?= lang('VideoClip.form.submit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" iconRight="arrow-right-fill" class="self-end"><?= lang('VideoClip.form.submit') ?></x-Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -12,9 +12,9 @@
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col items-start">
|
||||
<Heading class="flex items-center gap-x-2"><?= icon('alert-fill', [
|
||||
<x-Heading class="flex items-center gap-x-2"><?= icon('alert-fill', [
|
||||
'class' => 'flex-shrink-0 text-xl text-orange-600',
|
||||
]) ?><?= lang('VideoClip.requirements.title') ?></Heading>
|
||||
]) ?><?= lang('VideoClip.requirements.title') ?></x-Heading>
|
||||
<p class="max-w-sm font-semibold text-gray-500"><?= lang('VideoClip.requirements.missing') ?></p>
|
||||
<div class="flex flex-col mt-4">
|
||||
<?php foreach ($checks as $requirement => $value): ?>
|
||||
|
@ -12,19 +12,19 @@
|
||||
<?= publication_pill(
|
||||
$episode->published_at,
|
||||
$episode->publication_status,
|
||||
'text-sm ml-2 align-middle',
|
||||
'text-sm align-middle',
|
||||
) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php if ($episode->publication_status === 'published'): ?>
|
||||
<?php // @icon('history-fill')?>
|
||||
<IconButton
|
||||
<x-IconButton
|
||||
uri="<?= route_to('episode-publish_date_edit', $podcast->id, $episode->id) ?>"
|
||||
glyph="history-fill"
|
||||
variant="secondary"
|
||||
glyphClass="text-xl"
|
||||
><?= lang('Episode.publish_date_edit') ?></IconButton>
|
||||
><?= lang('Episode.publish_date_edit') ?></x-IconButton>
|
||||
<?php endif; ?>
|
||||
<?= publication_button(
|
||||
$podcast->id,
|
||||
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Charts.XY title="<?= lang('Charts.episode_by_day') ?>" dataUrl="<?= route_to(
|
||||
<x-Charts.XY title="<?= lang('Charts.episode_by_day') ?>" dataUrl="<?= route_to(
|
||||
'analytics-filtered-data',
|
||||
$podcast->id,
|
||||
'PodcastByEpisode',
|
||||
@ -49,7 +49,7 @@
|
||||
$episode->id,
|
||||
) ?>"/>
|
||||
|
||||
<Charts.XY title="<?= lang('Charts.episode_by_month') ?>" dataUrl="<?= route_to(
|
||||
<x-Charts.XY title="<?= lang('Charts.episode_by_month') ?>" dataUrl="<?= route_to(
|
||||
'analytics-filtered-data',
|
||||
$podcast->id,
|
||||
'PodcastByEpisode',
|
||||
|
@ -14,12 +14,12 @@
|
||||
<form action="<?= route_to('fediverse-attempt-block-actor') ?>" method="POST" class="flex flex-col max-w-md">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="handle"
|
||||
label="<?= esc(lang('Fediverse.block_lists_form.handle')) ?>"
|
||||
hint="<?= esc(lang('Fediverse.block_lists_form.handle_hint')) ?>"
|
||||
required="true" />
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
|
||||
isRequired="true" />
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></x-Button>
|
||||
</form>
|
||||
|
||||
<?= data_table(
|
||||
@ -40,7 +40,7 @@
|
||||
$blockedActor->id .
|
||||
'" />' .
|
||||
csrf_field() .
|
||||
'<Button uri="' . route_to('fediverse-unblock-actor', esc($blockedActor->username)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</Button>' .
|
||||
'<x-Button uri="' . route_to('fediverse-unblock-actor', esc($blockedActor->username)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</x-Button>' .
|
||||
'</form>';
|
||||
},
|
||||
],
|
||||
|
@ -14,11 +14,11 @@
|
||||
<form action="<?= route_to('fediverse-attempt-block-domain') ?>" method="POST" class="flex flex-col max-w-md">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="domain"
|
||||
label="<?= esc(lang('Fediverse.block_lists_form.domain')) ?>"
|
||||
required="true" />
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></Button>
|
||||
isRequired="true" />
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Fediverse.block_lists_form.submit') ?></x-Button>
|
||||
</form>
|
||||
|
||||
<?= data_table(
|
||||
@ -39,7 +39,7 @@
|
||||
esc($blockedDomain->name) .
|
||||
'" />' .
|
||||
csrf_field() .
|
||||
'<Button uri="' . route_to('fediverse-unblock-domain', esc($blockedDomain->name)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</Button>' .
|
||||
'<x-Button uri="' . route_to('fediverse-unblock-domain', esc($blockedDomain->name)) . '" variant="info" size="small" type="submit">' . lang('Fediverse.list.unblock') . '</x-Button>' .
|
||||
'</form>';
|
||||
},
|
||||
],
|
||||
|
@ -38,9 +38,9 @@ use Modules\PodcastImport\Entities\TaskStatus;
|
||||
'passed' => '',
|
||||
];
|
||||
|
||||
$errorHint = $importTask->status === TaskStatus::Failed ? hint_tooltip(esc($importTask->error), 'ml-1') : '';
|
||||
$errorHint = $importTask->status === TaskStatus::Failed ? '<x-Hint class="ml-1">' . esc($importTask->error) . '</x-Hint>' : '';
|
||||
|
||||
return '<div class="flex items-center"><Pill variant="' . $pillVariantMap[$importTask->status->value] . '" icon="' . $pillIconMap[$importTask->status->value] . '" iconClass="' . $pillIconClassMap[$importTask->status->value] . '" hint="' . lang('PodcastImport.queue.status.' . $importTask->status->value . '_hint') . '">' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '</Pill>' . $errorHint . '</div>';
|
||||
return '<div class="flex items-center"><x-Pill variant="' . $pillVariantMap[$importTask->status->value] . '" icon="' . $pillIconMap[$importTask->status->value] . '" iconClass="' . $pillIconClassMap[$importTask->status->value] . '" hint="' . lang('PodcastImport.queue.status.' . $importTask->status->value . '_hint') . '">' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '</x-Pill>' . $errorHint . '</div>';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -86,10 +86,10 @@ use Modules\PodcastImport\Entities\TaskStatus;
|
||||
'cell' => function (PodcastImportTask $importTask) {
|
||||
if ($importTask->episodes_count) {
|
||||
$progressPercentage = (int) ($importTask->getProgress() * 100) . '%';
|
||||
$moreInfoHelper = hint_tooltip(lang('PodcastImport.queue.imported_episodes_hint', [
|
||||
$moreInfoHelper = '<x-Hint class="ml-1">' . lang('PodcastImport.queue.imported_episodes_hint', [
|
||||
'newlyImportedCount' => $importTask->episodes_newly_imported,
|
||||
'alreadyImportedCount' => $importTask->episodes_already_imported,
|
||||
]), 'ml-1');
|
||||
]) . '</x-Hint>';
|
||||
return <<<HTML
|
||||
<div class="flex flex-col">
|
||||
<span>{$progressPercentage}</span>
|
||||
@ -134,10 +134,10 @@ use Modules\PodcastImport\Entities\TaskStatus;
|
||||
}
|
||||
|
||||
return '<div class="inline-flex items-center gap-x-2">' .
|
||||
'<button id="more-dropdown-' . $importTask->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $importTask->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
'<button id="more-dropdown-' . $importTask->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $importTask->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
icon('more-2-fill') .
|
||||
'</button>' .
|
||||
'<DropdownMenu id="more-dropdown-' . $importTask->id . '-menu" labelledby="more-dropdown-' . $importTask->id . '" offsetY="-24" items="' . esc(json_encode($menuItems)) . '" />' .
|
||||
'<x-DropdownMenu id="more-dropdown-' . $importTask->id . '-menu" labelledby="more-dropdown-' . $importTask->id . '" offsetY="-24" items="' . esc(json_encode($menuItems)) . '" />' .
|
||||
'</div>';
|
||||
},
|
||||
],
|
||||
|
@ -13,51 +13,51 @@
|
||||
<form action="<?= route_to('import') ?>" method="POST" enctype='multipart/form-data' class="flex flex-col w-full max-w-xl gap-y-8">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('PodcastImport.old_podcast_section_title') ?>">
|
||||
<?php // @icon('scales-3-fill')?>
|
||||
<Alert glyph="scales-3-fill" variant="info" title="<?= lang('PodcastImport.old_podcast_legal_disclaimer_title') ?>"><?= lang('PodcastImport.old_podcast_legal_disclaimer') ?></Alert>
|
||||
<Forms.Field
|
||||
<x-Alert glyph="scales-3-fill" variant="info" title="<?= lang('PodcastImport.old_podcast_legal_disclaimer_title') ?>"><?= lang('PodcastImport.old_podcast_legal_disclaimer') ?></x-Alert>
|
||||
<x-Forms.Field
|
||||
name="imported_feed_url"
|
||||
label="<?= esc(lang('PodcastImport.imported_feed_url')) ?>"
|
||||
hint="<?= esc(lang('PodcastImport.imported_feed_url_hint')) ?>"
|
||||
placeholder="https://…"
|
||||
type="url"
|
||||
required="true" />
|
||||
</Forms.Section>
|
||||
isRequired="true" />
|
||||
</x-Forms.Section>
|
||||
|
||||
|
||||
<Forms.Section
|
||||
<x-Forms.Section
|
||||
title="<?= lang('PodcastImport.new_podcast_section_title') ?>" >
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Forms.Label for="handle" hint="<?= esc(lang('Podcast.form.handle_hint')) ?>"><?= lang('Podcast.form.handle') ?></Forms.Label>
|
||||
<x-Forms.Label for="handle" hint="<?= esc(lang('Podcast.form.handle_hint')) ?>"><?= lang('Podcast.form.handle') ?></x-Forms.Label>
|
||||
<div class="relative">
|
||||
<?= icon('at-line', [
|
||||
'class' => 'absolute inset-0 h-full text-xl opacity-40 left-3',
|
||||
]) ?>
|
||||
<Forms.Input name="handle" class="w-full pl-8" required="true" />
|
||||
<x-Forms.Input name="handle" class="w-full pl-8" isRequired="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="language"
|
||||
label="<?= esc(lang('Podcast.form.language')) ?>"
|
||||
selected="<?= $browserLang ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
options="<?= esc(json_encode($languageOptions)) ?>" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="category"
|
||||
label="<?= esc(lang('Podcast.form.category')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
options="<?= esc(json_encode($categoryOptions)) ?>" />
|
||||
|
||||
</Forms.Section>
|
||||
</x-Forms.Section>
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('PodcastImport.submit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('PodcastImport.submit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('loop-left-fill')?>
|
||||
<Button uri="<?= route_to('podcast-imports-sync', $podcast->id) ?>" variant="primary" iconLeft="loop-left-fill"><?= lang('PodcastImport.syncForm.title') ?></Button>
|
||||
<x-Button uri="<?= route_to('podcast-imports-sync', $podcast->id) ?>" variant="primary" iconLeft="loop-left-fill"><?= lang('PodcastImport.syncForm.title') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
@ -11,14 +11,14 @@
|
||||
<?= $this->section('content') ?>
|
||||
<form action="<?= route_to('podcast-imports-sync', $podcast->id) ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="feed_url"
|
||||
label="<?= esc(lang('PodcastImport.syncForm.feed_url')) ?>"
|
||||
hint="<?= esc(lang('PodcastImport.syncForm.feed_url_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
value="<?= $podcast->imported_feed_url ?? '' ?>"
|
||||
/>
|
||||
<Button variant="primary" class="self-end" type="submit"><?= lang('PodcastImport.syncForm.submit') ?></Button>
|
||||
<x-Button variant="primary" class="self-end" type="submit"><?= lang('PodcastImport.syncForm.submit') ?></x-Button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('podcast-imports-add') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.import') ?></Button>
|
||||
<x-Button uri="<?= route_to('podcast-imports-add') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.import') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
|
@ -13,18 +13,18 @@
|
||||
|
||||
<form action="<?= route_to('change-password') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4">
|
||||
<?= csrf_field() ?>
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="password"
|
||||
label="<?= esc(lang('User.form.password')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
type="password" />
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="new_password"
|
||||
label="<?= esc(lang('User.form.new_password')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
type="password"
|
||||
autocomplete="new-password" />
|
||||
<Button variant="primary" class="self-end" type="submit"><?= lang('User.form.submit_password_change') ?></Button>
|
||||
<x-Button variant="primary" class="self-end" type="submit"><?= lang('User.form.submit_password_change') ?></x-Button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
@ -14,29 +14,29 @@
|
||||
<form action="<?= route_to('page-create') ?>" method="POST" class="flex flex-col max-w-3xl gap-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('Page.form.title')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title"
|
||||
class="max-w-sm" />
|
||||
|
||||
<div class="flex flex-col max-w-sm">
|
||||
<Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
|
||||
<x-Forms.Label for="slug"><?= lang('Page.form.permalink') ?></x-Forms.Label>
|
||||
<permalink-edit class="inline-flex items-center w-full text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= base_url('pages') ?>">
|
||||
<span slot="domain" class="flex-shrink-0">…/pages/</span>
|
||||
<Forms.Input name="slug" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
<x-Forms.Input name="slug" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" />
|
||||
</permalink-edit>
|
||||
</div>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="content"
|
||||
label="<?= esc(lang('Page.form.content')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
rows="20" />
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_create') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_create') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -14,32 +14,32 @@
|
||||
<form action="<?= route_to('page-edit', $page->id) ?>" method="POST" class="flex flex-col max-w-3xl gap-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="title"
|
||||
label="<?= esc(lang('Page.form.title')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title"
|
||||
value="<?= esc($page->title) ?>"
|
||||
slot="slug-input"
|
||||
class="max-w-sm" />
|
||||
|
||||
<div class="flex flex-col max-w-sm">
|
||||
<Forms.Label for="slug"><?= lang('Page.form.permalink') ?></Forms.Label>
|
||||
<x-Forms.Label for="slug"><?= lang('Page.form.permalink') ?></x-Forms.Label>
|
||||
<permalink-edit class="inline-flex items-center text-xs" edit-label="<?= lang('Common.edit') ?>" copy-label="<?= lang('Common.copy') ?>" copied-label="<?= lang('Common.copied') ?>" permalink-base="<?= base_url('pages') ?>">
|
||||
<span slot="domain" class="flex-shrink-0">…/pages/<span>
|
||||
<Forms.Input name="slug" value="<?= esc($page->slug) ?>" required="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= esc($page->slug) ?>"/>
|
||||
<x-Forms.Input name="slug" value="<?= esc($page->slug) ?>" isRequired="true" data-slugify="slug" slot="slug-input" class="flex-1 text-xs" value="<?= esc($page->slug) ?>"/>
|
||||
</permalink-edit>
|
||||
</div>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="content"
|
||||
label="<?= esc(lang('Page.form.content')) ?>"
|
||||
value="<?= esc($page->content_markdown) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
rows="20" />
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_edit') ?></Button>
|
||||
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Page.form.submit_edit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button uri="<?= route_to('page-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Page.create') ?></Button>
|
||||
<x-Button uri="<?= route_to('page-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Page.create') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
@ -31,9 +31,9 @@
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($page) {
|
||||
return '<Button uri="' . route_to('page', esc($page->slug)) . '" variant="secondary" size="small">' . lang('Page.go_to_page') . '</Button>' .
|
||||
'<Button uri="' . route_to('page-edit', $page->id) . '" variant="info" size="small">' . lang('Page.edit') . '</Button>' .
|
||||
'<Button uri="' . route_to('page-delete', $page->id) . '" variant="danger" size="small">' . lang('Page.delete') . '</Button>';
|
||||
return '<x-Button uri="' . route_to('page', esc($page->slug)) . '" variant="secondary" size="small">' . lang('Page.go_to_page') . '</x-Button>' .
|
||||
'<x-Button uri="' . route_to('page-edit', $page->id) . '" variant="info" size="small">' . lang('Page.edit') . '</x-Button>' .
|
||||
'<x-Button uri="' . route_to('page-delete', $page->id) . '" variant="danger" size="small">' . lang('Page.delete') . '</x-Button>';
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?php // @icon('add-fill')?>
|
||||
<Button variant="primary" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add-fill"><?= lang('Page.edit') ?></Button>
|
||||
<x-Button variant="primary" uri="<?= route_to('page-edit', $page->id) ?>" iconLeft="add-fill"><?= lang('Page.edit') ?></x-Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
@ -8,8 +8,8 @@
|
||||
<h2 class="px-4 py-2 font-semibold leading-tight"><?= esc($person->full_name) ?></h2>
|
||||
</div>
|
||||
</a>
|
||||
<button class="absolute top-0 right-0 z-10 p-2 mt-2 mr-2 text-white transition -translate-y-12 rounded-full opacity-0 focus:ring-accent focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $person->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $person->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||
<DropdownMenu id="more-dropdown-<?= $person->id ?>-menu" labelledby="more-dropdown-<?= $person->id ?>" offsetY="-32" items="<?= esc(json_encode([
|
||||
<button class="absolute top-0 right-0 z-10 p-2 mt-2 mr-2 text-white transition -translate-y-12 rounded-full opacity-0 focus:opacity-100 focus:-translate-y-0 group-hover:translate-y-0 bg-black/50 group-hover:opacity-100" id="more-dropdown-<?= $person->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $person->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
|
||||
<x-DropdownMenu id="more-dropdown-<?= $person->id ?>-menu" labelledby="more-dropdown-<?= $person->id ?>" offsetY="-32" items="<?= esc(json_encode([
|
||||
[
|
||||
'type' => 'link',
|
||||
'title' => lang('Person.view'),
|
||||
|
@ -14,32 +14,32 @@
|
||||
<form action="<?= route_to('person-create') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="avatar"
|
||||
label="<?= esc(lang('Person.form.avatar')) ?>"
|
||||
helper="<?= esc(lang('Person.form.avatar_size_hint')) ?>"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="full_name"
|
||||
label="<?= esc(lang('Person.form.full_name')) ?>"
|
||||
hint="<?= esc(lang('Person.form.full_name_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="unique_name"
|
||||
label="<?= esc(lang('Person.form.unique_name')) ?>"
|
||||
hint="<?= esc(lang('Person.form.unique_name_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="slug" />
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="information_url"
|
||||
label="<?= esc(lang('Person.form.information_url')) ?>"
|
||||
hint="<?= esc(lang('Person.form.information_url_hint')) ?>" />
|
||||
|
||||
<Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_create') ?></Button>
|
||||
<x-Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_create') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
@ -14,36 +14,36 @@
|
||||
<form action="<?= route_to('person-edit', $person->id) ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="avatar"
|
||||
label="<?= esc(lang('Person.form.avatar')) ?>"
|
||||
helper="<?= esc(lang('Person.form.avatar_size_hint')) ?>"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="full_name"
|
||||
value="<?= esc($person->full_name) ?>"
|
||||
label="<?= esc(lang('Person.form.full_name')) ?>"
|
||||
hint="<?= esc(lang('Person.form.full_name_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="title" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="unique_name"
|
||||
value="<?= esc($person->unique_name) ?>"
|
||||
label="<?= esc(lang('Person.form.unique_name')) ?>"
|
||||
hint="<?= esc(lang('Person.form.unique_name_hint')) ?>"
|
||||
required="true"
|
||||
isRequired="true"
|
||||
data-slugify="slug" />
|
||||
|
||||
<Forms.Field
|
||||
<x-Forms.Field
|
||||
name="information_url"
|
||||
label="<?= esc(lang('Person.form.information_url')) ?>"
|
||||
hint="<?= esc(lang('Person.form.information_url_hint')) ?>"
|
||||
value="<?= esc($person->information_url) ?>" />
|
||||
|
||||
<Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_edit') ?></Button>
|
||||
<x-Button variant="primary" class="self-end" type="submit"><?= lang('Person.form.submit_edit') ?></x-Button>
|
||||
|
||||
</form>
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user