Commit 40a0535f authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(public-ui): adapt public podcast and episode pages to wireframes

- adapt wireframes with responsive design
- refactor models methods to cache requests for faster queries
- update public controllers to cache pages while retaining analytics hits
- add platform links to podcast page
- add previous / next episodes in episode page
- update npm packages to latest versions

closes #30, #13
parent 2517808c
......@@ -26,7 +26,7 @@ class Contributor extends BaseController
public function _remap($method, ...$params)
{
$this->podcast = (new PodcastModel())->find($params[0]);
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
if (count($params) > 1) {
if (
......
......@@ -25,7 +25,7 @@ class Episode extends BaseController
public function _remap($method, ...$params)
{
$this->podcast = (new PodcastModel())->find($params[0]);
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
if (count($params) > 1) {
if (
......
......@@ -25,7 +25,11 @@ class Podcast extends BaseController
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
......@@ -58,26 +62,8 @@ class Podcast extends BaseController
{
helper(['form', 'misc']);
$categories = (new CategoryModel())->findAll();
$languages = (new LanguageModel())->findAll();
$languageOptions = array_reduce(
$languages,
function ($result, $language) {
$result[$language->code] = $language->native_name;
return $result;
},
[]
);
$categoryOptions = array_reduce(
$categories,
function ($result, $category) {
$result[$category->id] = lang(
'Podcast.category_options.' . $category->code
);
return $result;
},
[]
);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
......@@ -157,26 +143,8 @@ class Podcast extends BaseController
{
helper(['form', 'misc']);
$categories = (new CategoryModel())->findAll();
$languages = (new LanguageModel())->findAll();
$languageOptions = array_reduce(
$languages,
function ($result, $language) {
$result[$language->code] = $language->native_name;
return $result;
},
[]
);
$categoryOptions = array_reduce(
$categories,
function ($result, $category) {
$result[$category->id] = lang(
'Podcast.category_options.' . $category->code
);
return $result;
},
[]
);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
......@@ -373,26 +341,8 @@ class Podcast extends BaseController
{
helper('form');
$categories = (new CategoryModel())->findAll();
$languages = (new LanguageModel())->findAll();
$languageOptions = array_reduce(
$languages,
function ($result, $language) {
$result[$language->code] = $language->native_name;
return $result;
},
[]
);
$categoryOptions = array_reduce(
$categories,
function ($result, $category) {
$result[$category->id] = lang(
'Podcast.category_options.' . $category->code
);
return $result;
},
[]
);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'podcast' => $this->podcast,
......
......@@ -21,7 +21,9 @@ class PodcastSettings extends BaseController
public function _remap($method, ...$params)
{
if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[0]);
......@@ -40,7 +42,9 @@ class PodcastSettings extends BaseController
$data = [
'podcast' => $this->podcast,
'platforms' => (new PlatformModel())->getPlatformsWithLinks(),
'platforms' => (new PlatformModel())->getPlatformsWithLinks(
$this->podcast->id
),
];
replace_breadcrumb_params([0 => $this->podcast->title]);
......
......@@ -26,7 +26,7 @@ class BaseController extends Controller
*
* @var array
*/
protected $helpers = ['analytics'];
protected $helpers = ['analytics', 'svg'];
/**
* Constructor.
......
......@@ -25,18 +25,14 @@ class Episode extends BaseController
public function _remap($method, ...$params)
{
$this->podcast = (new PodcastModel())
->where('name', $params[0])
->first();
$this->podcast = (new PodcastModel())->getPodcastByName($params[0]);
if (
count($params) > 1 &&
!($this->episode = (new EpisodeModel())
->where([
'podcast_id' => $this->podcast->id,
'slug' => $params[1],
])
->first())
!($this->episode = (new EpisodeModel())->getEpisodeBySlug(
$this->podcast->id,
$params[1]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
......@@ -46,15 +42,31 @@ class Episode extends BaseController
public function index()
{
// The page cache is set to a decade so it is deleted manually upon podcast update
$this->cachePage(DECADE);
self::triggerWebpageHit($this->episode->podcast_id);
self::triggerWebpageHit($this->podcast->id);
if (
!($cachedView = cache(
"page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}"
))
) {
$previousNextEpisodes = (new EpisodeModel())->getPreviousNextEpisodes(
$this->episode,
$this->podcast->type
);
$data = [
'previousEpisode' => $previousNextEpisodes['previous'],
'nextEpisode' => $previousNextEpisodes['next'],
'episode' => $this->episode,
];
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode', $data, [
'cache' => DECADE,
'cache_name' => "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}",
]);
}
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
return view('episode', $data);
return $cachedView;
}
}
......@@ -8,6 +8,7 @@
namespace App\Controllers;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
class Podcast extends BaseController
......@@ -21,9 +22,9 @@ class Podcast extends BaseController
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())
->where('name', $params[0])
->first())
!($this->podcast = (new PodcastModel())->getPodcastByName(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
......@@ -34,15 +35,100 @@ class Podcast extends BaseController
public function index()
{
// The page cache is set to a decade so it is deleted manually upon podcast update
$this->cachePage(DECADE);
self::triggerWebpageHit($this->podcast->id);
$data = [
'podcast' => $this->podcast,
'episodes' => $this->podcast->episodes,
];
return view('podcast', $data);
$yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season');
if (!$yearQuery and !$seasonQuery) {
$defaultQuery = (new EpisodeModel())->getDefaultQuery(
$this->podcast->id
);
if ($defaultQuery['type'] == 'season') {
$seasonQuery = $defaultQuery['data']['season_number'];
} elseif ($defaultQuery['type'] == 'year') {
$yearQuery = $defaultQuery['data']['year'];
}
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast{$this->podcast->id}",
$yearQuery,
$seasonQuery ? 'season' . $seasonQuery : null,
])
);
if (!($found = cache($cacheName))) {
// The page cache is set to a decade so it is deleted manually upon podcast update
// $this->cachePage(DECADE);
$episodeModel = new EpisodeModel();
// Build navigation array
$years = $episodeModel->getYears($this->podcast->id);
$seasons = $episodeModel->getSeasons($this->podcast->id);
$episodesNavigation = [];
$activeQuery = null;
foreach ($years as $year) {
$isActive = $yearQuery == $year['year'];
if ($isActive) {
$activeQuery = ['type' => 'year', 'value' => $year['year']];
}
array_push($episodesNavigation, [
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
'route' =>
route_to('podcast', $this->podcast->name) .
'?year=' .
$year['year'],
'is_active' => $isActive,
]);
}
foreach ($seasons as $season) {
$isActive = $seasonQuery == $season['season_number'];
if ($isActive) {
$activeQuery = [
'type' => 'season',
'value' => $season['season_number'],
];
}
array_push($episodesNavigation, [
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
'route' =>
route_to('podcast', $this->podcast->name) .
'?season=' .
$season['season_number'],
'is_active' => $isActive,
]);
}
$data = [
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id,
$this->podcast->type,
$yearQuery,
$seasonQuery
),
];
return view('podcast', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
}
......@@ -45,6 +45,22 @@ class AddEpisodes extends Migration
'type' => 'VARCHAR',
'constraint' => 1024,
],
'enclosure_duration' => [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
'comment' => 'Playtime in seconds',
],
'enclosure_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'enclosure_filesize' => [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
'comment' => 'File size in bytes',
],
'description' => [
'type' => 'TEXT',
'null' => true,
......
......@@ -36,6 +36,7 @@ class AddAnalyticsEpisodesByCountry extends Migration
'country_code' => [
'type' => 'VARCHAR',
'constraint' => 3,
'comment' => 'ISO 3166-1 code.',
],
'date' => [
'type' => 'date',
......
......@@ -54,11 +54,6 @@ class Episode extends Entity
*/
protected $enclosure_url;
/**
* @var array
*/
protected $enclosure_metadata;
/**
* @var string
*/
......@@ -76,6 +71,9 @@ class Episode extends Entity
'slug' => 'string',
'title' => 'string',
'enclosure_uri' => 'string',
'enclosure_duration' => 'integer',
'enclosure_mimetype' => 'string',
'enclosure_filesize' => 'integer',
'description' => 'string',
'image_uri' => '?string',
'explicit' => 'boolean',
......@@ -106,19 +104,6 @@ class Episode extends Entity
$this->getPodcast()->name,
$this->attributes['slug']
);
} elseif (
$APICdata = $this->getEnclosureMetadata()['attached_picture']
) {
// if the user didn't input an image,
// check if the uploaded audio file has an attached cover and store it
$cover_image = new \CodeIgniter\Files\File('episode_cover');
file_put_contents($cover_image, $APICdata);
$this->attributes['image_uri'] = save_podcast_media(
$cover_image,
$this->getPodcast()->name,
$this->attributes['slug']
);
}
return $this;
......@@ -155,13 +140,22 @@ class Episode extends Entity
(!($enclosure instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
$enclosure->isValid())
) {
helper('media');
helper(['media', 'id3']);
$enclosure_metadata = get_file_tags($enclosure);
$this->attributes['enclosure_uri'] = save_podcast_media(
$enclosure,
$this->getPodcast()->name,
$this->attributes['slug']
);
$this->attributes['enclosure_duration'] = round(
$enclosure_metadata['playtime_seconds']
);
$this->attributes['enclosure_mimetype'] =
$enclosure_metadata['mime_type'];
$this->attributes['enclosure_filesize'] =
$enclosure_metadata['filesize'];
return $this;
}
......@@ -191,13 +185,6 @@ class Episode extends Entity
);
}
public function getEnclosureMetadata()
{
helper('id3');
return get_file_tags($this->getEnclosure());
}
public function getLink()
{
return base_url(
......@@ -218,7 +205,9 @@ class Episode extends Entity
public function getPodcast()
{
return (new PodcastModel())->find($this->attributes['podcast_id']);
return (new PodcastModel())->getPodcastById(
$this->attributes['podcast_id']
);
}
public function getDescriptionHtml()
......
......@@ -8,7 +8,9 @@
namespace App\Entities;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity;
use App\Models\UserModel;
use League\CommonMark\CommonMarkConverter;
......@@ -40,6 +42,11 @@ class Podcast extends Entity
*/
protected $episodes;
/**
* @var \App\Entities\Category
*/
protected $category;
/**
* @var \App\Entities\User[]
*/
......@@ -50,6 +57,11 @@ class Podcast extends Entity
*/
protected $description_html;
/**
* @var \App\Entities\Platform
*/
protected $platforms;
protected $casts = [
'id' => 'integer',
'title' => 'string',
......@@ -134,13 +146,34 @@ class Podcast extends Entity
if (empty($this->episodes)) {
$this->episodes = (new EpisodeModel())->getPodcastEpisodes(
$this->id
$this->id,
$this->type
);
}
return $this->episodes;
}
/**
* Returns the podcast category entity
*
* @return \App\Entities\Category
*/
public function getCategory()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting category.'
);
}
if (empty($this->category)) {
$this->category = (new CategoryModel())->find($this->category_id);
}
return $this->category;
}
/**
* Returns all podcast contributors
*
......@@ -186,4 +219,26 @@ class Podcast extends Entity
return $this;
}
/**
* Returns the podcast's platform links
*
* @return \App\Entities\Platform[]
*/
public function getPlatforms()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting platform links.'
);
}
if (empty($this->platforms)) {
$this->platforms = (new PlatformModel())->getPodcastPlatformLinks(
$this->id
);
}
return $this->platforms;
}
}
......@@ -63,7 +63,9 @@ class User extends \Myth\Auth\Entities\User
}
if (empty($this->podcast)) {
$this->podcast = (new PodcastModel())->find($this->podcast_id);
$this->podcast = (new PodcastModel())->getPodcastById(
$this->podcast_id
);
}
return $this->podcast;
......
......@@ -25,9 +25,6 @@ function get_file_tags($file)
'filesize' => $FileInfo['filesize'],
'mime_type' => $FileInfo['mime_type'],
'playtime_seconds' => $FileInfo['playtime_seconds'],
'attached_picture' => array_key_exists('comments', $FileInfo)
? $FileInfo['comments']['picture'][0]['data']
: null,
];
}
......
......@@ -11,17 +11,20 @@ use App\Models\PageModel;
/**
* Returns instance pages as links inside nav tag
*
* @param string $class
* @return string html pages navigation
*/
function render_page_links()
function render_page_links($class = null)
{
$pages = (new PageModel())->findAll();
$links = '';
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',
]);
}
return '<nav class="inline-flex">' . $links . '</nav>';
return '<nav class="' . $class . '">' . $links . '</nav>';
}
......@@ -115,10 +115,9 @@ function get_rss_feed($podcast)
$item->addChild('title', $episode->title);
$enclosure = $item->addChild('enclosure');
$enclosure_metadata = $episode->enclosure_metadata;