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:
Yassine Doghri 2024-05-09 17:55:41 +00:00
commit dfb7888aeb
193 changed files with 1630 additions and 1346 deletions

View file

@ -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);
}
}

View file

@ -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',

View file

@ -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',
]);

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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>]+

View file

@ -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, ',');
}
}

View file

@ -15,10 +15,6 @@
&:hover {
@apply underline;
}
&:focus {
@apply ring-accent;
}
}
.breadcrumb-item.active {

View file

@ -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;

View file

@ -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,

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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()}

View file

@ -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']);
}
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -6,6 +6,8 @@ namespace App\Views\Components\Forms;
class XMLEditor extends FormComponent
{
protected array $props = ['content'];
/**
* @var array<string, string>
*/

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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,
]);
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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: ?>

View file

@ -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,

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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',

View file

@ -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;
}

View file

@ -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};
}

View file

@ -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',

View 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;
}

View file

@ -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;
}

View file

@ -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": {

View file

@ -139,6 +139,9 @@ module.exports = {
textDecoration: "none",
},
},
input: {
margin: 0,
},
},
},
sm: {

View file

@ -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>

View file

@ -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; ?>

View file

@ -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,

View file

@ -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>

View file

@ -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; ?>

View file

@ -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',

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>';
},
],
],

View file

@ -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',

View file

@ -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>

View file

@ -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', [

View file

@ -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>

View file

@ -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>

View file

@ -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 ?>

View file

@ -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() ?>

View file

@ -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)) . '" />';
},
],
],

View file

@ -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>';
},
],
],

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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'),

View file

@ -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>

View file

@ -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>

View file

@ -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'),

View file

@ -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>

View file

@ -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): ?>

View file

@ -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',

View file

@ -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>';
},
],

View file

@ -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>';
},
],

View file

@ -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>';
},
],

View file

@ -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>

View file

@ -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') ?>

View file

@ -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() ?>

View file

@ -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() ?>

View file

@ -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() ?>

View file

@ -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>

View file

@ -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>

View file

@ -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>';
},
],
],

View file

@ -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') ?>

View file

@ -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'),

View file

@ -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>

View file

@ -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>

View file

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('person-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Person.create') ?></Button>
<x-Button uri="<?= route_to('person-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Person.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -11,7 +11,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('pencil-fill')?>
<Button uri="<?= route_to('person-edit', $person->id) ?>" variant="secondary" iconLeft="pencil-fill"><?= lang('Person.edit') ?></Button>
<x-Button uri="<?= route_to('person-edit', $person->id) ?>" variant="secondary" iconLeft="pencil-fill"><?= lang('Person.edit') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -1,42 +1,70 @@
<article class="flex flex-col p-4 rounded-xl relative bg-elevated border-3 <?= $plugin->isActive() ? 'border-accent-base' : 'border-subtle' ?>">
<?php if ($plugin->getSettings() !== []): ?>
<?php // @icon('equalizer-fill')?>
<IconButton class="absolute top-0 right-0 mt-4 mr-4" uri="<?= route_to('plugins-general-settings', $plugin->getKey()) ?>" glyph="equalizer-fill"><?= lang('Plugins.settings', [
'pluginName' => $plugin->getName(),
]) ?></IconButton>
<div class="self-end -mb-6">
<?php if($plugin->isActive()): ?>
<x-Pill variant="success" icon="check-fill" class="lowercase" size="small"><?= lang('Plugins.active') ?></x-Pill>
<?php else: ?>
<x-Pill variant="default" icon="close-fill" class="lowercase" size="small"><?= lang('Plugins.inactive') ?></x-Pill>
<?php endif; ?>
</div>
<img class="rounded-full min-w-16 max-w-16 aspect-square" src="<?= $plugin->getIconSrc() ?>">
<div class="flex flex-col items-start mt-2">
<h2 class="flex items-center text-xl font-bold font-display gap-x-2"><?= $plugin->getName() ?><span class="px-1 font-mono text-xs rounded-full bg-subtle"><?= $plugin->getVersion() ?></span></h2>
<p class="font-mono text-xs tracking-wide bg-gray-100"><a href="<?= route_to('plugins-vendor', $plugin->getVendor()) ?>" class="underline underline-offset-2 decoration-2 decoration-dotted hover:decoration-solid decoration-accent"><?= $plugin->getVendor() ?></a>/<?= $plugin->getPackage() ?></p>
<h2 class="flex items-center text-xl font-bold font-display gap-x-2"><a href="<?= route_to('plugins-view', $plugin->getKey()) ?>" class="hover:underline decoration-accent"><?= $plugin->getName() ?></a></h2>
<p class="inline-flex font-mono text-xs">
<span class="inline-flex tracking-wide bg-gray-100">
<a href="<?= route_to('plugins-vendor', $plugin->getVendor()) ?>" class="underline underline-offset-2 decoration-2 decoration-dotted hover:decoration-solid decoration-accent"><?= $plugin->getVendor() ?></a>
<span>/</span>
<a class="underline underline-offset-2 decoration-2 decoration-dotted hover:decoration-solid decoration-accent" href="<?= route_to('plugins-view', $plugin->getKey()) ?>"><?= $plugin->getPackage() ?></a></span>
<span class="mx-1"></span><span class="px-1 font-mono text-xs"><?= $plugin->getVersion() ?></span>
</p>
<p class="mt-2 text-gray-600"><?= $plugin->getDescription() ?></p>
</div>
<footer class="flex items-center justify-between mt-4">
<a href="<?= $plugin->getHomepage() ?>" class="inline-flex items-center text-sm font-semibold underline hover:no-underline gap-x-1" target="_blank" rel="noopener noreferrer"><?= icon('link', [
'class' => 'text-gray-500',
]) . lang('Plugins.website') ?></a>
<footer class="flex items-center justify-between mt-6">
<div class="flex gap-x-2">
<?php if($plugin->isActive()): ?>
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></Button>
</form>
<?php else: ?>
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></Button>
</form>
<?php if ($plugin->getHomepage()): ?>
<?php // @icon('earth-fill')?>
<x-IconButton glyph="earth-fill" uri="<?= $plugin->getHomepage() ?>" isExternal="true"><?= lang('Plugins.website') ?></x-IconButton>
<?php endif; ?>
<?php if ($plugin->getRepository()): ?>
<?php // @icon('git-repository-fill')?>
<x-IconButton glyph="git-repository-fill" uri="<?= $plugin->getRepository()->url ?>" isExternal="true"><?= lang('Plugins.repository') ?></x-IconButton>
<?php endif; ?>
</div>
<div class="flex gap-x-2">
<?php if($plugin->isActive()): ?>
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<x-Button type="submit" variant="danger" size="small"><?= lang('Plugins.deactivate') ?></x-Button>
</form>
<?php else: ?>
<form class="flex flex-col items-end justify-end gap-2" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<x-Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></x-Button>
</form>
<?php endif; ?>
<?php if ($plugin->getSettings() !== []): ?>
<?php // @icon('equalizer-fill')?>
<x-IconButton uri="<?= route_to('plugins-general-settings', $plugin->getKey()) ?>" glyph="equalizer-fill"><?= lang('Plugins.settings') ?></x-IconButton>
<?php endif; ?>
<button class="p-2 rounded-full" id="more-dropdown-<?= $plugin->getKey() ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $plugin->getKey() ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
<?php $items = [[
'type' => 'link',
'title' => icon('delete-bin-fill', [
'class' => 'text-gray-500',
]) . lang('Plugins.uninstall'),
'uri' => route_to('plugins-uninstall', $plugin->getKey()),
'class' => 'font-semibold text-red-600',
]]; ?>
<DropdownMenu id="more-dropdown-<?= $plugin->getKey() ?>-menu" labelledby="more-dropdown-<?= $plugin->getKey() ?>" placement="top-end" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
<?php $items = [
[
'type' => 'link',
'title' => lang('Plugins.view'),
'uri' => route_to('plugins-view', $plugin->getKey()),
],
[
'type' => 'separator',
],
[
'type' => 'link',
'title' => icon('delete-bin-fill', [
'class' => 'text-gray-500',
]) . lang('Plugins.uninstall'),
'uri' => route_to('plugins-uninstall', $plugin->getKey()),
'class' => 'font-semibold text-red-600',
],
]; ?>
<x-DropdownMenu id="more-dropdown-<?= $plugin->getKey() ?>-menu" labelledby="more-dropdown-<?= $plugin->getKey() ?>" placement="bottom-end" offsetY="-32" items="<?= esc(json_encode($items)) ?>" />
</div>
</footer>
</article>

View file

@ -1,7 +1,7 @@
<form method="POST" action="<?= $action ?>" class="flex flex-col max-w-sm gap-4" >
<?= csrf_field() ?>
<?php foreach ($plugin->getSettingsFields($type) as $field): ?>
<Forms.Field
<x-Forms.Field
name="<?= esc($field->key) ?>"
label="<?= esc($field->label) ?>"
hint="<?= esc($field->hint) ?>"
@ -10,5 +10,5 @@
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
/>
<?php endforeach; ?>
<Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></Button>
<x-Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></x-Button>
</form>

View file

@ -1,15 +1,11 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Plugins.installed', [
'count' => $total,
]) ?>
<?= lang('Plugins.installed') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Plugins.installed', [
'count' => $total,
]) ?>
<?= lang('Plugins.installed') . ' (' . $total . ')' ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -1,16 +1,83 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Plugins.view') ?>
<?= $plugin->getName() ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Plugins.view') ?>
<?= $plugin->getName() ?>
<?= $this->endSection() ?>
<?= $this->section('headerLeft') ?>
<?php if($plugin->isActive()): ?>
<x-Pill variant="success" icon="check-fill" class="lowercase"><?= lang('Plugins.active') ?></x-Pill>
<?php else: ?>
<x-Pill variant="default" icon="close-fill" class="lowercase"><?= lang('Plugins.inactive') ?></x-Pill>
<?php endif; ?>
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<?php if($plugin->isActive()): ?>
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<x-Button type="submit" variant="danger"><?= lang('Plugins.deactivate') ?></x-Button>
</form>
<?php else: ?>
<form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getKey()) ?>">
<?= csrf_field() ?>
<x-Button type="submit" variant="secondary"><?= lang('Plugins.activate') ?></x-Button>
</form>
<?php endif; ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<section class="prose">
<?= $plugin->getReadmeHTML() ?>
</section>
<div class="flex flex-col items-start justify-center gap-8 mx-auto lg:flex-row-reverse">
<aside class="w-full pb-8 border-b lg:sticky lg:max-w-xs top-28 border-subtle lg:border-none">
<h2 class="mb-2 text-2xl font-bold font-display"><?= lang('Plugins.about') ?></h2>
<p><?= $plugin->getDescription() ?></p>
<a href="<?= $plugin->getHomepage() ?>" class="inline-flex items-center mt-2 font-semibold hover:underline gap-x-2"><?= icon('link') . $plugin->getHomepage() ?></a>
<?php if ($plugin->getKeywords() !== []): ?>
<div class="mt-2">
<?php foreach ($plugin->getKeywords() as $keyword): ?>
<span class="px-2 text-sm rounded-full bg-subtle"><?= $keyword ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<ul class="flex flex-col gap-2 mt-4">
<?php if ($plugin->getRepository()): ?>
<li><a href="<?= $plugin->getRepository()->url ?>" class="inline-flex items-center text-sm gap-x-2 hover:underline" target="_blank" rel="noopener noreferrer"><?= icon('git-repository-fill', [
'class' => 'text-gray-500 text-xl',
]) . lang('Plugins.repository') ?></a></li>
<?php endif; ?>
<li class="inline-flex items-center text-sm gap-x-2"><?= icon('scales-3-fill', [
'class' => 'text-gray-500 text-xl',
]) . $plugin->getLicense() ?></li>
</ul>
<?php if ($plugin->getAuthors() !== []): ?>
<h3 class="mt-6 text-lg font-bold font-display"><?= lang('Plugins.authors') ?></h3>
<ul>
<?php foreach ($plugin->getAuthors() as $author): ?>
<li>
<?= $author->name ?>
<?php if ($author->email): ?>
<?php // @icon('mail-fill')?>
<x-IconButton glyph="mail-fill" uri="mailto:<?= $author->email ?>" size="small" isExternal="true"><?= lang('Plugins.author_email', [
'authorName' => $author->name,
]) ?></x-IconButton>
<?php endif; ?>
<?php if ($author->url): ?>
<?php // @icon('earth-fill')?>
<x-IconButton glyph="earth-fill" uri="<?= $author->url ?>" size="small" isExternal="true"><?= lang('Plugins.author_homepage', [
'authorName' => $author->name,
]) ?></x-IconButton>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</aside>
<section class="max-w-2xl prose prose-headings:font-display">
<?= $plugin->getReadmeHTML() ?>
</section>
</div>
<?= $this->endSection() ?>

View file

@ -39,8 +39,8 @@
<p class="text-sm transition duration-150 opacity-0 group-focus:opacity-100 group-hover:opacity-100">@<?= esc($podcast->handle) ?></p>
</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-<?= $podcast->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $podcast->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
<DropdownMenu id="more-dropdown-<?= $podcast->id ?>-menu" labelledby="more-dropdown-<?= $podcast->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-<?= $podcast->id ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $podcast->id ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button>
<x-DropdownMenu id="more-dropdown-<?= $podcast->id ?>-menu" labelledby="more-dropdown-<?= $podcast->id ?>" offsetY="-32" items="<?= esc(json_encode([
[
'type' => 'link',
'title' => lang('Podcast.view'),

View file

@ -30,20 +30,20 @@
<div class="flex flex-col flex-1 mt-4">
<div class="inline-flex ml-8 -mt-6 gap-x-1">
<a
href="<?= $platform->home_url ?>" class="px-3 py-1 text-xs font-semibold leading-6 underline rounded-full focus:ring-accent text-accent-base hover:no-underline"
href="<?= $platform->home_url ?>" class="px-3 py-1 text-xs font-semibold leading-6 underline rounded-full text-accent-base hover:no-underline"
target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.home_url', [
'platformName' => $platform->label,
]) ?>" data-tooltip="bottom"><?= lang('Platforms.website') ?></a>
<?php if ($platform->submit_url !== null): ?>
<a
href="<?= $platform->submit_url ?>" class="px-3 py-1 text-xs font-semibold leading-6 underline rounded-full focus:ring-accent text-accent-base hover:no-underline"
href="<?= $platform->submit_url ?>" class="px-3 py-1 text-xs font-semibold leading-6 underline rounded-full text-accent-base hover:no-underline"
target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.submit_url', [
'platformName' => $platform->label,
]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></a>
<?php endif; ?>
</div>
<fieldset>
<Forms.Field
<x-Forms.Field
label="<?= esc(lang('Platforms.your_link')) ?>"
class="w-full mt-4"
id="<?= esc($platform->slug) . '_link_url' ?>"
@ -51,14 +51,14 @@
value="<?= esc($platform->link_url) ?>"
type="url"
placeholder="https://…" />
<Forms.Field
<x-Forms.Field
label="<?= esc(lang("Platforms.your_id.{$platform->type}")) ?>"
class="w-full mt-2"
id="<?= esc($platform->slug) . '_account_id' ?>"
name="<?= 'platforms[' . esc($platform->slug) . '][account_id]' ?>"
value="<?= esc($platform->account_id) ?>"
placeholder="<?= lang("Platforms.description.{$platform->type}") ?>" />
<Forms.Toggler size="small" class="mt-4 text-sm" id="<?= esc($platform->slug) . '_visible' ?>" name="<?= 'platforms[' . esc($platform->slug) . '][visible]'?>" value="yes" checked="<?= old(esc($platform->slug) . '_visible', $platform->is_visible ? 'true' : 'false') ?>"><?= lang('Platforms.visible') ?></Forms.Toggler>
<x-Forms.Toggler size="small" class="mt-4 text-sm" id="<?= esc($platform->slug) . '_visible' ?>" name="<?= 'platforms[' . esc($platform->slug) . '][visible]'?>" isChecked="<?= old(esc($platform->slug) . '_visible', $platform->is_visible ? 'true' : 'false') ?>"><?= lang('Platforms.visible') ?></x-Forms.Toggler>
</fieldset>
</div>
</article>

View file

@ -92,7 +92,7 @@ $podcastNavigation = [
foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
$route = route_to('plugins-podcast-settings', $podcast->id, $plugin->getKey());
$podcastNavigation['plugins']['items'][] = $route;
$podcastNavigation['plugins']['items-labels'][] = $plugin->getName();
$podcastNavigation['plugins']['items-labels'][$route] = $plugin->getName();
$podcastNavigation['plugins']['items-permissions'][$route] = 'edit';
}
@ -118,7 +118,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
<a href="<?= route_to(
'podcast-activity',
esc($podcast->handle),
) ?>" class="inline-flex items-center text-sm hover:underline focus:ring-accent"
) ?>" class="inline-flex items-center text-sm hover:underline"
data-tooltip="bottom" title="<?= lang(
'PodcastNavigation.go_to_page',
) ?>">@<?= esc($podcast->handle) ?>
@ -128,7 +128,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
</a>
</div>
<div class="flex flex-col items-start gap-1">
<a href="<?= $podcast->feed_url ?>" class="inline-flex items-center text-xs gap-x-1 focus:ring-accent group hover:underline" target="_blank" rel="noopener noreferrer" data-tooltip="bottom" title="<?= lang('PodcastNavigation.rss_feed') ?>">
<a href="<?= $podcast->feed_url ?>" class="inline-flex items-center text-xs gap-x-1 group hover:underline" target="_blank" rel="noopener noreferrer" data-tooltip="bottom" title="<?= lang('PodcastNavigation.rss_feed') ?>">
<?= icon('rss-fill', [
'class' => 'text-xl text-orange-400 inline-flex items-center justify-center rounded',
]) . 'RSS Feed' . icon('external-link-fill', [
@ -136,7 +136,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
]) ?>
</a>
<?php if ($podcast->is_op3_enabled): ?>
<a href="<?= $podcast->op3_url ?>" class="inline-flex items-center text-xs gap-x-1 focus:ring-accent group hover:underline" data-tooltip="bottom" target="_blank" rel="noopener noreferrer" title="<?= lang('Podcast.form.op3_link') ?>">
<a href="<?= $podcast->op3_url ?>" class="inline-flex items-center text-xs gap-x-1 group hover:underline" data-tooltip="bottom" target="_blank" rel="noopener noreferrer" title="<?= lang('Podcast.form.op3_link') ?>">
<?= icon('line-chart-fill', [
'class' => 'text-xl text-white inline-flex items-center justify-center rounded',
]) . 'OP3' . icon('external-link-fill', [

View file

@ -11,21 +11,21 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_day') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_day') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'ByDay',
) ?>"/>
<Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_month') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_month') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'ByMonth',
) ?>"/>
<Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_bandwidth') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.podcast_by_bandwidth') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',

View file

@ -11,14 +11,14 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.XYDuration class="col-span-1" title="<?= lang('Charts.daily_listening_time') ?>" dataUrl="<?= route_to(
<x-Charts.XYDuration class="col-span-1" title="<?= lang('Charts.daily_listening_time') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'TotalListeningTimeByDay',
) ?>"/>
<Charts.XYDuration class="col-span-1" title="<?= lang('Charts.monthly_listening_time') ?>" dataUrl="<?= route_to(
<x-Charts.XYDuration class="col-span-1" title="<?= lang('Charts.monthly_listening_time') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',

View file

@ -11,19 +11,19 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.Pie title="<?= lang('Charts.by_country_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_country_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByCountry',
'Weekly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_country_yearly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_country_yearly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByCountry',
'Yearly',
) ?>" />
<Charts.Map class="col-span-2" title="<?= lang('Charts.podcast_by_region') ?>" dataUrl="<?= route_to(
<x-Charts.Map class="col-span-2" title="<?= lang('Charts.podcast_by_region') ?>" dataUrl="<?= route_to(
'analytics-full-data',
$podcast->id,
'PodcastByRegion',

View file

@ -11,31 +11,31 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.Pie title="<?= lang('Charts.by_player_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_player_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByPlayer',
'ByAppWeekly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_service_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_service_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByService',
'ByServiceWeekly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_device_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_device_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByPlayer',
'ByDeviceWeekly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_os_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_os_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByPlayer',
'ByOsWeekly',
) ?>" />
<Charts.XY class="col-span-2" title="<?= lang('Charts.podcast_bots') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-2" title="<?= lang('Charts.podcast_bots') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'PodcastByPlayer',

View file

@ -11,13 +11,13 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.Bar title="<?= lang('Charts.by_weekday') ?>" dataUrl="<?= route_to(
<x-Charts.Bar title="<?= lang('Charts.by_weekday') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'ByWeekday',
) ?>" />
<Charts.Bar title="<?= lang('Charts.by_hour') ?>" dataUrl="<?= route_to(
<x-Charts.Bar title="<?= lang('Charts.by_hour') ?>" dataUrl="<?= route_to(
'analytics-full-data',
$podcast->id,
'PodcastByHour',

View file

@ -11,14 +11,14 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.XY class="col-span-1" title="<?= lang('Charts.unique_daily_listeners') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.unique_daily_listeners') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'UniqueListenersByDay',
) ?>"/>
<Charts.XY class="col-span-1" title="<?= lang('Charts.unique_monthly_listeners') ?>" dataUrl="<?= route_to(
<x-Charts.XY class="col-span-1" title="<?= lang('Charts.unique_monthly_listeners') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',

View file

@ -11,24 +11,24 @@
<?= $this->section('content') ?>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Charts.Pie title="<?= lang('Charts.by_domain_weekly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_domain_weekly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'WebsiteByReferer',
'ByDomainWeekly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_domain_yearly') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_domain_yearly') ?>" dataUrl="<?= route_to(
'analytics-data',
$podcast->id,
'WebsiteByReferer',
'ByDomainYearly',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_entry_page') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_entry_page') ?>" dataUrl="<?= route_to(
'analytics-full-data',
$podcast->id,
'WebsiteByEntryPage',
) ?>" />
<Charts.Pie title="<?= lang('Charts.by_browser') ?>" dataUrl="<?= route_to(
<x-Charts.Pie title="<?= lang('Charts.by_browser') ?>" dataUrl="<?= route_to(
'analytics-full-data',
$podcast->id,
'WebsiteByBrowser',

View file

@ -17,87 +17,87 @@
<form action="<?= route_to('podcast-create') ?>" method="POST" enctype='multipart/form-data' class="flex flex-col w-full max-w-xl gap-y-6">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.identity_section_title') ?>"
subtitle="<?= lang('Podcast.form.identity_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="cover"
label="<?= esc(lang('Podcast.form.cover')) ?>"
helper="<?= esc(lang('Podcast.form.cover_size_hint')) ?>"
type="file"
required="true"
isRequired="true"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Podcast.form.title')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description"
label="<?= esc(lang('Podcast.form.description')) ?>"
required="true"
isRequired="true"
disallowList="header,quote" />
<fieldset>
<legend><?= lang('Podcast.form.type.label') ?></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="episodic"
name="type"
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>"
isChecked="true'" ><?= lang('Podcast.form.type.episodic') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="true'" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="serial"
name="type"
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>"
isChecked="false" ><?= lang('Podcast.form.type.serial') ?></Forms.RadioButton>
isChecked="false" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton>
</div>
</fieldset>
<fieldset>
<legend><?= lang('Podcast.form.medium.label') ?></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="podcast"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>"
isChecked="true" ><?= lang('Podcast.form.medium.podcast') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="true" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="music"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>"
isChecked="false" ><?= lang('Podcast.form.medium.music') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="false" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="audiobook"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>"
isChecked="false" ><?= lang('Podcast.form.medium.audiobook') ?></Forms.RadioButton>
isChecked="false" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.classification_section_title') ?>"
subtitle="<?= lang('Podcast.form.classification_section_subtitle') ?>" >
<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.Field
<x-Forms.Field
as="MultiSelect"
name="other_categories[]"
label="<?= esc(lang('Podcast.form.other_categories')) ?>"
@ -105,114 +105,113 @@
options="<?= esc(json_encode($categoryOptions)) ?>" />
<fieldset class="mb-4">
<legend><?= lang('Podcast.form.parental_advisory.label') .
hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?></legend>
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="undefined"
name="parental_advisory"
isChecked="true" ><?= lang('Podcast.form.parental_advisory.undefined') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="true" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="clean"
name="parental_advisory"
isChecked="false" ><?= lang('Podcast.form.parental_advisory.clean') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="false" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="explicit"
name="parental_advisory"
isChecked="false" ><?= lang('Podcast.form.parental_advisory.explicit') ?></Forms.RadioButton>
isChecked="false" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.author_section_title') ?>"
subtitle="<?= lang('Podcast.form.author_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="owner_name"
label="<?= esc(lang('Podcast.form.owner_name')) ?>"
hint="<?= esc(lang('Podcast.form.owner_name_hint')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="owner_email"
type="email"
label="<?= esc(lang('Podcast.form.owner_email')) ?>"
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
required="true" />
isRequired="true" />
<Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" value="yes" checked="true" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></Forms.Toggler>
<x-Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" isChecked="true" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></x-Forms.Toggler>
<Forms.Field
<x-Forms.Field
name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>"
hint="<?= esc(lang('Podcast.form.publisher_hint')) ?>" />
<Forms.Field
<x-Forms.Field
name="copyright"
label="<?= esc(lang('Podcast.form.copyright')) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.fediverse_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
name="banner"
label="<?= esc(lang('Podcast.form.banner')) ?>"
helper="<?= esc(lang('Podcast.form.banner_size_hint')) ?>"
type="file"
accept=".jpg,.jpeg,.png" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section title="<?= lang('Podcast.form.premium') ?>">
<Forms.Toggler class="mt-2" name="premium_by_default" value="yes" checked="false" hint="<?= esc(lang('Podcast.form.premium_by_default_hint')) ?>">
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Section title="<?= lang('Podcast.form.premium') ?>">
<x-Forms.Toggler class="mt-2" name="premium_by_default" isChecked="false" hint="<?= esc(lang('Podcast.form.premium_by_default_hint')) ?>">
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.op3') ?>"
subtitle="<?= lang('Podcast.form.op3_hint') ?>">
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline focus:ring-accent"><?= icon('link', [
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline"><?= icon('link', [
'class' => 'text-sm',
]) ?>op3.dev</a>
<Forms.Toggler name="enable_op3" value="yes" checked="false" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Toggler name="enable_op3" isChecked="false" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="location_name"
label="<?= esc(lang('Podcast.form.location_name')) ?>"
hint="<?= esc(lang('Podcast.form.location_name_hint')) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.advanced_section_title') ?>" >
<Forms.Field
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>"
rows="8" />
<Forms.Field
<x-Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
@ -220,19 +219,19 @@
helper="<?= esc(lang('Podcast.form.verify_txt_helper')) ?>"
rows="5" />
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="true" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<x-Forms.Toggler class="mb-2" name="lock" isChecked="true" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="false" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
</x-Forms.Toggler>
<x-Forms.Toggler class="mb-2" name="block" isChecked="false" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="false">
</x-Forms.Toggler>
<x-Forms.Toggler name="complete" isChecked="false">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
</x-Forms.Toggler>
</Forms.Section>
</x-Forms.Section>
<Button variant="primary" type="submit" class="self-end"><?= lang('Podcast.form.submit_create') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Podcast.form.submit_create') ?></x-Button>
</form>

View file

@ -13,13 +13,13 @@
<form action="<?= route_to('podcast-delete', $podcast->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('Podcast.delete_form.disclaimer') ?></Alert>
<x-Alert variant="danger" class="font-semibold"><?= lang('Podcast.delete_form.disclaimer') ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Podcast.delete_form.understand') ?></Forms.Checkbox>
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Podcast.delete_form.understand') ?></x-Forms.Checkbox>
<div class="self-end mt-4">
<Button uri="<?= route_to('podcast-view', $podcast->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('Podcast.delete_form.submit') ?></Button>
<x-Button uri="<?= route_to('podcast-view', $podcast->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('Podcast.delete_form.submit') ?></x-Button>
</div>
</form>

View file

@ -13,7 +13,7 @@
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<Button variant="primary" type="submit" form="podcast-edit-form"><?= lang('Podcast.form.submit_edit') ?></Button>
<x-Button variant="primary" type="submit" form="podcast-edit-form"><?= lang('Podcast.form.submit_edit') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -21,9 +21,10 @@
<form id="podcast-edit-form" action="<?= route_to('podcast-edit', $podcast->id) ?>" method="POST" enctype='multipart/form-data' class="flex flex-col items-start justify-end gap-4 xl:flex-row-reverse">
<?= csrf_field() ?>
<div class="z-40 flex flex-col w-full max-w-xs overflow-hidden shadow-sm xl:sticky bg-elevated border-3 border-subtle top-24 rounded-xl">
<?php if ($podcast->banner_id !== null): ?>
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast focus:ring-accent top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>" data-tooltip="bottom"><?= icon('delete-bin-fill') ?></a>
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>" data-tooltip="bottom"><?= icon('delete-bin-fill') ?></a>
<?php endif; ?>
<img src="<?= get_podcast_banner_url($podcast, 'small') ?>" alt="" class="w-full aspect-[3/1] bg-header" loading="lazy" />
<div class="flex px-4 py-2">
@ -38,90 +39,89 @@
<div class="flex flex-col w-full max-w-xl gap-y-6">
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.identity_section_title') ?>"
subtitle="<?= lang('Podcast.form.identity_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="cover"
label="<?= esc(lang('Podcast.form.cover')) ?>"
helper="<?= esc(lang('Podcast.form.cover_size_hint')) ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
<x-Forms.Field
name="title"
label="<?= esc(lang('Podcast.form.title')) ?>"
value="<?= esc($podcast->title) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="MarkdownEditor"
name="description"
label="<?= esc(lang('Podcast.form.description')) ?>"
value="<?= esc($podcast->description_markdown) ?>"
required="true"
isRequired="true"
disallowList="header,quote" />
<fieldset>
<legend><?= lang('Podcast.form.type.label') ?></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="episodic"
name="type"
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>"
isChecked="<?= $podcast->type === 'episodic' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.episodic') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $podcast->type === 'episodic' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="serial"
name="type"
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>"
isChecked="<?= $podcast->type === 'serial' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.serial') ?></Forms.RadioButton>
isChecked="<?= $podcast->type === 'serial' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton>
</div>
</fieldset>
<fieldset>
<legend><?= lang('Podcast.form.medium.label') .
hint_tooltip(lang('Podcast.form.medium.hint'), 'ml-1') ?></legend>
<legend><?= lang('Podcast.form.medium.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.medium.hint') ?></x-Hint></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="podcast"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>"
isChecked="<?= $podcast->medium === 'podcast' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.podcast') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $podcast->medium === 'podcast' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="music"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>"
isChecked="<?= $podcast->medium === 'music' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.music') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $podcast->medium === 'music' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="audiobook"
name="medium"
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>"
isChecked="<?= $podcast->medium === 'audiobook' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.audiobook') ?></Forms.RadioButton>
isChecked="<?= $podcast->medium === 'audiobook' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.classification_section_title') ?>"
subtitle="<?= lang('Podcast.form.classification_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
as="Select"
name="language"
label="<?= esc(lang('Podcast.form.language')) ?>"
selected="<?= $podcast->language_code ?>"
options="<?= esc(json_encode($languageOptions)) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="Select"
name="category"
label="<?= esc(lang('Podcast.form.category')) ?>"
selected="<?= $podcast->category_id ?>"
options="<?= esc(json_encode($categoryOptions)) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="MultiSelect"
name="other_categories[]"
label="<?= esc(lang('Podcast.form.other_categories')) ?>"
@ -130,115 +130,114 @@
options="<?= esc(json_encode($categoryOptions)) ?>" />
<fieldset class="mb-4">
<legend><?= lang('Podcast.form.parental_advisory.label') .
hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?></legend>
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend>
<div class="flex gap-2">
<Forms.RadioButton
<x-Forms.RadioButton
value="undefined"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.undefined') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $podcast->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="clean"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.clean') ?></Forms.RadioButton>
<Forms.RadioButton
isChecked="<?= $podcast->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton>
<x-Forms.RadioButton
value="explicit"
name="parental_advisory"
isChecked="<?= $podcast->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.explicit') ?></Forms.RadioButton>
isChecked="<?= $podcast->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.author_section_title') ?>"
subtitle="<?= lang('Podcast.form.author_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="owner_name"
label="<?= esc(lang('Podcast.form.owner_name')) ?>"
value="<?= esc($podcast->owner_name) ?>"
hint="<?= esc(lang('Podcast.form.owner_name_hint')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="owner_email"
type="email"
label="<?= esc(lang('Podcast.form.owner_email')) ?>"
value="<?= esc($podcast->owner_email) ?>"
hint="<?= esc(lang('Podcast.form.owner_email_hint')) ?>"
required="true" />
isRequired="true" />
<Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" value="yes" checked="<?= $podcast->is_owner_email_removed_from_feed ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></Forms.Toggler>
<x-Forms.Toggler class="mt-2" name="is_owner_email_removed_from_feed" isChecked="<?= $podcast->is_owner_email_removed_from_feed ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.is_owner_email_removed_from_feed_hint')) ?>">
<?= lang('Podcast.form.is_owner_email_removed_from_feed') ?></x-Forms.Toggler>
<Forms.Field
<x-Forms.Field
name="publisher"
label="<?= esc(lang('Podcast.form.publisher')) ?>"
value="<?= esc($podcast->publisher) ?>"
hint="<?= esc(lang('Podcast.form.publisher_hint')) ?>" />
<Forms.Field
<x-Forms.Field
name="copyright"
label="<?= esc(lang('Podcast.form.copyright')) ?>"
value="<?= esc($podcast->copyright) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.fediverse_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" value="<?= $podcast->handle ?>" class="w-full pl-8" required="true" readonly="true" />
<x-Forms.Input name="handle" value="<?= $podcast->handle ?>" class="w-full pl-8" isRequired="true" isReadonly="true" />
</div>
</div>
<Forms.Field
<x-Forms.Field
name="banner"
label="<?= esc(lang('Podcast.form.banner')) ?>"
helper="<?= esc(lang('Podcast.form.banner_size_hint')) ?>"
type="file"
accept=".jpg,.jpeg,.png" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section title="<?= lang('Podcast.form.premium') ?>">
<Forms.Toggler class="mt-2" name="premium_by_default" value="yes" checked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.premium_by_default_hint')) ?>">
<?= lang('Podcast.form.premium_by_default') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Section title="<?= lang('Podcast.form.premium') ?>">
<x-Forms.Toggler class="mt-2" name="premium_by_default" isChecked="<?= $podcast->is_premium_by_default ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.premium_by_default_hint')) ?>">
<?= lang('Podcast.form.premium_by_default') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.op3') ?>"
subtitle="<?= lang('Podcast.form.op3_hint') ?>">
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline focus:ring-accent"><?= icon('link', [
<a href="https://op3.dev" target="_blank" rel="noopener noreferrer" class="inline-flex self-start text-xs font-semibold underline gap-x-1 text-skin-muted hover:no-underline"><?= icon('link', [
'class' => 'text-sm',
]) ?>op3.dev</a>
<Forms.Toggler name="enable_op3" value="yes" checked="<?= service('settings')
->get('Analytics.enableOP3', 'podcast:' . $podcast->id) ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></Forms.Toggler>
</Forms.Section>
<x-Forms.Toggler name="enable_op3" isChecked="<?= service('settings')
->get('Analytics.enableOP3', 'podcast:' . $podcast->id) ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.op3_enable_hint')) ?>"><?= lang('Podcast.form.op3_enable') ?></x-Forms.Toggler>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="location_name"
label="<?= esc(lang('Podcast.form.location_name')) ?>"
value="<?= esc($podcast->location_name) ?>"
hint="<?= esc(lang('Podcast.form.location_name_hint')) ?>" />
</Forms.Section>
</x-Forms.Section>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.advanced_section_title') ?>"
subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
@ -246,7 +245,7 @@
content="<?= esc($podcast->custom_rss_string) ?>"
rows="8" />
<Forms.Field
<x-Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
@ -255,7 +254,7 @@
value="<?= esc($podcast->verify_txt) ?>"
rows="5" />
<Forms.Field
<x-Forms.Field
name="new_feed_url"
type="url"
label="<?= esc(lang('Podcast.form.new_feed_url')) ?>"
@ -263,22 +262,22 @@ hint="<?= esc(lang('Podcast.form.new_feed_url_hint')) ?>"
value="<?= esc($podcast->new_feed_url) ?>"
/>
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<x-Forms.Toggler class="mb-2" name="lock" isChecked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="<?= $podcast->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
</x-Forms.Toggler>
<x-Forms.Toggler class="mb-2" name="block" isChecked="<?= $podcast->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="<?= $podcast->is_completed ? 'true' : 'false' ?>">
</x-Forms.Toggler>
<x-Forms.Toggler name="complete" isChecked="<?= $podcast->is_completed ? 'true' : 'false' ?>">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
</x-Forms.Toggler>
</Forms.Section>
</x-Forms.Section>
</div>
</form>
<?php // @icon('delete-bin-fill')?>
<Button class="mt-8" variant="danger" uri="<?= route_to('podcast-delete', $podcast->id) ?>" iconLeft="delete-bin-fill"><?= lang('Podcast.delete') ?></Button>
<x-Button class="mt-8" variant="danger" uri="<?= route_to('podcast-delete', $podcast->id) ?>" iconLeft="delete-bin-fill"><?= lang('Podcast.delete') ?></x-Button>
<?= $this->endSection() ?>

View file

@ -1,10 +1,10 @@
<section class="flex flex-col">
<header class="flex justify-between">
<Heading tagName="h2"><?= lang('Podcast.latest_episodes') ?></Heading>
<x-Heading tagName="h2"><?= lang('Podcast.latest_episodes') ?></x-Heading>
<a href="<?= route_to(
'episode-list',
$podcast->id,
) ?>" class="inline-flex items-center text-sm underline hover:no-underline focus:ring-accent">
) ?>" class="inline-flex items-center text-sm underline hover:no-underline">
<?= lang('Podcast.see_all_episodes') ?>
<?= icon('arrow-right-s-fill', [
'class' => 'ml-2',

View file

@ -13,8 +13,8 @@
// @icon('import-fill')
// @icon('add-fill')
?>
<Button uri="<?= route_to('podcast-imports-add') ?>" variant="secondary" iconLeft="import-fill"><?= lang('Podcast.import') ?></Button>
<Button uri="<?= route_to('podcast-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.create') ?></Button>
<x-Button uri="<?= route_to('podcast-imports-add') ?>" variant="secondary" iconLeft="import-fill"><?= lang('Podcast.import') ?></x-Button>
<x-Button uri="<?= route_to('podcast-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Podcast.create') ?></x-Button>
<?= $this->endSection() ?>

View file

@ -12,36 +12,36 @@
<form id="podcast-edit-form" action="<?= route_to('podcast-monetization-edit', $podcast->id) ?>" method="POST" class="flex flex-col w-full max-w-xl gap-y-6">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Podcast.form.monetization_section_title') ?>"
subtitle="<?= lang('Podcast.form.monetization_section_subtitle') ?>" >
<Forms.Field
<x-Forms.Field
name="payment_pointer"
label="<?= esc(lang('Podcast.form.payment_pointer')) ?>"
value="<?= esc($podcast->payment_pointer) ?>"
hint="<?= esc(lang('Podcast.form.payment_pointer_hint')) ?>" />
<fieldset class="flex flex-col items-start p-4 rounded bg-base">
<Heading tagName="legend" class="float-left" size="small"><?= lang('Podcast.form.partnership') ?></Heading>
<x-Heading tagName="legend" class="float-left" size="small"><?= lang('Podcast.form.partnership') ?></x-Heading>
<div class="flex flex-col w-full clear-left gap-x-2 gap-y-4 md:flex-row">
<div class="flex flex-col flex-shrink w-32">
<Forms.Label for="partner_id" hint="<?= esc(lang('Podcast.form.partner_id_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_id') ?></Forms.Label>
<Forms.Input name="partner_id" value="<?= esc($podcast->partner_id) ?>" />
<x-Forms.Label for="partner_id" hint="<?= esc(lang('Podcast.form.partner_id_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_id') ?></x-Forms.Label>
<x-Forms.Input name="partner_id" value="<?= esc($podcast->partner_id) ?>" />
</div>
<div class="flex flex-col flex-1">
<Forms.Label for="partner_link_url" hint="<?= esc(lang('Podcast.form.partner_link_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_link_url') ?></Forms.Label>
<Forms.Input name="partner_link_url" value="<?= esc($podcast->partner_link_url) ?>" />
<x-Forms.Label for="partner_link_url" hint="<?= esc(lang('Podcast.form.partner_link_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_link_url') ?></x-Forms.Label>
<x-Forms.Input name="partner_link_url" value="<?= esc($podcast->partner_link_url) ?>" />
</div>
</div>
<div class="flex flex-col w-full mt-2">
<Forms.Label for="partner_image_url" hint="<?= esc(lang('Podcast.form.partner_image_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_image_url') ?></Forms.Label>
<Forms.Input name="partner_image_url" value="<?= esc($podcast->partner_image_url) ?>" />
<x-Forms.Label for="partner_image_url" hint="<?= esc(lang('Podcast.form.partner_image_url_hint')) ?>" isOptional="true"><?= lang('Podcast.form.partner_image_url') ?></x-Forms.Label>
<x-Forms.Input name="partner_image_url" value="<?= esc($podcast->partner_image_url) ?>" />
</div>
</fieldset>
</Forms.Section>
</x-Forms.Section>
<Button variant="primary" type="submit" class="self-end"><?= lang('Common.forms.save') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Common.forms.save') ?></x-Button>
</form>
<?= $this->endSection() ?>

View file

@ -9,7 +9,7 @@
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<Button uri="<?= route_to('notification-mark-all-as-read', $podcast->id) ?>" variant="primary"><?= lang('Notifications.mark_all_as_read') ?></Button>
<x-Button uri="<?= route_to('notification-mark-all-as-read', $podcast->id) ?>" variant="primary"><?= lang('Notifications.mark_all_as_read') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('person-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Person.create') ?></Button>
<x-Button uri="<?= route_to('person-create') ?>" variant="primary" iconLeft="add-fill"><?= lang('Person.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -18,12 +18,12 @@
<form action="<?= route_to('podcast-persons-manage', $podcast->id) ?>" method="POST" class="max-w-xl">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Person.podcast_form.add_section_title') ?>"
subtitle="<?= lang('Person.podcast_form.add_section_subtitle') ?>"
>
<Forms.Field
<x-Forms.Field
as="MultiSelect"
id="persons"
name="persons[]"
@ -31,9 +31,9 @@
hint="<?= esc(lang('Person.podcast_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[]"
@ -43,9 +43,9 @@
selected="<?= esc(json_encode(old('roles', []))) ?>"
/>
<Button variant="primary" class="self-end" type="submit"><?= lang('Person.podcast_form.submit_add') ?></Button>
<x-Button variant="primary" class="self-end" type="submit"><?= lang('Person.podcast_form.submit_add') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>
<?= data_table(
@ -85,7 +85,7 @@
'header' => lang('Common.actions'),
'cell' => function ($person): string {
// @icon('delete-bin-fill')
return '<Button uri="' . route_to('podcast-person-remove', $person->podcast_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.podcast_form.remove') . '</Button>';
return '<x-Button uri="' . route_to('podcast-person-remove', $person->podcast_id, $person->id) . '" variant="danger" size="small" iconLeft="delete-bin-fill">' . lang('Person.podcast_form.remove') . '</x-Button>';
},
],
],

View file

@ -9,7 +9,7 @@
<?= $this->endSection() ?>
<?= $this->section('headerRight') ?>
<Button form="platforms-form" variant="primary" type="submit" class="self-end"><?= lang('Platforms.submit') ?></Button>
<x-Button form="platforms-form" variant="primary" type="submit" class="self-end"><?= lang('Platforms.submit') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -16,7 +16,7 @@
'class' => 'mr-2 text-lg',
]) . lang('Podcast.publish_form.back_to_podcast_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('Podcast.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
<x-Forms.Textarea name="message" placeholder="<?= lang('Podcast.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
</div>
<footer class="flex justify-around px-6 py-3">
<span class="inline-flex items-center"><?= icon('chat-4-fill', [
@ -58,14 +58,14 @@
<legend class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Podcast.publish_form.publication_method.now') ?></Forms.Radio>
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Podcast.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('Podcast.publish_form.publication_method.schedule') ?></label>
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Podcast.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('Podcast.publish_form.scheduled_publication_date')) ?>"
@ -76,11 +76,11 @@
</div>
</fieldset>
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Podcast.publish_form.message_warning') ?>"><?= lang('Podcast.publish_form.message_warning_hint') ?></Alert>
<x-Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Podcast.publish_form.message_warning') ?>"><?= lang('Podcast.publish_form.message_warning_hint') ?></x-Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit') ?>"><?= lang('Podcast.publish_form.submit') ?></Button>
<x-Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></x-Button>
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit') ?>"><?= lang('Podcast.publish_form.submit') ?></x-Button>
</div>
</form>

View file

@ -40,7 +40,7 @@
</div>
</div>
<div class="px-4 mb-2">
<Forms.Textarea name="message" placeholder="<?= lang('Podcast.publish_form.message_placeholder') ?>" autofocus="" value="<?= $post !== null ? esc($post->message) : '' ?>" rows="2" />
<x-Forms.Textarea name="message" placeholder="<?= lang('Podcast.publish_form.message_placeholder') ?>" autofocus="" value="<?= $post !== null ? esc($post->message) : '' ?>" rows="2" />
</div>
<footer class="flex justify-around px-6 py-3">
<span class="inline-flex items-center"><?= icon('chat-4-fill', [
@ -59,14 +59,14 @@
<legend class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Podcast.publish_form.publication_method.now') ?></Forms.Radio>
<x-Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Podcast.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('Podcast.publish_form.publication_method.schedule') ?></label>
<x-Label for="schedule" class="pl-2 leading-8"><?= lang('Podcast.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('Podcast.publish_form.scheduled_publication_date')) ?>"
@ -77,11 +77,11 @@
</div>
</fieldset>
<Alert id="publish-warning" variant="warning" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Podcast.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('Podcast.publish_form.message_warning_hint') ?></x-Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit_edit') ?>"><?= lang('Podcast.publish_form.submit_edit') ?></Button>
<x-Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></x-Button>
<x-Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit_edit') ?>"><?= lang('Podcast.publish_form.submit_edit') ?></x-Button>
</div>
</form>

View file

@ -13,8 +13,8 @@
// @icon('pencil-fill')
// @icon('add-fill')
?>
<Button uri="<?= route_to('podcast-edit', $podcast->id) ?>" variant="secondary" class="[&>span]:hidden [&>span]:md:block py-3 md:py-2" iconLeft="pencil-fill"><?= lang('Podcast.edit') ?></Button>
<Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" class="[&>span]:hidden [&>span]:md:block py-3 md:py-2" iconLeft="add-fill"><?= lang('Episode.create') ?></Button>
<x-Button uri="<?= route_to('podcast-edit', $podcast->id) ?>" variant="secondary" class="[&>span]:hidden [&>span]:md:block py-3 md:py-2" iconLeft="pencil-fill"><?= lang('Podcast.edit') ?></x-Button>
<x-Button uri="<?= route_to('episode-create', $podcast->id) ?>" variant="primary" class="[&>span]:hidden [&>span]:md:block py-3 md:py-2" iconLeft="add-fill"><?= lang('Episode.create') ?></x-Button>
<?= $this->endSection() ?>
<?= $this->section('content') ?>

View file

@ -12,7 +12,7 @@
<form action="<?= route_to('update') ?>" method="POST">
<?= csrf_field() ?>
<button type="submit" name="action" value="database" class="inline-flex items-center px-4 py-2 text-lg font-semibold transition-colors rounded-full shadow group gap-x-2 bg-elevated hover:border-accent-hover focus:ring-accent border-3 border-subtle">
<button type="submit" name="action" value="database" class="inline-flex items-center px-4 py-2 text-lg font-semibold transition-colors rounded-full shadow group gap-x-2 bg-elevated hover:border-accent-hover border-3 border-subtle">
<div class="relative">
<?= icon('database-2-fill', [
'class' => 'text-4xl text-accent-base',

View file

@ -18,27 +18,27 @@
<form action="<?= route_to('settings-instance') ?>" method="POST" enctype="multipart/form-data" class="max-w-xl">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Settings.instance.title') ?>">
<Forms.Field
<x-Forms.Field
name="site_name"
label="<?= esc(lang('Settings.instance.site_name')) ?>"
value="<?= esc(service('settings')
->get('App.siteName')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="Textarea"
name="site_description"
label="<?= esc(lang('Settings.instance.site_description')) ?>"
value="<?= esc(service('settings')
->get('App.siteDescription')) ?>"
required="true"
isRequired="true"
rows="4" />
<div class="flex items-center">
<Forms.Field
<x-Forms.Field
name="site_icon"
type="file"
label="<?= esc(lang('Settings.instance.site_icon')) ?>"
@ -49,45 +49,45 @@
/>
<?php if (config('App')->siteIcon['ico'] !== service('settings')->get('App.siteIcon')['ico']): ?>
<div class="relative ml-2">
<a href="<?= route_to('settings-instance-delete-icon') ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast -top-3 -right-3 focus:ring-accent" title="<?= lang('Settings.instance.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin-fill') ?></a>
<a href="<?= route_to('settings-instance-delete-icon') ?>" class="absolute p-1 text-red-700 bg-red-100 border-2 rounded-full hover:text-red-900 border-contrast -top-3 -right-3" title="<?= lang('Settings.instance.site_icon_delete') ?>" data-tooltip="top"><?= icon('delete-bin-fill') ?></a>
<img src="<?= get_site_icon_url('64') ?>" alt="<?= esc(service('settings')->get('App.siteName')) ?> Favicon" class="w-10 h-10 aspect-square" loading="lazy" />
</div>
<?php endif; ?>
</div>
<Button variant="primary" type="submit" class="self-end"><?= lang('Settings.instance.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Settings.instance.submit') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>
<form action="<?= route_to('settings-images-regenerate') ?>" method="POST" class="flex flex-col w-full max-w-xl gap-y-4">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Settings.images.title') ?>"
subtitle="<?= lang('Settings.images.subtitle') ?>">
<?php // @icon('refresh-fill')?>
<Button variant="primary" type="submit" iconLeft="refresh-fill"><?= lang('Settings.images.regenerate') ?></Button>
<x-Button variant="primary" type="submit" iconLeft="refresh-fill"><?= lang('Settings.images.regenerate') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>
<form action="<?= route_to('settings-housekeeping-run') ?>" method="POST" class="flex flex-col w-full max-w-xl gap-y-4">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Settings.housekeeping.title') ?>"
subtitle="<?= lang('Settings.housekeeping.subtitle') ?>" >
<Forms.Toggler name="reset_counts" value="yes" size="small" checked="false" hint="<?= esc(lang('Settings.housekeeping.reset_counts_helper')) ?>"><?= lang('Settings.housekeeping.reset_counts') ?></Forms.Toggler>
<Forms.Toggler name="rename_episodes_files" value="yes" size="small" checked="false" hint="<?= esc(lang('Settings.housekeeping.rename_episodes_files_hint')) ?>"><?= lang('Settings.housekeeping.rename_episodes_files') ?></Forms.Toggler>
<Forms.Toggler name="clear_cache" value="yes" size="small" checked="false" hint="<?= esc(lang('Settings.housekeeping.clear_cache_helper')) ?>"><?= lang('Settings.housekeeping.clear_cache') ?></Forms.Toggler>
<x-Forms.Toggler name="reset_counts" size="small" isChecked="false" hint="<?= esc(lang('Settings.housekeeping.reset_counts_helper')) ?>"><?= lang('Settings.housekeeping.reset_counts') ?></x-Forms.Toggler>
<x-Forms.Toggler name="rename_episodes_files" size="small" isChecked="false" hint="<?= esc(lang('Settings.housekeeping.rename_episodes_files_hint')) ?>"><?= lang('Settings.housekeeping.rename_episodes_files') ?></x-Forms.Toggler>
<x-Forms.Toggler name="clear_cache" size="small" isChecked="false" hint="<?= esc(lang('Settings.housekeeping.clear_cache_helper')) ?>"><?= lang('Settings.housekeeping.clear_cache') ?></x-Forms.Toggler>
<?php // @icon('home-gear-fill')?>
<Button variant="primary" type="submit" iconLeft="home-gear-fill"><?= lang('Settings.housekeeping.run') ?></Button>
<x-Button variant="primary" type="submit" iconLeft="home-gear-fill"><?= lang('Settings.housekeeping.run') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>

View file

@ -16,24 +16,24 @@
<form action="<?= route_to('settings-theme') ?>" method="POST" class="flex flex-col w-full max-w-xl gap-y-4" enctype="multipart/form-data">
<?= csrf_field() ?>
<Forms.Section
<x-Forms.Section
title="<?= lang('Settings.theme.accent_section_title') ?>"
subtitle="<?= lang('Settings.theme.accent_section_subtitle') ?>">
<div class="grid gap-4 grid-cols-colorButtons">
<?php foreach (config('Colors')->themes as $themeName => $color): ?>
<Forms.ColorRadioButton
<x-Forms.ColorRadioButton
class="theme-<?= $themeName ?> mx-auto"
value="<?= esc($themeName) ?>"
name="theme"
isChecked="<?= $themeName === service('settings')
->get('App.theme') ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
->get('App.theme') ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
<?php endforeach; ?>
</div>
<Button variant="primary" type="submit" class="self-end"><?= lang('Settings.theme.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Settings.theme.submit') ?></x-Button>
</Forms.Section>
</x-Forms.Section>
</form>
<?= $this->endSection() ?>

View file

@ -15,20 +15,20 @@
<?= csrf_field() ?>
<input type="hidden" name="client_timezone" value="UTC" />
<Forms.Field
<x-Forms.Field
name="email"
type="email"
label="<?= esc(lang('Subscription.form.email')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="DatetimePicker"
name="expiration_date"
label="<?= esc(lang('Subscription.form.expiration_date')) ?>"
hint="<?= esc(lang('Subscription.form.expiration_date_hint')) ?>"
/>
<Button type="submit" class="self-end" variant="primary"><?= lang('Subscription.form.submit_create') ?></Button>
<x-Button type="submit" class="self-end" variant="primary"><?= lang('Subscription.form.submit_create') ?></x-Button>
</form>

View file

@ -13,15 +13,15 @@
<form action="<?= route_to('subscription-delete', $podcast->id, $subscription->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('Subscription.delete_form.disclaimer', [
<x-Alert variant="danger" class="font-semibold"><?= lang('Subscription.delete_form.disclaimer', [
'subscriber' => $subscription->email,
]) ?></Alert>
]) ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Subscription.delete_form.understand') ?></Forms.Checkbox>
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('Subscription.delete_form.understand') ?></x-Forms.Checkbox>
<div class="flex items-center self-end mt-4 gap-x-2">
<Button uri="<?= route_to('subscription-list', $podcast->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('Subscription.delete_form.submit') ?></Button>
<x-Button uri="<?= route_to('subscription-list', $podcast->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('Subscription.delete_form.submit') ?></x-Button>
</div>
</form>

View file

@ -24,7 +24,7 @@
</dd>
</div>
<Forms.Field
<x-Forms.Field
as="DatetimePicker"
name="expiration_date"
label="<?= esc(lang('Subscription.form.expiration_date')) ?>"
@ -32,7 +32,7 @@
value="<?= $subscription->expires_at ?>"
/>
<Button type="submit" class="self-end" variant="primary"><?= lang('Subscription.form.submit_edit') ?></Button>
<x-Button type="submit" class="self-end" variant="primary"><?= lang('Subscription.form.submit_edit') ?></x-Button>
</form>

View file

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('add-fill')?>
<Button uri="<?= route_to('subscription-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Subscription.add') ?></Button>
<x-Button uri="<?= route_to('subscription-create', $podcast->id) ?>" variant="primary" iconLeft="add-fill"><?= lang('Subscription.add') ?></x-Button>
<?= $this->endSection() ?>
@ -18,7 +18,7 @@
<form method="POST" action="<?= route_to('subscription-link-save', $podcast->id) ?>" class="flex flex-col items-start max-w-sm gap-y-1">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
class="w-full"
type="url"
name="subscription_link"
@ -27,7 +27,7 @@
placeholder="https://…"
value="<?= service('settings')
->get('Subscription.link', 'podcast:' . $podcast->id) ?>" />
<Button variant="primary" type="submit"><?= lang('Subscription.form_link_add.submit') ?></Button>
<x-Button variant="primary" type="submit"><?= lang('Subscription.form_link_add.submit') ?></x-Button>
</form>
<hr class="my-6 border-subtle">
@ -67,7 +67,7 @@
'expired' => 'default',
];
return '<Pill variant="' . $statusMapping[$subscription->status] . '" class="lowercase">' . lang('Subscription.status.' . $subscription->status) . '</Pill>';
return '<x-Pill variant="' . $statusMapping[$subscription->status] . '" class="lowercase">' . lang('Subscription.status.' . $subscription->status) . '</x-Pill>';
},
],
[
@ -116,10 +116,10 @@
array_splice($items, 3, 0, $suspendAction);
return '<button id="more-dropdown-' . $subscription->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $subscription->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
return '<button id="more-dropdown-' . $subscription->id . '" type="button" class="inline-flex items-center p-1 rounded-full" data-dropdown="button" data-dropdown-target="more-dropdown-' . $subscription->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
icon('more-2-fill') .
'</button>' .
'<DropdownMenu id="more-dropdown-' . $subscription->id . '-menu" labelledby="more-dropdown-' . $subscription->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
'<x-DropdownMenu id="more-dropdown-' . $subscription->id . '-menu" labelledby="more-dropdown-' . $subscription->id . '" offsetY="-24" items="' . esc(json_encode($items)) . '" />';
},
],
],

View file

@ -13,11 +13,11 @@
<form action="<?= route_to('subscription-suspend', $podcast->id, $subscription->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="warning" class="font-semibold"><?= lang('Subscription.suspend_form.disclaimer', [
<x-Alert variant="warning" class="font-semibold"><?= lang('Subscription.suspend_form.disclaimer', [
'email' => $subscription->email,
]) ?></Alert>
]) ?></x-Alert>
<Forms.Field
<x-Forms.Field
as="Textarea"
name="reason"
label="<?= esc(lang('Subscription.suspend_form.reason')) ?>"
@ -27,9 +27,9 @@
/>
<div class="flex items-center self-end mt-4 gap-x-2">
<Button uri="<?= route_to('subscription-list', $podcast->id) ?>"><?= lang('Common.cancel') ?></Button>
<x-Button uri="<?= route_to('subscription-list', $podcast->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<?php // @icon('pause-fill')?>
<Button type="submit" variant="warning" iconLeft="pause-fill"><?= lang('Subscription.suspend_form.submit') ?></Button>
<x-Button type="submit" variant="warning" iconLeft="pause-fill"><?= lang('Subscription.suspend_form.submit') ?></x-Button>
</div>
</form>

View file

@ -14,26 +14,26 @@
<form action="<?= route_to('user-create') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="username"
label="<?= esc(lang('User.form.username')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="email"
type="email"
label="<?= esc(lang('User.form.email')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
as="Select"
name="role"
label="<?= esc(lang('User.form.role')) ?>"
options="<?= esc(json_encode($roleOptions)) ?>"
selected="<?= setting('AuthGroups.defaultGroup') ?>"
required="true" />
isRequired="true" />
<Button variant="primary" type="submit" class="self-end"><?= lang('User.form.submit_create') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('User.form.submit_create') ?></x-Button>
</form>

View file

@ -17,17 +17,17 @@
<form action="<?= route_to('user-delete', $user->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
<?= csrf_field() ?>
<Alert variant="danger" class="font-semibold"><?= lang('User.delete_form.disclaimer', [
<x-Alert variant="danger" class="font-semibold"><?= lang('User.delete_form.disclaimer', [
'user' => $user->username,
]) ?></Alert>
]) ?></x-Alert>
<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('User.delete_form.understand', [
<x-Forms.Checkbox class="mt-2" name="understand" isRequired="true" isChecked="false"><?= lang('User.delete_form.understand', [
'user' => $user->username,
]) ?></Forms.Checkbox>
]) ?></x-Forms.Checkbox>
<div class="self-end mt-4">
<Button uri="<?= route_to('user-view', $user->id) ?>"><?= lang('Common.cancel') ?></Button>
<Button type="submit" variant="danger"><?= lang('User.delete_form.submit') ?></Button>
<x-Button uri="<?= route_to('user-view', $user->id) ?>"><?= lang('Common.cancel') ?></x-Button>
<x-Button type="submit" variant="danger"><?= lang('User.delete_form.submit') ?></x-Button>
</div>
</form>

View file

@ -18,15 +18,15 @@
<form action="<?= route_to('user-edit', $user->id) ?>" method="POST" class="flex flex-col max-w-sm">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
as="Select"
name="role"
label="<?= esc(lang('User.form.role')) ?>"
options="<?= esc(json_encode($roleOptions)) ?>"
selected="<?= esc(get_instance_group($user)) ?>"
required="true" />
isRequired="true" />
<Button variant="primary" type="submit" class="self-end mt-4"><?= lang('User.form.submit_edit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end mt-4"><?= lang('User.form.submit_edit') ?></x-Button>
</form>

View file

@ -10,7 +10,7 @@
<?= $this->section('headerRight') ?>
<?php // @icon('user-add-fill')?>
<Button uri="<?= route_to('user-create') ?>" variant="primary" iconLeft="user-add-fill"><?= lang('User.create') ?></Button>
<x-Button uri="<?= route_to('user-create') ?>" variant="primary" iconLeft="user-add-fill"><?= lang('User.create') ?></x-Button>
<?= $this->endSection() ?>
@ -34,20 +34,20 @@
$role = get_group_info(get_instance_group($user))['title'];
if ((bool) $user->is_owner) {
$role = '<div class="inline-flex items-center"><span class="mr-2 focus:ring-accent" tabindex="0" data-tooltip="bottom" title="' . lang('Auth.instance_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.instance_groups.owner.title') . '">' . icon('shield-user-fill') . '</span>' . $role . '</div>';
}
// @icon('pencil-fill')
return $role . '<IconButton uri="' . route_to('user-edit', $user->id) . '" glyph="pencil-fill" variant="info">' . lang('User.edit_role', [
return $role . '<x-IconButton uri="' . route_to('user-edit', $user->id) . '" glyph="pencil-fill" variant="info">' . lang('User.edit_role', [
'username' => esc($user->username),
]) . '</IconButton>';
]) . '</x-IconButton>';
},
],
[
'header' => lang('Common.actions'),
'cell' => function ($user) {
return '<button id="more-dropdown-' . $user->id . '" type="button" class="inline-flex items-center p-1 focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $user->id . '-menu" aria-haspopup="true" aria-expanded="false">' . icon('more-2-fill') . '</button>' .
'<DropdownMenu id="more-dropdown-' . $user->id . '-menu" labelledby="more-dropdown-' . $user->id . '" items="' . esc(json_encode([
return '<button id="more-dropdown-' . $user->id . '" type="button" class="inline-flex items-center p-1" data-dropdown="button" data-dropdown-target="more-dropdown-' . $user->id . '-menu" aria-haspopup="true" aria-expanded="false">' . icon('more-2-fill') . '</button>' .
'<x-DropdownMenu id="more-dropdown-' . $user->id . '-menu" labelledby="more-dropdown-' . $user->id . '" items="' . esc(json_encode([
[
'type' => 'link',
'title' => lang('User.delete'),

View file

@ -4,10 +4,10 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
<div class="sticky top-0 left-0 z-50 flex items-center justify-between w-full h-10 text-white border-b bg-navigation border-navigation">
<div class="inline-flex items-center h-full">
<a href="<?= route_to('home') ?>" class="inline-flex items-center h-full px-2 border-r border-navigation focus:ring-inset focus:ring-accent">
<a href="<?= route_to('home') ?>" class="inline-flex items-center h-full px-2 border-r border-navigation">
<?= svg('castopod-logo-base', 'h-6') ?>
</a>
<a href="<?= route_to('admin') ?>" 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_admin') ?>">
<a href="<?= route_to('admin') ?>" class="inline-flex items-center h-full px-2 text-sm font-semibold sm:px-6 hover:underline" title="<?= lang('Navigation.go_to_admin') ?>">
<span class="hidden sm:block"><?= lang('Navigation.go_to_admin') ?></span>
<?= icon('external-link-fill', [
'class' => 'sm:ml-1 text-xl sm:text-base sm:opacity-60',
@ -16,7 +16,7 @@ $userPodcasts = get_podcasts_user_can_interact_with(auth()->user()); ?>
</div>
<div class="inline-flex items-center h-full">
<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',
]) ?>
@ -67,11 +67,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"
@ -153,6 +153,6 @@ 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)) ?>" />
</div>
</div>

View file

@ -1,19 +1,19 @@
<?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; ?>

View file

@ -21,7 +21,7 @@
<div class="flex flex-col">
<h4 class="text-sm font-semibold">
<?php if ($person->information_url): ?>
<a href="<?= esc($person->information_url) ?>" class="hover:underline focus:ring-accent" target="_blank" rel="noopener noreferrer"><?= esc($person->full_name) ?></a>
<a href="<?= esc($person->information_url) ?>" class="hover:underline" target="_blank" rel="noopener noreferrer"><?= esc($person->full_name) ?></a>
<?php else: ?>
<?= esc($person->full_name) ?>
<?php endif; ?>

View file

@ -34,7 +34,7 @@
</a>
<?php if ($episode->is_premium && ! is_unlocked($podcast->handle)): ?>
<?php // @icon('lock-fill')?>
<Button variant="primary" class="mt-auto mb-2" iconLeft="lock-fill" uri="<?= $episode->link ?>" target="_blank" rel="noopener noreferrer"><?= lang('PremiumPodcasts.unlock') ?></Button>
<x-Button variant="primary" class="mt-auto mb-2" iconLeft="lock-fill" uri="<?= $episode->link ?>" target="_blank" rel="noopener noreferrer"><?= lang('PremiumPodcasts.unlock') ?></x-Button>
<?php else: ?>
<vm-player
id="castopod-vm-player"

View file

@ -46,7 +46,7 @@
<?php endif; ?>
<nav class="flex items-center justify-between h-10 col-start-2 text-white bg-header">
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2" title="<?= lang('Episode.back_to_episodes', [
'podcast' => esc($podcast->title),
]) ?>">
<?= icon('arrow-left-line', [
@ -64,7 +64,7 @@
</a>
<div class="inline-flex items-center self-end h-full px-2 gap-x-2">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<?php endif; ?>
<?= anchor_popup(
route_to('follow', esc($podcast->handle)),
@ -77,7 +77,7 @@
[
'width' => 420,
'height' => 620,
'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow focus:ring-accent bg-white',
'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow bg-white',
],
) ?>
</div>
@ -100,7 +100,7 @@
<h1 class="inline-flex items-baseline max-w-lg mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></h1>
<div class="flex items-center w-full mt-4 gap-x-8">
<?php if ($episode->persons !== []): ?>
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden">
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline" data-toggle="persons-list" data-toggle-class="hidden">
<span class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($episode->persons as $person): ?>
@ -148,7 +148,7 @@
<div class="col-start-2 px-8 py-4 text-white bg-header">
<h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
<?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
<SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore>
<x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></x-SeeMore>
<?php else: ?>
<div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div>
<?php endif; ?>
@ -171,20 +171,20 @@
<?php if (auth()->loggedIn()): ?>
<?php if (in_array($episode->publication_status, ['scheduled', 'with_podcast'], true)): ?>
<?php // @icon('upload-cloud-fill')?>
<Button
<x-Button
iconLeft="upload-cloud-fill"
variant="primary"
size="small"
class="ml-auto"
uri="<?= route_to('episode-publish_edit', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish_edit') ?></Button>
uri="<?= route_to('episode-publish_edit', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish_edit') ?></x-Button>
<?php else: ?>
<?php // @icon('upload-cloud-fill')?>
<Button
<x-Button
iconLeft="upload-cloud-fill"
variant="secondary"
size="small"
class="ml-auto"
uri="<?= route_to('episode-publish', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish') ?></Button>
uri="<?= route_to('episode-publish', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish') ?></x-Button>
<?php endif; ?>
<?php endif; ?>
</div>

View file

@ -43,7 +43,7 @@
<?php endif; ?>
<nav class="flex items-center justify-between h-10 col-start-2 text-white bg-header">
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2" title="<?= lang('Episode.back_to_episodes', [
'podcast' => esc($podcast->title),
]) ?>">
<?= icon('arrow-left-line', [
@ -61,7 +61,7 @@
</a>
<div class="inline-flex items-center self-end h-full px-2 gap-x-2">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<?php endif; ?>
<?= anchor_popup(
route_to('follow', esc($podcast->handle)),
@ -74,7 +74,7 @@
[
'width' => 420,
'height' => 620,
'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow focus:ring-accent bg-white',
'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow bg-white',
],
) ?>
</div>
@ -97,7 +97,7 @@
<h1 class="inline-flex items-baseline max-w-lg mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></h1>
<div class="flex items-center w-full mt-4 gap-x-8">
<?php if ($episode->persons !== []): ?>
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden">
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline" data-toggle="persons-list" data-toggle-class="hidden">
<span class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($episode->persons as $person): ?>
@ -141,7 +141,7 @@
<div class="col-start-2 px-8 py-4 text-white bg-header">
<h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
<?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
<SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore>
<x-SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></x-SeeMore>
<?php else: ?>
<div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div>
<?php endif; ?>

View file

@ -21,7 +21,7 @@
<p class="text-xs text-gray-600 md:text-sm line-clamp-3"><?= $episode->description ?></p>
</div>
<?php if ($episode->is_premium && ! is_unlocked($podcast->handle)): ?>
<a href="<?= route_to('episode', $episode->podcast->handle, $episode->slug) ?>" class="p-3 rounded-full bg-brand bg-accent-base text-accent-contrast hover:bg-accent-hover focus:ring-accent" title="<?= lang('PremiumPodcasts.unlock_episode') ?>" data-tooltip="bottom">
<a href="<?= route_to('episode', $episode->podcast->handle, $episode->slug) ?>" class="p-3 rounded-full bg-brand bg-accent-base text-accent-contrast hover:bg-accent-hover" title="<?= lang('PremiumPodcasts.unlock_episode') ?>" data-tooltip="bottom">
<?= icon('lock-fill', [
'class' => 'text-xl',
]) ?>

View file

@ -10,7 +10,7 @@
) ?>"><?= icon('heart-fill', [
'class' => 'text-xl mr-1 text-gray-400 group-hover:text-red-600',
]) . $comment->likes_count ?></button>
<Button uri="<?= route_to('episode-comment', esc($comment->episode->podcast->handle), esc($comment->episode->slug), $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button>
<x-Button uri="<?= route_to('episode-comment', esc($comment->episode->podcast->handle), esc($comment->episode->slug), $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></x-Button>
</form>
<?php if ($comment->replies_count): ?>
<?= anchor(

View file

@ -10,7 +10,7 @@
) ?>"><?= icon('heart-fill', [
'class' => 'text-xl mr-1 text-gray-400 group-hover:text-red-600',
]) . $comment->likes_count ?></button>
<Button uri="<?= route_to('post', esc($podcast->handle), $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></Button>
<x-Button uri="<?= route_to('post', esc($podcast->handle), $comment->id) ?>" size="small"><?= lang('Comment.reply') ?></x-Button>
</form>
<?php if ($comment->replies_count): ?>
<?= anchor(

View file

@ -11,7 +11,7 @@
) ?>"><?= icon('heart-fill', [
'class' => 'text-lg mr-1 text-gray-400 group-hover:text-red-600',
]) . $reply->likes_count ?></button>
<Button uri="<?= route_to('episode-comment', esc($reply->episode->podcast->handle), esc($reply->episode->slug), $reply->id) ?>" size="small"><?= lang('Comment.reply') ?></Button>
<x-Button uri="<?= route_to('episode-comment', esc($reply->episode->podcast->handle), esc($reply->episode->slug), $reply->id) ?>" size="small"><?= lang('Comment.reply') ?></x-Button>
</form>
<?php else: ?>
<button type="submit" name="action" class="inline-flex items-center opacity-50 cursor-not-allowed" disabled="disabled" title="<?= lang(

View file

@ -19,16 +19,16 @@ if ($comment->in_reply_to_id): ?>
->avatar_image_url ?>" alt="<?= esc(interact_as_actor()
->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col flex-1">
<Forms.Textarea
<x-Forms.Textarea
name="message"
required="true"
isRequired="true"
class="w-full mb-4"
placeholder="<?= lang('Comment.form.reply_to_placeholder', [
'actorUsername' => esc($comment->actor->username),
]) ?>"
rows="1" />
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end" iconRight="send-plane-2-fill"><?= lang('Comment.form.submit_reply') ?></Button>
<x-Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end" iconRight="send-plane-2-fill"><?= lang('Comment.form.submit_reply') ?></x-Button>
</div>
</form>
<?php endif; ?>

View file

@ -52,7 +52,7 @@ if ($episode->publication_status === 'published') {
<nav class="sticky z-40 flex col-start-2 pt-4 shadow bg-elevated md:px-8 gap-x-2 md:gap-x-4 -top-4 rounded-conditional-b-xl">
<?php foreach ($navigationItems as $item): ?>
<?php $isActive = url_is($item['uri']); ?>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-black border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?><span class="px-2 ml-1 font-semibold rounded-full bg-base"><?= $item['labelInfo'] ?></span></a>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase border-b-4<?= $isActive ? ' border-b-4 text-black border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?><span class="px-2 ml-1 font-semibold rounded-full bg-base"><?= $item['labelInfo'] ?></span></a>
<?php endforeach; ?>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
</nav>

View file

@ -20,7 +20,7 @@
<a href="<?= $episode->link ?>" class="flex items-baseline font-semibold line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></a>
</div>
<?php if ($episode->is_premium && ! is_unlocked($episode->podcast->handle)): ?>
<a href="<?= route_to('episode', $episode->podcast->handle, $episode->slug) ?>" class="p-3 mr-4 rounded-full bg-brand bg-accent-base text-accent-contrast hover:bg-accent-hover focus:ring-accent" title="<?= lang('PremiumPodcasts.unlock_episode') ?>" data-tooltip="bottom">
<a href="<?= route_to('episode', $episode->podcast->handle, $episode->slug) ?>" class="p-3 mr-4 rounded-full bg-brand bg-accent-base text-accent-contrast hover:bg-accent-hover" title="<?= lang('PremiumPodcasts.unlock_episode') ?>" data-tooltip="bottom">
<?= icon('lock-fill', [
'class' => 'text-xl',
]) ?>

View file

@ -12,13 +12,13 @@
->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2">
<input name="episode_url" value="<?= esc($episode->link) ?>" type="hidden" />
<Forms.Textarea
<x-Forms.Textarea
name="message"
placeholder="<?= lang('Post.form.episode_message_placeholder') ?>"
required="true"
isRequired="true"
rows="2" />
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit') ?></Button>
<x-Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit') ?></x-Button>
</div>
</form>
<hr class="my-4 border-subtle">

View file

@ -4,7 +4,7 @@
<div class="max-w-2xl px-6 mx-auto">
<nav class="mb-2">
<a href="<?= route_to('episode', esc($podcast->handle), esc($episode->slug)) ?>"
class="inline-flex items-center px-4 py-2 text-sm focus:ring-accent"><?= icon(
class="inline-flex items-center px-4 py-2 text-sm"><?= icon(
'arrow-left-line',
'mr-2 text-lg',
) . lang('Comment.back_to_comments') ?></a>

View file

@ -11,13 +11,13 @@
->avatar_image_url ?>" alt="<?= esc(interact_as_actor()
->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2">
<Forms.Textarea
<x-Forms.Textarea
name="message"
required="true"
isRequired="true"
placeholder="<?= lang('Comment.form.episode_message_placeholder') ?>"
rows="2" />
<?php // @icon('send-plane-2-fill')?>
<Button class="self-end" variant="primary" size="small" type="submit" iconRight="send-plane-2-fill"><?= lang('Comment.form.submit') ?></Button>
<x-Button class="self-end" variant="primary" size="small" type="submit" iconRight="send-plane-2-fill"><?= lang('Comment.form.submit') ?></x-Button>
</div>
</form>
<?php endif; ?>

View file

@ -4,9 +4,9 @@
<?php if (isset($captions)) : ?>
<div class="flex flex-col gap-2">
<Button uri="<?= $transcript->file_url ?>" size="small" iconLeft="download" class="self-start" variant="secondary" target="_blank" download="" rel="noopener noreferrer"><?= lang('Episode.download_transcript', [
<x-Button uri="<?= $transcript->file_url ?>" size="small" iconLeft="download" class="self-start" variant="secondary" target="_blank" download="" rel="noopener noreferrer"><?= lang('Episode.download_transcript', [
'extension' => '.' . $transcript->file_extension,
]) ?></Button>
]) ?></x-Button>
<?php
$previousSpeaker = '';
$previousStartTime = '';

View file

@ -4,9 +4,9 @@
<?php if (isset($captions)) : ?>
<div class="flex flex-col gap-2">
<Button uri="<?= $transcript->file_url ?>" size="small" iconLeft="download" class="self-start" variant="secondary" target="_blank" download="" rel="noopener noreferrer"><?= lang('Episode.download_transcript', [
<x-Button uri="<?= $transcript->file_url ?>" size="small" iconLeft="download" class="self-start" variant="secondary" target="_blank" download="" rel="noopener noreferrer"><?= lang('Episode.download_transcript', [
'extension' => '.' . $transcript->file_extension,
]) ?></Button>
]) ?></x-Button>
<?php
$previousSpeaker = '';
$previousStartTime = '';

View file

@ -48,13 +48,13 @@
</header>
<main class="container flex-1 px-4 py-10 mx-auto">
<div class="flex flex-wrap items-center justify-between py-2 border-b border-subtle gap-x-4">
<Heading tagName="h2" class="inline-block"><?= lang('Home.all_podcasts') ?> (<?= count(
<x-Heading tagName="h2" class="inline-block"><?= lang('Home.all_podcasts') ?> (<?= count(
$podcasts,
) ?>)</Heading>
<button class="inline-flex items-center px-2 py-1 text-sm font-semibold focus:ring-accent" id="sortby-dropdown" data-dropdown="button" data-dropdown-target="sortby-dropdown-menu" aria-haspopup="true" aria-expanded="false"><?= icon('material-symbols:sort', [
) ?>)</x-Heading>
<button class="inline-flex items-center px-2 py-1 text-sm font-semibold" id="sortby-dropdown" data-dropdown="button" data-dropdown-target="sortby-dropdown-menu" aria-haspopup="true" aria-expanded="false"><?= icon('material-symbols:sort', [
'class' => 'mr-1 text-xl opacity-50',
]) . lang('Home.sort_by') ?></button>
<DropdownMenu id="sortby-dropdown-menu" labelledby="sortby-dropdown" items="<?= esc(json_encode([
<x-DropdownMenu id="sortby-dropdown-menu" labelledby="sortby-dropdown" items="<?= esc(json_encode([
[
'type' => 'link',
'title' => ($sortBy === 'activity' ? '✓ ' : '') . lang('Home.sort_options.activity'),
@ -78,7 +78,7 @@
<div class="grid gap-4 mt-4 grid-cols-cards">
<?php if ($podcasts): ?>
<?php foreach ($podcasts as $podcast): ?>
<a href="<?= $podcast->link ?>" class="relative w-full h-full overflow-hidden transition shadow focus:ring-accent rounded-xl hover:shadow-xl focus:shadow-xl group border-3 <?= $podcast->is_premium ? 'border-accent-base' : 'border-subtle' ?>">
<a href="<?= $podcast->link ?>" class="relative w-full h-full overflow-hidden transition shadow rounded-xl hover:shadow-xl focus:shadow-xl group border-3 <?= $podcast->is_premium ? 'border-accent-base' : 'border-subtle' ?>">
<article class="text-white">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient mix-blend-multiply"></div>
<div class="w-full h-full overflow-hidden bg-header">
@ -109,7 +109,7 @@
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t border-subtle">
<?= render_page_links() ?>
<small><?= 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>',
], null, false) ?></small>

View file

@ -40,11 +40,11 @@
<header class="py-8 border-b bg-elevated border-subtle">
<div class="container flex flex-col items-start px-2 py-4 mx-auto">
<a href="<?= route_to('home') ?>"
class="inline-flex items-center mb-2 text-sm focus:ring-accent"><?= icon(
class="inline-flex items-center mb-2 text-sm"><?= icon(
'arrow-left-line',
'mr-2',
) . lang('Page.back_to_home') ?></a>
<Heading tagName="h1" size="large"><?= esc($page->title) ?></Heading>
<x-Heading tagName="h1" size="large"><?= esc($page->title) ?></x-Heading>
</div>
</header>
<main class="container flex-1 px-4 py-6 mx-auto">
@ -53,7 +53,7 @@
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t border-subtle">
<?= render_page_links() ?>
<small><?= lang('Common.powered_by', [
'castopod' => '<a class="underline hover:no-underline focus:ring-accent" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
'castopod' => '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
], null, false) ?></small>
</footer>
</body>

View file

@ -43,11 +43,11 @@
<header class="py-8 border-b border-subtle bg-elevated">
<div class="container flex flex-col items-start px-2 py-4 mx-auto">
<a href="<?= route_to('home') ?>"
class="inline-flex items-center mb-2 text-sm focus:ring-accent"><?= icon(
class="inline-flex items-center mb-2 text-sm"><?= icon(
'arrow-left-line',
'mr-2',
) . lang('Page.back_to_home') ?></a>
<Heading tagName="h1" size="large"><?= lang('Page.map.title') ?></Heading>
<x-Heading tagName="h1" size="large"><?= lang('Page.map.title') ?></x-Heading>
</div>
</header>
<main class="flex-1 w-full h-full">
@ -56,7 +56,7 @@
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right">
<?= render_page_links() ?>
<small><?= 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>',
], null, false) ?></small>

View file

@ -58,7 +58,7 @@
</div>
<div class="z-10 inline-flex items-center self-end mt-2 mr-2 sm:mb-4 sm:mr-4 gap-x-2">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<button class="inline-flex items-center px-4 text-xs font-semibold leading-8 tracking-wider text-red-600 uppercase bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden"><?= icon('heart-fill', [
<button class="inline-flex items-center px-4 text-xs font-semibold leading-8 tracking-wider text-red-600 uppercase bg-white rounded-full shadow hover:text-red-500" data-toggle="funding-links" data-toggle-class="hidden"><?= icon('heart-fill', [
'class' => 'mr-2 text-sm',
]) ?><?= lang('Podcast.sponsor') ?></button>
<?php endif; ?>
@ -71,7 +71,7 @@
[
'width' => 420,
'height' => 620,
'class' => 'group inline-flex items-center px-4 text-xs tracking-wider font-semibold text-black uppercase rounded-full leading-8 shadow focus:ring-accent bg-white',
'class' => 'group inline-flex items-center px-4 text-xs tracking-wider font-semibold text-black uppercase rounded-full leading-8 shadow bg-white',
],
) ?>
</div>

View file

@ -14,7 +14,7 @@
data-toggle="funding-links"
data-toggle-class="hidden"
aria-label="<?= lang('Common.close') ?>"
class="self-start p-1 text-2xl rounded-full focus:ring-accent"><?= icon('close-fill') ?></button>
class="self-start p-1 text-2xl rounded-full"><?= icon('close-fill') ?></button>
</div>
<div class="flex flex-col items-start p-4 space-y-4 overflow-hidden">
<?php foreach ($podcast->fundingPlatforms as $fundingPlatform): ?>
@ -24,7 +24,7 @@
title="<?= esc($fundingPlatform->account_id) ?>"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center w-full font-semibold text-accent-base hover:text-accent-hover focus:ring-accent">
class="inline-flex items-center w-full font-semibold text-accent-base hover:text-accent-hover">
<?= icon(
esc($fundingPlatform->slug),
'mr-2 flex-shrink-0',

View file

@ -18,7 +18,7 @@ $navigationItems = [
<nav class="sticky z-40 flex col-start-2 pt-8 shadow bg-elevated gap-x-2 md:gap-x-4 md:px-8 -top-6 md:-top-10 rounded-conditional-b-xl md:pt-12 ">
<?php foreach ($navigationItems as $item): ?>
<?php $isActive = url_is($item['uri']); ?>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase focus:ring-accent border-b-4<?= $isActive ? ' border-b-4 text-black border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?></a>
<a href="<?= $item['uri'] ?>" class="px-4 py-1 text-sm font-semibold uppercase border-b-4<?= $isActive ? ' border-b-4 text-black border-accent-base' : ' text-skin-muted hover:text-skin-base hover:border-subtle border-transparent' ?>"><?= $item['label'] ?></a>
<?php endforeach; ?>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden focus:ring-accent" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
<button type="button" class="p-2 ml-auto rotate-180 rounded-full md:hidden" data-sidebar-toggler="toggler" aria-label="<?= lang('Navigation.toggle_sidebar') ?>"><?= icon('menu-2-fill') ?></button>
</nav>

View file

@ -11,7 +11,7 @@ if ($podcast->is_premium): ?>
<p class="inline-flex items-center text-sm md:pl-4 gap-x-2"><?= $isUnlocked ? lang('PremiumPodcasts.banner_lock') : lang('PremiumPodcasts.banner_unlock') ?></p>
<?php if ($subscriptionLink = service('settings')->get('Subscription.link', 'podcast:' . $podcast->id)): ?>
<div class="flex items-center self-end gap-x-2">
<Button
<x-Button
variant="primary"
class="group"
size="small"
@ -24,19 +24,19 @@ if ($podcast->is_premium): ?>
'class' => 'hidden text-sm group-focus:block group-hover:block',
]) ?>
<?= $isUnlocked ? lang('PremiumPodcasts.lock') : lang('PremiumPodcasts.unlock') ?>
</Button>
</x-Button>
<?php // @icon('external-link-fill')?>
<Button
<x-Button
iconLeft="external-link-fill"
target="_blank"
rel="noopener noreferrer"
variant="secondary"
size="small"
class="tracking-wider uppercase"
uri="<?= $subscriptionLink ?>"><?= lang('PremiumPodcasts.subscribe') ?></Button>
uri="<?= $subscriptionLink ?>"><?= lang('PremiumPodcasts.subscribe') ?></x-Button>
</div>
<?php else: ?>
<Button
<x-Button
variant="primary"
class="self-end group"
size="small"
@ -49,7 +49,7 @@ if ($podcast->is_premium): ?>
'class' => 'hidden text-sm group-focus:block group-hover:block',
]) ?>
<?= $isUnlocked ? lang('PremiumPodcasts.lock') : lang('PremiumPodcasts.unlock') ?>
</Button>
</x-Button>
<?php endif; ?>
</div>
<?php endif; ?>

View file

@ -1,7 +1,7 @@
<div data-sidebar-toggler="backdrop" class="absolute top-0 left-0 z-10 hidden w-full h-full bg-backdrop/75 md:hidden" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>"></div>
<aside id="podcast-sidebar" data-sidebar-toggler="sidebar" data-toggle-class="hidden" data-hide-class="hidden" class="z-20 hidden h-full col-span-1 col-start-2 row-start-1 p-4 py-6 shadow-2xl md:shadow-none md:block bg-base">
<div class="sticky z-10 bg-base top-12">
<a href="<?= $podcast->feed_url ?>" class="inline-flex items-center mb-6 text-sm font-semibold focus:ring-accent text-skin-muted hover:text-skin-base group" target="_blank" rel="noopener noreferrer">
<a href="<?= $podcast->feed_url ?>" class="inline-flex items-center mb-6 text-sm font-semibold text-skin-muted hover:text-skin-base group" target="_blank" rel="noopener noreferrer">
<?= icon('rss-fill', [
'class' => ' mr-2 bg-orange-500 text-xl text-white group-hover:bg-orange-700 p-1 w-6 h-6 inline-flex items-center justify-center rounded-lg',
]) . lang('Podcast.feed') ?>
@ -58,7 +58,7 @@
<div class="flex flex-col">
<p><?= esc($podcast->copyright) ?></p>
<p><?= lang('Common.powered_by', [
'castopod' => '<a class="inline-flex font-semibold text-skin-muted 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 text-skin-muted hover:underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod' . icon('social:castopod', [
'class' => 'ml-1 text-lg',
]) . '</a>',
], null, false) ?></p>

View file

@ -17,7 +17,7 @@
<div class="flex items-center mt-4 gap-x-8">
<?php if ($podcast->persons !== []): ?>
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden">
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline" data-toggle="persons-list" data-toggle-class="hidden">
<span class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($podcast->persons as $person): ?>

View file

@ -12,17 +12,17 @@
->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col flex-1 min-w-0 gap-y-2">
<?= view('_message_block') ?>
<Forms.Textarea
<x-Forms.Textarea
name="message"
required="true"
isRequired="true"
placeholder="<?= lang('Post.form.message_placeholder') ?>"
rows="2" />
<Forms.Input
<x-Forms.Input
name="episode_url"
type="url"
placeholder="<?= lang('Post.form.episode_url_placeholder') . ' (' . lang('Common.optional') . ')' ?>" />
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit') ?></Button>
<x-Button variant="primary" size="small" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit') ?></x-Button>
</div>
</form>
<hr class="my-4 border-subtle">

View file

@ -18,7 +18,7 @@
<?php endif; ?>
</h1>
<?php if ($activeQuery): ?>
<button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold focus:ring-accent" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false">
<button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false">
<?= $activeQuery['label'] . icon('arrow-drop-down-fill', [
'class' => 'ml-2 text-xl',
]) ?>

View file

@ -54,21 +54,21 @@
<form action="<?= route_to('attempt-follow', esc($actor->username)) ?>" method="POST" class="flex flex-col gap-y-2">
<?= csrf_field() ?>
<?= view('_message_block') ?>
<Forms.Field
<x-Forms.Field
name="handle"
label="<?= esc(lang('Fediverse.your_handle')) ?>"
hint="<?= esc(lang('Fediverse.your_handle_hint')) ?>"
required="true"
isRequired="true"
/>
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Fediverse.follow.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Fediverse.follow.submit') ?></x-Button>
</form>
</main>
<footer
class="flex-col w-full px-2 py-4 mt-auto text-xs text-center border-t text-skin-muted border-subtle">
<?= 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>',
], null, false) ?>

View file

@ -49,7 +49,7 @@
</a>
<div class="z-10 flex flex-wrap items-center justify-center gap-2 p-2 mt-6 shadow-xl rounded-conditional-full bg-accent-base shadow-accent/20">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<button class="inline-flex items-center px-4 text-xs font-semibold leading-8 tracking-wider text-red-600 uppercase bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden"><?= icon('heart-fill', [
<button class="inline-flex items-center px-4 text-xs font-semibold leading-8 tracking-wider text-red-600 uppercase bg-white rounded-full shadow hover:text-red-500" data-toggle="funding-links" data-toggle-class="hidden"><?= icon('heart-fill', [
'class' => 'mr-2 text-sm',
]) ?><?= lang('Podcast.sponsor') ?></button>
<?php endif; ?>
@ -61,16 +61,16 @@
[
'width' => 420,
'height' => 620,
'class' => 'group inline-flex items-center px-4 text-xs tracking-wider font-semibold text-black uppercase rounded-full leading-8 shadow focus:ring-accent bg-white',
'class' => 'group inline-flex items-center px-4 text-xs tracking-wider font-semibold text-black uppercase rounded-full leading-8 shadow bg-white',
],
) ?>
<a href="<?= $podcast->feed_url ?>" title="<?= lang('Podcast.feed') ?>" data-tooltip="bottom" class="flex items-center justify-center w-8 h-8 p-1 text-xl text-orange-500 rounded-full shadow bg-elevated focus:ring-accent" target="_blank" rel="noopener noreferrer"><?= icon('rss-fill') ?></a>
<a href="<?= $podcast->feed_url ?>" title="<?= lang('Podcast.feed') ?>" data-tooltip="bottom" class="flex items-center justify-center w-8 h-8 p-1 text-xl text-orange-500 rounded-full shadow bg-elevated" target="_blank" rel="noopener noreferrer"><?= icon('rss-fill') ?></a>
</div>
</div>
</header>
<main class="grid w-full max-w-2xl gap-4 px-4 py-6 mx-auto sm:grid-cols-2">
<a
class="inline-flex items-center justify-center flex-shrink-0 px-3 py-2 text-sm font-semibold leading-5 bg-white border-2 rounded-full shadow-xs gap-x-2 focus:ring-accent border-accent-base text-accent-base hover:border-accent-hover hover:text-accent-hover"
class="inline-flex items-center justify-center flex-shrink-0 px-3 py-2 text-sm font-semibold leading-5 bg-white border-2 rounded-full shadow-xs gap-x-2 border-accent-base text-accent-base hover:border-accent-hover hover:text-accent-hover"
href="<?= $podcast->link ?>"
target="_blank"
rel="noopener noreferrer"><?= icon('podcasting:castopod', [
@ -80,7 +80,7 @@
<?php foreach ($podcast->podcastingPlatforms as $podcastingPlatform): ?>
<?php if ($podcastingPlatform->is_visible && $podcastingPlatform->slug !== 'castopod'): ?>
<a
class="inline-flex items-center justify-center flex-shrink-0 px-3 py-2 text-sm font-semibold leading-5 bg-white border-2 rounded-full shadow-xs gap-x-2 focus:ring-accent border-accent-base text-accent-base hover:border-accent-hover hover:text-accent-hover"
class="inline-flex items-center justify-center flex-shrink-0 px-3 py-2 text-sm font-semibold leading-5 bg-white border-2 rounded-full shadow-xs gap-x-2 border-accent-base text-accent-base hover:border-accent-hover hover:text-accent-hover"
href="<?= $podcastingPlatform->link_url ?>"
target="_blank"
rel="noopener noreferrer"><?= icon($podcastingPlatform->type . ':' . $podcastingPlatform->slug, [

View file

@ -43,12 +43,12 @@
<?= icon('lock-fill', [
'class' => 'p-4 text-6xl rounded-full bg-base text-accent-base',
]) ?>
<Heading tagName="h1" size="large" class="mt-2"><?= lang('PremiumPodcasts.unlock_form.title') ?></Heading>
<x-Heading tagName="h1" size="large" class="mt-2"><?= lang('PremiumPodcasts.unlock_form.title') ?></x-Heading>
<p class="max-w-sm text-skin-muted"><?= lang('PremiumPodcasts.unlock_form.subtitle', [
'podcastTitle' => esc($podcast->title),
]) ?></p>
<?= view('_message_block') ?>
<Forms.Field
<x-Forms.Field
class="self-stretch mt-4 text-left"
name="token"
type="password"
@ -56,10 +56,10 @@
hint="<?= lang('PremiumPodcasts.unlock_form.token_hint', [
'podcastTitle' => esc($podcast->title),
]) ?>"
required="true"
isRequired="true"
/>
<?php // @icon('lock-unlock-fill')?>
<Button type="submit" variant="primary" iconLeft="lock-unlock-fill" class="self-center mt-2"><?= lang('PremiumPodcasts.unlock_form.submit') ?></Button>
<x-Button type="submit" variant="primary" iconLeft="lock-unlock-fill" class="self-center mt-2"><?= lang('PremiumPodcasts.unlock_form.submit') ?></x-Button>
<?php if ($subscriptionLink = service('settings')
->get('Subscription.link', 'podcast:' . $podcast->id)): ?>
<p class="max-w-xs mt-4 text-xs">
@ -88,7 +88,7 @@
</div>
<div class="inline-flex items-center self-end mt-2 mr-2 sm:mb-4 sm:mr-4 gap-x-2">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" data-tooltip="bottom" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500" data-toggle="funding-links" data-toggle-class="hidden" data-tooltip="bottom" title="<?= lang('Podcast.sponsor') ?>"><?= icon('heart-fill') ?></button>
<?php endif; ?>
</div>
</header>

View file

@ -30,7 +30,7 @@
) ?>"><?= icon('heart-fill', [
'class' => 'text-2xl mr-1 opacity-40',
]) . $post->favourites_count ?></button>
<button id="<?= $post->id . '-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-skin-muted focus:ring-accent" data-dropdown="button" data-dropdown-target="<?= $post->id . '-more-dropdown-menu' ?>" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more-2-fill') ?></button>
<button id="<?= $post->id . '-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-skin-muted" data-dropdown="button" data-dropdown-target="<?= $post->id . '-more-dropdown-menu' ?>" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more-2-fill') ?></button>
</form>
<nav id="<?= $post->id .
'-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm rounded-lg shadow border-3 border-subtle bg-elevated" aria-labelledby="<?= $post->id .

View file

@ -23,7 +23,7 @@
</div>
</header>
<?php if (substr_count($post->message, "\n") >= 3 || strlen($post->message) > 250): ?>
<ReadMore id="<?= $index ?>" class="px-6 mb-4 post-content"><?= $post->message_html ?></ReadMore>
<x-ReadMore id="<?= $index ?>" class="px-6 mb-4 post-content"><?= $post->message_html ?></x-ReadMore>
<?php else: ?>
<div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php endif; ?>

View file

@ -24,16 +24,16 @@ if ($post->in_reply_to_id): ?>
->avatar_image_url ?>" alt="<?= esc(interact_as_actor()
->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col flex-1">
<Forms.Textarea
<x-Forms.Textarea
name="message"
class="w-full mb-4"
required="true"
isRequired="true"
placeholder="<?= lang('Post.form.reply_to_placeholder', [
'actorUsername' => esc($post->actor->username),
]) ?>"
rows="1" />
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit_reply') ?></Button>
<x-Button variant="primary" size="small" type="submit" name="action" value="reply" class="self-end" iconRight="send-plane-2-fill"><?= lang('Post.form.submit_reply') ?></x-Button>
</div>
</form>
<?php else: ?>

View file

@ -32,7 +32,7 @@ if (can_user_interact()): ?>
) ?>"><?= icon('heart-fill', [
'class' => 'text-lg mr-1 opacity-40',
]) . $reply->favourites_count ?></button>
<button id="<?= $reply->id . '-more-dropdown' ?>" type="button" class="text-xl text-skin-muted focus:ring-accent" data-dropdown="button" data-dropdown-target="<?= $reply->id . '-more-dropdown-menu' ?>" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more-2-fill') ?></button>
<button id="<?= $reply->id . '-more-dropdown' ?>" type="button" class="text-xl text-skin-muted" data-dropdown="button" data-dropdown-target="<?= $reply->id . '-more-dropdown-menu' ?>" aria-label="<?= lang('Common.more') ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more-2-fill') ?></button>
</form>
<nav id="<?= $reply->id . '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm rounded-lg shadow border-3 border-subtle bg-elevated" aria-labelledby="<?= $reply->id . '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom">
<?= anchor(

View file

@ -3,7 +3,7 @@
<?= $this->section('content') ?>
<nav class="py-2">
<a href="<?= route_to('podcast-activity', esc($podcast->handle)) ?>"
class="inline-flex items-center px-4 py-2 text-sm focus:ring-accent"><?= icon(
class="inline-flex items-center px-4 py-2 text-sm"><?= icon(
'arrow-left-line',
[
'class' => 'mr-2 text-lg',

View file

@ -46,19 +46,19 @@
<?= csrf_field() ?>
<?= view('_message_block') ?>
<Forms.Field
<x-Forms.Field
name="handle"
label="<?= esc(lang('Fediverse.your_handle')) ?>"
hint="<?= esc(lang('Fediverse.your_handle_hint')) ?>"
required="true" />
isRequired="true" />
<?php // @icon('send-plane-2-fill')?>
<Button variant="primary" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Fediverse.' . $action . '.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end" iconRight="send-plane-2-fill"><?= lang('Fediverse.' . $action . '.submit') ?></x-Button>
</form>
</main>
<footer
class="flex-col w-full px-2 py-4 mt-auto text-xs text-center border-t text-skin-muted border-subtle">
<?= 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>',
], null, false) ?>

View file

@ -19,21 +19,21 @@
<header class="mb-4">
<a href="<?= route_to(
'home',
) ?>" class="inline-flex items-baseline text-4xl font-bold font-display text-accent-base focus:ring-accent">
) ?>" class="inline-flex items-baseline text-4xl font-bold font-display text-accent-base">
<?= 'castopod' . svg('castopod-logo', 'h-8 ml-2') ?>
</a>
</header>
<main class="flex flex-col w-full max-w-md px-6 py-4 mx-auto rounded-lg bg-elevated border-3 border-subtle gap-y-4">
<Heading tagName="h1" size="large" class="self-center"><?= $this->renderSection(
<x-Heading tagName="h1" size="large" class="self-center"><?= $this->renderSection(
'title',
) ?></Heading>
) ?></x-Heading>
<?= view('_message_block') ?>
<?= $this->renderSection('content') ?>
</main>
<footer class="flex flex-col text-sm">
<?= $this->renderSection('footer') ?>
<small class="py-4 text-center border-t border-subtle"><?= 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>',
], null, false) ?></small>

View file

@ -1,19 +1,19 @@
<?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; ?>

View file

@ -12,18 +12,18 @@
<form action="<?= url_to('auth-action-handle') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="email"
label="<?= esc(lang('Auth.email')) ?>"
helper="<?= esc(lang('Auth.confirmEmailAddress')) ?>"
required="true"
isRequired="true"
type="email"
inputmode="email"
autocomplete="email"
value="<?= $user->email ?>"
/>
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></x-Button>
</form>

View file

@ -11,19 +11,19 @@
<form action="<?= url_to('auth-action-verify') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="token"
label="<?= esc(lang('Auth.code')) ?>"
helper="<?= esc(lang('Auth.emailConfirmCode')) ?>"
pattern="[0-9]*"
placeholder="000000"
required="true"
isRequired="true"
type="number"
inputmode="numeric"
autocomplete="one-time-code"
/>
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.confirm') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.confirm') ?></x-Button>
</form>
<?= $this->endSection() ?>

View file

@ -14,10 +14,10 @@
<?= csrf_field() ?>
<!-- Code -->
<Forms.Field
<x-Forms.Field
name="token"
label="<?= esc(lang('Auth.token')) ?>"
required="true"
isRequired="true"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
@ -25,7 +25,7 @@
placeholder="000000"
/>
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></x-Button>
</form>
<?= $this->endSection() ?>

View file

@ -12,30 +12,30 @@
<form actions="<?= url_to('login') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="email"
label="<?= esc(lang('Auth.email')) ?>"
required="true"
isRequired="true"
type="email"
inputmode="email"
autocomplete="username"
autofocus="autofocus"
/>
<Forms.Field
<x-Forms.Field
name="password"
label="<?= esc(lang('Auth.password')) ?>"
type="password"
inputmode="text"
autocomplete="current-password"
required="true" />
isRequired="true" />
<!-- Remember me -->
<?php if (setting('Auth.sessionConfig')['allowRemembering']): ?>
<Forms.Toggler name="remember" value="yes" checked="<?= old('remember') ?>" size="small"><?= lang('Auth.rememberMe') ?></Forms.Toggler>
<x-Forms.Toggler name="remember" isChecked="<?= old('remember') ?>" size="small"><?= lang('Auth.rememberMe') ?></x-Forms.Toggler>
<?php endif; ?>
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.login') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.login') ?></x-Button>
</form>
<?= $this->endSection() ?>

View file

@ -11,17 +11,17 @@
<form actions="<?= url_to('magic-link') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="email"
label="<?= esc(lang('Auth.email')) ?>"
required="true"
isRequired="true"
inputmode="email"
autocomplete="email"
autofocus="autofocus"
value="<?= old('email', auth()->user()->email ?? null) ?>"
/>
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.send') ?></x-Button>
</form>
<?= $this->endSection() ?>

View file

@ -14,15 +14,15 @@
<form action="<?= url_to('magic-link-set-password') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="new_password"
label="<?= esc(lang('Auth.password')) ?>"
type="password"
required="true"
isRequired="true"
inputmode="text"
autocomplete="new-password" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.set_password') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.set_password') ?></x-Button>
</form>

View file

@ -14,38 +14,38 @@
<form action="<?= url_to('register') ?>" method="POST" class="flex flex-col w-full gap-y-4">
<?= csrf_field() ?>
<Forms.Field
<x-Forms.Field
name="username"
label="<?= esc(lang('Auth.username')) ?>"
autocomplete="username"
inputmode="text"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="email"
label="<?= esc(lang('Auth.email')) ?>"
type="email"
inputmode="email"
autocomplete="email"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="password"
label="<?= esc(lang('Auth.password')) ?>"
type="password"
required="true"
isRequired="true"
inputmode="text"
autocomplete="new-password" />
<Forms.Field
<x-Forms.Field
name="password_confirm"
label="<?= esc(lang('Auth.passwordConfirm')) ?>"
type="password"
required="true"
isRequired="true"
inputmode="text"
autocomplete="new-password" />
<Button variant="primary" type="submit" class="self-end"><?= lang('Auth.register') ?></Button>
<x-Button variant="primary" type="submit" class="self-end"><?= lang('Auth.register') ?></x-Button>
</form>

View file

@ -29,7 +29,7 @@
</main>
<footer class="container px-2 py-4 mx-auto text-sm text-right border-t border-subtle">
<small><?= 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>',
], null, false) ?></small>

View file

@ -1,19 +1,19 @@
<?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; ?>

View file

@ -8,7 +8,7 @@
<div class="flex flex-col mb-2">
<div class="flex items-center">
<span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider border-4 rounded-full text-accent-base border-accent-base">3/4</span>
<Heading tagName="h1"><?= lang('Install.form.cache_config') ?></h1>
<x-Heading tagName="h1"><?= lang('Install.form.cache_config') ?></h1>
</div>
<p class="mt-2 text-sm text-skin-muted"><?= lang(
@ -16,7 +16,7 @@
) ?></p>
</div>
<Forms.Field
<x-Forms.Field
as="Select"
name="cache_handler"
label="<?= esc(lang('Install.form.cache_handler')) ?>"
@ -26,9 +26,9 @@
'predis' => lang('Install.form.cacheHandlerOptions.predis'),
])) ?>"
selected="file"
required="true" />
isRequired="true" />
<?php // @icon('arrow-right-fill')?>
<Button variant="primary" class="self-end" iconRight="arrow-right-fill" type="submit"><?= lang('Install.form.next') ?></Button>
<x-Button variant="primary" class="self-end" iconRight="arrow-right-fill" type="submit"><?= lang('Install.form.next') ?></x-Button>
<?= form_close() ?>

View file

@ -7,29 +7,29 @@
<div class="flex items-center mb-2">
<span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider border-4 rounded-full text-accent-base border-accent-base">4/4</span>
<Heading tagName="h1"><?= lang('Install.form.create_superadmin') ?></Heading>
<x-Heading tagName="h1"><?= lang('Install.form.create_superadmin') ?></x-Heading>
</div>
<Forms.Field
<x-Forms.Field
name="username"
label="<?= esc(lang('Install.form.username')) ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="email"
label="<?= esc(lang('Install.form.email')) ?>"
type="email"
autocomplete="username"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="password"
label="<?= esc(lang('Install.form.password')) ?>"
type="password"
required="true"
isRequired="true"
autocomplete="new-password" />
<?php // @icon('check-fill')?>
<Button variant="primary" type="submit" class="self-end" iconLeft="check-fill"><?= lang('Install.form.submit') ?></Button>
<x-Button variant="primary" type="submit" class="self-end" iconLeft="check-fill"><?= lang('Install.form.submit') ?></x-Button>
</form>

View file

@ -11,9 +11,9 @@
<div class="flex flex-col mb-2">
<div class="flex items-center">
<span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider border-4 rounded-full text-accent-base border-accent-base">2/4</span>
<Heading tagName="h1"><?= lang(
<x-Heading tagName="h1"><?= lang(
'Install.form.database_config',
) ?></Heading>
) ?></x-Heading>
</div>
<p class="mt-2 text-sm text-skin-muted"><?= lang(
@ -21,41 +21,41 @@
) ?></p>
</div>
<Forms.Field
<x-Forms.Field
name="db_hostname"
label="<?= esc(lang('Install.form.db_hostname')) ?>"
value="<?= config('Database')->default['hostname'] ?>"
required="true"
isRequired="true"
/>
<Forms.Field
<x-Forms.Field
name="db_name"
label="<?= esc(lang('Install.form.db_name')) ?>"
value="<?= config('Database')->default['database'] ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="db_username"
label="<?= esc(lang('Install.form.db_username')) ?>"
value="<?= config('Database')->default['username'] ?>"
required="true"
isRequired="true"
autocomplete="off" />
<Forms.Field
<x-Forms.Field
name="db_password"
label="<?= esc(lang('Install.form.db_password')) ?>"
value="<?= config('Database')->default['password'] ?>"
type="password"
required="true"
isRequired="true"
autocomplete="off" />
<Forms.Field
<x-Forms.Field
name="db_prefix"
label="<?= esc(lang('Install.form.db_prefix')) ?>"
hint="<?= esc(lang('Install.form.db_prefix_hint')) ?>"
value="<?= config('Database')->default['DBPrefix'] ?>" />
<?php // @icon('arrow-right-fill')?>
<Button variant="primary" type="submit" class="self-end" iconRight="arrow-right-fill"><?= lang('Install.form.next') ?></Button>
<x-Button variant="primary" type="submit" class="self-end" iconRight="arrow-right-fill"><?= lang('Install.form.next') ?></x-Button>
</form>

View file

@ -10,35 +10,35 @@
<div class="flex items-center mb-2">
<span class="inline-flex items-center justify-center w-12 h-12 mr-2 text-sm font-semibold tracking-wider border-4 rounded-full text-accent-base border-accent-base">1/4</span>
<Heading tagName="h1"><?= lang('Install.form.instance_config') ?></Heading>
<x-Heading tagName="h1"><?= lang('Install.form.instance_config') ?></x-Heading>
</div>
<Forms.Field
<x-Forms.Field
name="hostname"
label="<?= esc(lang('Install.form.hostname')) ?>"
value="<?= host_url() === null ? config('App')->baseURL : host_url() ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="media_base_url"
label="<?= esc(lang('Install.form.media_base_url')) ?>"
hint="<?= esc(lang('Install.form.media_base_url_hint')) ?>" />
<Forms.Field
<x-Forms.Field
name="admin_gateway"
label="<?= esc(lang('Install.form.admin_gateway')) ?>"
hint="<?= esc(lang('Install.form.admin_gateway_hint')) ?>"
value="<?= config('Admin')->gateway ?>"
required="true" />
isRequired="true" />
<Forms.Field
<x-Forms.Field
name="auth_gateway"
label="<?= esc(lang('Install.form.auth_gateway')) ?>"
hint="<?= esc(lang('Install.form.auth_gateway_hint')) ?>"
value="<?= config('Auth')->gateway ?>"
required="true" />
isRequired="true" />
<?php // @icon('arrow-right-fill')?>
<Button class="self-end" variant="primary" type="submit" iconRight="arrow-right-fill"><?= lang('Install.form.next') ?></Button>
<x-Button class="self-end" variant="primary" type="submit" iconRight="arrow-right-fill"><?= lang('Install.form.next') ?></x-Button>
</form>
<?= $this->endSection() ?>