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
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?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',
|
||||
'parental_advisory' => '?string',
|
||||
'number' => '?integer',
|
||||
'season_number' => '?integer',
|
||||
'type' => 'string',
|
||||
|
|
|
@ -37,6 +37,16 @@ class Podcast extends Entity
|
|||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\Category[]
|
||||
*/
|
||||
protected $other_categories;
|
||||
|
||||
/**
|
||||
* @var integer[]
|
||||
*/
|
||||
protected $other_categories_ids;
|
||||
|
||||
/**
|
||||
* @var \App\Entities\User[]
|
||||
*/
|
||||
|
@ -60,8 +70,8 @@ class Podcast extends Entity
|
|||
'image_uri' => 'string',
|
||||
'language' => 'string',
|
||||
'category_id' => 'integer',
|
||||
'explicit' => 'boolean',
|
||||
'author' => '?string',
|
||||
'parental_advisory' => '?string',
|
||||
'publisher' => '?string',
|
||||
'owner_name' => '?string',
|
||||
'owner_email' => '?string',
|
||||
'type' => 'string',
|
||||
|
@ -69,7 +79,6 @@ class Podcast extends Entity
|
|||
'block' => 'boolean',
|
||||
'complete' => 'boolean',
|
||||
'episode_description_footer' => '?string',
|
||||
'custom_html_head' => '?string',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'imported_feed_url' => '?string',
|
||||
|
@ -225,4 +234,33 @@ class Podcast extends Entity
|
|||
|
||||
return $this->platforms;
|
||||
}
|
||||
|
||||
public function getOtherCategories()
|
||||
{
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting other categories.'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->other_categories)) {
|
||||
$this->other_categories = (new CategoryModel())->getPodcastCategories(
|
||||
$this->id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->other_categories;
|
||||
}
|
||||
|
||||
public function getOtherCategoriesIds()
|
||||
{
|
||||
if (empty($this->other_categories_ids)) {
|
||||
$this->other_categories_ids = array_column(
|
||||
$this->getOtherCategories(),
|
||||
'id'
|
||||
);
|
||||
}
|
||||
|
||||
return $this->other_categories_ids;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Models\PodcastModel;
|
||||
|
|
|
@ -9,16 +9,15 @@
|
|||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Returns the inline svg icon
|
||||
* Renders the breadcrumb navigation through the Breadcrumb service
|
||||
*
|
||||
* @param string $name name of the icon file without the .svg extension
|
||||
* @param string $class to be added to the svg string
|
||||
* @param string $class to be added to the breadcrumb nav
|
||||
* @return string html breadcrumb
|
||||
*/
|
||||
function render_breadcrumb()
|
||||
function render_breadcrumb($class = null)
|
||||
{
|
||||
$breadcrumb = Services::breadcrumb();
|
||||
return $breadcrumb->render();
|
||||
return $breadcrumb->render($class);
|
||||
}
|
||||
|
||||
function replace_breadcrumb_params($newParams)
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
if (!function_exists('button')) {
|
||||
/**
|
||||
* Button component
|
||||
*
|
||||
* Creates a stylized button or button like anchor tag if the URL is defined.
|
||||
*
|
||||
* @param string $label The button label
|
||||
* @param mixed|null $uri URI string or array of URI segments
|
||||
* @param array $customOptions button options: variant, size, iconLeft, iconRight
|
||||
* @param array $customAttributes Additional attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function button(
|
||||
string $label = '',
|
||||
$uri = null,
|
||||
$customOptions = [],
|
||||
$customAttributes = []
|
||||
): string {
|
||||
$defaultOptions = [
|
||||
'variant' => 'default',
|
||||
'size' => 'base',
|
||||
'iconLeft' => null,
|
||||
'iconRight' => null,
|
||||
'isRoundedFull' => false,
|
||||
'isSquared' => false,
|
||||
];
|
||||
$options = array_merge($defaultOptions, $customOptions);
|
||||
|
||||
$baseClass =
|
||||
'inline-flex items-center shadow-xs outline-none focus:shadow-outline';
|
||||
|
||||
$variantClass = [
|
||||
'default' => 'bg-gray-300 hover:bg-gray-400',
|
||||
'primary' => 'text-white bg-green-500 hover:bg-green-600',
|
||||
'secondary' => 'text-white bg-gray-700 hover:bg-gray-800',
|
||||
'success' => 'text-white bg-green-600 hover:bg-green-700',
|
||||
'danger' => 'text-white bg-red-600 hover:bg-red-700',
|
||||
'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
|
||||
'info' => 'text-white bg-teal-500 hover:bg-teal-600',
|
||||
];
|
||||
|
||||
$sizeClass = [
|
||||
'small' => 'text-xs md:text-sm ',
|
||||
'base' => 'text-sm md:text-base',
|
||||
'large' => 'text-lg md:text-xl',
|
||||
];
|
||||
|
||||
$basePaddings = [
|
||||
'small' => 'px-1 md:px-2 md:py-1',
|
||||
'base' => 'px-2 py-1 md:px-3 md:py-2',
|
||||
'large' => 'px-3 py-2 md:px-4 md:py-2',
|
||||
];
|
||||
|
||||
$squaredPaddings = [
|
||||
'small' => 'p-1',
|
||||
'base' => 'p-2',
|
||||
'large' => 'p-3',
|
||||
];
|
||||
|
||||
$roundedClass = [
|
||||
'full' => 'rounded-full',
|
||||
'small' => 'rounded-sm md:rounded',
|
||||
'base' => 'rounded md:rounded-md',
|
||||
'large' => 'rounded-md md:rounded-lg',
|
||||
];
|
||||
|
||||
$buttonClass =
|
||||
$baseClass .
|
||||
' ' .
|
||||
($options['isRoundedFull']
|
||||
? $roundedClass['full']
|
||||
: $roundedClass[$options['size']]) .
|
||||
' ' .
|
||||
($options['isSquared']
|
||||
? $squaredPaddings[$options['size']]
|
||||
: $basePaddings[$options['size']]) .
|
||||
' ' .
|
||||
$sizeClass[$options['size']] .
|
||||
' ' .
|
||||
$variantClass[$options['variant']];
|
||||
|
||||
if (!empty($customAttributes['class'])) {
|
||||
$buttonClass .= ' ' . $customAttributes['class'];
|
||||
unset($customAttributes['class']);
|
||||
}
|
||||
|
||||
if ($options['iconLeft']) {
|
||||
$label = icon($options['iconLeft'], 'mr-2') . $label;
|
||||
}
|
||||
|
||||
if ($options['iconRight']) {
|
||||
$label .= icon($options['iconRight'], 'ml-2');
|
||||
}
|
||||
|
||||
if ($uri) {
|
||||
return anchor(
|
||||
$uri,
|
||||
$label,
|
||||
array_merge(
|
||||
[
|
||||
'class' => $buttonClass,
|
||||
],
|
||||
$customAttributes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$defaultButtonAttributes = [
|
||||
'type' => 'button',
|
||||
];
|
||||
$attributes = array_merge($defaultButtonAttributes, $customAttributes);
|
||||
|
||||
return '<button class="' .
|
||||
$buttonClass .
|
||||
'"' .
|
||||
stringify_attributes($attributes) .
|
||||
'>' .
|
||||
$label .
|
||||
'</button>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('icon_button')) {
|
||||
/**
|
||||
* Icon Button component
|
||||
*
|
||||
* Abstracts the `button()` helper to create a stylized icon button
|
||||
*
|
||||
* @param string $label The button label
|
||||
* @param mixed|null $uri URI string or array of URI segments
|
||||
* @param array $customOptions button options: variant, size, iconLeft, iconRight
|
||||
* @param array $customAttributes Additional attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function icon_button(
|
||||
string $icon,
|
||||
string $title,
|
||||
$uri = null,
|
||||
$customOptions = [],
|
||||
$customAttributes = []
|
||||
): string {
|
||||
$defaultOptions = [
|
||||
'isRoundedFull' => true,
|
||||
'isSquared' => true,
|
||||
];
|
||||
$options = array_merge($defaultOptions, $customOptions);
|
||||
|
||||
$defaultAttributes = [
|
||||
'title' => $title,
|
||||
'data-toggle' => 'tooltip',
|
||||
'data-placement' => 'bottom',
|
||||
];
|
||||
$attributes = array_merge($defaultAttributes, $customAttributes);
|
||||
|
||||
return button(icon($icon), $uri, $options, $attributes);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function hint_tooltip(string $hintText = '', string $class = ''): string
|
||||
{
|
||||
$tooltip =
|
||||
'<span data-toggle="tooltip" data-placement="bottom" tabindex="0" title="' .
|
||||
$hintText .
|
||||
'" class="inline-block align-middle outline-none focus:shadow-outline';
|
||||
|
||||
if ($class !== '') {
|
||||
$tooltip .= ' ' . $class;
|
||||
}
|
||||
|
||||
return $tooltip . '">' . icon('question') . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('data_table')) {
|
||||
/**
|
||||
* Data table component
|
||||
*
|
||||
* Creates a stylized table.
|
||||
*
|
||||
* @param array $columns array of associate arrays with `header` and `cell` keys where `cell` is a function with a row of $data as parameter
|
||||
* @param array $data data to loop through and display in rows
|
||||
* @param array ...$rest Any other argument to pass to the `cell` function
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function data_table($columns, $data = [], ...$rest): string
|
||||
{
|
||||
$table = new \CodeIgniter\View\Table();
|
||||
|
||||
$template = [
|
||||
'table_open' => '<table class="w-full whitespace-no-wrap">',
|
||||
|
||||
'thead_open' =>
|
||||
'<thead class="text-xs font-semibold text-left text-gray-500 uppercase border-b">',
|
||||
|
||||
'heading_cell_start' => '<th class="px-4 py-2">',
|
||||
'cell_start' => '<td class="px-4 py-2">',
|
||||
'cell_alt_start' => '<td class="px-4 py-2">',
|
||||
|
||||
'row_start' => '<tr class="bg-gray-100 hover:bg-green-100">',
|
||||
'row_alt_start' => '<tr class="hover:bg-green-100">',
|
||||
];
|
||||
|
||||
$table->setTemplate($template);
|
||||
|
||||
$tableHeaders = [];
|
||||
foreach ($columns as $column) {
|
||||
array_push($tableHeaders, $column['header']);
|
||||
}
|
||||
|
||||
$table->setHeading($tableHeaders);
|
||||
|
||||
if ($dataCount = count($data)) {
|
||||
for ($i = 0; $i < $dataCount; $i++) {
|
||||
$row = $data[$i];
|
||||
$rowData = [];
|
||||
foreach ($columns as $column) {
|
||||
array_push($rowData, $column['cell']($row, ...$rest));
|
||||
}
|
||||
$table->addRow($rowData);
|
||||
}
|
||||
} else {
|
||||
return lang('Common.no_data');
|
||||
}
|
||||
|
||||
return '<div class="overflow-x-auto bg-white rounded-lg shadow" >' .
|
||||
$table->generate() .
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
if (!function_exists('form_section')) {
|
||||
/**
|
||||
* Form section
|
||||
*
|
||||
* Used to produce a responsive form section with a title and subtitle. To close section,
|
||||
* use form_section_close()
|
||||
*
|
||||
* @param string $title The section title
|
||||
* @param string $subtitle The section subtitle
|
||||
* @param array $attributes Additional attributes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function form_section(
|
||||
string $title = '',
|
||||
string $subtitle = '',
|
||||
array $attributes = []
|
||||
): string {
|
||||
$section =
|
||||
'<div class="flex flex-wrap w-full gap-6 mb-8"' .
|
||||
stringify_attributes($attributes) .
|
||||
">\n";
|
||||
|
||||
$info =
|
||||
'<div class="w-full max-w-xs"><h2 class="text-lg font-semibold">' .
|
||||
$title .
|
||||
'</h2><p class="text-sm text-gray-600">' .
|
||||
$subtitle .
|
||||
'</p></div>';
|
||||
|
||||
return $section . $info . '<div class="flex flex-col w-full max-w-lg">';
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('form_section_close')) {
|
||||
/**
|
||||
* Form Section close Tag
|
||||
*
|
||||
* @param string $extra
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function form_section_close(string $extra = ''): string
|
||||
{
|
||||
return '</div></div>' . $extra;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('form_switch')) {
|
||||
/**
|
||||
* Form Checkbox Switch
|
||||
*
|
||||
* Abstracts form_label to stylize it as a switch toggle
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $value
|
||||
* @param boolean $checked
|
||||
* @param mixed $extra
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function form_switch(
|
||||
$label = '',
|
||||
$data = '',
|
||||
string $value = '',
|
||||
bool $checked = false,
|
||||
$class = '',
|
||||
$extra = ''
|
||||
): string {
|
||||
$data['class'] = 'form-switch';
|
||||
|
||||
return '<label class="relative inline-flex items-center' .
|
||||
' ' .
|
||||
$class .
|
||||
'">' .
|
||||
form_checkbox($data, $value, $checked, $extra) .
|
||||
'<span class="form-switch-slider"></span>' .
|
||||
'<span class="ml-2">' .
|
||||
$label .
|
||||
'</span></label>';
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('form_label')) {
|
||||
/**
|
||||
* Form Label Tag
|
||||
*
|
||||
* @param string $label_text The text to appear onscreen
|
||||
* @param string $id The id the label applies to
|
||||
* @param array $attributes Additional attributes
|
||||
* @param string $hintText Hint text to add next to the label
|
||||
* @param boolean $isOptional adds an optional text if true
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function form_label(
|
||||
string $label_text = '',
|
||||
string $id = '',
|
||||
array $attributes = [],
|
||||
string $hintText = '',
|
||||
bool $isOptional = false
|
||||
): string {
|
||||
$label = '<label';
|
||||
|
||||
if ($id !== '') {
|
||||
$label .= ' for="' . $id . '"';
|
||||
}
|
||||
|
||||
if (is_array($attributes) && $attributes) {
|
||||
foreach ($attributes as $key => $val) {
|
||||
$label .= ' ' . $key . '="' . $val . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$label_content = $label_text;
|
||||
if ($isOptional) {
|
||||
$label_content .=
|
||||
'<small class="ml-1 lowercase">(' .
|
||||
lang('Common.optional') .
|
||||
')</small>';
|
||||
}
|
||||
|
||||
if ($hintText !== '') {
|
||||
$label_content .= hint_tooltip($hintText, 'ml-1');
|
||||
}
|
||||
|
||||
return $label . '>' . $label_content . '</label>';
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('form_multiselect')) {
|
||||
/**
|
||||
* Multi-select menu
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $options
|
||||
* @param array $selected
|
||||
* @param mixed $extra
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function form_multiselect(
|
||||
string $name = '',
|
||||
array $options = [],
|
||||
array $selected = [],
|
||||
$customExtra = ''
|
||||
): string {
|
||||
$defaultExtra = [
|
||||
'data-class' => $customExtra['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'
|
||||
),
|
||||
];
|
||||
$extra = stringify_attributes(array_merge($defaultExtra, $customExtra));
|
||||
|
||||
if (stripos($extra, 'multiple') === false) {
|
||||
$extra .= ' multiple="multiple"';
|
||||
}
|
||||
|
||||
return form_dropdown($name, $options, $selected, $extra);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
use App\Libraries\SimpleRSSElement;
|
||||
use App\Models\CategoryModel;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
|
@ -18,14 +17,8 @@ use CodeIgniter\I18n\Time;
|
|||
*/
|
||||
function get_rss_feed($podcast)
|
||||
{
|
||||
$category_model = new CategoryModel();
|
||||
|
||||
$episodes = $podcast->episodes;
|
||||
|
||||
$podcast_category = $category_model
|
||||
->where('id', $podcast->category_id)
|
||||
->first();
|
||||
|
||||
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||
|
||||
$rss = new SimpleRSSElement(
|
||||
|
@ -60,39 +53,20 @@ function get_rss_feed($podcast)
|
|||
$itunes_image->addAttribute('href', $podcast->image->url);
|
||||
$channel->addChild('language', $podcast->language);
|
||||
|
||||
$itunes_category = $channel->addChild('category', null, $itunes_namespace);
|
||||
$itunes_category->addAttribute(
|
||||
'text',
|
||||
$podcast_category->parent
|
||||
? $podcast_category->parent->apple_category
|
||||
: $podcast_category->apple_category
|
||||
);
|
||||
|
||||
if ($podcast_category->parent) {
|
||||
$itunes_category_child = $itunes_category->addChild(
|
||||
'category',
|
||||
null,
|
||||
$itunes_namespace
|
||||
);
|
||||
$itunes_category_child->addAttribute(
|
||||
'text',
|
||||
$podcast_category->apple_category
|
||||
);
|
||||
$channel->addChild(
|
||||
'category',
|
||||
$podcast_category->parent->apple_category
|
||||
);
|
||||
// set main category first, then other categories as apple
|
||||
add_category_tag($channel, $podcast->category);
|
||||
foreach ($podcast->other_categories as $other_category) {
|
||||
add_category_tag($channel, $other_category);
|
||||
}
|
||||
$channel->addChild('category', $podcast_category->apple_category);
|
||||
|
||||
$channel->addChild(
|
||||
'explicit',
|
||||
$podcast->explicit ? 'true' : 'false',
|
||||
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
|
||||
$itunes_namespace
|
||||
);
|
||||
|
||||
$podcast->author &&
|
||||
$channel->addChild('author', $podcast->author, $itunes_namespace);
|
||||
$podcast->publisher &&
|
||||
$channel->addChild('author', $podcast->publisher, $itunes_namespace);
|
||||
$channel->addChild('link', $podcast->link);
|
||||
|
||||
$owner = $channel->addChild('owner', null, $itunes_namespace);
|
||||
|
@ -137,11 +111,13 @@ function get_rss_feed($podcast)
|
|||
$itunes_namespace
|
||||
);
|
||||
$episode_itunes_image->addAttribute('href', $episode->image->feed_url);
|
||||
$item->addChild(
|
||||
'explicit',
|
||||
$episode->explicit ? 'true' : 'false',
|
||||
$itunes_namespace
|
||||
);
|
||||
|
||||
$episode->parental_advisory &&
|
||||
$item->addChild(
|
||||
'explicit',
|
||||
$episode->parental_advisory === 'explicit' ? 'true' : 'false',
|
||||
$itunes_namespace
|
||||
);
|
||||
|
||||
$item->addChild('episode', $episode->number, $itunes_namespace);
|
||||
$episode->season_number &&
|
||||
|
@ -157,3 +133,35 @@ function get_rss_feed($podcast)
|
|||
|
||||
return $rss->asXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds <itunes:category> and <category> tags to node for a given category
|
||||
*
|
||||
* @param \SimpleXMLElement $node
|
||||
* @param \App\Entities\Category $category
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function add_category_tag($node, $category)
|
||||
{
|
||||
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
|
||||
|
||||
$itunes_category = $node->addChild('category', null, $itunes_namespace);
|
||||
$itunes_category->addAttribute(
|
||||
'text',
|
||||
$category->parent
|
||||
? $category->parent->apple_category
|
||||
: $category->apple_category
|
||||
);
|
||||
|
||||
if ($category->parent) {
|
||||
$itunes_category_child = $itunes_category->addChild(
|
||||
'category',
|
||||
null,
|
||||
$itunes_namespace
|
||||
);
|
||||
$itunes_category_child->addAttribute('text', $category->apple_category);
|
||||
$node->addChild('category', $category->parent->apple_category);
|
||||
}
|
||||
$node->addChild('category', $category->apple_category);
|
||||
}
|
||||
|
|
|
@ -13,16 +13,17 @@
|
|||
* @param string $class to be added to the svg string
|
||||
* @return string svg contents
|
||||
*/
|
||||
function icon($name, $class = null)
|
||||
function icon(string $name, string $class = '')
|
||||
{
|
||||
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
|
||||
if ($class) {
|
||||
if ($class !== '') {
|
||||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
);
|
||||
}
|
||||
|
||||
return $svg_contents;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'go_to_website' => 'Go to website',
|
||||
'dashboard' => 'Dashboard',
|
||||
'podcasts' => 'Podcasts',
|
||||
'users' => 'Users',
|
||||
'pages' => 'Pages',
|
||||
'admin' => 'Home',
|
||||
'podcasts' => 'Podcasts',
|
||||
'podcast-list' => 'All podcasts',
|
||||
'podcast-create' => 'New podcast',
|
||||
'podcast-import' => 'Import a podcast',
|
||||
'users' => 'Users',
|
||||
'user-list' => 'All users',
|
||||
'user-create' => 'New user',
|
||||
'pages' => 'Pages',
|
||||
'page-list' => 'All pages',
|
||||
'page-create' => 'New Page',
|
||||
'go_to_website' => 'Go to website',
|
||||
];
|
||||
|
|
|
@ -7,12 +7,25 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'optional' => 'Optional',
|
||||
'no_data' => 'No data found!',
|
||||
'home' => 'Home',
|
||||
'explicit' => 'Explicit',
|
||||
'mediumDate' => '{0,date,medium}',
|
||||
'duration' => '{0,duration}',
|
||||
'powered_by' => 'Powered by {castopod}.',
|
||||
'actions' => 'Actions',
|
||||
'pageInfo' => 'Page {currentPage} out of {pageCount}',
|
||||
'forms' => [
|
||||
'multiSelect' => [
|
||||
'selectText' => 'Press to select',
|
||||
'loadingText' => 'Loading...',
|
||||
'noResultsText' => 'No results found',
|
||||
'noChoicesText' => 'No choices to choose from',
|
||||
'maxItemText' => 'Cannot add more items',
|
||||
],
|
||||
'image_size_hint' =>
|
||||
'Image must be squared with at least 1400px wide and tall.',
|
||||
],
|
||||
|
|
|
@ -14,6 +14,10 @@ return [
|
|||
'edit_role' => 'Update role for {0}',
|
||||
'edit' => 'Edit',
|
||||
'remove' => 'Remove',
|
||||
'list' => [
|
||||
'username' => 'Username',
|
||||
'role' => 'Role',
|
||||
],
|
||||
'form' => [
|
||||
'user' => 'User',
|
||||
'role' => 'Role',
|
||||
|
|
|
@ -13,6 +13,9 @@ return [
|
|||
'next_season' => 'Next season',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'number' => 'Episode {episodeNumber}',
|
||||
'number_abbr' => 'Ep. {episodeNumber}',
|
||||
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
|
||||
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
|
||||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'edit' => 'Edit',
|
||||
|
@ -20,50 +23,51 @@ return [
|
|||
'go_to_page' => 'Go to page',
|
||||
'create' => 'Add an episode',
|
||||
'form' => [
|
||||
'enclosure' => 'Audio file',
|
||||
'enclosure' => 'Choose an .mp3 or .m4a audio file…',
|
||||
'info_section_title' => 'Episode info',
|
||||
'info_section_subtitle' => '',
|
||||
'image' => 'Cover image',
|
||||
'image_hint' =>
|
||||
'If you do not set an image, the podcast cover will be used instead.',
|
||||
'title' => 'Title',
|
||||
'title_help' =>
|
||||
'This episode title. It should contain a clear, concise name for your episode. Don’t specify the episode number or season number here.',
|
||||
'title_hint' =>
|
||||
'Should contain a clear and concise episode name. Do not specify the episode or season numbers here.',
|
||||
'slug' => 'Slug',
|
||||
'slug_help' =>
|
||||
'This episode slug. It will be used for its URL address.',
|
||||
'description' => 'Description',
|
||||
'description_help' =>
|
||||
'This is where you type the episode show notes. You may add rich text, links, images…',
|
||||
'image' => 'Image',
|
||||
'image_help' =>
|
||||
'This episode image. If an image is already in the audio file, you don’t need to add one here. If you add no image to this episode, the podcast image will be used instead.',
|
||||
'explicit' => 'Explicit',
|
||||
'explicit_help' =>
|
||||
'The episode parental advisory information for this episode.',
|
||||
'published_at' => [
|
||||
'label' => 'Publication date',
|
||||
'date' => 'Publication date',
|
||||
'time' => 'Publication time',
|
||||
],
|
||||
'published_at_help' =>
|
||||
'The date and time when this episode was released. It can be in the past or in the future.',
|
||||
'slug_hint' => 'Used for generating the episode URL.',
|
||||
'season_number' => 'Season',
|
||||
'episode_number' => 'Episode',
|
||||
'type' => [
|
||||
'label' => 'Type',
|
||||
'hint' =>
|
||||
'- <strong>full</strong>: complete content the episode.<br/>- <strong>trailer</strong>: short, promotional piece of content that represents a preview of the current show.<br/>- <strong>bonus</strong>: extra content for the show (for example, behind the scenes info or interviews with the cast) or cross-promotional content for another show.',
|
||||
'full' => 'Full',
|
||||
'full_help' =>
|
||||
'Specify full when you are submitting the complete content of your episode.',
|
||||
'trailer' => 'Trailer',
|
||||
'trailer_help' =>
|
||||
'Specify trailer when you are submitting a short, promotional piece of content that represents a preview of your current show.',
|
||||
'bonus' => 'Bonus',
|
||||
'bonus_help' =>
|
||||
'Specify bonus when you are submitting extra content for your show (for example, behind the scenes information or interviews with the cast) or cross-promotional content for another show.',
|
||||
],
|
||||
'episode_number' => 'Episode number',
|
||||
'episode_number_help' =>
|
||||
'The episode number is mandatory for serial podcasts but optional for episodic podcasts.',
|
||||
'season_number' => 'Season number',
|
||||
'season_number_help' =>
|
||||
'Season number is a non-zero integer (1, 2, 3, etc.) representing this episode season number.',
|
||||
'block' => 'Block',
|
||||
'block_help' =>
|
||||
'This episode show or hide status. If you want this episode removed from the Apple directory, use this tag.',
|
||||
'show_notes_section_title' => 'Show notes',
|
||||
'show_notes_section_subtitle' =>
|
||||
'Up to 4000 characters, be clear and concise. Show notes help potential listeners in finding the episode.',
|
||||
'description' => 'Description',
|
||||
'description_footer' => 'Description footer',
|
||||
'description_footer_hint' =>
|
||||
'This text is added at the end of each episode description, it is a good place to input your social links for example.',
|
||||
'publication_section_title' => 'Publication info',
|
||||
'publication_section_subtitle' => '',
|
||||
'published_at' => [
|
||||
'label' => 'Publication date',
|
||||
'date' => 'Date',
|
||||
'time' => 'Time',
|
||||
],
|
||||
'parental_advisory' => [
|
||||
'label' => 'Parental advisory',
|
||||
'hint' => 'Does the episode contain explicit content?',
|
||||
'undefined' => 'undefined',
|
||||
'clean' => 'Clean',
|
||||
'explicit' => 'Explicit',
|
||||
],
|
||||
'block' => 'Episode should be hidden from all platforms',
|
||||
'block_hint' =>
|
||||
'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.',
|
||||
'submit_create' => 'Create episode',
|
||||
'submit_edit' => 'Save episode',
|
||||
],
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'page' => 'Page',
|
||||
'all_pages' => 'All pages',
|
||||
'create' => 'New page',
|
||||
'go_to_page' => 'Go to page',
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'pageNavigation' => 'Page navigation',
|
||||
'first' => 'First',
|
||||
'previous' => 'Previous',
|
||||
'next' => 'Next',
|
||||
'last' => 'Last',
|
||||
'older' => 'Older',
|
||||
'newer' => 'Newer',
|
||||
'invalidTemplate' => '{0} is not a valid Pager template.',
|
||||
'invalidPaginationGroup' => '{0} is not a valid Pagination group.',
|
||||
];
|
|
@ -9,8 +9,8 @@
|
|||
return [
|
||||
'all_podcasts' => 'All podcasts',
|
||||
'no_podcast' => 'No podcast found!',
|
||||
'create' => 'Create a Podcast',
|
||||
'import' => 'Create and Import a Podcast from an existing Feed',
|
||||
'create' => 'Create a podcast',
|
||||
'import' => 'Import a podcast',
|
||||
'new_episode' => 'New Episode',
|
||||
'feed' => 'RSS feed',
|
||||
'view' => 'View podcast',
|
||||
|
@ -19,93 +19,55 @@ return [
|
|||
'see_episodes' => 'See episodes',
|
||||
'see_contributors' => 'See contributors',
|
||||
'go_to_page' => 'Go to page',
|
||||
'latest_episodes' => 'Latest episodes',
|
||||
'see_all_episodes' => 'See all episodes',
|
||||
'form' => [
|
||||
'identity_section_title' => 'Podcast identity',
|
||||
'identity_section_subtitle' => 'These fields allow you to get noticed.',
|
||||
'image' => 'Cover image',
|
||||
'title' => 'Title',
|
||||
'title_help' =>
|
||||
'The podcast title will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).',
|
||||
'name' => 'Name',
|
||||
'name_help' =>
|
||||
'The podcast will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).',
|
||||
'description' => 'Description',
|
||||
'description_help' =>
|
||||
'It will be shown on all podcasts platforms (such as Apple Podcasts) and players (such as Podcast Addict).',
|
||||
'episode_description_footer' => 'Episode description footer',
|
||||
'episode_description_footer_help' =>
|
||||
'This text will be automatically added at the end of each episode description, so that you don’t have to copy/paste it a gazillion times.',
|
||||
'image' => 'Image',
|
||||
'image_help' =>
|
||||
'This podcast image. It must be square, JPEG or PNG, minimum 1400 x 1400 pixels and maximum 3000 x 3000 pixels.',
|
||||
'language' => 'Language',
|
||||
'language_help' => 'The language spoken on the podcast.',
|
||||
'category' => 'Category',
|
||||
'category_help' =>
|
||||
'This podcast category. Because no one uses subcategories, Castopod does not allow you te use one.',
|
||||
'explicit' => 'Explicit',
|
||||
'explicit_help' =>
|
||||
'The podcast parental advisory information. Does it contain explicit content?',
|
||||
'owner_name' => 'Owner name',
|
||||
'owner_name_help' =>
|
||||
'For administrative use only. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.',
|
||||
'owner_email' => 'Owner email',
|
||||
'owner_email_help' =>
|
||||
'It will be used by most platforms to verify this podcast ownership. It will not be shown on podcasts platforms (such as Apple Podcasts) nor players (such as Podcast Addict) but it is visible in the public RSS feed.',
|
||||
'author' => 'Author',
|
||||
'author_help' =>
|
||||
'The group responsible for creating the show. Show author most often refers to the parent company or network of a podcast. This field is sometimes labeled as ’Author’.',
|
||||
'name_hint' => 'Used for generating the podcast URL.',
|
||||
'type' => [
|
||||
'label' => 'Type',
|
||||
'hint' =>
|
||||
'- <strong>episodic</strong>: if episodes are intended to be consumed without any specific order. Newest episodes will be presented first.<br/>- <strong>serial</strong>: if episodes are intended to be consumed in sequential order. The oldest episodes will be presented first.',
|
||||
'episodic' => 'Episodic',
|
||||
'episodic_help' =>
|
||||
'Specify episodic when episodes are intended to be consumed without any specific order. The newest episodes will be presented first.',
|
||||
'serial' => 'Serial',
|
||||
'serial_help' =>
|
||||
'Specify serial when episodes are intended to be consumed in sequential order. The oldest episodes will be presented first.',
|
||||
],
|
||||
'description' => 'Description',
|
||||
'classification_section_title' => 'Classification',
|
||||
'classification_section_subtitle' =>
|
||||
'These fields will impact your audience and competition.',
|
||||
'language' => 'Language',
|
||||
'category' => 'Category',
|
||||
'other_categories' => 'Other categories',
|
||||
'parental_advisory' => [
|
||||
'label' => 'Parental advisory',
|
||||
'hint' => 'Does it contain explicit content?',
|
||||
'undefined' => 'undefined',
|
||||
'clean' => 'Clean',
|
||||
'explicit' => 'Explicit',
|
||||
],
|
||||
'author_section_title' => 'Author',
|
||||
'author_section_subtitle' => 'Who is managing the podcast?',
|
||||
'owner_name' => 'Owner name',
|
||||
'owner_name_hint' =>
|
||||
'For administrative use only. Visible in the public RSS feed.',
|
||||
'owner_email' => 'Owner email',
|
||||
'owner_email_hint' =>
|
||||
'Will be used by most platforms to verify the podcast ownership. Visible in the public RSS feed.',
|
||||
'publisher' => 'Publisher',
|
||||
'publisher_hint' =>
|
||||
'The group responsible for creating the show. Often refers to the parent company or network of a podcast. This field is sometimes labeled as ’Author’.',
|
||||
'copyright' => 'Copyright',
|
||||
'copyright_help' =>
|
||||
'The podcast copyright details, such as "2020 (cc)(by-nc-sa)" or "©2020".',
|
||||
'block' => 'Block',
|
||||
'block_help' =>
|
||||
'If you want your show removed from all platforms, use this tag.',
|
||||
'complete' => 'Complete',
|
||||
'complete_help' =>
|
||||
'Check this if you will never publish another episode to your podcast.',
|
||||
'custom_html_head' => 'Custom HTML code in <head/>',
|
||||
'custom_html_head_help' =>
|
||||
'Add here any HTML code that you would like to see on all this podcast pages within the <head/> tag.',
|
||||
'status_section_title' => 'Status',
|
||||
'status_section_subtitle' => 'Dead or alive?',
|
||||
'block' => 'Podcast should be hidden from all platforms',
|
||||
'complete' => 'Podcast will not be having new episodes',
|
||||
'submit_create' => 'Create podcast',
|
||||
'submit_edit' => 'Save podcast',
|
||||
],
|
||||
'form_import' => [
|
||||
'name' => 'Name',
|
||||
'name_help' =>
|
||||
'This podcast name. It will be used in the URL address. It will be used as a Fediverse actor name, (for instance, it will be the podcast Mastodon’s name).',
|
||||
'imported_feed_url' => 'Feed URL',
|
||||
'imported_feed_url_help' =>
|
||||
'Make sure you are legally allowed to copy that podcast.',
|
||||
'force_renumber' => 'Force episodes renumbering',
|
||||
'force_renumber_help' =>
|
||||
'Use this if your old podcast does not have number but you want some on your new one.',
|
||||
'season_number' => 'Season number',
|
||||
'season_number_help' =>
|
||||
'Use this if your old podcast does not have season number but you want one on your new one. Leave blank otherwise.',
|
||||
'slug_field' => [
|
||||
'label' => 'Which field should be used to calculate episode slug',
|
||||
'link' => '<link>',
|
||||
'title' => '<title>',
|
||||
],
|
||||
'description_field' => [
|
||||
'label' => 'Source field used for episode description / show notes',
|
||||
'description' => '<description>',
|
||||
'summary' => '<itunes:summary>',
|
||||
'subtitle_summary' =>
|
||||
'<itunes:subtitle> <itunes:summary>',
|
||||
],
|
||||
'max_episodes' => 'Maximum number of episodes to import',
|
||||
'max_episodes_helper' => 'Leave blank to import all episodes',
|
||||
'submit_import' => 'Import podcast',
|
||||
'submit_importing' => 'Importing podcast, this could take a while…',
|
||||
],
|
||||
'category_options' => [
|
||||
'uncategorized' => 'uncategorized',
|
||||
'arts' => 'Arts',
|
||||
|
@ -219,7 +181,7 @@ return [
|
|||
'film_reviews' => 'Film Reviews',
|
||||
'tv_reviews' => 'TV Reviews',
|
||||
],
|
||||
'by' => 'By {author}',
|
||||
'by' => 'By {publisher}',
|
||||
'season' => 'Season {seasonNumber}',
|
||||
'list_of_episodes_year' => '{year} episodes',
|
||||
'list_of_episodes_season' => 'Season {seasonNumber} episodes',
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'old_podcast_section_title' => 'The podcast to import',
|
||||
'old_podcast_section_subtitle' => '',
|
||||
'imported_feed_url' => 'Feed URL',
|
||||
'imported_feed_url_hint' =>
|
||||
'The feed must be in `.xml` format. Make sure you are legally allowed to copy the podcast.',
|
||||
'new_podcast_section_title' => 'The new podcast',
|
||||
'new_podcast_section_subtitle' => '',
|
||||
'name' => 'Name',
|
||||
'name_hint' => 'Used for generating the podcast URL.',
|
||||
'advanced_params_section_title' => 'Advanced parameters',
|
||||
'advanced_params_section_subtitle' =>
|
||||
'Keep the default values if you have no idea of what the fields are for.',
|
||||
'slug_field' => [
|
||||
'label' => 'Which field should be used to calculate episode slug',
|
||||
'link' => '<link>',
|
||||
'title' => '<title>',
|
||||
],
|
||||
'description_field' => [
|
||||
'label' => 'Source field used for episode description / show notes',
|
||||
'description' => '<description>',
|
||||
'summary' => '<itunes:summary>',
|
||||
'subtitle_summary' =>
|
||||
'<itunes:subtitle> + <itunes:summary>',
|
||||
],
|
||||
'force_renumber' => 'Force episodes renumbering',
|
||||
'force_renumber_hint' =>
|
||||
'Use this if your podcast does not have episode numbers but wish to set them during import.',
|
||||
'season_number' => 'Season number',
|
||||
'season_number_hint' =>
|
||||
'Use this if your podcast does not have a season number but wish to set one during import. Leave blank otherwise.',
|
||||
'max_episodes' => 'Maximum number of episodes to import',
|
||||
'max_episodes_hint' => 'Leave blank to import all episodes',
|
||||
'submit' => 'Import podcast',
|
||||
];
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'go_to_page' => 'Go to podcast page',
|
||||
'dashboard' => 'Podcast dashboard',
|
||||
'podcast-view' => 'Home',
|
||||
'podcast-edit' => 'Edit podcast',
|
||||
'episodes' => 'Episodes',
|
||||
'episode-list' => 'All episodes',
|
||||
'episode-create' => 'New episode',
|
||||
'analytics' => 'Analytics',
|
||||
'contributors' => 'Contributors',
|
||||
'contributor-list' => 'All contributors',
|
||||
'contributor-add' => 'Add contributor',
|
||||
'settings' => 'Settings',
|
||||
'platforms' => 'Podcast platforms',
|
||||
];
|
|
@ -12,15 +12,21 @@ return [
|
|||
'ban' => 'Ban',
|
||||
'unban' => 'Unban',
|
||||
'delete' => 'Delete',
|
||||
'create' => 'Create a user',
|
||||
'create' => 'New user',
|
||||
'view' => '{username}\'s info',
|
||||
'all_users' => 'All users',
|
||||
'list' => [
|
||||
'user' => 'User',
|
||||
'roles' => 'Roles',
|
||||
'banned' => 'Banned?',
|
||||
],
|
||||
'form' => [
|
||||
'email' => 'Email',
|
||||
'username' => 'Username',
|
||||
'password' => 'Password',
|
||||
'new_password' => 'New Password',
|
||||
'roles' => 'Roles',
|
||||
'permissions' => 'Permissions',
|
||||
'submit_create' => 'Create user',
|
||||
'submit_edit' => 'Save',
|
||||
'submit_password_change' => 'Change!',
|
||||
|
|
|
@ -75,7 +75,7 @@ class Breadcrumb
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
public function render($class = null)
|
||||
{
|
||||
$listItems = '';
|
||||
$keys = array_keys($this->links);
|
||||
|
@ -97,7 +97,9 @@ class Breadcrumb
|
|||
|
||||
return '<nav aria-label="' .
|
||||
lang('Breadcrumb.label') .
|
||||
'"><ol class="breadcrumb">' .
|
||||
'"><ol class="breadcrumb ' .
|
||||
$class .
|
||||
'">' .
|
||||
$listItems .
|
||||
'</ol></nav>';
|
||||
}
|
||||
|
|
|
@ -53,4 +53,72 @@ class CategoryModel extends Model
|
|||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets categories for a given podcast
|
||||
*
|
||||
* @param int $podcastId
|
||||
* @param array $categories
|
||||
*
|
||||
* @return integer|false Number of rows inserted or FALSE on failure
|
||||
*/
|
||||
public function setPodcastCategories($podcastId, $categories)
|
||||
{
|
||||
cache()->delete("podcasts{$podcastId}_categories");
|
||||
|
||||
// Remove already previously set categories to overwrite them
|
||||
$this->db
|
||||
->table('podcasts_categories')
|
||||
->delete(['podcast_id' => $podcastId]);
|
||||
|
||||
if (!empty($categories)) {
|
||||
// prepare data for `podcasts_categories` table
|
||||
$data = array_reduce(
|
||||
$categories,
|
||||
function ($result, $categoryId) use ($podcastId) {
|
||||
$result[] = [
|
||||
'podcast_id' => $podcastId,
|
||||
'category_id' => $categoryId,
|
||||
];
|
||||
|
||||
return $result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Set podcast categories
|
||||
return $this->db->table('podcasts_categories')->insertBatch($data);
|
||||
}
|
||||
|
||||
// no row has been inserted after deletion
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the podcast categories
|
||||
*
|
||||
* @param int $podcastId
|
||||
*
|
||||
* @return \App\Entities\Category[]
|
||||
*/
|
||||
public function getPodcastCategories($podcastId)
|
||||
{
|
||||
if (!($categories = cache("podcasts{$podcastId}_categories"))) {
|
||||
$categories = $this->select('categories.*')
|
||||
->join(
|
||||
'podcasts_categories',
|
||||
'podcasts_categories.category_id = categories.id'
|
||||
)
|
||||
->where('podcasts_categories.podcast_id', $podcastId)
|
||||
->findAll();
|
||||
|
||||
cache()->save(
|
||||
"podcasts{$podcastId}_categories",
|
||||
$categories,
|
||||
DECADE
|
||||
);
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ class EpisodeModel extends Model
|
|||
'enclosure_filesize',
|
||||
'description',
|
||||
'image_uri',
|
||||
'explicit',
|
||||
'parental_advisory',
|
||||
'number',
|
||||
'season_number',
|
||||
'type',
|
||||
|
@ -47,7 +47,6 @@ class EpisodeModel extends Model
|
|||
'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]',
|
||||
'enclosure_uri' => 'required',
|
||||
'description' => 'required',
|
||||
'image_uri' => 'required',
|
||||
'number' => 'is_natural_no_zero|permit_empty',
|
||||
'season_number' => 'is_natural_no_zero|permit_empty',
|
||||
'type' => 'required',
|
||||
|
|
|
@ -24,15 +24,14 @@ class PodcastModel extends Model
|
|||
'image_uri',
|
||||
'language',
|
||||
'category_id',
|
||||
'explicit',
|
||||
'parental_advisory',
|
||||
'owner_name',
|
||||
'owner_email',
|
||||
'author',
|
||||
'publisher',
|
||||
'type',
|
||||
'copyright',
|
||||
'block',
|
||||
'complete',
|
||||
'custom_html_head',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'imported_feed_url',
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import Dropdown from "./modules/Dropdown";
|
||||
import HTMLEditor from "./modules/HTMLEditor";
|
||||
import EnclosureInput from "./modules/EnclosureInput";
|
||||
import MarkdownEditor from "./modules/MarkdownEditor";
|
||||
import MultiSelect from "./modules/MultiSelect";
|
||||
import SidebarToggler from "./modules/SidebarToggler";
|
||||
import Slugify from "./modules/Slugify";
|
||||
import Tooltip from "./modules/Tooltip";
|
||||
|
||||
Dropdown();
|
||||
Tooltip();
|
||||
MarkdownEditor();
|
||||
HTMLEditor();
|
||||
MultiSelect();
|
||||
Slugify();
|
||||
SidebarToggler();
|
||||
EnclosureInput();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/>
|
||||
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 226 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M14 12l-4 4V8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 166 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 214 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M13 10h5l-6 6-6-6h5V3h2v7zm-9 9h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2v7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 231 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0H24V24H0z"/>
|
||||
<path d="M5 3v16h16v2H3V3h2zm15.293 3.293l1.414 1.414L16 13.414l-3-2.999-4.293 4.292-1.414-1.414L13 7.586l3 2.999 4.293-4.292z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 269 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M3 4h18v2H3V4zm0 7h12v2H3v-2zm0 7h18v2H3v-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 196 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-5h2v2h-2v-2zm2-1.645V14h-2v-1.5a1 1 0 0 1 1-1 1.5 1.5 0 1 0-1.471-1.794l-1.962-.393A3.501 3.501 0 1 1 13 13.355z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 376 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M2 12c0-.865.11-1.703.316-2.504A3 3 0 0 0 4.99 4.867a9.99 9.99 0 0 1 4.335-2.505 3 3 0 0 0 5.348 0 9.99 9.99 0 0 1 4.335 2.505 3 3 0 0 0 2.675 4.63c.206.8.316 1.638.316 2.503 0 .865-.11 1.703-.316 2.504a3 3 0 0 0-2.675 4.629 9.99 9.99 0 0 1-4.335 2.505 3 3 0 0 0-5.348 0 9.99 9.99 0 0 1-4.335-2.505 3 3 0 0 0-2.675-4.63C2.11 13.704 2 12.866 2 12zm4.804 3c.63 1.091.81 2.346.564 3.524.408.29.842.541 1.297.75A4.993 4.993 0 0 1 12 18c1.26 0 2.438.471 3.335 1.274.455-.209.889-.46 1.297-.75A4.993 4.993 0 0 1 17.196 15a4.993 4.993 0 0 1 2.77-2.25 8.126 8.126 0 0 0 0-1.5A4.993 4.993 0 0 1 17.195 9a4.993 4.993 0 0 1-.564-3.524 7.989 7.989 0 0 0-1.297-.75A4.993 4.993 0 0 1 12 6a4.993 4.993 0 0 1-3.335-1.274 7.99 7.99 0 0 0-1.297.75A4.993 4.993 0 0 1 6.804 9a4.993 4.993 0 0 1-2.77 2.25 8.126 8.126 0 0 0 0 1.5A4.993 4.993 0 0 1 6.805 15zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M14 14.252v2.09A6 6 0 0 0 6 22l-2-.001a8 8 0 0 1 10-7.748zM12 13c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm6 6v-3h2v3h3v2h-3v3h-2v-3h-3v-2h3z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 366 B |
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 312 B |
|
@ -0,0 +1,24 @@
|
|||
const EnclosureInput = (): void => {
|
||||
const enclosureInput = document.querySelector(
|
||||
".form-enclosure-input"
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (enclosureInput) {
|
||||
const label = enclosureInput?.nextElementSibling?.querySelector(
|
||||
"span"
|
||||
) as HTMLSpanElement;
|
||||
const labelVal = label.innerHTML;
|
||||
|
||||
enclosureInput.addEventListener("change", (e: Event) => {
|
||||
const fileName = (e.target as HTMLInputElement).value.split("\\").pop();
|
||||
|
||||
if (fileName) {
|
||||
label.innerHTML = fileName;
|
||||
} else {
|
||||
label.innerHTML = labelVal;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default EnclosureInput;
|
|
@ -1,19 +0,0 @@
|
|||
import CodeMirror from "codemirror";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
|
||||
const HTMLEditor = (): void => {
|
||||
const allHTMLEditors: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll(
|
||||
"textarea[data-editor='html']"
|
||||
);
|
||||
|
||||
for (let j = 0; j < allHTMLEditors.length; j++) {
|
||||
const textarea = allHTMLEditors[j];
|
||||
|
||||
CodeMirror.fromTextArea(textarea, {
|
||||
lineNumbers: true,
|
||||
mode: { name: "xml", htmlMode: true },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default HTMLEditor;
|
|
@ -59,7 +59,7 @@ class ProseMirrorView {
|
|||
}
|
||||
},
|
||||
attributes: {
|
||||
class: "prose-sm px-3 py-2 overflow-y-auto",
|
||||
class: "prose-sm px-3 py-2 overflow-y-auto focus:shadow-outline",
|
||||
style: "min-height: 200px; max-height: 500px",
|
||||
},
|
||||
});
|
||||
|
@ -95,12 +95,22 @@ const MarkdownEditor = (): void => {
|
|||
"px-2",
|
||||
"bg-white",
|
||||
"border",
|
||||
"text-xs"
|
||||
"text-xs",
|
||||
"outline-none",
|
||||
"focus:shadow-outline"
|
||||
);
|
||||
wysiwygBtn.setAttribute("type", "button");
|
||||
wysiwygBtn.innerHTML = "Wysiwyg";
|
||||
const markdownBtn = document.createElement("button");
|
||||
markdownBtn.classList.add("py-1", "px-2", "bg-white", "border", "text-xs");
|
||||
markdownBtn.classList.add(
|
||||
"py-1",
|
||||
"px-2",
|
||||
"bg-white",
|
||||
"border",
|
||||
"text-xs",
|
||||
"outline-none",
|
||||
"focus:shadow-outline"
|
||||
);
|
||||
markdownBtn.setAttribute("type", "button");
|
||||
markdownBtn.innerHTML = "Markdown";
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import Choices from "choices.js";
|
||||
|
||||
const MultiSelect = (): void => {
|
||||
// Pass single element
|
||||
const multiSelects: NodeListOf<HTMLSelectElement> = document.querySelectorAll(
|
||||
"select[multiple]"
|
||||
);
|
||||
|
||||
for (let i = 0; i < multiSelects.length; i++) {
|
||||
const multiSelect = multiSelects[i];
|
||||
|
||||
new Choices(multiSelect, {
|
||||
maxItemCount: parseInt(multiSelect.dataset.maxItemCount || "-1"),
|
||||
itemSelectText: multiSelect.dataset.selectText,
|
||||
maxItemText: multiSelect.dataset.maxItemText,
|
||||
removeItemButton: true,
|
||||
classNames: {
|
||||
containerOuter:
|
||||
"multiselect" +
|
||||
(multiSelect.dataset.class ? ` ${multiSelect.dataset.class}` : ""),
|
||||
containerInner: "multiselect__inner",
|
||||
input: "multiselect__input",
|
||||
inputCloned: "multiselect__input--cloned",
|
||||
list: "multiselect__list",
|
||||
listItems: "multiselect__list--multiple",
|
||||
listDropdown: "multiselect__list--dropdown",
|
||||
item: "multiselect__item",
|
||||
itemSelectable: "multiselect__item--selectable",
|
||||
itemDisabled: "multiselect__item--disabled",
|
||||
itemChoice: "multiselect__item--choice",
|
||||
placeholder: "multiselect__placeholder",
|
||||
group: "multiselect__group",
|
||||
groupHeading: "multiselect__heading",
|
||||
button: "multiselect__button",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
|
@ -0,0 +1,62 @@
|
|||
const SidebarToggler = (): void => {
|
||||
const sidebar = document.querySelector(
|
||||
"aside[id='admin-sidebar']"
|
||||
) as HTMLElement;
|
||||
const toggler = document.querySelector(
|
||||
"button[id='sidebar-toggler']"
|
||||
) as HTMLButtonElement;
|
||||
const sidebarBackdrop = document.querySelector(
|
||||
"div[id='sidebar-backdrop']"
|
||||
) as HTMLElement;
|
||||
|
||||
const setAriaExpanded = (isExpanded: "true" | "false") => {
|
||||
toggler.setAttribute("aria-expanded", isExpanded);
|
||||
sidebarBackdrop.setAttribute("aria-expanded", isExpanded);
|
||||
};
|
||||
|
||||
const hideSidebar = () => {
|
||||
setAriaExpanded("false");
|
||||
sidebar.classList.add("-translate-x-full");
|
||||
sidebarBackdrop.classList.add("hidden");
|
||||
toggler.style.transform = "translateX(0px)";
|
||||
};
|
||||
|
||||
const showSidebar = () => {
|
||||
setAriaExpanded("true");
|
||||
sidebar.classList.remove("-translate-x-full");
|
||||
sidebarBackdrop.classList.remove("hidden");
|
||||
toggler.style.transform =
|
||||
"translateX(" + sidebar.getBoundingClientRect().width + "px)";
|
||||
};
|
||||
|
||||
toggler.addEventListener("click", () => {
|
||||
if (sidebar.classList.contains("-translate-x-full")) {
|
||||
showSidebar();
|
||||
} else {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
sidebarBackdrop.addEventListener("click", () => {
|
||||
if (!sidebar.classList.contains("-translate-x-full")) {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
const setAriaExpandedOnWindowEvent = () => {
|
||||
const isExpanded =
|
||||
!sidebar.classList.contains("-translate-x-full") ||
|
||||
window.innerWidth >= 768;
|
||||
const ariaExpanded = toggler.getAttribute("aria-expanded");
|
||||
if (isExpanded && (!ariaExpanded || ariaExpanded === "false")) {
|
||||
setAriaExpanded("true");
|
||||
} else if (!isExpanded && (!ariaExpanded || ariaExpanded === "true")) {
|
||||
setAriaExpanded("false");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("load", setAriaExpandedOnWindowEvent);
|
||||
window.addEventListener("resize", setAriaExpandedOnWindowEvent);
|
||||
};
|
||||
|
||||
export default SidebarToggler;
|
|
@ -10,10 +10,10 @@ const Tooltip = (): void => {
|
|||
const tooltipContent = tooltipReference.title;
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.setAttribute("id", "tooltip");
|
||||
tooltip.setAttribute("id", "tooltip" + i);
|
||||
tooltip.setAttribute(
|
||||
"class",
|
||||
"px-2 py-1 text-sm bg-gray-900 text-white rounded"
|
||||
"px-2 py-1 text-sm bg-gray-900 text-white rounded max-w-xs z-50"
|
||||
);
|
||||
tooltip.innerHTML = tooltipContent;
|
||||
|
||||
|
@ -31,13 +31,13 @@ const Tooltip = (): void => {
|
|||
|
||||
const show = () => {
|
||||
tooltipReference.removeAttribute("title");
|
||||
tooltipReference.setAttribute("aria-describedby", "tooltip");
|
||||
tooltipReference.setAttribute("aria-describedby", "tooltip" + i);
|
||||
document.body.appendChild(tooltip);
|
||||
popper.update();
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
const element = document.getElementById("tooltip");
|
||||
const element = document.getElementById("tooltip" + i);
|
||||
tooltipReference.removeAttribute("aria-describedby");
|
||||
tooltipReference.setAttribute("title", tooltipContent);
|
||||
if (element) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.breadcrumb {
|
||||
@apply inline-flex flex-wrap px-1 py-2 text-sm text-gray-800;
|
||||
@apply inline-flex flex-wrap px-1 py-2 text-sm;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
.form-enclosure-input {
|
||||
@apply absolute w-0 h-0 opacity-0;
|
||||
}
|
||||
|
||||
.form-enclosure-input + label {
|
||||
@apply inline-flex items-center justify-center w-full py-2 text-lg font-semibold text-green-600 bg-white border-2 border-green-500 rounded-lg shadow cursor-pointer;
|
||||
}
|
||||
|
||||
.form-enclosure-input:focus + label,
|
||||
.form-enclosure-input + label:hover {
|
||||
@apply text-green-700 border-green-700 shadow-md;
|
||||
}
|
||||
|
||||
.form-enclosure-input:focus + label {
|
||||
@apply shadow-outline;
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
@import "./tailwind.css";
|
||||
@import "./layout.css";
|
||||
@import "./breadcrumb.css";
|
||||
@import "./multiSelect.css";
|
||||
@import "./radioBtn.css";
|
||||
@import "./switch.css";
|
||||
@import "./enclosureInput.css";
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
.holy-grail-grid {
|
||||
@apply grid;
|
||||
grid-template: auto 1fr auto / auto 1fr auto;
|
||||
@apply grid min-h-screen overflow-y-auto;
|
||||
grid-template: 1fr auto / auto 1fr;
|
||||
|
||||
& .holy-grail-header {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
|
||||
& .holy-grail-sidenav {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 4;
|
||||
& .holy-grail-sidebar {
|
||||
@apply w-64 col-start-1 col-end-2 row-start-1 row-end-3;
|
||||
}
|
||||
|
||||
& .holy-grail-main {
|
||||
grid-column: 2 / 4;
|
||||
@apply w-full col-start-1 col-end-3 row-start-1 row-end-2;
|
||||
}
|
||||
|
||||
& .holy-grail-footer {
|
||||
grid-column: 2 / 4;
|
||||
@apply w-full col-start-1 col-end-3 row-start-2 row-end-3;
|
||||
}
|
||||
|
||||
@screen md {
|
||||
& .holy-grail-main {
|
||||
@apply col-start-2;
|
||||
}
|
||||
|
||||
& .holy-grail-footer {
|
||||
@apply col-start-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/*===============================
|
||||
= MultiSelect =
|
||||
===============================*/
|
||||
.multiselect {
|
||||
@apply relative;
|
||||
|
||||
&:focus {
|
||||
@apply shadow-outline outline-none;
|
||||
}
|
||||
&:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
&.is-disabled {
|
||||
&.multiselect__inner,
|
||||
&.multiselect__input {
|
||||
@apply bg-gray-300 cursor-not-allowed select-none;
|
||||
}
|
||||
&.multiselect__item {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
& [hidden] {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect[data-type*="select-multiple"],
|
||||
.multiselect[data-type*="text"] {
|
||||
& .multiselect__inner {
|
||||
@apply cursor-text;
|
||||
}
|
||||
& .multiselect__button {
|
||||
@apply relative inline-block w-2 pl-4 mt-0 mb-0 ml-1 opacity-75;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==);
|
||||
background-size: 8px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__inner {
|
||||
@apply inline-block w-full px-2 pt-2 pb-1 overflow-hidden align-top bg-white border rounded;
|
||||
|
||||
&.is-focused,
|
||||
&.is-open {
|
||||
@apply shadow-outline;
|
||||
}
|
||||
&.is-open {
|
||||
@apply rounded-b-none;
|
||||
}
|
||||
&.is-flipped.is-open {
|
||||
@apply rounded-t-none;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__list {
|
||||
@apply p-0 m-0 list-none;
|
||||
}
|
||||
|
||||
.multiselect__list--multiple {
|
||||
@apply inline;
|
||||
|
||||
& .multiselect__item {
|
||||
@apply inline-flex px-2 py-1 mb-1 mr-2 text-sm text-white break-all bg-green-500 rounded;
|
||||
|
||||
&[data-deletable] {
|
||||
@apply pr-1;
|
||||
}
|
||||
& [dir="rtl"] {
|
||||
@apply ml-2 mr-0;
|
||||
}
|
||||
&.is-highlighted {
|
||||
@apply bg-green-700;
|
||||
}
|
||||
&.is-disabled {
|
||||
@apply bg-gray-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__list--dropdown {
|
||||
@apply absolute z-10 invisible w-full overflow-hidden break-all bg-white border border-t-0 rounded-b shadow-lg;
|
||||
top: 100%;
|
||||
will-change: visibility;
|
||||
|
||||
&.is-active {
|
||||
@apply visible;
|
||||
}
|
||||
&.is-open {
|
||||
@apply shadow-outline;
|
||||
}
|
||||
&.is-flipped {
|
||||
@apply top-auto mt-0 rounded-t;
|
||||
bottom: 100%;
|
||||
}
|
||||
& .multiselect__list {
|
||||
@apply relative overflow-auto;
|
||||
max-height: 300px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
& .multiselect__item {
|
||||
@apply relative p-3;
|
||||
|
||||
& [dir="rtl"] {
|
||||
@apply text-right;
|
||||
}
|
||||
}
|
||||
& .multiselect__item--selectable {
|
||||
@screen sm {
|
||||
padding-right: 100px;
|
||||
&:after {
|
||||
@apply absolute text-sm transform -translate-y-1/2 opacity-0;
|
||||
content: attr(data-select-text);
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
}
|
||||
& [dir="rtl"] {
|
||||
@apply text-right;
|
||||
padding-left: 100px;
|
||||
padding-right: 10px;
|
||||
&:after {
|
||||
@apply right-auto;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-highlighted {
|
||||
@apply bg-gray-100;
|
||||
&:after {
|
||||
@apply opacity-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__item {
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
.multiselect__item--selectable {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.multiselect__item--disabled {
|
||||
@apply opacity-50 cursor-not-allowed select-none;
|
||||
}
|
||||
|
||||
.multiselect__heading {
|
||||
@apply p-3 font-semibold text-gray-600 border-b;
|
||||
}
|
||||
|
||||
.multiselect__button {
|
||||
@apply bg-transparent bg-center bg-no-repeat border-0 appearance-none cursor-pointer;
|
||||
text-indent: -9999px;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
@apply inline-block max-w-full py-1 pl-1 mb-1 align-baseline bg-transparent border-0 rounded-none;
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
& [dir="rtl"] {
|
||||
@apply pl-0 pr-1;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
/*===== End of Choices ======*/
|
|
@ -0,0 +1,24 @@
|
|||
.form-radio-btn {
|
||||
@apply absolute opacity-0;
|
||||
}
|
||||
|
||||
.form-radio-btn:focus + label {
|
||||
@apply shadow-outline;
|
||||
}
|
||||
|
||||
.form-radio-btn + label {
|
||||
@apply px-2 py-1 text-sm text-black bg-white border rounded cursor-pointer;
|
||||
|
||||
&:hover {
|
||||
@apply bg-green-100;
|
||||
}
|
||||
}
|
||||
|
||||
.form-radio-btn:checked + label {
|
||||
@apply text-white bg-green-500;
|
||||
|
||||
&::before {
|
||||
@apply mr-2 text-green-200;
|
||||
content: "✓";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
.form-switch {
|
||||
@apply absolute w-0 h-0 opacity-0;
|
||||
|
||||
&:checked + .form-switch-slider {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
&:focus + .form-switch-slider {
|
||||
@apply shadow-outline;
|
||||
}
|
||||
|
||||
&:checked + .form-switch-slider::before {
|
||||
@apply transform translate-x-5;
|
||||
}
|
||||
}
|
||||
|
||||
.form-switch-slider {
|
||||
@apply relative inset-0 flex-shrink-0 w-10 h-5 transition duration-200 bg-gray-400 rounded-full cursor-pointer;
|
||||
|
||||
&::before {
|
||||
@apply absolute w-4 h-4 transition duration-200 bg-white rounded-full shadow-xs;
|
||||
content: "";
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<?= helper('page') ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="<?= service('request')->getLocale() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
|
@ -9,9 +9,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
<?php if (isset($podcast)): ?>
|
||||
<?= $podcast->custom_html_head ?>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen mx-auto">
|
||||
|
@ -25,6 +22,9 @@
|
|||
</main>
|
||||
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t">
|
||||
<?= render_page_links() ?>
|
||||
<p>Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.</p>
|
||||
<small><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||
]) ?></small>
|
||||
</footer>
|
||||
</body>
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<header class="<?= $class ?>">
|
||||
<div class="w-64">
|
||||
<a href="<?= route_to(
|
||||
'admin'
|
||||
) ?>" class="inline-flex items-center text-xl">
|
||||
<?= svg('logo-castopod', 'h-10 mr-2') ?>
|
||||
Admin
|
||||
</a>
|
||||
</div>
|
||||
<?= render_breadcrumb() ?>
|
||||
<div class="relative ml-auto" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center px-2 py-1 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
Hey <?= user()->username ?>
|
||||
<?= icon('caret-down', 'ml-2') ?>
|
||||
</button>
|
||||
<nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="bottom-end">
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'my-account'
|
||||
) ?>">My Account</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'change-password'
|
||||
) ?>">Change password</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'logout'
|
||||
) ?>">Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
|
@ -1,31 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="<?= service('request')->getLocale() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Castopod Admin</title>
|
||||
<title><?= $this->renderSection('title') ?> | Castopod Admin</title>
|
||||
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/admin.css"/>
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
<script src="/assets/admin.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-gray-100 holy-grail-grid">
|
||||
<?= view('admin/_header', [
|
||||
'class' => 'flex items-center px-4 py-2 holy-grail-header',
|
||||
]) ?>
|
||||
<?= view('admin/_sidenav', [
|
||||
'class' => 'flex flex-col w-64 py-6 holy-grail-sidenav',
|
||||
]) ?>
|
||||
<main class="container px-4 py-6 mx-auto holy-grail-main">
|
||||
<h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1>
|
||||
<?= view('_message_block') ?>
|
||||
<?= $this->renderSection('content') ?>
|
||||
<body class="relative bg-gray-100 holy-grail-grid">
|
||||
<div id="sidebar-backdrop" role="button" tabIndex="0" aria-label="Close" class="fixed z-50 hidden w-full h-full bg-gray-900 bg-opacity-50 md:hidden"></div>
|
||||
<aside id="admin-sidebar" class="sticky top-0 z-50 flex flex-col w-64 max-h-screen transition duration-200 ease-in-out transform -translate-x-full bg-white border-r holy-grail-sidebar md:translate-x-0">
|
||||
<?php if (isset($podcast)): ?>
|
||||
<?= $this->include('admin/podcast/_sidebar') ?>
|
||||
<?php else: ?>
|
||||
<?= $this->include('admin/_sidebar') ?>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
<main class="overflow-hidden holy-grail-main">
|
||||
<header class="text-white bg-gradient-to-tr from-gray-900 to-gray-800">
|
||||
<div class="container flex flex-wrap items-end justify-between px-2 py-10 mx-auto md:px-12 gap-y-6 gap-x-6">
|
||||
<div class="flex flex-col">
|
||||
<?= render_breadcrumb('text-gray-300') ?>
|
||||
<h1 class="text-3xl leading-none"><?= $this->renderSection(
|
||||
'pageTitle'
|
||||
) ?></h1>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-y-2"><?= $this->renderSection(
|
||||
'headerRight'
|
||||
) ?></div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container px-2 py-8 mx-auto md:px-12">
|
||||
<?= view('_message_block') ?>
|
||||
<?= $this->renderSection('content') ?>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="w-full px-2 py-4 mx-auto text-xs text-right border-t holy-grail-footer">
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.
|
||||
<footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer">
|
||||
<small><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||
]) ?></small>
|
||||
</footer>
|
||||
|
||||
<script src="/assets/admin.js"></script>
|
||||
<button
|
||||
type="button"
|
||||
id="sidebar-toggler"
|
||||
class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline"
|
||||
style="transform: translateX(0px);"><?= icon('menu') ?></button>
|
||||
</body>
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<article class="flex w-full max-w-lg mb-4 bg-white border rounded shadow">
|
||||
<img
|
||||
loading="lazy"
|
||||
src="<?= $episode->image->thumbnail_url ?>"
|
||||
alt="<?= $episode->title ?>" class="object-cover w-32 h-32 rounded-l" />
|
||||
<div class="flex flex-col flex-1 px-4 py-2">
|
||||
<a href="<?= route_to(
|
||||
'episode-view',
|
||||
$episode->podcast->id,
|
||||
$episode->id
|
||||
) ?>">
|
||||
<h3 class="text-xl font-semibold">
|
||||
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
|
||||
<span class="text-base font-bold text-gray-600">#<?= $episode->number ?></span>
|
||||
</h3>
|
||||
</a>
|
||||
<div class="relative ml-auto" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
<?= icon('more') ?>
|
||||
</button>
|
||||
<nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="bottom-start" data-popper-offset-x="0" data-popper-offset-y="0" >
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-edit',
|
||||
$episode->podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode',
|
||||
$episode->podcast->name,
|
||||
$episode->slug
|
||||
) ?>"><?= lang('Episode.go_to_page') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-delete',
|
||||
$episode->podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.delete') ?></a>
|
||||
</nav>
|
||||
</div>
|
||||
<audio controls class="mt-auto" preload="none">
|
||||
<source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
</div>
|
||||
</article>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="flex flex-col py-4">
|
||||
<?php if ($episodes): ?>
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<?= view('admin/_partials/_episode-card', [
|
||||
'episode' => $episode,
|
||||
]) ?>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
|
@ -1,29 +0,0 @@
|
|||
<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
|
||||
<img
|
||||
alt="<?= $podcast->title ?>"
|
||||
src="<?= $podcast->image
|
||||
->thumbnail_url ?>" class="object-cover w-full h-40" />
|
||||
<div class="p-2">
|
||||
<a href="<?= route_to(
|
||||
'podcast-view',
|
||||
$podcast->id
|
||||
) ?>" class="hover:underline">
|
||||
<h2 class="font-semibold"><?= $podcast->title ?></h2>
|
||||
</a>
|
||||
<p class="text-gray-600">@<?= $podcast->name ?></p>
|
||||
</div>
|
||||
<footer class="flex items-center justify-end p-2">
|
||||
<a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
|
||||
'podcast-edit',
|
||||
$podcast->id
|
||||
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
|
||||
'Podcast.edit'
|
||||
) ?>"><?= icon('edit') ?></a>
|
||||
<a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
|
||||
'podcast-view',
|
||||
$podcast->id
|
||||
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
|
||||
'Podcast.view'
|
||||
) ?>"><?= icon('eye') ?></a>
|
||||
</footer>
|
||||
</article>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Email
|
||||
<?= lang('User.form.email') ?>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<?= $user->email ?>
|
||||
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Username
|
||||
<?= lang('User.form.username') ?>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<?= $user->username ?>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Roles
|
||||
<?= lang('User.form.roles') ?>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
[<?= implode(', ', $user->roles) ?>]
|
||||
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Permissions
|
||||
<?= lang('User.form.permissions') ?>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
[<?= implode(', ', $user->permissions) ?>]
|
||||
|
|
|
@ -9,7 +9,19 @@ $navigation = [
|
|||
'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']],
|
||||
]; ?>
|
||||
|
||||
<nav class="<?= $class ?>">
|
||||
<a href="<?= route_to(
|
||||
'admin'
|
||||
) ?>" class="inline-flex items-center px-4 py-2 mb-2 text-xl">
|
||||
<?= svg('logo-castopod', 'h-8 mr-2') ?>
|
||||
Castopod
|
||||
</a>
|
||||
<a href="<?= route_to(
|
||||
'home'
|
||||
) ?>" class="inline-flex items-center px-6 py-2 mb-2 text-sm underline outline-none hover:no-underline focus:shadow-outline">
|
||||
<?= lang('AdminNavigation.go_to_website') ?>
|
||||
<?= icon('external-link', 'ml-2 text-gray-500') ?>
|
||||
</a>
|
||||
<nav class="flex flex-col flex-1 overflow-y-auto">
|
||||
<?php foreach ($navigation as $section => $data): ?>
|
||||
<div class="mb-4">
|
||||
<button class="inline-flex items-center w-full px-6 py-1 outline-none focus:shadow-outline" type="button">
|
||||
|
@ -30,11 +42,23 @@ $navigation = [
|
|||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<a href="<?= route_to(
|
||||
'home'
|
||||
) ?>" class="inline-flex items-center px-4 py-1 mt-auto text-sm underline outline-none hover:no-underline focus:shadow-outline">
|
||||
<?= lang('AdminNavigation.go_to_website') ?>
|
||||
<?= icon('external-link', 'ml-2 text-gray-500') ?>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="w-full mt-auto border-t" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center w-full px-6 py-2 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
<?= icon('user', 'text-gray-500 mr-2') ?>
|
||||
<?= user()->username ?>
|
||||
<?= icon('caret-right', 'ml-auto') ?>
|
||||
</button>
|
||||
<nav class="absolute z-50 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="right-end">
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'my-account'
|
||||
) ?>">My Account</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'change-password'
|
||||
) ?>">Change password</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'logout'
|
||||
) ?>">Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Contributor.add_contributor', [$podcast->title]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Contributor.add_contributor', [$podcast->title]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -26,11 +30,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Contributor.form.submit_add'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Contributor.form.submit_add'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Contributor.edit_role', [$user->username]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Contributor.edit_role', [$user->username]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -19,11 +23,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Contributor.form.submit_edit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Contributor.form.submit_edit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -2,47 +2,67 @@
|
|||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Contributor.podcast_contributors') ?>
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'contributor-add',
|
||||
$podcast->id
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Contributor.add') ?></a>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Contributor.podcast_contributors') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(lang('Contributor.add'), route_to('contributor-add', $podcast->id), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'add',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<table class="table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Username</th>
|
||||
<th class="px-4 py-2">Role</th>
|
||||
<th class="px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($podcast->contributors as $contributor): ?>
|
||||
<tr>
|
||||
<td class="px-4 py-2 border"><?= $contributor->username ?></td>
|
||||
<td class="px-4 py-2 border"><?= lang(
|
||||
'Contributor.roles.' . $contributor->podcast_role
|
||||
) ?></td>
|
||||
<td class="px-4 py-2 border">
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'contributor-edit',
|
||||
$podcast->id,
|
||||
$contributor->id
|
||||
) ?>"><?= lang('Contributor.edit') ?></a>
|
||||
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
|
||||
'contributor-remove',
|
||||
$podcast->id,
|
||||
$contributor->id
|
||||
) ?>"><?= lang('Contributor.remove') ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?= data_table(
|
||||
[
|
||||
[
|
||||
'header' => lang('Contributor.list.username'),
|
||||
'cell' => function ($contributor) {
|
||||
return $contributor->username;
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Contributor.list.role'),
|
||||
'cell' => function ($contributor) {
|
||||
return lang('Contributor.roles.' . $contributor->podcast_role);
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($contributor, $podcast) {
|
||||
return button(
|
||||
lang('Contributor.edit'),
|
||||
route_to(
|
||||
'contributor-edit',
|
||||
$podcast->id,
|
||||
$contributor->id
|
||||
),
|
||||
[
|
||||
'variant' => 'info',
|
||||
'size' => 'small',
|
||||
],
|
||||
['class' => 'mr-2']
|
||||
) .
|
||||
button(
|
||||
lang('Contributor.remove'),
|
||||
route_to(
|
||||
'contributor-remove',
|
||||
$podcast->id,
|
||||
$contributor->id
|
||||
),
|
||||
['variant' => 'danger', 'size' => 'small'],
|
||||
['class' => 'mr-2']
|
||||
);
|
||||
},
|
||||
],
|
||||
],
|
||||
$podcast->contributors,
|
||||
$podcast
|
||||
) ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
<?= helper('components') ?>
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
Welcome to the admin dashboard!
|
||||
Dashboard
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
Admin dashboard
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
Welcome to the admin area!
|
||||
<?= $this->endsection() ?>
|
||||
|
|
|
@ -4,26 +4,44 @@
|
|||
<?= lang('Episode.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open_multipart(route_to('episode-create', $podcast->id), [
|
||||
'method' => 'post',
|
||||
'class' => 'flex flex-col max-w-md',
|
||||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.enclosure'), 'enclosure') ?>
|
||||
<?= form_input([
|
||||
'id' => 'enclosure',
|
||||
'name' => 'enclosure',
|
||||
'class' => 'form-input mb-4',
|
||||
'required' => 'required',
|
||||
'type' => 'file',
|
||||
'accept' => '.mp3,.m4a',
|
||||
]) ?>
|
||||
<div class="flex w-full mb-6">
|
||||
<?= form_input([
|
||||
'id' => 'enclosure',
|
||||
'name' => 'enclosure',
|
||||
'class' => 'form-enclosure-input',
|
||||
'required' => 'required',
|
||||
'type' => 'file',
|
||||
'accept' => '.mp3,.m4a',
|
||||
]) ?>
|
||||
<label for="enclosure"><?= icon('upload', 'mr-2 text-') ?>
|
||||
<span><?= lang('Episode.form.enclosure') ?></span></label>
|
||||
</div>
|
||||
|
||||
<?= form_label(lang('Episode.form.image'), 'image') ?>
|
||||
<?= form_section(
|
||||
lang('Episode.form.info_section_title'),
|
||||
lang('Episode.form.info_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Episode.form.image'),
|
||||
'image',
|
||||
[],
|
||||
lang('Episode.form.image_hint'),
|
||||
true
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'image',
|
||||
'name' => 'image',
|
||||
|
@ -35,7 +53,12 @@
|
|||
'Common.forms.image_size_hint'
|
||||
) ?></small>
|
||||
|
||||
<?= form_label(lang('Episode.form.title'), 'title') ?>
|
||||
<?= form_label(
|
||||
lang('Episode.form.title'),
|
||||
'title',
|
||||
[],
|
||||
lang('Episode.form.title_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'title',
|
||||
'name' => 'title',
|
||||
|
@ -45,7 +68,12 @@
|
|||
'data-slugify' => 'title',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.slug'), 'slug') ?>
|
||||
<?= form_label(
|
||||
lang('Episode.form.slug'),
|
||||
'slug',
|
||||
[],
|
||||
lang('Episode.form.slug_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'slug',
|
||||
'name' => 'slug',
|
||||
|
@ -55,6 +83,74 @@
|
|||
'data-slugify' => 'slug',
|
||||
]) ?>
|
||||
|
||||
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'season_number',
|
||||
'name' => 'season_number',
|
||||
'class' => 'form-input w-full',
|
||||
'value' => old('season_number'),
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'episode_number',
|
||||
'name' => 'episode_number',
|
||||
'class' => 'form-input w-full',
|
||||
'value' => old('episode_number'),
|
||||
'required' => 'required',
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Episode.form.type.label') .
|
||||
hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'full',
|
||||
old('type') ? old('type') == 'full' : true
|
||||
) ?>
|
||||
<label for="full" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.full') ?>
|
||||
</label>
|
||||
<?= form_radio(
|
||||
['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'trailer',
|
||||
old('type') ? old('type') == 'trailer' : false
|
||||
) ?>
|
||||
<label for="trailer" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.trailer') ?>
|
||||
</label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'bonus',
|
||||
'name' => 'type',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'bonus',
|
||||
old('type') ? old('type') == 'bonus' : false
|
||||
) ?>
|
||||
<label for="bonus" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.bonus') ?>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.show_notes_section_title'),
|
||||
lang('Episode.form.show_notes_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Episode.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
|
@ -69,6 +165,36 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(
|
||||
lang('Episode.form.description_footer'),
|
||||
'description_footer',
|
||||
[],
|
||||
lang('Episode.form.description_footer_hint')
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'description_footer',
|
||||
'name' => 'description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old(
|
||||
'description_footer',
|
||||
$podcast->episode_description_footer ?? '',
|
||||
false
|
||||
),
|
||||
'data-editor="markdown"'
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.publication_section_title'),
|
||||
lang('Episode.form.publication_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.published_at.label') ?></legend>
|
||||
<div class="flex flex-col flex-1">
|
||||
|
@ -99,76 +225,69 @@
|
|||
</div>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'season_number',
|
||||
'name' => 'season_number',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('season_number'),
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'episode_number',
|
||||
'name' => 'episode_number',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('episode_number'),
|
||||
'required' => 'required',
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('explicit', false)
|
||||
<?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Episode.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'undefined',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'undefined',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'undefined'
|
||||
: true
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.explicit') ?></span>
|
||||
</label>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<label for="full" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'full', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'full',
|
||||
old('type') ? old('type') == 'full' : true
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.full') ?></span>
|
||||
</label>
|
||||
<label for="trailer" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'trailer',
|
||||
old('type') ? old('type') == 'trailer' : false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>
|
||||
</label>
|
||||
<label for="bonus" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'bonus',
|
||||
old('type') ? old('type') == 'bonus' : false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>
|
||||
</label>
|
||||
<label for="undefined"><?= lang(
|
||||
'Episode.form.parental_advisory.undefined'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'clean',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'clean',
|
||||
old('parental_advisory') ? old('parental_advisory') === 'clean' : false
|
||||
) ?>
|
||||
<label for="clean"><?= lang(
|
||||
'Episode.form.parental_advisory.clean'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'explicit',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'explicit',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'explicit'
|
||||
: false
|
||||
) ?>
|
||||
<label for="explicit"><?= lang(
|
||||
'Episode.form.parental_advisory.explicit'
|
||||
) ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('block', false)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.block') ?></span>
|
||||
</label>
|
||||
<?= form_switch(
|
||||
lang('Episode.form.block') .
|
||||
hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'),
|
||||
['id' => 'block', 'name' => 'block'],
|
||||
'yes',
|
||||
old('block', false)
|
||||
) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Episode.form.submit_create'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<?= button(
|
||||
lang('Episode.form.submit_create'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,25 +4,43 @@
|
|||
<?= lang('Episode.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open_multipart(
|
||||
route_to('episode-edit', $episode->podcast->id, $episode->id),
|
||||
['method' => 'post', 'class' => 'flex flex-col max-w-md']
|
||||
) ?>
|
||||
<?= form_open_multipart(route_to('episode-edit', $podcast->id, $episode->id), [
|
||||
'method' => 'post',
|
||||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.enclosure'), 'enclosure') ?>
|
||||
<?= form_input([
|
||||
'id' => 'enclosure',
|
||||
'name' => 'enclosure',
|
||||
'class' => 'form-input mb-4',
|
||||
'type' => 'file',
|
||||
'accept' => '.mp3,.m4a',
|
||||
]) ?>
|
||||
<div class="flex w-full mb-6">
|
||||
<?= form_input([
|
||||
'id' => 'enclosure',
|
||||
'name' => 'enclosure',
|
||||
'class' => 'form-enclosure-input',
|
||||
'type' => 'file',
|
||||
'accept' => '.mp3,.m4a',
|
||||
]) ?>
|
||||
<label for="enclosure"><?= icon('upload', 'mr-2 text-') ?>
|
||||
<span><?= lang('Episode.form.enclosure') ?></span></label>
|
||||
</div>
|
||||
|
||||
<?= form_label(lang('Episode.form.image'), 'image') ?>
|
||||
<?= form_section(
|
||||
lang('Episode.form.info_section_title'),
|
||||
lang('Episode.form.info_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Episode.form.image'),
|
||||
'image',
|
||||
[],
|
||||
lang('Episode.form.image_hint'),
|
||||
true
|
||||
) ?>
|
||||
<img
|
||||
src="<?= $episode->image->thumbnail_url ?>"
|
||||
alt="<?= $episode->title ?>"
|
||||
|
@ -39,7 +57,12 @@
|
|||
'Common.forms.image_size_hint'
|
||||
) ?></small>
|
||||
|
||||
<?= form_label(lang('Episode.form.title'), 'title') ?>
|
||||
<?= form_label(
|
||||
lang('Episode.form.title'),
|
||||
'title',
|
||||
[],
|
||||
lang('Episode.form.title_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'title',
|
||||
'name' => 'title',
|
||||
|
@ -49,7 +72,12 @@
|
|||
'data-slugify' => 'title',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.slug'), 'slug') ?>
|
||||
<?= form_label(
|
||||
lang('Episode.form.slug'),
|
||||
'slug',
|
||||
[],
|
||||
lang('Episode.form.slug_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'slug',
|
||||
'name' => 'slug',
|
||||
|
@ -59,6 +87,69 @@
|
|||
'data-slugify' => 'slug',
|
||||
]) ?>
|
||||
|
||||
<div class="flex flex-col mb-4 gap-x-2 gap-y-4 md:flex-row">
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'season_number',
|
||||
'name' => 'season_number',
|
||||
'class' => 'form-input w-full',
|
||||
'value' => old('season_number', $episode->season_number),
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'episode_number',
|
||||
'name' => 'episode_number',
|
||||
'class' => 'form-input w-full',
|
||||
'value' => old('episode_number', $episode->number),
|
||||
'required' => 'required',
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Episode.form.type.label') .
|
||||
hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
['id' => 'full', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'full',
|
||||
old('type') ? old('type') === 'full' : $episode->type === 'full'
|
||||
) ?>
|
||||
<label for="full" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.full') ?>
|
||||
</label>
|
||||
<?= form_radio(
|
||||
['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'trailer',
|
||||
old('type') ? old('type') === 'trailer' : $episode->type === 'trailer'
|
||||
) ?>
|
||||
<label for="trailer" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.trailer') ?>
|
||||
</label>
|
||||
<?= form_radio(
|
||||
['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'bonus',
|
||||
old('type') ? old('type') === 'bonus' : $episode->type === 'bonus'
|
||||
) ?>
|
||||
<label for="bonus" class="inline-flex items-center">
|
||||
<?= lang('Episode.form.type.bonus') ?>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.show_notes_section_title'),
|
||||
lang('Episode.form.show_notes_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Episode.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
|
@ -73,6 +164,36 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(
|
||||
lang('Episode.form.description_footer'),
|
||||
'description_footer',
|
||||
[],
|
||||
lang('Episode.form.description_footer_hint')
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'description_footer',
|
||||
'name' => 'description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old(
|
||||
'description_footer',
|
||||
$podcast->episode_description_footer ?? '',
|
||||
false
|
||||
),
|
||||
'data-editor="markdown"'
|
||||
) ?>
|
||||
</div>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Episode.form.publication_section_title'),
|
||||
lang('Episode.form.publication_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.published_at.label') ?></legend>
|
||||
<div class="flex flex-col flex-1">
|
||||
|
@ -111,76 +232,76 @@
|
|||
</div>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'season_number',
|
||||
'name' => 'season_number',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('season_number', $episode->season_number),
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Episode.form.episode_number'), 'episode_number') ?>
|
||||
<?= form_input([
|
||||
'id' => 'episode_number',
|
||||
'name' => 'episode_number',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('episode_number', $episode->number),
|
||||
'required' => 'required',
|
||||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('explicit', $episode->explicit)
|
||||
<?= form_fieldset('', ['class' => 'flex mb-6 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Episode.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Episode.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'undefined',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'undefined',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'undefined'
|
||||
: $episode->parental_advisory === null
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.explicit') ?></span>
|
||||
</label>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<label for="full" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'full', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'full',
|
||||
old('type') ? old('type') == 'full' : $episode->type == 'full'
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.full') ?></span>
|
||||
</label>
|
||||
<label for="trailer" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'trailer', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'trailer',
|
||||
old('type') ? old('type') == 'trailer' : $episode->type == 'trailer'
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>
|
||||
</label>
|
||||
<label for="bonus" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'bonus', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'bonus',
|
||||
old('type') ? old('type') == 'bonus' : $episode->type == 'bonus'
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>
|
||||
</label>
|
||||
<label for="undefined"><?= lang(
|
||||
'Episode.form.parental_advisory.undefined'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'clean',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'clean',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'clean'
|
||||
: $episode->parental_advisory === 'clean'
|
||||
) ?>
|
||||
<label for="clean"><?= lang(
|
||||
'Episode.form.parental_advisory.clean'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'explicit',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'explicit',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'explicit'
|
||||
: $episode->parental_advisory === 'explicit'
|
||||
) ?>
|
||||
<label for="explicit"><?= lang(
|
||||
'Episode.form.parental_advisory.explicit'
|
||||
) ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('block', $episode->block)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Episode.form.block') ?></span>
|
||||
</label>
|
||||
<?= form_switch(
|
||||
lang('Episode.form.block') .
|
||||
hint_tooltip(lang('Episode.form.block_hint'), 'ml-1'),
|
||||
['id' => 'block', 'name' => 'block'],
|
||||
'yes',
|
||||
old(
|
||||
'block',
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Episode.form.submit_edit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
$episode->block
|
||||
)
|
||||
) ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<?= button(
|
||||
lang('Episode.form.submit_edit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -1,23 +1,130 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Episode.all_podcast_episodes') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>)
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'episode-create',
|
||||
$podcast->id
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Episode.create') ?></a>
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.all_podcast_episodes') ?> (<?= $pager->getDetails()[
|
||||
'total'
|
||||
] ?>)
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(
|
||||
lang('Episode.create'),
|
||||
route_to('episode-create', $podcast->id),
|
||||
|
||||
['variant' => 'primary', 'iconLeft' => 'add']
|
||||
) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= view('admin/_partials/_episode-list.php', [
|
||||
'episodes' => $podcast->episodes,
|
||||
]) ?>
|
||||
<p class="mb-4 text-sm italic text-gray-700"><?= lang('Common.pageInfo', [
|
||||
'currentPage' => $pager->getDetails()['currentPage'],
|
||||
'pageCount' => $pager->getDetails()['pageCount'],
|
||||
]) ?></p>
|
||||
<div class="flex flex-wrap mb-6">
|
||||
<?php if ($episodes): ?>
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<article class="flex w-full max-w-lg p-4 mx-auto">
|
||||
<img
|
||||
loading="lazy"
|
||||
src="<?= $episode->image->thumbnail_url ?>"
|
||||
alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex">
|
||||
<a class="flex-1 text-sm hover:underline" href="<?= route_to(
|
||||
'episode-view',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>">
|
||||
<h2 class="inline-flex justify-between w-full font-bold leading-none group">
|
||||
<span class="mr-1 group-hover:underline"><?= $episode->title ?></span>
|
||||
<?php if (
|
||||
$episode->season_number &&
|
||||
$episode->number
|
||||
): ?>
|
||||
<abbr class="text-xs font-bold text-gray-600" title="<?= lang(
|
||||
'Episode.season_episode',
|
||||
[
|
||||
'seasonNumber' =>
|
||||
$episode->season_number,
|
||||
'episodeNumber' => $episode->number,
|
||||
]
|
||||
) ?>"><?= lang('Episode.season_episode_abbr', [
|
||||
'seasonNumber' => $episode->season_number,
|
||||
'episodeNumber' => $episode->number,
|
||||
]) ?></abbr>
|
||||
<?php elseif (
|
||||
!$episode->season_number &&
|
||||
$episode->number
|
||||
): ?>
|
||||
<abbr class="text-xs font-bold text-gray-600" title="<?= lang(
|
||||
'Episode.number',
|
||||
[
|
||||
'episodeNumber' => $episode->number,
|
||||
]
|
||||
) ?>"><?= lang('Episode.number_abbr', [
|
||||
'episodeNumber' => $episode->number,
|
||||
]) ?></abbr>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
</a>
|
||||
<div class="relative" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
<?= icon('more') ?>
|
||||
</button>
|
||||
<nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="bottom-end" data-popper-offset-x="0" data-popper-offset-y="-24" >
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-edit',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>"><?= lang('Episode.go_to_page') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-delete',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.delete') ?></a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2 text-xs">
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
$episode->published_at,
|
||||
]) ?>
|
||||
</time>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= $episode->enclosure_duration ?>S">
|
||||
<?= lang('Common.duration', [
|
||||
$episode->enclosure_duration,
|
||||
]) ?>
|
||||
</time>
|
||||
</div>
|
||||
<audio controls preload="none" class="w-full mt-auto">
|
||||
<source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= $pager->links() ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
|
|
|
@ -4,40 +4,46 @@
|
|||
<?= $episode->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= $episode->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<img
|
||||
src="<?= $episode->image->medium_url ?>"
|
||||
alt="Episode cover"
|
||||
class="object-cover w-40 h-40 mb-6"
|
||||
/>
|
||||
<audio controls preload="none" class="mb-12">
|
||||
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full max-w-sm mb-6 md:mr-4">
|
||||
<img
|
||||
src="<?= $episode->image->medium_url ?>"
|
||||
alt="Episode cover"
|
||||
class="object-cover w-full"
|
||||
/>
|
||||
<audio controls preload="none" class="w-full mb-6">
|
||||
<source src="/<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
|
||||
<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'episode-edit',
|
||||
$episode->podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode',
|
||||
$episode->podcast->name,
|
||||
$episode->slug
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-gray-700 hover:bg-gray-800"><?= lang(
|
||||
'Episode.go_to_page'
|
||||
) ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode-delete',
|
||||
$episode->podcast->id,
|
||||
$episode->id
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
|
||||
'Episode.delete'
|
||||
) ?></a>
|
||||
<div class="flex justify-around">
|
||||
<?= button(
|
||||
lang('Episode.edit'),
|
||||
route_to('episode-edit', $podcast->id, $episode->id),
|
||||
['variant' => 'info', 'iconLeft' => 'edit']
|
||||
) ?>
|
||||
<?= button(
|
||||
lang('Episode.go_to_page'),
|
||||
route_to('episode', $podcast->name, $episode->slug),
|
||||
['variant' => 'secondary', 'iconLeft' => 'external-link']
|
||||
) ?>
|
||||
<?= button(
|
||||
lang('Episode.delete'),
|
||||
route_to('episode-delete', $podcast->id, $episode->id),
|
||||
['variant' => 'danger', 'iconLeft' => 'delete-bin']
|
||||
) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="prose">
|
||||
<?= $episode->description_html ?>
|
||||
</section>
|
||||
<section class="w-full max-w-sm prose">
|
||||
<?= $episode->description_html ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -31,11 +31,12 @@
|
|||
'autocomplete' => 'new-password',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('User.form.submit_password_change'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('User.form.submit_password_change'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Page.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Page.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -46,11 +50,13 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Page.form.submit_create'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
|
||||
<?= button(
|
||||
lang('Page.form.submit_create'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Page.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Page.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -46,11 +50,12 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Page.form.submit_edit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Page.form.submit_edit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -1,47 +1,62 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Page.all_pages') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Page.all_pages') ?> (<?= count($pages) ?>)
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'page-create'
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Page.create') ?></a>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(lang('Page.create'), route_to('page-create'), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'add',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<table class="table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Title</th>
|
||||
<th class="px-4 py-2">Slug</th>
|
||||
<th class="px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pages as $page): ?>
|
||||
<tr>
|
||||
<td class="px-4 py-2 border"><?= $page->title ?></td>
|
||||
<td class="px-4 py-2 border"><?= $page->slug ?></td>
|
||||
<td class="px-4 py-2 border">
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
|
||||
'page',
|
||||
$page->slug
|
||||
) ?>"><?= lang('Page.go_to_page') ?></a>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'page-edit',
|
||||
$page->id
|
||||
) ?>"><?= lang('Page.edit') ?></a>
|
||||
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
|
||||
'page-delete',
|
||||
$page->id
|
||||
) ?>"><?= lang('Page.delete') ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?= data_table(
|
||||
[
|
||||
[
|
||||
'header' => lang('Page.page'),
|
||||
'cell' => function ($page) {
|
||||
return '<div class="flex flex-col">' .
|
||||
$page->title .
|
||||
'<span class="text-sm text-gray-600">/' .
|
||||
$page->slug .
|
||||
'</span></div>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($page) {
|
||||
return button(
|
||||
lang('Page.go_to_page'),
|
||||
route_to('page', $page->slug),
|
||||
[
|
||||
'variant' => 'secondary',
|
||||
'size' => 'small',
|
||||
],
|
||||
['class' => 'mr-2']
|
||||
) .
|
||||
button(
|
||||
lang('Page.edit'),
|
||||
route_to('page-edit', $page->id),
|
||||
['variant' => 'info', 'size' => 'small'],
|
||||
['class' => 'mr-2']
|
||||
) .
|
||||
button(
|
||||
lang('Page.delete'),
|
||||
route_to('page-delete', $page->id),
|
||||
['variant' => 'danger', 'size' => 'small']
|
||||
);
|
||||
},
|
||||
],
|
||||
],
|
||||
$pages
|
||||
) ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
<?= $this->section('title') ?>
|
||||
<?= $page->title ?>
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-teal-500 rounded shadow-xs outline-none hover:bg-teal-600 focus:shadow-outline" href="<?= route_to(
|
||||
'page-edit',
|
||||
$page->id
|
||||
) ?>">
|
||||
<?= icon('edit', 'mr-2') ?>
|
||||
<?= lang('Page.edit') ?></a>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= $page->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(lang('Page.edit'), route_to('page-edit', $page->id), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'add',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
$podcastNavigation = [
|
||||
'dashboard' => [
|
||||
'icon' => 'dashboard',
|
||||
'items' => ['podcast-view', 'podcast-edit'],
|
||||
],
|
||||
'episodes' => [
|
||||
'icon' => 'mic',
|
||||
'items' => ['episode-list', 'episode-create'],
|
||||
],
|
||||
'analytics' => [
|
||||
'icon' => 'line-chart',
|
||||
'items' => [],
|
||||
],
|
||||
'contributors' => [
|
||||
'icon' => 'group',
|
||||
'items' => ['contributor-list', 'contributor-add'],
|
||||
],
|
||||
'settings' => [
|
||||
'icon' => 'settings',
|
||||
'items' => ['platforms'],
|
||||
],
|
||||
]; ?>
|
||||
|
||||
<a href="<?= route_to(
|
||||
'admin'
|
||||
) ?>" class="inline-flex items-center px-4 py-2 border-b">
|
||||
<?= icon('arrow-left', 'mr-4') ?>
|
||||
<?= svg('logo-castopod', 'h-8 mr-2') ?>
|
||||
Castopod
|
||||
</a>
|
||||
<div class="flex items-center border-b">
|
||||
<img
|
||||
src="<?= $podcast->image->thumbnail_url ?>"
|
||||
alt="<?= $podcast->title ?>"
|
||||
class="object-cover w-16 h-16 mr-2"
|
||||
/>
|
||||
<div class="flex flex-col items-start flex-1">
|
||||
<span class="font-semibold truncate"><?= $podcast->title ?></span>
|
||||
<a href="<?= route_to(
|
||||
'podcast',
|
||||
$podcast->name
|
||||
) ?>" class="inline-flex items-center text-sm underline outline-none hover:no-underline focus:shadow-outline"
|
||||
data-toggle="tooltip" data-placement="bottom" title="<?= lang(
|
||||
'PodcastNavigation.go_to_page'
|
||||
) ?>">@<?= $podcast->name ?>
|
||||
<?= icon('external-link', 'ml-1 text-gray-500') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex flex-col flex-1 py-6 overflow-y-auto">
|
||||
<?php foreach ($podcastNavigation as $section => $data): ?>
|
||||
<div class="mb-4">
|
||||
<button class="inline-flex items-center w-full px-6 py-1 outline-none focus:shadow-outline" type="button">
|
||||
<?= icon($data['icon'], 'text-gray-500') ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'PodcastNavigation.' . $section
|
||||
) ?></span>
|
||||
</button>
|
||||
<ul>
|
||||
<?php foreach ($data['items'] as $item): ?>
|
||||
<?php $isActive =
|
||||
base_url(route_to($item, $podcast->id)) == current_url(); ?>
|
||||
<li>
|
||||
<a class="block py-1 pl-12 pr-2 text-sm text-gray-600 outline-none hover:text-gray-900 focus:shadow-outline <?= $isActive
|
||||
? 'font-semibold text-gray-900'
|
||||
: '' ?>" href="<?= route_to(
|
||||
$item,
|
||||
$podcast->id
|
||||
) ?>"><?= lang('PodcastNavigation.' . $item) ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<div class="w-full mt-auto border-t" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center w-full px-6 py-2 outline-none focus:shadow-outline" id="my-accountDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
<?= icon('user', 'text-gray-500 mr-2') ?>
|
||||
<?= user()->username ?>
|
||||
<?= icon('caret-right', 'ml-auto') ?>
|
||||
</button>
|
||||
<nav class="absolute z-50 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="my-accountDropdown" data-popper="menu" data-popper-placement="right-end">
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'my-account'
|
||||
) ?>">My Account</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'change-password'
|
||||
) ?>">Change password</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'logout'
|
||||
) ?>">Logout</a>
|
||||
</nav>
|
||||
</div>
|
|
@ -4,20 +4,30 @@
|
|||
<?= lang('Podcast.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Podcast.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open_multipart(route_to('podcast-create'), [
|
||||
'method' => 'post',
|
||||
'class' => 'flex flex-col max-w-md',
|
||||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.identity_section_title'),
|
||||
lang('Podcast.form.identity_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.image'), 'image') ?>
|
||||
<?= form_input([
|
||||
'id' => 'image',
|
||||
'name' => 'image',
|
||||
'class' => 'form-input',
|
||||
|
||||
'required' => 'required',
|
||||
'type' => 'file',
|
||||
'accept' => '.jpg,.jpeg,.png',
|
||||
|
@ -35,7 +45,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.name'), 'name') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.name'),
|
||||
'name',
|
||||
[],
|
||||
lang('Podcast.form.name_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'name',
|
||||
'name' => 'name',
|
||||
|
@ -44,6 +59,33 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Podcast.form.type.label') .
|
||||
hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'episodic',
|
||||
'name' => 'type',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'episodic',
|
||||
old('type') ? old('type') == 'episodic' : true
|
||||
) ?>
|
||||
<label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'serial',
|
||||
'name' => 'type',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'serial',
|
||||
old('type') ? old('type') == 'serial' : false
|
||||
) ?>
|
||||
<label for="serial"><?= lang('Podcast.form.type.serial') ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
|
@ -58,21 +100,13 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(
|
||||
lang('Podcast.form.episode_description_footer'),
|
||||
'episode_description_footer'
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'episode_description_footer',
|
||||
'name' => 'episode_description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old('episode_description_footer', '', false),
|
||||
'data-editor="markdown"'
|
||||
) ?>
|
||||
</div>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.classification_section_title'),
|
||||
lang('Podcast.form.classification_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.language'), 'language') ?>
|
||||
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
|
||||
|
@ -88,16 +122,87 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('explicit', false)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.explicit') ?></span>
|
||||
</label>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.other_categories'),
|
||||
|
||||
<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?>
|
||||
'other_categories',
|
||||
[],
|
||||
'',
|
||||
true
|
||||
) ?>
|
||||
<?= form_multiselect(
|
||||
'other_categories[]',
|
||||
$categoryOptions,
|
||||
old('other_categories', []),
|
||||
[
|
||||
'id' => 'other_categories',
|
||||
'class' => 'mb-4',
|
||||
'required' => 'required',
|
||||
'data-max-item-count' => '2',
|
||||
]
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend>
|
||||
<?= lang('Podcast.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'undefined',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'undefined',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'undefined'
|
||||
: true
|
||||
) ?>
|
||||
<label for="undefined"><?= lang(
|
||||
'Podcast.form.parental_advisory.undefined'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'clean',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'clean',
|
||||
old('parental_advisory') ? old('parental_advisory') === 'clean' : false
|
||||
) ?>
|
||||
<label for="clean"><?= lang(
|
||||
'Podcast.form.parental_advisory.clean'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'explicit',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'explicit',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'explicit'
|
||||
: false
|
||||
) ?>
|
||||
<label for="explicit"><?= lang(
|
||||
'Podcast.form.parental_advisory.explicit'
|
||||
) ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.author_section_title'),
|
||||
lang('Podcast.form.author_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Podcast.form.owner_name'),
|
||||
'owner_name',
|
||||
[],
|
||||
lang('Podcast.form.owner_name_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'owner_name',
|
||||
'name' => 'owner_name',
|
||||
|
@ -106,7 +211,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.owner_email'),
|
||||
'owner_email',
|
||||
[],
|
||||
lang('Podcast.form.owner_email_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'owner_email',
|
||||
'name' => 'owner_email',
|
||||
|
@ -116,37 +226,21 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.author'), 'author') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.publisher'),
|
||||
'publisher',
|
||||
[],
|
||||
lang('Podcast.form.publisher_hint'),
|
||||
true
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'author',
|
||||
'name' => 'author',
|
||||
'id' => 'publisher',
|
||||
'name' => 'publisher',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('author'),
|
||||
'value' => old('publisher'),
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', [
|
||||
'class' => 'flex flex-col mb-4',
|
||||
]) ?>
|
||||
<legend><?= lang('Podcast.form.type.label') ?></legend>
|
||||
<label for="episodic" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'episodic',
|
||||
old('type') ? old('type') == 'episodic' : true
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
|
||||
</label>
|
||||
<label for="serial" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'serial', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'serial',
|
||||
old('type') ? old('type') == 'serial' : false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.copyright'), 'copyright') ?>
|
||||
<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?>
|
||||
<?= form_input([
|
||||
'id' => 'copyright',
|
||||
'name' => 'copyright',
|
||||
|
@ -154,42 +248,39 @@
|
|||
'value' => old('copyright'),
|
||||
]) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('block', false)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.block') ?></span>
|
||||
</label>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'complete', 'name' => 'complete', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('complete', false)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.complete') ?></span>
|
||||
</label>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.custom_html_head'), 'custom_html_head') ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'custom_html_head',
|
||||
'name' => 'custom_html_head',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old('custom_html_head', '', false),
|
||||
'data-editor="html"'
|
||||
) ?>
|
||||
</div>
|
||||
<?= form_section(
|
||||
lang('Podcast.form.status_section_title'),
|
||||
lang('Podcast.form.status_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_switch(
|
||||
lang('Podcast.form.block'),
|
||||
['id' => 'block', 'name' => 'block'],
|
||||
'yes',
|
||||
old('block', false),
|
||||
'mb-2'
|
||||
) ?>
|
||||
|
||||
<?= form_switch(
|
||||
lang('Podcast.form.complete'),
|
||||
['id' => 'complete', 'name' => 'complete'],
|
||||
'yes',
|
||||
old('complete', false)
|
||||
) ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= button(
|
||||
lang('Podcast.form.submit_create'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Podcast.form.submit_create'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,15 +4,23 @@
|
|||
<?= lang('Podcast.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Podcast.edit') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open_multipart(route_to('podcast-edit', $podcast->id), [
|
||||
'method' => 'post',
|
||||
'class' => 'flex flex-col max-w-md',
|
||||
'class' => 'flex flex-col',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.identity_section_title'),
|
||||
lang('Podcast.form.identity_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.image'), 'image') ?>
|
||||
<img
|
||||
src="<?= $podcast->image->thumbnail_url ?>"
|
||||
|
@ -39,7 +47,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.name'), 'name') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.name'),
|
||||
'name',
|
||||
[],
|
||||
lang('Podcast.form.name_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'name',
|
||||
'name' => 'name',
|
||||
|
@ -48,6 +61,24 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend><?= lang('Podcast.form.type.label') .
|
||||
hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'episodic',
|
||||
old('type') ? old('type') == 'episodic' : $podcast->type == 'episodic'
|
||||
) ?>
|
||||
<label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label>
|
||||
<?= form_radio(
|
||||
['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'],
|
||||
'serial',
|
||||
old('type') ? old('type') == 'serial' : $podcast->type == 'serial'
|
||||
) ?>
|
||||
<label for="serial"><?= lang('Podcast.form.type.serial') ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.description'), 'description') ?>
|
||||
<?= form_textarea(
|
||||
|
@ -62,25 +93,13 @@
|
|||
) ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(
|
||||
lang('Podcast.form.episode_description_footer'),
|
||||
'episode_description_footer'
|
||||
) ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'episode_description_footer',
|
||||
'name' => 'episode_description_footer',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old(
|
||||
'episode_description_footer',
|
||||
$podcast->episode_description_footer,
|
||||
false
|
||||
),
|
||||
'data-editor="markdown"'
|
||||
) ?>
|
||||
</div>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.classification_section_title'),
|
||||
lang('Podcast.form.classification_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.language'), 'language') ?>
|
||||
<?= form_dropdown(
|
||||
|
@ -98,7 +117,7 @@
|
|||
<?= form_dropdown(
|
||||
'category',
|
||||
$categoryOptions,
|
||||
old('category', $podcast->category_id),
|
||||
old('category', (string) $podcast->category_id),
|
||||
[
|
||||
'id' => 'category',
|
||||
'class' => 'form-select mb-4',
|
||||
|
@ -106,16 +125,85 @@
|
|||
]
|
||||
) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'explicit', 'name' => 'explicit', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('explicit', $podcast->explicit)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.explicit') ?></span>
|
||||
</label>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.other_categories'),
|
||||
'other_categories',
|
||||
[],
|
||||
'',
|
||||
true
|
||||
) ?>
|
||||
<?= form_multiselect(
|
||||
'other_categories[]',
|
||||
$categoryOptions,
|
||||
old('other_categories', $podcast->other_categories_ids),
|
||||
[
|
||||
'id' => 'other_categories',
|
||||
'class' => 'mb-4',
|
||||
'data-max-item-count' => '2',
|
||||
]
|
||||
) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?>
|
||||
<?= form_fieldset('', ['class' => 'flex mb-4 gap-1']) ?>
|
||||
<legend><?= lang('Podcast.form.parental_advisory.label') .
|
||||
hint_tooltip(lang('Podcast.form.parental_advisory.hint'), 'ml-1') ?>
|
||||
</legend>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'undefined',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'undefined',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'undefined'
|
||||
: $podcast->parental_advisory === null
|
||||
) ?>
|
||||
<label for="undefined"><?= lang(
|
||||
'Podcast.form.parental_advisory.undefined'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'clean',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'clean',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'clean'
|
||||
: $podcast->parental_advisory === 'clean'
|
||||
) ?>
|
||||
<label for="clean"><?= lang(
|
||||
'Podcast.form.parental_advisory.clean'
|
||||
) ?></label>
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'explicit',
|
||||
'name' => 'parental_advisory',
|
||||
'class' => 'form-radio-btn',
|
||||
],
|
||||
'explicit',
|
||||
old('parental_advisory')
|
||||
? old('parental_advisory') === 'explicit'
|
||||
: $podcast->parental_advisory === 'explicit'
|
||||
) ?>
|
||||
<label for="explicit"><?= lang(
|
||||
'Podcast.form.parental_advisory.explicit'
|
||||
) ?></label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<?= form_section(
|
||||
lang('Podcast.form.author_section_title'),
|
||||
lang('Podcast.form.author_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Podcast.form.owner_name'),
|
||||
'owner_name',
|
||||
[],
|
||||
lang('Podcast.form.owner_name_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'owner_name',
|
||||
'name' => 'owner_name',
|
||||
|
@ -124,7 +212,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.owner_email'),
|
||||
'owner_email',
|
||||
[],
|
||||
lang('Podcast.form.owner_email_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'owner_email',
|
||||
'name' => 'owner_email',
|
||||
|
@ -134,37 +227,21 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.author'), 'author') ?>
|
||||
<?= form_label(
|
||||
lang('Podcast.form.publisher'),
|
||||
'publisher',
|
||||
[],
|
||||
lang('Podcast.form.publisher_hint'),
|
||||
true
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'author',
|
||||
'name' => 'author',
|
||||
'id' => 'publisher',
|
||||
'name' => 'publisher',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('author', $podcast->author),
|
||||
'value' => old('publisher', $podcast->publisher),
|
||||
]) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
|
||||
<legend><?= lang('Podcast.form.type.label') ?></legend>
|
||||
<label for="episodic" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'episodic',
|
||||
old('type')
|
||||
? old('type') == 'episodic'
|
||||
: $podcast->type == 'episodic'
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
|
||||
</label>
|
||||
<label for="serial" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'serial', 'name' => 'type', 'class' => 'form-radio'],
|
||||
'serial',
|
||||
old('type') ? old('type') == 'serial' : $podcast->type == 'serial'
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.copyright'), 'copyright') ?>
|
||||
<?= form_label(lang('Podcast.form.copyright'), 'copyright', [], '', true) ?>
|
||||
<?= form_input([
|
||||
'id' => 'copyright',
|
||||
'name' => 'copyright',
|
||||
|
@ -172,42 +249,37 @@
|
|||
'value' => old('copyright', $podcast->copyright),
|
||||
]) ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'block', 'name' => 'block', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('block', $podcast->block)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.block') ?></span>
|
||||
</label>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<label class="inline-flex items-center mb-4">
|
||||
<?= form_checkbox(
|
||||
['id' => 'complete', 'name' => 'complete', 'class' => 'form-checkbox'],
|
||||
'yes',
|
||||
old('complete', $podcast->complete)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form.complete') ?></span>
|
||||
</label>
|
||||
|
||||
<div class="mb-4">
|
||||
<?= form_label(lang('Podcast.form.custom_html_head'), 'custom_html_head') ?>
|
||||
<?= form_textarea(
|
||||
[
|
||||
'id' => 'custom_html_head',
|
||||
'name' => 'custom_html_head',
|
||||
'class' => 'form-textarea',
|
||||
],
|
||||
old('custom_html_head', $podcast->custom_html_head, false),
|
||||
'data-editor="html"'
|
||||
) ?>
|
||||
</div>
|
||||
<?= form_section(
|
||||
lang('Podcast.form.status_section_title'),
|
||||
lang('Podcast.form.status_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Podcast.form.submit_edit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= form_switch(
|
||||
lang('Podcast.form.block'),
|
||||
['id' => 'block', 'name' => 'block'],
|
||||
'yes',
|
||||
old('block', $podcast->block),
|
||||
'mb-2'
|
||||
) ?>
|
||||
|
||||
<?= form_switch(
|
||||
lang('Podcast.form.complete'),
|
||||
['id' => 'complete', 'name' => 'complete'],
|
||||
'yes',
|
||||
old('complete', $podcast->complete)
|
||||
) ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<?= button(
|
||||
lang('Podcast.form.submit_edit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,16 +4,53 @@
|
|||
<?= lang('Podcast.import') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Podcast.import') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open_multipart(route_to('rzqr'), [
|
||||
<?= form_open_multipart(route_to('podcast-import'), [
|
||||
'method' => 'post',
|
||||
'class' => 'flex flex-col max-w-md',
|
||||
'class' => 'flex flex-col items-start',
|
||||
]) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form_import.name'), 'name') ?>
|
||||
<?= form_section(
|
||||
lang('PodcastImport.old_podcast_section_title'),
|
||||
lang('PodcastImport.old_podcast_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('PodcastImport.imported_feed_url'),
|
||||
'imported_feed_url',
|
||||
[],
|
||||
lang('PodcastImport.imported_feed_url_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'imported_feed_url',
|
||||
'name' => 'imported_feed_url',
|
||||
'class' => 'form-input',
|
||||
'value' => old('imported_feed_url'),
|
||||
'placeholder' => 'https://...',
|
||||
'type' => 'url',
|
||||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('PodcastImport.new_podcast_section_title'),
|
||||
lang('PodcastImport.new_podcast_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('PodcastImport.name'),
|
||||
'name',
|
||||
[],
|
||||
lang('PodcastImport.name_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'name',
|
||||
'name' => 'name',
|
||||
|
@ -22,19 +59,6 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(
|
||||
lang('Podcast.form_import.imported_feed_url'),
|
||||
'imported_feed_url'
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'imported_feed_url',
|
||||
'name' => 'imported_feed_url',
|
||||
'class' => 'form-input mb-4',
|
||||
'value' => old('imported_feed_url'),
|
||||
'type' => 'url',
|
||||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form.language'), 'language') ?>
|
||||
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
|
||||
'id' => 'language',
|
||||
|
@ -49,38 +73,50 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_section_close() ?>
|
||||
|
||||
|
||||
<?= form_section(
|
||||
lang('PodcastImport.advanced_params_section_title'),
|
||||
lang('PodcastImport.advanced_params_section_subtitle')
|
||||
) ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
|
||||
<legend><?= lang('Podcast.form_import.slug_field.label') ?></legend>
|
||||
<legend><?= lang('PodcastImport.slug_field.label') ?></legend>
|
||||
<label for="link" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'link', 'name' => 'slug_field', 'class' => 'form-radio'],
|
||||
[
|
||||
'id' => 'link',
|
||||
'name' => 'slug_field',
|
||||
'class' => 'form-radio text-green-500',
|
||||
],
|
||||
'link',
|
||||
old('slug_field') ? old('slug_field') == 'link' : true
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'Podcast.form_import.slug_field.link'
|
||||
) ?></span>
|
||||
<span class="ml-2"><?= lang('PodcastImport.slug_field.link') ?></span>
|
||||
</label>
|
||||
<label for="title" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
['id' => 'title', 'name' => 'slug_field', 'class' => 'form-radio'],
|
||||
[
|
||||
'id' => 'title',
|
||||
'name' => 'slug_field',
|
||||
'class' => 'form-radio text-green-500',
|
||||
],
|
||||
'title',
|
||||
old('slug_field') ? old('slug_field') == 'title' : false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'Podcast.form_import.slug_field.title'
|
||||
) ?></span>
|
||||
<span class="ml-2"><?= lang('PodcastImport.slug_field.title') ?></span>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
||||
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
|
||||
<legend><?= lang('Podcast.form_import.description_field.label') ?></legend>
|
||||
<legend><?= lang('PodcastImport.description_field.label') ?></legend>
|
||||
<label for="description" class="inline-flex items-center">
|
||||
<?= form_radio(
|
||||
[
|
||||
'id' => 'description',
|
||||
'name' => 'description_field',
|
||||
'class' => 'form-radio',
|
||||
'class' => 'form-radio text-green-500',
|
||||
],
|
||||
'description',
|
||||
old('description_field')
|
||||
|
@ -88,7 +124,7 @@
|
|||
: true
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'Podcast.form_import.description_field.description'
|
||||
'PodcastImport.description_field.description'
|
||||
) ?></span>
|
||||
</label>
|
||||
<label for="summary" class="inline-flex items-center">
|
||||
|
@ -96,7 +132,7 @@
|
|||
[
|
||||
'id' => 'summary',
|
||||
'name' => 'description_field',
|
||||
'class' => 'form-radio',
|
||||
'class' => 'form-radio text-green-500',
|
||||
],
|
||||
'summary',
|
||||
old('description_field')
|
||||
|
@ -104,7 +140,7 @@
|
|||
: false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'Podcast.form_import.description_field.summary'
|
||||
'PodcastImport.description_field.summary'
|
||||
) ?></span>
|
||||
</label>
|
||||
<label for="subtitle_summary" class="inline-flex items-center">
|
||||
|
@ -112,7 +148,7 @@
|
|||
[
|
||||
'id' => 'subtitle_summary',
|
||||
'name' => 'description_field',
|
||||
'class' => 'form-radio',
|
||||
'class' => 'form-radio text-green-500',
|
||||
],
|
||||
'subtitle_summary',
|
||||
old('description_field')
|
||||
|
@ -120,7 +156,7 @@
|
|||
: false
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang(
|
||||
'Podcast.form_import.description_field.subtitle_summary'
|
||||
'PodcastImport.description_field.subtitle_summary'
|
||||
) ?></span>
|
||||
</label>
|
||||
<?= form_fieldset_close() ?>
|
||||
|
@ -131,15 +167,21 @@
|
|||
[
|
||||
'id' => 'force_renumber',
|
||||
'name' => 'force_renumber',
|
||||
'class' => 'form-checkbox',
|
||||
'class' => 'form-checkbox text-green-500',
|
||||
],
|
||||
'yes',
|
||||
old('force_renumber', false)
|
||||
) ?>
|
||||
<span class="ml-2"><?= lang('Podcast.form_import.force_renumber') ?></span>
|
||||
<span class="ml-2"><?= lang('PodcastImport.force_renumber') ?></span>
|
||||
<?= hint_tooltip(lang('PodcastImport.force_renumber_hint'), 'ml-1') ?>
|
||||
</label>
|
||||
|
||||
<?= form_label(lang('Podcast.form_import.season_number'), 'season_number') ?>
|
||||
<?= form_label(
|
||||
lang('PodcastImport.season_number'),
|
||||
'season_number',
|
||||
[],
|
||||
lang('PodcastImport.season_number_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'season_number',
|
||||
'name' => 'season_number',
|
||||
|
@ -148,7 +190,12 @@
|
|||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<?= form_label(lang('Podcast.form_import.max_episodes'), 'max_episodes') ?>
|
||||
<?= form_label(
|
||||
lang('PodcastImport.max_episodes'),
|
||||
'max_episodes',
|
||||
[],
|
||||
lang('PodcastImport.max_episodes_hint')
|
||||
) ?>
|
||||
<?= form_input([
|
||||
'id' => 'max_episodes',
|
||||
'name' => 'max_episodes',
|
||||
|
@ -157,11 +204,14 @@
|
|||
'type' => 'number',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Podcast.form_import.submit_import'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= form_section_close() ?>
|
||||
|
||||
<?= button(
|
||||
lang('PodcastImport.submit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<section class="flex flex-col">
|
||||
<header class="flex justify-between py-2">
|
||||
<h1 class="text-xl"><?= lang('Podcast.latest_episodes') ?></h1>
|
||||
<a href="<?= route_to(
|
||||
'episode-list',
|
||||
$podcast->id
|
||||
) ?>" class="inline-flex items-center text-sm underline hover:no-underline">
|
||||
<?= lang('Podcast.see_all_episodes') ?>
|
||||
<?= icon('chevron-right', 'ml-2') ?>
|
||||
</a>
|
||||
</header>
|
||||
<?php if ($episodes): ?>
|
||||
<div class="flex justify-between gap-4 overflow-x-auto">
|
||||
<?php foreach ($episodes as $episode): ?>
|
||||
<article class="flex flex-col w-56 mb-4 bg-white border rounded shadow" style="min-width: 12rem;">
|
||||
<img
|
||||
src="<?= $episode->image->thumbnail_url ?>"
|
||||
alt="<?= $episode->title ?>" class="object-cover" />
|
||||
<div class="flex justify-between p-2">
|
||||
<div class="flex flex-col">
|
||||
<a href="<?= route_to(
|
||||
'episode-view',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"
|
||||
class="text-sm font-semibold hover:underline"
|
||||
><?= $episode->title ?>
|
||||
</a>
|
||||
<div class="text-xs">
|
||||
<?php if (
|
||||
$episode->season_number &&
|
||||
$episode->number
|
||||
): ?>
|
||||
<abbr class="font-bold text-gray-600" title="<?= lang(
|
||||
'Episode.season_episode',
|
||||
[
|
||||
'seasonNumber' =>
|
||||
$episode->season_number,
|
||||
'episodeNumber' => $episode->number,
|
||||
]
|
||||
) ?>"><?= lang(
|
||||
'Episode.season_episode_abbr',
|
||||
[
|
||||
'seasonNumber' => $episode->season_number,
|
||||
'episodeNumber' => $episode->number,
|
||||
]
|
||||
) ?></abbr>
|
||||
<?php elseif (
|
||||
!$episode->season_number &&
|
||||
$episode->number
|
||||
): ?>
|
||||
<abbr class="font-bold text-gray-600" title="<?= lang(
|
||||
'Episode.number',
|
||||
[
|
||||
'episodeNumber' => $episode->number,
|
||||
]
|
||||
) ?>"><?= lang('Episode.number_abbr', [
|
||||
'episodeNumber' => $episode->number,
|
||||
]) ?></abbr>
|
||||
<?php endif; ?>
|
||||
<span class="mx-1">•</span>
|
||||
<time
|
||||
pubdate
|
||||
datetime="<?= $episode->published_at->toDateTimeString() ?>"
|
||||
title="<?= $episode->published_at ?>">
|
||||
<?= lang('Common.mediumDate', [
|
||||
$episode->published_at,
|
||||
]) ?>
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" data-toggle="dropdown">
|
||||
<button type="button" class="inline-flex items-center p-1 outline-none focus:shadow-outline" id="moreDropdown" data-popper="button" aria-haspopup="true" aria-expanded="false">
|
||||
<?= icon('more') ?>
|
||||
</button>
|
||||
<nav class="absolute z-10 flex-col hidden py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="moreDropdown" data-popper="menu" data-popper-placement="top-end" data-popper-offset-x="0" data-popper-offset-y="-24" >
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-edit',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>"><?= lang('Episode.go_to_page') ?></a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'episode-delete',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.delete') ?></a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
|
@ -1,17 +1,24 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Podcast.all_podcasts') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Podcast.all_podcasts') ?> (<?= count($podcasts) ?>)
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'podcast-create'
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Podcast.create') ?></a>
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'podcast-import'
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Podcast.import') ?></a>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(
|
||||
lang('Podcast.create'),
|
||||
route_to('podcast-create'),
|
||||
['variant' => 'primary', 'iconLeft' => 'add'],
|
||||
['class' => 'mr-2']
|
||||
) ?>
|
||||
<?= button(lang('Podcast.import'), route_to('podcast-import'), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'download',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
|
@ -20,9 +27,35 @@
|
|||
<div class="flex flex-wrap">
|
||||
<?php if (!empty($podcasts)): ?>
|
||||
<?php foreach ($podcasts as $podcast): ?>
|
||||
<?= view('admin/_partials/_podcast-card', [
|
||||
'podcast' => $podcast,
|
||||
]) ?>
|
||||
<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
|
||||
<img
|
||||
alt="<?= $podcast->title ?>"
|
||||
src="<?= $podcast->image
|
||||
->thumbnail_url ?>" class="object-cover w-full h-40" />
|
||||
<div class="p-2">
|
||||
<a href="<?= route_to(
|
||||
'podcast-view',
|
||||
$podcast->id
|
||||
) ?>" class="hover:underline">
|
||||
<h2 class="font-semibold"><?= $podcast->title ?></h2>
|
||||
</a>
|
||||
<p class="text-gray-600">@<?= $podcast->name ?></p>
|
||||
</div>
|
||||
<footer class="flex items-center justify-end p-2">
|
||||
<a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
|
||||
'podcast-edit',
|
||||
$podcast->id
|
||||
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
|
||||
'Podcast.edit'
|
||||
) ?>"><?= icon('edit') ?></a>
|
||||
<a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
|
||||
'podcast-view',
|
||||
$podcast->id
|
||||
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
|
||||
'Podcast.view'
|
||||
) ?>"><?= icon('eye') ?></a>
|
||||
</footer>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_podcast') ?></p>
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Podcast.platforms.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Podcast.platforms.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
Podcast settings...
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('Platforms.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Platforms.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open(route_to('platforms', $podcast->id), [
|
||||
|
@ -88,11 +92,12 @@
|
|||
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Platforms.submit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Platforms.submit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -2,45 +2,29 @@
|
|||
|
||||
<?= $this->section('title') ?>
|
||||
<?= $podcast->title ?>
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-4 text-sm text-white bg-teal-500 rounded shadow-xs outline-none hover:bg-teal-600 focus:shadow-outline" href="<?= route_to(
|
||||
'podcast-edit',
|
||||
$podcast->id
|
||||
) ?>">
|
||||
<?= icon('edit', 'mr-2') ?>
|
||||
<?= lang('Podcast.edit') ?>
|
||||
</a>
|
||||
<a class="inline-flex items-center px-2 py-1 mb-2 ml-2 text-sm text-white bg-green-500 rounded shadow-xs outline-none hover:bg-green-600 focus:shadow-outline" href="<?= route_to(
|
||||
'episode-create',
|
||||
$podcast->id
|
||||
) ?>">
|
||||
<?= icon('add', 'mr-2') ?>
|
||||
<?= lang('Episode.create') ?></a>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= $podcast->title ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(
|
||||
lang('Podcast.edit'),
|
||||
route_to('podcast-edit', $podcast->id),
|
||||
['variant' => 'secondary', 'iconLeft' => 'edit'],
|
||||
['class' => 'mr-2']
|
||||
) ?>
|
||||
<?= button(lang('Episode.create'), route_to('episode-create', $podcast->id), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'add',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<img
|
||||
class="w-64 mb-4"
|
||||
src="<?= $podcast->image->medium_url ?>"
|
||||
alt="<?= $podcast->title ?>"
|
||||
/>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to(
|
||||
'contributor-list',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Podcast.see_contributors') ?></a>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to(
|
||||
'platforms',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Platforms.title') ?></a>
|
||||
<a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
|
||||
'podcast',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.go_to_page') ?></a>
|
||||
<a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
|
||||
'podcast-delete',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Podcast.delete') ?></a>
|
||||
|
||||
<?= view('admin/_partials/_episode-list.php', [
|
||||
'episodes' => $podcast->episodes,
|
||||
]) ?>
|
||||
<?= view_cell('\App\Controllers\Admin\Podcast::latestEpisodes', [
|
||||
'limit' => 5,
|
||||
]) ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
<?= lang('User.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('User.create') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= form_open(route_to('user-create'), [
|
||||
'class' => 'flex flex-col max-w-sm',
|
||||
]) ?>
|
||||
<?= form_open(route_to('user-create'), ['class' => 'flex flex-col max-w-sm']) ?>
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<?= form_label(lang('User.form.email'), 'email') ?>
|
||||
|
@ -33,16 +35,18 @@
|
|||
<?= form_input([
|
||||
'id' => 'password',
|
||||
'name' => 'password',
|
||||
|
||||
'class' => 'form-input mb-4',
|
||||
'type' => 'password',
|
||||
'autocomplete' => 'new-password',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('User.form.submit_create'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('User.form.submit_create'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<?= lang('User.edit_roles', ['username' => $user->username]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('User.edit_roles', ['username' => $user->username]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -15,14 +19,15 @@
|
|||
<?= form_label(lang('User.form.roles'), 'roles') ?>
|
||||
<?= form_multiselect('roles[]', $roleOptions, $user->roles, [
|
||||
'id' => 'roles',
|
||||
'class' => 'form-multiselect mb-4',
|
||||
'class' => 'mb-4',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('User.form.submit_edit'),
|
||||
'type' => 'submit',
|
||||
'class' => 'self-end px-4 py-2 bg-gray-200',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('User.form.submit_edit'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -1,63 +1,89 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('User.all_users') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('User.all_users') ?> (<?= count($users) ?>)
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<?= button(lang('User.create'), route_to('user-create'), [
|
||||
'variant' => 'primary',
|
||||
'iconLeft' => 'user-add',
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<table class="table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Username</th>
|
||||
<th class="px-4 py-2">Email</th>
|
||||
<th class="px-4 py-2">Roles</th>
|
||||
<th class="px-4 py-2">Banned?</th>
|
||||
<th class="px-4 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td class="px-4 py-2 border"><?= $user->username ?></td>
|
||||
<td class="px-4 py-2 border"><?= $user->email ?></td>
|
||||
<td class="px-4 py-2 border">
|
||||
[<?= implode(', ', $user->roles) ?>]
|
||||
<a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
|
||||
'user-edit',
|
||||
$user->id
|
||||
) ?>" data-toggle="tooltip" data-placement="bottom"
|
||||
title="<?= lang('User.edit_roles', [
|
||||
'username' => $user->username,
|
||||
]) ?>">
|
||||
<?= icon('edit') ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-2 border"><?= $user->isBanned()
|
||||
? 'Yes'
|
||||
: 'No' ?></td>
|
||||
<td class="px-4 py-2 border">
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
|
||||
'user-force_pass_reset',
|
||||
$user->id
|
||||
) ?>"><?= lang('User.forcePassReset') ?></a>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
|
||||
$user->isBanned() ? 'user-unban' : 'user-ban',
|
||||
$user->id
|
||||
) ?>">
|
||||
<?= $user->isBanned()
|
||||
? lang('User.unban')
|
||||
: lang('User.ban') ?></a>
|
||||
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
|
||||
'user-delete',
|
||||
$user->id
|
||||
) ?>"><?= lang('User.delete') ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?= data_table(
|
||||
[
|
||||
[
|
||||
'header' => lang('User.list.user'),
|
||||
'cell' => function ($user) {
|
||||
return '<div class="flex flex-col">' .
|
||||
$user->username .
|
||||
'<span class="text-sm text-gray-600">' .
|
||||
$user->email .
|
||||
'</span></div>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('User.list.roles'),
|
||||
'cell' => function ($user) {
|
||||
return implode(',', $user->roles) .
|
||||
icon_button(
|
||||
'edit',
|
||||
lang('User.edit_roles', [
|
||||
'username' => $user->username,
|
||||
]),
|
||||
route_to('user-edit', $user->id),
|
||||
['variant' => 'info'],
|
||||
['class' => 'ml-2']
|
||||
);
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('User.list.banned'),
|
||||
'cell' => function ($user) {
|
||||
return $user->isBanned()
|
||||
? lang('Common.yes')
|
||||
: lang('Common.no');
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($user) {
|
||||
return button(
|
||||
lang('User.forcePassReset'),
|
||||
route_to('user-force_pass_reset', $user->id),
|
||||
[
|
||||
'variant' => 'secondary',
|
||||
'size' => 'small',
|
||||
],
|
||||
['class' => 'mr-2']
|
||||
) .
|
||||
button(
|
||||
lang('User.' . ($user->isBanned() ? 'unban' : 'ban')),
|
||||
route_to(
|
||||
$user->isBanned() ? 'user-unban' : 'user-ban',
|
||||
$user->id
|
||||
),
|
||||
['variant' => 'warning', 'size' => 'small'],
|
||||
['class' => 'mr-2']
|
||||
) .
|
||||
button(
|
||||
lang('User.delete'),
|
||||
route_to('user-delete', $user->id),
|
||||
['variant' => 'danger', 'size' => 'small']
|
||||
);
|
||||
},
|
||||
],
|
||||
],
|
||||
$users
|
||||
) ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
|
|
|
@ -24,8 +24,9 @@
|
|||
</main>
|
||||
<footer class="flex flex-col text-sm">
|
||||
<?= $this->renderSection('footer') ?>
|
||||
<p class="py-4 border-t">
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.
|
||||
</p>
|
||||
<small class="py-4 text-center border-t"><?= lang('Common.powered_by', [
|
||||
'castopod' =>
|
||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||
]) ?></small>
|
||||
</footer>
|
||||
</body>
|
||||
|
|
|
@ -22,11 +22,12 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Auth.sendInstructions'),
|
||||
'type' => 'submit',
|
||||
'class' => 'px-4 py-2 ml-auto border',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Auth.sendInstructions'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -28,11 +28,13 @@
|
|||
'required' => 'required',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Auth.loginAction'),
|
||||
'class' => 'px-4 py-2 ml-auto border',
|
||||
'type' => 'submit',
|
||||
]) ?>
|
||||
|
||||
<?= button(
|
||||
lang('Auth.loginAction'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -44,11 +44,12 @@
|
|||
'autocomplete' => 'new-password',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Auth.register'),
|
||||
'class' => 'px-4 py-2 ml-auto border',
|
||||
'type' => 'submit',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Auth.register'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|
|
@ -42,11 +42,12 @@
|
|||
'autocomplete' => 'new-password',
|
||||
]) ?>
|
||||
|
||||
<?= form_button([
|
||||
'content' => lang('Auth.resetPassword'),
|
||||
'class' => 'px-4 py-2 ml-auto border',
|
||||
'type' => 'submit',
|
||||
]) ?>
|
||||
<?= button(
|
||||
lang('Auth.resetPassword'),
|
||||
null,
|
||||
['variant' => 'primary'],
|
||||
['type' => 'submit', 'class' => 'self-end']
|
||||
) ?>
|
||||
|
||||
<?= form_close() ?>
|
||||
|
||||
|
|