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\Router\RouterContainer;
use Aura\Session\SessionFactory; use Aura\Session\SessionFactory;
use Aviat\AnimeClient\API\{Anilist, Kitsu}; use Aviat\AnimeClient\API\{Anilist, Kitsu};
use Aviat\AnimeClient\Component;
use Aviat\AnimeClient\Model; use Aviat\AnimeClient\Model;
use Aviat\Banker\Teller; use Aviat\Banker\Teller;
use Aviat\Ion\Config; use Aviat\Ion\Config;
@ -31,6 +32,9 @@ use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
define('APP_DIR', __DIR__);
define('TEMPLATE_DIR', APP_DIR . '/templates');
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Setup DI container // Setup DI container
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@ -72,28 +76,45 @@ return static function (array $configArray = []): Container {
// Create Aura Router Object // Create Aura Router Object
$container->set('aura-router', fn() => new RouterContainer); $container->set('aura-router', fn() => new RouterContainer);
// Create Html helper Object // Create Html helpers
$container->set('html-helper', static function(ContainerInterface $container) { $container->set('html-helper', static function(ContainerInterface $container) {
$htmlHelper = (new HelperLocatorFactory)->newInstance(); $htmlHelper = (new HelperLocatorFactory)->newInstance();
$htmlHelper->set('menu', static function() use ($container) { $helpers = [
$menuHelper = new Helper\Menu(); 'menu' => Helper\Menu::class,
$menuHelper->setContainer($container); 'field' => Helper\Form::class,
return $menuHelper; 'picture' => Helper\Picture::class,
}); ];
$htmlHelper->set('field', static function() use ($container) {
$formHelper = new Helper\Form(); foreach ($helpers as $name => $class)
$formHelper->setContainer($container); {
return $formHelper; $htmlHelper->set($name, static function() use ($class, $container) {
}); $helper = new $class;
$htmlHelper->set('picture', static function() use ($container) { $helper->setContainer($container);
$pictureHelper = new Helper\Picture(); return $helper;
$pictureHelper->setContainer($container); });
return $pictureHelper; }
});
return $htmlHelper; 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 // Create Request Object
$container->set('request', fn () => ServerRequestFactory::fromGlobals( $container->set('request', fn () => ServerRequestFactory::fromGlobals(
$_SERVER, $_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 <?php
use Aviat\AnimeClient\API\Kitsu; use Aviat\AnimeClient\API\Kitsu;
use function Aviat\AnimeClient\getLocalImg; use function Aviat\AnimeClient\getLocalImg;
?> ?>
<main class="details fixed"> <main class="details fixed">
<section class="flex"> <section class="flex" unselectable>
<aside class="info"> <aside class="info">
<?= $helper->picture("images/anime/{$data['id']}-original.webp") ?> <?= $helper->picture("images/anime/{$data['id']}-original.webp") ?>
@ -115,81 +117,69 @@ use function Aviat\AnimeClient\getLocalImg;
<?php endif ?> <?php endif ?>
<?php if ( ! empty($data['trailer_id'])): ?> <?php if ( ! empty($data['trailer_id'])): ?>
<div class="responsive-iframe"> <div class="responsive-iframe">
<h4>Trailer</h4> <h4>Trailer</h4>
<iframe <iframe
width="560" width="560"
height="315" height="315"
src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>" role='img'
frameborder="0" src="https://www.youtube.com/embed/<?= $data['trailer_id'] ?>"
allow="autoplay; encrypted-media" allow="autoplay; encrypted-media"
allowfullscreen allowfullscreen
></iframe> tabindex='0'
title="<?= $data['title'] ?> trailer video"
></iframe>
</div> </div>
<?php endif ?> <?php endif ?>
</article> </article>
</section> </section>
<?php if (count($data['characters']) > 0): ?> <?php if (count($data['characters']) > 0): ?>
<section> <section>
<h2>Characters</h2> <h2>Characters</h2>
<div class="tabs"> <?= $component->tabs('character-types', $data['characters'], static function ($characterList, $role)
<?php $i = 0 ?> use ($component, $url, $helper) {
<?php foreach ($data['characters'] as $role => $list): ?> $rendered = [];
<input foreach ($characterList as $id => $character):
type="radio" name="character-types" if (empty($character['image']['original']))
id="character-types-<?= $i ?>" <?= ($i === 0) ? 'checked' : '' ?> /> {
<label for="character-types-<?= $i ?>"><?= ucfirst($role) ?></label> continue;
<section class="content media-wrap flex flex-wrap flex-justify-start"> }
<?php foreach ($list as $id => $char): ?> $rendered[] = $component->character(
<?php if ( ! empty($char['image']['original'])): ?> $character['name'],
<article class="<?= $role === 'supporting' ? 'small-' : '' ?>character"> $url->generate('character', ['slug' => $character['slug']]),
<?php $link = $url->generate('character', ['slug' => $char['slug']]) ?> $helper->picture("images/characters/{$id}.webp"),
<div class="name"> (strtolower($role) !== 'main') ? 'small-character' : 'character'
<?= $helper->a($link, $char['name']) ?> );
</div> endforeach;
<a href="<?= $link ?>">
<?= $helper->picture("images/characters/{$id}.webp") ?> return implode('', array_map('mb_trim', $rendered));
</a> }) ?>
</article> </section>
<?php endif ?>
<?php endforeach ?>
</section>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?> <?php endif ?>
<?php if (count($data['staff']) > 0): ?> <?php if (count($data['staff']) > 0): ?>
<section> <section>
<h2>Staff</h2> <h2>Staff</h2>
<div class="vertical-tabs"> <?= $component->verticalTabs('staff-role', $data['staff'], static function ($staffList)
<?php $i = 0; ?> use ($component, $url, $helper) {
<?php foreach ($data['staff'] as $role => $people): ?> $rendered = [];
<div class="tab"> foreach ($staffList as $id => $person):
<input type="radio" name="staff-roles" id="staff-role<?= $i ?>" <?= $i === 0 ? 'checked' : '' ?> /> if (empty($person['image']['original']))
<label for="staff-role<?= $i ?>"><?= $role ?></label> {
<section class='content media-wrap flex flex-wrap flex-justify-start'> continue;
<?php foreach ($people as $pid => $person): ?> }
<article class='character small-person'> $rendered[] = $component->character(
<?php $link = $url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]) ?> $person['name'],
<div class="name"> $url->generate('person', ['id' => $person['id'], 'slug' => $person['slug']]),
<a href="<?= $link ?>"> $helper->picture(getLocalImg($person['image']['original'] ?? NULL)),
<?= $person['name'] ?> 'character small-person',
</a> );
</div> endforeach;
<a href="<?= $link ?>">
<?= $helper->picture(getLocalImg($person['image']['original'] ?? NULL)) ?> return implode('', array_map('mb_trim', $rendered));
</a> }) ?>
</article> </section>
<?php endforeach ?>
</section>
</div>
<?php $i++; ?>
<?php endforeach ?>
</div>
</section>
<?php endif ?> <?php endif ?>
</main> </main>

View File

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

View File

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

View File

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

View File

@ -94,6 +94,7 @@ a:hover, a:active {
iframe { iframe {
display: block; display: block;
margin: 0 auto; 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 ContainerException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function __construct(ContainerInterface $container) private function __construct(ContainerInterface $container)
{ {
$this->helper = $container->get('html-helper'); $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 * Generate the html structure of the form
* *

View File

@ -35,6 +35,6 @@ final class Form {
*/ */
public function __invoke(string $name, array $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) public function __invoke($menuName)
{ {
$generator = new MenuGenerator($this->container); return MenuGenerator::new($this->container)->generate($menuName);
return $generator->generate($menuName);
} }
} }

View File

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

View File

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