Create component system to help cut down on view duplication, see #31

This commit is contained in:
Timothy Warren 2020-08-21 12:30:01 -04:00
parent 9749c59549
commit 7aeb74874b
20 changed files with 461 additions and 186 deletions

View File

@ -20,6 +20,7 @@ use Aura\Html\HelperLocatorFactory;
use Aura\Router\RouterContainer;
use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{Anilist, Kitsu};
use Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Model;
use Aviat\Banker\Teller;
use Aviat\Ion\Config;
@ -31,6 +32,9 @@ use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Psr\SimpleCache\CacheInterface;
define('APP_DIR', __DIR__);
define('TEMPLATE_DIR', APP_DIR . '/templates');
// -----------------------------------------------------------------------------
// Setup DI container
// -----------------------------------------------------------------------------
@ -72,28 +76,45 @@ return static function (array $configArray = []): Container {
// Create Aura Router Object
$container->set('aura-router', fn() => new RouterContainer);
// Create Html helper Object
// Create Html helpers
$container->set('html-helper', static function(ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory)->newInstance();
$htmlHelper->set('menu', static function() use ($container) {
$menuHelper = new Helper\Menu();
$menuHelper->setContainer($container);
return $menuHelper;
});
$htmlHelper->set('field', static function() use ($container) {
$formHelper = new Helper\Form();
$formHelper->setContainer($container);
return $formHelper;
});
$htmlHelper->set('picture', static function() use ($container) {
$pictureHelper = new Helper\Picture();
$pictureHelper->setContainer($container);
return $pictureHelper;
$helpers = [
'menu' => Helper\Menu::class,
'field' => Helper\Form::class,
'picture' => Helper\Picture::class,
];
foreach ($helpers as $name => $class)
{
$htmlHelper->set($name, static function() use ($class, $container) {
$helper = new $class;
$helper->setContainer($container);
return $helper;
});
}
return $htmlHelper;
});
// Create Component helpers
$container->set('component-helper', static function () {
$helper = (new HelperLocatorFactory)->newInstance();
$components = [
'character' => Component\Character::class,
'media' => Component\Media::class,
'tabs' => Component\Tabs::class,
'verticalTabs' => Component\VerticalTabs::class,
];
foreach ($components as $name => $componentClass)
{
$helper->set($name, fn () => new $componentClass);
}
return $helper;
});
// Create Request Object
$container->set('request', fn () => ServerRequestFactory::fromGlobals(
$_SERVER,

View File

@ -0,0 +1,6 @@
<article class="<?= $className ?>">
<div class="name">
<a href="<?= $link ?>"><?= $name ?></a>
</div>
<a href="<?= $link ?>"><?= $picture ?></a>
</article>

12
app/templates/media.php Normal file
View File

@ -0,0 +1,12 @@
<article class="<?= $className ?>">
<a href="<?= $link ?>"><?= $picture ?></a>
<div class="name">
<a href="<?= $link ?>">
<?= array_shift($titles) ?>
<?php foreach ($titles as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>

23
app/templates/tabs.php Normal file
View File

@ -0,0 +1,23 @@
<div class="tabs">
<?php $i = 0; foreach ($data as $tabName => $tabData): ?>
<?php if ( ! empty($tabData)): ?>
<?php $id = "{$name}-{$i}"; ?>
<input
role='tab'
aria-controls="_<?= $id ?>"
type="radio"
name="<?= $name ?>"
id="<?= $id ?>"
<?= ($i === 0) ? 'checked="checked"' : '' ?>
/>
<label for="<?= $id ?>"><?= ucfirst($tabName) ?></label>
<section
id="_<?= $id ?>"
role="tabpanel"
class="<?= $className ?>"
>
<?= $callback($tabData, $tabName) ?>
</section>
<?php endif ?>
<?php $i++; endforeach ?>
</div>

View File

@ -0,0 +1,25 @@
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($data as $tabName => $tabData): ?>
<?php $id = "{$name}-{$i}" ?>
<div class="tab">
<input
type="radio"
role='tab'
aria-controls="_<?= $id ?>"
name="staff-roles"
id="<?= $id ?>"
<?= $i === 0 ? 'checked="checked"' : '' ?>
/>
<label for="<?= $id ?>"><?= $tabName ?></label>
<section
id='_<?= $id ?>'
role="tabpanel"
class="<?= $className ?>"
>
<?= $callback($tabData, $tabName) ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>

View File

@ -1,9 +1,11 @@
<?php
use Aviat\AnimeClient\API\Kitsu;
use function Aviat\AnimeClient\getLocalImg;
?>
<main class="details fixed">
<section class="flex">
<section class="flex" unselectable>
<aside class="info">
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
@ -119,10 +121,12 @@ use function Aviat\AnimeClient\getLocalImg;
<iframe
width="560"
height="315"
role='img'
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
tabindex='0'
title="<?= $data['title'] ?> trailer video"
></iframe>
</div>
<?php endif ?>
@ -133,31 +137,24 @@ use function Aviat\AnimeClient\getLocalImg;
<section>
<h2>Characters</h2>
<div class="tabs">
<?php $i = 0 ?>
<?php foreach ($data['characters'] as $role => $list): ?>
<input
type="radio" name="character-types"
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> />
<label for="character-types-<?= $i ?>"><?= ucfirst($role) ?></label>
<section class="content media-wrap flex flex-wrap flex-justify-start">
<?php foreach ($list as $id => $char): ?>
<?php if ( ! empty($char['image']['original'])): ?>
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character">
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?>
<div class="name">
<?= $helper->a($link, $char['name']) ?>
</div>
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?>
</a>
</article>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
<?= $component->tabs('character-types', $data['characters'], static function ($characterList, $role)
use ($component, $url, $helper) {
$rendered = [];
foreach ($characterList as $id => $character):
if (empty($character['image']['original']))
{
continue;
}
$rendered[] = $component->character(
$character['name'],
$url->generate('character', ['slug' => $character['slug']]),
$helper->picture("images/characters/{$id}.webp"),
(strtolower($role) !== 'main') ? 'small-character' : 'character'
);
endforeach;
return implode('', array_map('mb_trim', $rendered));
}) ?>
</section>
<?php endif ?>
@ -165,31 +162,24 @@ use function Aviat\AnimeClient\getLocalImg;
<section>
<h2>Staff</h2>
<div class="vertical-tabs">
<?php $i = 0; ?>
<?php foreach ($data['staff'] as $role => $people): ?>
<div class="tab">
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> />
<label for="staff-role<?= $i ?>"><?= $role ?></label>
<section class='content media-wrap flex flex-wrap flex-justify-start'>
<?php foreach ($people as $pid => $person): ?>
<article class='character small-person'>
<?php $link = $url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]) ?>
<div class="name">
<a href="<?= $link ?>">
<?= $person['name'] ?>
</a>
</div>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($person['image']['original'] ?? NULL)) ?>
</a>
</article>
<?php endforeach ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
<?= $component->verticalTabs('staff-role', $data['staff'], static function ($staffList)
use ($component, $url, $helper) {
$rendered = [];
foreach ($staffList as $id => $person):
if (empty($person['image']['original']))
{
continue;
}
$rendered[] = $component->character(
$person['name'],
$url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]),
$helper->picture(getLocalImg($person['image']['original'] ?? NULL)),
'character small-person',
);
endforeach;
return implode('', array_map('mb_trim', $rendered));
}) ?>
</section>
<?php endif ?>
</main>

View File

@ -156,65 +156,50 @@ use Aviat\AnimeClient\API\Kitsu;
<?php if ( ! empty($vas)): ?>
<h4>Voice Actors</h4>
<div class="tabs">
<?php $i = 0; ?>
<?= $component->tabs('character-vas', $vas, static function ($casting) use ($url, $component, $helper) {
$castings = [];
foreach ($casting as $id => $c):
$person = $component->character(
$c['person']['name'],
$url->generate('person', [
'id' => $c['person']['id'],
'slug' => $c['person']['slug']
]),
$helper->picture(getLocalImg($c['person']['image']))
);
$medias = array_map(fn ($series) => $component->media(
array_merge([$series['title']], $series['titles']),
$url->generate('anime.details', ['id' => $series['slug']]),
$helper->picture(getLocalImg($series['posterImage'], TRUE))
), $c['series']);
$media = implode('', array_map('mb_trim', $medias));
<?php foreach ($vas as $language => $casting): ?>
<input <?= $i === 0 ? 'checked="checked"' : '' ?> type="radio" id="character-va<?= $i ?>"
name="character-vas"
/>
<label for="character-va<?= $i ?>"><?= $language ?></label>
<section class="content">
$castings[] = <<<HTML
<tr>
<td>{$person}</td>
<td width="75%">
<section class="align-left media-wrap-flex">
{$media}
</section>
</td>
</tr>
HTML;
endforeach;
$languages = implode('', array_map('mb_trim', $castings));
return <<<HTML
<table class="borderless max-table">
<thead>
<tr>
<th>Cast Member</th>
<th>Series</th>
</tr>
<?php foreach ($casting as $c): ?>
<tr>
<td>
<article class="character">
<?php
$link = $url->generate('person', ['id' => $c['person']['id'], 'slug' => $c['person']['slug']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($c['person']['image'])) ?>
<div class="name">
<?= $c['person']['name'] ?>
</div>
</a>
</article>
</td>
<td width="75%">
<section class="align-left media-wrap-flex">
<?php foreach ($c['series'] as $series): ?>
<article class="media">
<?php
$link = $url->generate('anime.details', ['id' => $series['slug']]);
?>
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($series['posterImage'], TRUE)) ?>
</a>
<div class="name">
<a href="<?= $link ?>">
<?= $series['title'] ?>
<?php foreach ($series['titles'] as $title): ?>
<br />
<small><?= $title ?></small>
<?php endforeach ?>
</a>
</div>
</article>
<?php endforeach ?>
</section>
</td>
</tr>
<?php endforeach ?>
</thead>
<tbody>{$languages}</tbody>
</table>
</section>
<?php $i++ ?>
<?php endforeach ?>
</div>
HTML;
}, 'content') ?>
<?php endif ?>
<?php endif ?>
</section>

View File

@ -26,7 +26,7 @@
</head>
<body class="<?= $escape->attr($url_type) ?> list">
<?php include 'setup-check.php' ?>
<header>
<header tabindex="0">
<?php
include 'main-menu.php';
if(isset($message) && is_array($message))
@ -39,5 +39,4 @@
}
}
?>
</header>

View File

@ -9,6 +9,9 @@ use ConsoleKit\Console;
$_SERVER['HTTP_HOST'] = 'localhost';
define('APP_DIR', __DIR__ . '/app');
define('TEMPLATE_DIR', APP_DIR . '/templates');
// -----------------------------------------------------------------------------
// Start console script
// -----------------------------------------------------------------------------

View File

@ -94,6 +94,7 @@ a:hover, a:active {
iframe {
display: block;
margin: 0 auto;
border: 0;
}
/* -----------------------------------------------------------------------------

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Character {
use ComponentTrait;
public function __invoke(string $name, string $link, string $picture, string $className = 'character'): string
{
return $this->render('character.php', [
'name' => $name,
'link' => $link,
'picture' => $picture,
'className' => $className,
]);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
/**
* Shared logic for component-based functionality, like Tabs
*/
trait ComponentTrait {
public function render(string $path, array $data): string
{
ob_start();
extract($data, EXTR_OVERWRITE);
include \TEMPLATE_DIR . '/' .$path;
return ob_get_clean();
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Media {
use ComponentTrait;
public function __invoke(array $titles, string $link, string $picture, string $className = 'media'): string
{
return $this->render('media.php', [
'titles' => $titles,
'link' => $link,
'picture' => $picture,
'className' => $className,
]);
}
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class Tabs {
use ComponentTrait;
/**
* Creates a tabbed content view
*
* @param string $name the name attribute for the input[type-option] form elements
* also used to generate id attributes
* @param array $tabData The data used to create the tab content, indexed by the tab label
* @param callable $cb The function to generate the tab content
* @return string
*/
public function __invoke(
string $name,
array $tabData,
callable $cb,
string $className = 'content media-wrap flex flex-wrap flex-justify-start'
): string
{
return $this->render('tabs.php', [
'name' => $name,
'data' => $tabData,
'callback' => $cb,
'className' => $className,
]);
}
}

View File

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/**
* Hummingbird Anime List Client
*
* An API client for Kitsu to manage anime and manga watch lists
*
* PHP version 7.4
*
* @package HummingbirdAnimeClient
* @author Timothy J. Warren <tim@timshomepage.net>
* @copyright 2015 - 2020 Timothy J. Warren
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @version 5.1
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
*/
namespace Aviat\AnimeClient\Component;
final class VerticalTabs {
use ComponentTrait;
/**
* Creates a vertical tab content view
*
* @param string $name the name attribute for the input[type-option] form elements
* also used to generate id attributes
* @param array $tabData The data used to create the tab content, indexed by the tab label
* @param callable $cb The function to generate the tab content
* @return string
*/
public function __invoke(
string $name,
array $tabData,
callable $cb,
string $className='content media-wrap flex flex-wrap flex-justify-start'
): string
{
return $this->render('vertical-tabs.php', [
'name' => $name,
'data' => $tabData,
'callback' => $cb,
'className' => $className,
]);
}
}

View File

@ -39,11 +39,30 @@ final class FormGenerator {
* @throws ContainerException
* @throws NotFoundException
*/
public function __construct(ContainerInterface $container)
private function __construct(ContainerInterface $container)
{
$this->helper = $container->get('html-helper');
}
/**
* Create a new FormGenerator
*
* @param ContainerInterface $container
* @return $this
*/
public static function new(ContainerInterface $container): self
{
try
{
return new static($container);
}
catch (\Throwable $e)
{
dump($e);
die();
}
}
/**
* Generate the html structure of the form
*

View File

@ -35,6 +35,6 @@ final class Form {
*/
public function __invoke(string $name, array $form)
{
return (new FormGenerator($this->container))->generate($name, $form);
return FormGenerator::new($this->container)->generate($name, $form);
}
}

View File

@ -34,8 +34,7 @@ final class Menu {
*/
public function __invoke($menuName)
{
$generator = new MenuGenerator($this->container);
return $generator->generate($menuName);
return MenuGenerator::new($this->container)->generate($menuName);
}
}

View File

@ -44,42 +44,22 @@ final class MenuGenerator extends UrlGenerator {
protected RequestInterface $request;
/**
* MenuGenerator constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
* @return static
*/
public function __construct(ContainerInterface $container)
public static function new(ContainerInterface $container): self
{
parent::__construct($container);
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
try
{
return new static($container);
}
/**
* Generate the full menu structure from the config files
*
* @param array $menus
* @return array
*/
protected function parseConfig(array $menus) : array
catch (\Throwable $e)
{
$parsed = [];
foreach ($menus as $name => $menu)
{
$parsed[$name] = [];
foreach ($menu['items'] as $pathName => $partialPath)
{
$title = (string)StringType::from($pathName)->humanize()->titleize();
$parsed[$name][$title] = (string)StringType::from($menu['route_prefix'])->append($partialPath);
dump($e);
die();
}
}
return $parsed;
}
/**
* Generate the html structure of the menu selected
*
@ -120,5 +100,42 @@ final class MenuGenerator extends UrlGenerator {
// Create the menu html
return (string) $this->helper->ul();
}
/**
* MenuGenerator constructor.
*
* @param ContainerInterface $container
* @throws ContainerException
* @throws NotFoundException
*/
private function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->helper = $container->get('html-helper');
$this->request = $container->get('request');
}
/**
* Generate the full menu structure from the config files
*
* @param array $menus
* @return array
*/
private function parseConfig(array $menus) : array
{
$parsed = [];
foreach ($menus as $name => $menu)
{
$parsed[$name] = [];
foreach ($menu['items'] as $pathName => $partialPath)
{
$title = (string)StringType::from($pathName)->humanize()->titleize();
$parsed[$name][$title] = (string)StringType::from($menu['route_prefix'])->append($partialPath);
}
}
return $parsed;
}
}
// End of MenuGenerator.php

View File

@ -16,7 +16,6 @@
namespace Aviat\Ion\View;
use Aura\Html\HelperLocator;
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Di\ContainerInterface;
use Aviat\Ion\Di\Exception\ContainerException;
@ -30,13 +29,6 @@ use const EXTR_OVERWRITE;
class HtmlView extends HttpView {
use ContainerAware;
/**
* HTML generator/escaper helper
*
* @var HelperLocator
*/
protected HelperLocator $helper;
/**
* Response mime type
*
@ -56,7 +48,6 @@ class HtmlView extends HttpView {
parent::__construct();
$this->setContainer($container);
$this->helper = $container->get('html-helper');
$this->response = new HtmlResponse('');
}
@ -69,8 +60,10 @@ class HtmlView extends HttpView {
*/
public function renderTemplate(string $path, array $data): string
{
$data['helper'] = $this->helper;
$data['escape'] = $this->helper->escape();
$helper = $this->container->get('html-helper');
$data['component'] = $this->container->get('component-helper');
$data['helper'] = $helper;
$data['escape'] = $helper->escape();
$data['container'] = $this->container;
ob_start();