feat: add pages table to store custom instance pages (eg. legal-notice, cookie policy, etc.)

- add pages  migration, model and entity
- add page controllers
- update routes config to input page forms and page view in public
- fix markdow editor focus area
- show pages links in public side footer

closes #24
This commit is contained in:
Yassine Doghri 2020-08-18 16:31:28 +00:00
parent a1a28de702
commit 9c224a8ac6
34 changed files with 704 additions and 80 deletions

View File

@ -30,7 +30,7 @@ $routes->setAutoRoute(false);
*/
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}');
$routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
/**
* --------------------------------------------------------------------
@ -53,15 +53,6 @@ $routes->group(config('App')->installGateway, function ($routes) {
]);
});
// Public routes
$routes->group('@(:podcastName)', function ($routes) {
$routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
$routes->get('(:episodeSlug)', 'Episode/$1/$2', [
'as' => 'episode',
]);
$routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
});
// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
'as' => 'analytics_hit',
@ -80,10 +71,6 @@ $routes->group(
'as' => 'admin',
]);
$routes->get('my-podcasts', 'Podcast::myPodcasts', [
'as' => 'my-podcasts',
]);
// Podcasts
$routes->group('podcasts', function ($routes) {
$routes->get('/', 'Podcast::list', [
@ -201,6 +188,27 @@ $routes->group(
});
});
// Pages
$routes->group('pages', function ($routes) {
$routes->get('/', 'Page::list', ['as' => 'page-list']);
$routes->get('new', 'Page::create', [
'as' => 'page-create',
]);
$routes->post('new', 'Page::attemptCreate');
$routes->group('(:num)', function ($routes) {
$routes->get('/', 'Page::view/$1', ['as' => 'page-view']);
$routes->get('edit', 'Page::edit/$1', [
'as' => 'page-edit',
]);
$routes->post('edit', 'Page::attemptEdit/$1');
$routes->add('delete', 'Page::delete/$1', [
'as' => 'page-delete',
]);
});
});
// Users
$routes->group('users', function ($routes) {
$routes->get('/', 'User::list', [
@ -294,6 +302,16 @@ $routes->group(config('App')->authGateway, function ($routes) {
$routes->post('reset-password', 'Auth::attemptReset');
});
// Public routes
$routes->group('@(:podcastName)', function ($routes) {
$routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
$routes->get('(:slug)', 'Episode/$1/$2', [
'as' => 'episode',
]);
$routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
});
$routes->get('/(:slug)', 'Page/$1', ['as' => 'page']);
/**
* --------------------------------------------------------------------
* Additional Routing

View File

@ -19,6 +19,7 @@ class Validation
\CodeIgniter\Validation\FormatRules::class,
\CodeIgniter\Validation\FileRules::class,
\CodeIgniter\Validation\CreditCardRules::class,
\App\Validation\Rules::class,
\Myth\Auth\Authentication\Passwords\ValidationRules::class,
];

View File

@ -0,0 +1,111 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PageModel;
class Page extends BaseController
{
/**
* @var \App\Entities\Page|null
*/
protected $page;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->page = (new PageModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
function list()
{
$data = [
'pages' => (new PageModel())->findAll(),
];
return view('admin/page/list', $data);
}
function view()
{
return view('admin/page/view', ['page' => $this->page]);
}
function create()
{
helper('form');
return view('admin/page/create');
}
function attemptCreate()
{
$page = new \App\Entities\Page([
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'content' => $this->request->getPost('content'),
]);
$pageModel = new PageModel();
if (!$pageModel->save($page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()
->route('page-list')
->with(
'message',
lang('Page.messages.createSuccess', [
'pageTitle' => $page->title,
])
);
}
function edit()
{
helper('form');
replace_breadcrumb_params([0 => $this->page->title]);
return view('admin/page/edit', ['page' => $this->page]);
}
function attemptEdit()
{
$this->page->title = $this->request->getPost('title');
$this->page->slug = $this->request->getPost('slug');
$this->page->content = $this->request->getPost('content');
$pageModel = new PageModel();
if (!$pageModel->save($this->page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()->route('page-list');
}
public function delete()
{
(new PageModel())->delete($this->page->id);
return redirect()->route('page-list');
}
}

View File

@ -31,23 +31,16 @@ class Podcast extends BaseController
return $this->$method();
}
public function myPodcasts()
{
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
];
return view('admin/podcast/list', $data);
}
public function list()
{
if (!has_permission('podcasts-list')) {
return redirect()->route('my-podcasts');
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
];
} else {
$data = ['podcasts' => (new PodcastModel())->findAll()];
}
$data = ['podcasts' => (new PodcastModel())->findAll()];
return view('admin/podcast/list', $data);
}
@ -155,7 +148,7 @@ class Podcast extends BaseController
$db->transComplete();
return redirect()->route('podcast-list');
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function edit()

45
app/Controllers/Page.php Normal file
View File

@ -0,0 +1,45 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\PageModel;
class Page extends BaseController
{
/**
* @var \App\Entities\Page|null
*/
protected $page;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->page = (new PageModel())
->where('slug', $params[0])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function index()
{
// The page cache is set to a decade so it is deleted manually upon page update
$this->cachePage(DECADE);
$data = [
'page' => $this->page,
];
return view('page', $data);
}
}

View File

@ -54,19 +54,17 @@ class AddPodcasts extends Migration
'constraint' => 1,
'default' => 0,
],
'author' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'null' => true,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'null' => true,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 1024,
],
'author' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'null' => true,
],
'type' => [

View File

@ -41,7 +41,6 @@ class AddEpisodes extends Migration
'type' => 'VARCHAR',
'constraint' => 1024,
],
'description' => [
'type' => 'TEXT',
'null' => true,

View File

@ -0,0 +1,58 @@
<?php
/**
* Class AddLanguages
* Creates languages 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 AddPages extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 1024,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
'unique' => true,
],
'content' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'TIMESTAMP',
],
'updated_at' => [
'type' => 'TIMESTAMP',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('pages');
}
public function down()
{
$this->forge->dropTable('pages');
}
}

47
app/Entities/Page.php Normal file
View File

@ -0,0 +1,47 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
use League\CommonMark\CommonMarkConverter;
class Page extends Entity
{
/**
* @var string
*/
protected $link;
/**
* @var string
*/
protected $content_html;
protected $casts = [
'id' => 'integer',
'title' => 'string',
'slug' => 'string',
'content' => 'string',
];
public function getLink()
{
return base_url($this->attributes['slug']);
}
public function getContentHtml()
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convertToHtml($this->attributes['content']);
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Models\PageModel;
/**
* Returns instance pages as links inside nav tag
*
* @return string html pages navigation
*/
function render_page_links()
{
$pages = (new PageModel())->findAll();
$links = '';
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',
]);
}
return '<nav class="inline-flex">' . $links . '</nav>';
}

View File

@ -95,13 +95,9 @@ function get_rss_feed($podcast)
$channel->addChild('author', $podcast->author, $itunes_namespace);
$channel->addChild('link', $podcast->link);
if ($podcast->owner_name || $podcast->owner_email) {
$owner = $channel->addChild('owner', null, $itunes_namespace);
$podcast->owner_name &&
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
$podcast->owner_email &&
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
}
$owner = $channel->addChild('owner', null, $itunes_namespace);
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
$channel->addChild('type', $podcast->type, $itunes_namespace);
$podcast->copyright && $channel->addChild('copyright', $podcast->copyright);

View File

@ -10,11 +10,13 @@ return [
'dashboard' => 'Dashboard',
'podcasts' => 'Podcasts',
'users' => 'Users',
'pages' => 'Pages',
'admin' => 'Home',
'my-podcasts' => 'My podcasts',
'podcast-list' => 'All podcasts',
'podcast-create' => 'New podcast',
'user-list' => 'All users',
'user-create' => 'New user',
'page-list' => 'All pages',
'page-create' => 'New Page',
'go_to_website' => 'Go to website',
];

View File

@ -9,10 +9,10 @@
return [
'label' => 'breadcrumb',
config('App')->adminGateway => 'Home',
'my-podcasts' => 'my podcasts',
'podcasts' => 'podcasts',
'episodes' => 'episodes',
'contributors' => 'contributors',
'pages' => 'pages',
'add' => 'add',
'new' => 'new',
'edit' => 'edit',

25
app/Language/en/Page.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'all_pages' => 'All pages',
'create' => 'New page',
'go_to_page' => 'Go to page',
'edit' => 'Edit page',
'delete' => 'Delete page',
'form' => [
'title' => 'Title',
'slug' => 'Slug',
'content' => 'Content',
'submit_create' => 'Create page',
'submit_edit' => 'Save',
],
'messages' => [
'createSuccess' => 'The {pageTitle} page was created successfully!',
],
];

View File

@ -42,15 +42,15 @@ return [
'explicit' => 'Explicit',
'explicit_help' =>
'The podcast parental advisory information. Does it contain explicit content?',
'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.',
'owner_name' => 'Owner name',
'owner_name_help' =>
'The podcast owner contact name. 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' =>
'The podcast owner contact e-mail. For administrative use only. It will mostly be used by some platforms to verify this podcast ownerhip. 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.',
'type' => [
'label' => 'Type',
'episodic' => 'Episodic',

View File

@ -0,0 +1,12 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'not_in_protected_slugs' =>
'The {field} field conflicts with one of the gateway routes (admin, auth or install).',
];

View File

@ -53,8 +53,10 @@ class EpisodeModel extends Model
];
protected $validationMessages = [];
protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache'];
protected $afterInsert = ['writeEnclosureMetadata'];
// clear cache beforeUpdate because if slug changes, so will the episode link
protected $beforeUpdate = ['clearCache'];
protected $afterUpdate = ['writeEnclosureMetadata'];
protected $beforeDelete = ['clearCache'];
protected function writeEnclosureMetadata(array $data)

52
app/Models/PageModel.php Normal file
View File

@ -0,0 +1,52 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class PageModel extends Model
{
protected $table = 'pages';
protected $primaryKey = 'id';
protected $allowedFields = ['id', 'title', 'slug', 'content'];
protected $returnType = \App\Entities\Page::class;
protected $useSoftDeletes = true;
protected $useTimestamps = true;
protected $validationRules = [
'title' => 'required',
'slug' =>
'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]|is_unique[pages.slug,id,{id}]|not_in_protected_slugs',
'content' => 'required',
];
protected $validationMessages = [];
// Before update because slug might change
protected $beforeUpdate = ['clearCache'];
protected $beforeDelete = ['clearCache'];
protected function clearCache(array $data)
{
$page = (new PageModel())->find(
is_array($data['id']) ? $data['id'][0] : $data['id']
);
// delete page cache
cache()->delete(md5($page->link));
// Clear the cache of all podcast and episode pages
// TODO: change the logic of page caching to prevent clearing all cache every time
cache()->clean();
return $data;
}
}

View File

@ -25,9 +25,9 @@ class PodcastModel extends Model
'language',
'category',
'explicit',
'author',
'owner_name',
'owner_email',
'author',
'type',
'copyright',
'block',
@ -50,6 +50,7 @@ class PodcastModel extends Model
'image_uri' => 'required',
'language' => 'required',
'category' => 'required',
'owner_name' => 'required',
'owner_email' => 'required|valid_email',
'type' => 'required',
'created_by' => 'required',
@ -57,8 +58,8 @@ class PodcastModel extends Model
];
protected $validationMessages = [];
protected $afterInsert = ['clearCache'];
protected $afterUpdate = ['clearCache'];
// clear cache before update if by any chance, the podcast name changes, and so will the podcast link
protected $beforeUpdate = ['clearCache'];
protected $beforeDelete = ['clearCache'];
/**

30
app/Validation/Rules.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Validation;
class Rules
{
/**
* Value should not be within the array of protected slugs (adminGateway, authGateway or installGateway)
*
* @param string $value
*
* @return boolean
*/
public function not_in_protected_slugs(string $value = null): bool
{
$appConfig = config('App');
$protectedSlugs = [
$appConfig->adminGateway,
$appConfig->authGateway,
$appConfig->installGateway,
];
return !in_array($value, $protectedSlugs, true);
}
}

View File

@ -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 8v12h14V8H5zm0-2h14V4H5v2zm15 16H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1zM7 10h4v4H7v-4zm0 6h10v2H7v-2zm6-5h4v2h-4v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -38,13 +38,7 @@ class ProseMirrorView {
constructor(target: HTMLTextAreaElement, content: string) {
this.editorContainer = document.createElement("div");
this.editorContainer.classList.add(
"bg-white",
"border",
"px-2",
"min-h-full",
"prose-sm"
);
this.editorContainer.classList.add("bg-white", "border");
this.editorContainer.style.minHeight = "200px";
const editor = target.parentNode?.insertBefore(
this.editorContainer,
@ -64,6 +58,10 @@ class ProseMirrorView {
target.innerHTML = this.content;
}
},
attributes: {
class: "prose-sm px-3 py-2 overflow-y-auto",
style: "min-height: 200px; max-height: 500px",
},
});
}

View File

@ -1,9 +1,10 @@
<?= helper('page') ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Castopod</title>
<title><?= $this->renderSection('title') ?></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" />
@ -22,7 +23,8 @@
<main class="container flex-1 px-4 py-10 mx-auto">
<?= $this->renderSection('content') ?>
</main>
<footer class="container px-2 py-4 mx-auto text-sm text-right 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.
<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>
</footer>
</body>

View File

@ -3,9 +3,10 @@ $navigation = [
'dashboard' => ['icon' => 'dashboard', 'items' => ['admin']],
'podcasts' => [
'icon' => 'mic',
'items' => ['my-podcasts', 'podcast-list', 'podcast-create'],
'items' => ['podcast-list', 'podcast-create'],
],
'users' => ['icon' => 'group', 'items' => ['user-list', 'user-create']],
'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']],
]; ?>
<nav class="<?= $class ?>">

View File

@ -0,0 +1,57 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Page.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open(route_to('page-create'), [
'class' => 'flex flex-col max-w-3xl',
]) ?>
<?= csrf_field() ?>
<?= form_label(lang('Page.form.title'), 'title', ['class' => 'max-w-sm']) ?>
<?= form_input([
'id' => 'title',
'name' => 'title',
'class' => 'form-input mb-4 max-w-sm',
'value' => old('title'),
'required' => 'required',
'data-slugify' => 'title',
]) ?>
<?= form_label(lang('Page.form.slug'), 'slug', ['class' => 'max-w-sm']) ?>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4 max-w-sm',
'value' => old('slug'),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
<div class="mb-4">
<?= form_label(lang('Page.form.content'), 'content') ?>
<?= form_textarea(
[
'id' => 'content',
'name' => 'content',
'class' => 'form-textarea',
'required' => 'required',
],
old('content', '', false),
'data-editor="markdown"'
) ?>
</div>
<?= form_button([
'content' => lang('Page.form.submit_create'),
'type' => 'submit',
'class' => 'self-end px-4 py-2 bg-gray-200',
]) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,57 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Page.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= form_open(route_to('page-edit', $page->id), [
'class' => 'flex flex-col max-w-3xl',
]) ?>
<?= csrf_field() ?>
<?= form_label(lang('Page.form.title'), 'title', ['class' => 'max-w-sm']) ?>
<?= form_input([
'id' => 'title',
'name' => 'title',
'class' => 'form-input mb-4 max-w-sm',
'value' => old('title', $page->title),
'required' => 'required',
'data-slugify' => 'title',
]) ?>
<?= form_label(lang('Page.form.slug'), 'slug', ['class' => 'max-w-sm']) ?>
<?= form_input([
'id' => 'slug',
'name' => 'slug',
'class' => 'form-input mb-4 max-w-sm',
'value' => old('slug', $page->slug),
'required' => 'required',
'data-slugify' => 'slug',
]) ?>
<div class="mb-4">
<?= form_label(lang('Page.form.content'), 'content') ?>
<?= form_textarea(
[
'id' => 'content',
'name' => 'content',
'class' => 'form-textarea',
'required' => 'required',
],
old('content', $page->content, false),
'data-editor="markdown"'
) ?>
</div>
<?= form_button([
'content' => lang('Page.form.submit_edit'),
'type' => 'submit',
'class' => 'self-end px-4 py-2 bg-gray-200',
]) ?>
<?= form_close() ?>
<?= $this->endSection() ?>

View File

@ -0,0 +1,47 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= 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('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>
<?= $this->endSection() ?>

View File

@ -0,0 +1,17 @@
<?= $this->extend('admin/_layout') ?>
<?= $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('content') ?>
<div class="prose">
<?= $page->content_html ?>
</div>
<?= $this->endSection() ?>

View File

@ -54,6 +54,7 @@
[
'id' => 'episode_description_footer',
'name' => 'episode_description_footer',
'class' => 'form-textarea',
],
old('episode_description_footer', '', false),
@ -94,20 +95,13 @@
<span class="ml-2"><?= lang('Podcast.form.explicit') ?></span>
</label>
<?= form_label(lang('Podcast.form.author'), 'author') ?>
<?= form_input([
'id' => 'author',
'name' => 'author',
'class' => 'form-input mb-4',
'value' => old('author'),
]) ?>
<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?>
<?= form_input([
'id' => 'owner_name',
'name' => 'owner_name',
'class' => 'form-input mb-4',
'value' => old('owner_name'),
'required' => 'required',
]) ?>
<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?>
@ -120,9 +114,15 @@
'required' => 'required',
]) ?>
<?= form_fieldset('', [
'class' => 'flex flex-col mb-4',
<?= form_label(lang('Podcast.form.author'), 'author') ?>
<?= form_input([
'id' => 'author',
'name' => 'author',
'class' => 'form-input mb-4',
'value' => old('author'),
]) ?>
<?= 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(

View File

@ -109,20 +109,13 @@
<span class="ml-2"><?= lang('Podcast.form.explicit') ?></span>
</label>
<?= form_label(lang('Podcast.form.author'), 'author') ?>
<?= form_input([
'id' => 'author',
'name' => 'author',
'class' => 'form-input mb-4',
'value' => old('author', $podcast->author),
]) ?>
<?= form_label(lang('Podcast.form.owner_name'), 'owner_name') ?>
<?= form_input([
'id' => 'owner_name',
'name' => 'owner_name',
'class' => 'form-input mb-4',
'value' => old('owner_name', $podcast->owner_name),
'required' => 'required',
]) ?>
<?= form_label(lang('Podcast.form.owner_email'), 'owner_email') ?>
@ -135,6 +128,14 @@
'required' => 'required',
]) ?>
<?= form_label(lang('Podcast.form.author'), 'author') ?>
<?= form_input([
'id' => 'author',
'name' => 'author',
'class' => 'form-input mb-4',
'value' => old('author', $podcast->author),
]) ?>
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
<legend><?= lang('Podcast.form.type.label') ?></legend>
<label for="episodic" class="inline-flex items-center">

View File

@ -1,5 +1,9 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= $episode->title ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<a class="underline hover:no-underline" href="<?= route_to(

View File

@ -1,5 +1,7 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>Castopod<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-2 text-xl"><?= lang('Home.all_podcasts') ?> (<?= count(

11
app/Views/page.php Normal file
View File

@ -0,0 +1,11 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= $page->title ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="prose">
<?= $page->content_html ?>
</div>
<?= $this->endSection() ?>

View File

@ -1,5 +1,9 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= $podcast->title ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<header class="py-4 border-b">
<h1 class="text-2xl"><?= $podcast->title ?></h1>