Commit 2d44b457 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: enhance admin ui with responsive design and ux improvements

- add podcast sidebar navigation
- add podcast dashboard with latest episodes
- add pagination to podcast episodes
- add components helper to reuse ui components (button, data_table, etc.)
- enhance podcast and episode forms by splitting them into form sections
- add hint tooltips to podcast and episode forms
- transform radio inputs as buttons for better ux
- replace explicit field by parental_advisory
- replace author field by publisher
- add podcasts_categories table to set multiple categories
- use choices.js to enhance multiselect fields
- update Language files
- update js dependencies to latest versions

closes #31, #9
parent 31b7828e
......@@ -17,9 +17,9 @@ Javascript dependencies:
- [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
- [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
- [CodeMirror](https://github.com/codemirror/CodeMirror) ([MIT License](https://github.com/codemirror/CodeMirror/blob/master/LICENSE))
- [ProseMirror](https://prosemirror.net/) ([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE))
- [D3: Data-Driven Documents](https://d3js.org) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE))
- [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
Other:
......
......@@ -20,7 +20,7 @@ class Pager extends BaseConfig
|
*/
public $templates = [
'default_full' => 'CodeIgniter\Pager\Views\default_full',
'default_full' => 'App\Views\pager\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
'default_head' => 'CodeIgniter\Pager\Views\default_head',
];
......
......@@ -26,7 +26,7 @@ class BaseController extends Controller
*
* @var array
*/
protected $helpers = ['auth', 'breadcrumb', 'svg'];
protected $helpers = ['auth', 'breadcrumb', 'svg', 'components'];
/**
* Constructor.
......
......@@ -166,7 +166,7 @@ class Contributor extends BaseController
public function remove()
{
if ($this->podcast->owner_id == $this->user->id) {
if ($this->podcast->created_by == $this->user->id) {
return redirect()
->back()
->with('errors', [
......
......@@ -45,8 +45,14 @@ class Episode extends BaseController
public function list()
{
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->orderBy('created_at', 'desc');
$data = [
'podcast' => $this->podcast,
'episodes' => $episodes->paginate(10),
'pager' => $episodes->pager,
];
replace_breadcrumb_params([
......@@ -57,7 +63,10 @@ class Episode extends BaseController
public function view()
{
$data = ['episode' => $this->episode];
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
......@@ -105,7 +114,10 @@ class Episode extends BaseController
'enclosure' => $this->request->getFile('enclosure'),
'description' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'),
'explicit' => $this->request->getPost('explicit') == 'yes',
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'number' => $this->request->getPost('episode_number'),
'season_number' => $this->request->getPost('season_number'),
'type' => $this->request->getPost('type'),
......@@ -120,14 +132,33 @@ class Episode extends BaseController
$episodeModel = new EpisodeModel();
if (!$episodeModel->save($newEpisode)) {
if (!($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->route('episode-list', [$this->podcast->id]);
// update podcast's episode_description_footer if changed
$podcastModel = new PodcastModel();
if ($this->podcast->hasChanged('episode_description_footer')) {
$this->podcast->episode_description_footer = $this->request->getPost(
'description_footer'
);
if (!$podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
return redirect()->route('episode-view', [
$this->podcast->id,
$newEpisodeId,
]);
}
public function edit()
......@@ -135,6 +166,7 @@ class Episode extends BaseController
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
......@@ -167,7 +199,10 @@ class Episode extends BaseController
$this->episode->title = $this->request->getPost('title');
$this->episode->slug = $this->request->getPost('slug');
$this->episode->description = $this->request->getPost('description');
$this->episode->explicit = $this->request->getPost('explicit') == 'yes';
$this->episode->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->episode->number = $this->request->getPost('episode_number');
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
......@@ -191,14 +226,32 @@ class Episode extends BaseController
$episodeModel = new EpisodeModel();
if (!$episodeModel->save($this->episode)) {
if (!$episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->route('episode-list', [$this->podcast->id]);
// update podcast's episode_description_footer if changed
$this->podcast->episode_description_footer = $this->request->getPost(
'description_footer'
);
if ($this->podcast->hasChanged('episode_description_footer')) {
$podcastModel = new PodcastModel();
if (!$podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
return redirect()->route('episode-view', [
$this->podcast->id,
$this->episode->id,
]);
}
public function delete()
......
......@@ -57,9 +57,8 @@ class MyAccount extends BaseController
}
user()->password = $this->request->getPost('new_password');
$userModel->save(user());
if (!$userModel->save(user())) {
if (!$userModel->update(user()->id, user())) {
return redirect()
->back()
->withInput()
......
......@@ -59,7 +59,7 @@ class Page extends BaseController
$pageModel = new PageModel();
if (!$pageModel->save($page)) {
if (!$pageModel->insert($page)) {
return redirect()
->back()
->withInput()
......@@ -92,7 +92,7 @@ class Page extends BaseController
$pageModel = new PageModel();
if (!$pageModel->save($this->page)) {
if (!$pageModel->update($this->page->id, $this->page)) {
return redirect()
->back()
->withInput()
......
......@@ -94,21 +94,20 @@ class Podcast extends BaseController
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description' => $this->request->getPost('description'),
'episode_description_footer' => $this->request->getPost(
'episode_description_footer'
),
'image' => $this->request->getFile('image'),
'language' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'explicit' => $this->request->getPost('explicit') == 'yes',
'author' => $this->request->getPost('author'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'block' => $this->request->getPost('block') == 'yes',
'complete' => $this->request->getPost('complete') == 'yes',
'custom_html_head' => $this->request->getPost('custom_html_head'),
'block' => $this->request->getPost('block') === 'yes',
'complete' => $this->request->getPost('complete') === 'yes',
'created_by' => user(),
'updated_by' => user(),
]);
......@@ -119,7 +118,7 @@ class Podcast extends BaseController
$db->transStart();
if (!($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transComplete();
$db->transRollback();
return redirect()
->back()
->withInput()
......@@ -135,6 +134,12 @@ class Podcast extends BaseController
$podcastAdminGroup->id
);
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$newPodcastId,
$this->request->getPost('other_categories')
);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
......@@ -205,20 +210,22 @@ class Podcast extends BaseController
'image' => download_file($nsItunes->image->attributes()),
'language' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'explicit' => empty($nsItunes->explicit)
? false
: $nsItunes->explicit == 'yes',
'author' => $nsItunes->author,
'parental_advisory' => empty($nsItunes->explicit)
? null
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
: null),
'owner_name' => $nsItunes->owner->name,
'owner_email' => $nsItunes->owner->email,
'publisher' => $nsItunes->author,
'type' => empty($nsItunes->type) ? 'episodic' : $nsItunes->type,
'copyright' => $feed->channel[0]->copyright,
'block' => empty($nsItunes->block)
? false
: $nsItunes->block == 'yes',
: $nsItunes->block === 'yes',
'complete' => empty($nsItunes->complete)
? false
: $nsItunes->complete == 'yes',
: $nsItunes->complete === 'yes',
'created_by' => user(),
'updated_by' => user(),
]);
......@@ -229,7 +236,7 @@ class Podcast extends BaseController
$db->transStart();
if (!($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transComplete();
$db->transRollback();
return redirect()
->back()
->withInput()
......@@ -265,7 +272,7 @@ class Podcast extends BaseController
);
$slug = slugify(
$this->request->getPost('slug_field') == 'title'
$this->request->getPost('slug_field') === 'title'
? $item->title
: basename($item->link)
);
......@@ -285,22 +292,23 @@ class Podcast extends BaseController
'slug' => $slug,
'enclosure' => download_file($item->enclosure->attributes()),
'description' => $converter->convert(
$this->request->getPost('description_field') == 'summary'
$this->request->getPost('description_field') === 'summary'
? $nsItunes->summary
: ($this->request->getPost('description_field') ==
: ($this->request->getPost('description_field') ===
'subtitle_summary'
? '<h3>' .
$nsItunes->subtitle .
"</h3>\n" .
$nsItunes->summary
? $nsItunes->subtitle . "\n" . $nsItunes->summary
: $item->description)
),
'image' => empty($nsItunes->image->attributes())
? null
: download_file($nsItunes->image->attributes()),
'explicit' => $nsItunes->explicit == 'yes',
'explicit' => $nsItunes->explicit
? (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
: null)
: null,
'number' =>
$this->request->getPost('force_renumber') == 'yes'
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
: $nsItunes->episode,
'season_number' => empty(
......@@ -313,7 +321,7 @@ class Podcast extends BaseController
: $nsItunes->episodeType,
'block' => empty($nsItunes->block)
? false
: $nsItunes->block == 'yes',
: $nsItunes->block === 'yes',
'created_by' => user(),
'updated_by' => user(),
]);
......@@ -324,8 +332,8 @@ class Podcast extends BaseController
$episodeModel = new EpisodeModel();
if (!$episodeModel->save($newEpisode)) {
// FIX: What shall we do?
if (!$episodeModel->insert($newEpisode)) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
......@@ -335,7 +343,7 @@ class Podcast extends BaseController
$db->transComplete();
return redirect()->route('podcast-list');
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function edit()
......@@ -372,9 +380,6 @@ class Podcast extends BaseController
$this->podcast->title = $this->request->getPost('title');
$this->podcast->name = $this->request->getPost('name');
$this->podcast->description = $this->request->getPost('description');
$this->podcast->episode_description_footer = $this->request->getPost(
'episode_description_footer'
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
......@@ -382,29 +387,50 @@ class Podcast extends BaseController
}
$this->podcast->language = $this->request->getPost('language');
$this->podcast->category_id = $this->request->getPost('category');
$this->podcast->explicit = $this->request->getPost('explicit') == 'yes';
$this->podcast->author = $this->request->getPost('author');
$this->podcast->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->block = $this->request->getPost('block') == 'yes';
$this->podcast->complete = $this->request->getPost('complete') == 'yes';
$this->podcast->custom_html_head = $this->request->getPost(
'custom_html_head'
);
$this->podcast->block = $this->request->getPost('block') === 'yes';
$this->podcast->complete =
$this->request->getPost('complete') === 'yes';
$this->updated_by = user();
$podcastModel = new PodcastModel();
$db = \Config\Database::connect();
$db->transStart();
if (!$podcastModel->save($this->podcast)) {
$podcastModel = new PodcastModel();
if (!$podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
return redirect()->route('podcast-list');
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$this->podcast->id,
$this->request->getPost('other_categories')
);
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function latestEpisodes(int $limit)
{
$episodes = (new EpisodeModel())
->orderBy('created_at', 'desc')
->findAll($limit);
return view('admin/podcast/latest_episodes', ['episodes' => $episodes]);
}
public function delete()
......
......@@ -86,7 +86,7 @@ class User extends BaseController
// Force user to reset his password on first connection
$user->forcePasswordReset();
if (!$userModel->save($user)) {
if (!$userModel->insert($user)) {
return redirect()
->back()
->withInput()
......@@ -150,7 +150,7 @@ class User extends BaseController
$userModel = new UserModel();
$this->user->forcePasswordReset();
if (!$userModel->save($this->user)) {
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
......@@ -184,7 +184,7 @@ class User extends BaseController
// TODO: add ban reason?
$this->user->ban('');
if (!$userModel->save($this->user)) {
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
......@@ -205,7 +205,7 @@ class User extends BaseController
$userModel = new UserModel();
$this->user->unBan();
if (!$userModel->save($this->user)) {
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
......
......@@ -12,6 +12,14 @@ use App\Entities\User;
class Auth extends \Myth\Auth\Controllers\AuthController
{
/**
* An array of helpers to be automatically loaded
* upon class instantiation.
*
* @var array
*/
protected $helpers = ['components'];
/**
* Attempt to register a new user.
*/
......
......@@ -26,7 +26,7 @@ class BaseController extends Controller
*
* @var array
*/
protected $helpers = ['analytics', 'svg'];
protected $helpers = ['analytics', 'svg', 'components'];
/**
* Constructor.
......
......@@ -57,6 +57,7 @@ class Episode extends BaseController
$data = [
'previousEpisode' => $previousNextEpisodes['previous'],
'nextEpisode' => $previousNextEpisodes['next'],
'podcast' => $this->podcast,
'episode' => $this->episode,
];
......
......@@ -50,10 +50,11 @@ class AddPodcasts extends Migration
'unsigned' => true,
'default' => 0,
],
'explicit' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'owner_name' => [
'type' => 'VARCHAR',
......@@ -63,7 +64,7 @@ class AddPodcasts extends Migration
'type' => 'VARCHAR',
'constraint' => 1024,
],
'author' => [
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'null' => true,
......@@ -92,10 +93,6 @@ class AddPodcasts extends Migration
'type' => 'TEXT',
'null' => true,
],
'custom_html_head' => [
'type' => 'TEXT',
'null' => true,
],
'created_by' => [
'type' => 'INT',
'constraint' => 11,
......
......@@ -70,10 +70,11 @@ class AddEpisodes extends Migration
'constraint' => 1024,
'null' => true,
],
'explicit' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'number' => [
'type' => 'INT',
......
<?php
/**
* Class AddPodcastsCategories
* Creates podcasts_categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsCategories extends Migration
{
public function up()
{
$this->forge->addField([
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
'category_id' => [
'type' => 'INT',
'constraint' => 10,
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->createTable('podcasts_categories');
}
public function down()
{
$this->forge->dropTable('podcasts_categories');
}
}
......@@ -66,7 +66,7 @@ class Episode extends Entity
'enclosure_filesize' => 'integer',
'description' => 'string',
'image_uri' => '?string',
'explicit' => 'boolean',