feat: enhance ui using javascript in admin area
- bundle js using parcel - add markdown editor, html editor, dropdown and tooltip features using third-party packages - integrate optimized inline svg icons from RemixIcon using svgo and a php helper - add scripts in package.json to bundle icons, images, css and js - update tailwind config to add purgecss lookups and typography plugin - refactor views to add missing pages in user journey - update admin's holy grail layout using css grid
@ -15,13 +15,15 @@
|
||||
"color-highlight.markerType": "dot-before"
|
||||
},
|
||||
"extensions": [
|
||||
"mikestead.dotenv",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"naumovs.color-highlight",
|
||||
"heybourn.headwind",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
"mikestead.dotenv",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"naumovs.color-highlight",
|
||||
"heybourn.headwind",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"esbenp.prettier-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"jamesbirtles.svelte-vscode",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
|
12
.eslintrc.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {}
|
||||
}
|
3
.gitignore
vendored
@ -129,6 +129,9 @@ nb-configuration.xml
|
||||
yarn.lock
|
||||
node_modules
|
||||
|
||||
# JS
|
||||
.cache
|
||||
|
||||
# public folder
|
||||
public/*
|
||||
!public/.htaccess
|
||||
|
9
.svgo.icons.yml
Normal file
@ -0,0 +1,9 @@
|
||||
plugins:
|
||||
- removeXMLNS: true
|
||||
- removeDimensions: true
|
||||
- addAttributesToSVGElement:
|
||||
attributes:
|
||||
- fill: currentColor
|
||||
- width: "1em"
|
||||
- height: "1em"
|
||||
- sortAttrs: true
|
8
.svgo.yml
Normal file
@ -0,0 +1,8 @@
|
||||
plugins:
|
||||
- removeXMLNS: true
|
||||
- removeDimensions: true
|
||||
- addAttributesToSVGElement:
|
||||
attributes:
|
||||
- width: "1em"
|
||||
- height: "1em"
|
||||
- sortAttrs: true
|
@ -2,15 +2,23 @@
|
||||
|
||||
Castopod uses the following components:
|
||||
|
||||
PHP Dependencies:
|
||||
|
||||
- [Code Igniter 4](https://codeigniter.com) ([MIT License](https://codeigniter.com/user_guide/license.html))
|
||||
- [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
|
||||
- [Tatter\Relations](https://github.com/tattersoftware/codeigniter4-relations) ([MIT License](https://github.com/tattersoftware/codeigniter4-relations/blob/develop/LICENSE))
|
||||
- [D3: Data-Driven Documents](https://github.com/d3/d3) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE))
|
||||
- [Rollup](https://github.com/rollup/rollup) ([MIT license](https://github.com/rollup/rollup/blob/master/LICENSE.md))
|
||||
- [Svelte](https://github.com/sveltejs/svelte) ([MIT license](https://github.com/sveltejs/svelte/blob/master/LICENSE))
|
||||
- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
|
||||
- [WhichBrowser/Parser-PHP](https://github.com/WhichBrowser/Parser-PHP) ([MIT License](https://github.com/WhichBrowser/Parser-PHP/blob/master/LICENSE))
|
||||
- [GeoIP2 PHP API](https://github.com/maxmind/GeoIP2-php) ([Apache License 2.0](https://github.com/maxmind/GeoIP2-php/blob/master/LICENSE))
|
||||
- [Quill Rich Text Editor](https://github.com/quilljs/quill) ([BSD 3-Clause "New" or "Revised" License](https://github.com/quilljs/quill/blob/develop/LICENSE))
|
||||
- [getID3](https://github.com/JamesHeinrich/getID3) ([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt))
|
||||
- [myth-auth](https://github.com/lonnieezell/myth-auth) ([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md))
|
||||
- [parsedown](https://github.com/erusev/parsedown) ([MIT license](https://github.com/erusev/parsedown/blob/master/LICENSE.txt))
|
||||
|
||||
Javascript dependencies:
|
||||
|
||||
- [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://github.com/d3/d3) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE))
|
||||
|
||||
Other:
|
||||
|
||||
- [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
|
||||
|
@ -61,12 +61,9 @@ $routes->group(
|
||||
['namespace' => 'App\Controllers\Admin'],
|
||||
function ($routes) {
|
||||
$routes->get('/', 'Home', [
|
||||
'as' => 'admin',
|
||||
'as' => 'admin_home',
|
||||
]);
|
||||
|
||||
$routes->get('my-podcasts', 'Podcast::myPodcasts', [
|
||||
'as' => 'my_podcasts',
|
||||
]);
|
||||
$routes->get('podcasts', 'Podcast::list', [
|
||||
'as' => 'podcast_list',
|
||||
'filter' => 'permission:podcasts-list',
|
||||
@ -81,6 +78,9 @@ $routes->group(
|
||||
|
||||
// Use ids in admin area to help permission and group lookups
|
||||
$routes->group('podcasts/(:num)', function ($routes) {
|
||||
$routes->get('/', 'Podcast::view/$1', [
|
||||
'as' => 'podcast_view',
|
||||
]);
|
||||
$routes->get('edit', 'Podcast::edit/$1', [
|
||||
'as' => 'podcast_edit',
|
||||
]);
|
||||
@ -98,6 +98,9 @@ $routes->group(
|
||||
]);
|
||||
$routes->post('new-episode', 'Episode::attemptCreate/$1');
|
||||
|
||||
$routes->get('episodes/(:num)', 'Episode::view/$1/$2', [
|
||||
'as' => 'episode_view',
|
||||
]);
|
||||
$routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
|
||||
'as' => 'episode_edit',
|
||||
]);
|
||||
|
@ -77,6 +77,13 @@ class Episode extends BaseController
|
||||
return view('admin/episode/list', $data);
|
||||
}
|
||||
|
||||
public function view()
|
||||
{
|
||||
$data = ['episode' => $this->episode];
|
||||
|
||||
return view('admin/episode/view', $data);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper(['form']);
|
||||
|
@ -18,6 +18,15 @@ class Podcast extends BaseController
|
||||
{
|
||||
if (count($params) > 0) {
|
||||
switch ($method) {
|
||||
case 'view':
|
||||
if (
|
||||
!has_permission('podcasts-view') ||
|
||||
!has_permission("podcasts:$params[0]-view")
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
lang('Auth.notEnoughPrivilege')
|
||||
);
|
||||
}
|
||||
case 'edit':
|
||||
if (
|
||||
!has_permission('podcasts-edit') ||
|
||||
@ -36,20 +45,6 @@ class Podcast extends BaseController
|
||||
lang('Auth.notEnoughPrivilege')
|
||||
);
|
||||
}
|
||||
case 'listContributors':
|
||||
case 'addContributor':
|
||||
case 'editContributor':
|
||||
case 'deleteContributor':
|
||||
if (
|
||||
!has_permission('podcasts-manage_contributors') ||
|
||||
!has_permission(
|
||||
"podcasts:$params[0]-manage_contributors"
|
||||
)
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
lang('Auth.notEnoughPrivilege')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$podcast_model = new PodcastModel();
|
||||
@ -61,24 +56,29 @@ class Podcast extends BaseController
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
public function myPodcasts()
|
||||
{
|
||||
$data = [
|
||||
'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
|
||||
];
|
||||
|
||||
return view('admin/podcast/list', $data);
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
$podcast_model = new PodcastModel();
|
||||
|
||||
$data = ['all_podcasts' => $podcast_model->findAll()];
|
||||
$all_podcasts = [];
|
||||
if (has_permission('podcasts-list')) {
|
||||
$all_podcasts = $podcast_model->findAll();
|
||||
} else {
|
||||
$all_podcasts = $podcast_model->getUserPodcasts(user()->id);
|
||||
}
|
||||
|
||||
$data = ['all_podcasts' => $all_podcasts];
|
||||
|
||||
return view('admin/podcast/list', $data);
|
||||
}
|
||||
|
||||
public function view()
|
||||
{
|
||||
$data = ['podcast' => $this->podcast];
|
||||
|
||||
return view('admin/podcast/view', $data);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper(['form', 'misc']);
|
||||
|
@ -65,6 +65,7 @@ class AuthSeeder extends Seeder
|
||||
'name' => 'list',
|
||||
'description' => 'List all podcasts and their episodes',
|
||||
],
|
||||
['name' => 'view', 'description' => 'View any podcast'],
|
||||
['name' => 'edit', 'description' => 'Edit any podcast'],
|
||||
[
|
||||
'name' => 'manage_contributors',
|
||||
|
@ -9,6 +9,8 @@ namespace App\Entities;
|
||||
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Entity;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use Parsedown;
|
||||
|
||||
class Episode extends Entity
|
||||
{
|
||||
@ -22,6 +24,7 @@ class Episode extends Entity
|
||||
protected string $enclosure_media_path;
|
||||
protected string $enclosure_url;
|
||||
protected array $enclosure_metadata;
|
||||
protected string $description_html;
|
||||
|
||||
protected $casts = [
|
||||
'slug' => 'string',
|
||||
@ -153,4 +156,26 @@ class Episode extends Entity
|
||||
|
||||
return $podcast_model->find($this->attributes['podcast_id']);
|
||||
}
|
||||
|
||||
public function getDescriptionHtml()
|
||||
{
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
'renderer' => [
|
||||
'soft_break' => '<br>',
|
||||
],
|
||||
]);
|
||||
|
||||
if (
|
||||
$description_footer = $this->getPodcast()
|
||||
->episode_description_footer
|
||||
) {
|
||||
return $converter->convertToHtml(
|
||||
$this->attributes['description'] . '---'
|
||||
) . $converter->convertToHtml($description_footer);
|
||||
}
|
||||
|
||||
return $converter->convertToHtml($this->attributes['description']);
|
||||
}
|
||||
}
|
||||
|
46
app/Helpers/html_helper.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the inline svg icon
|
||||
*
|
||||
* @param string $name name of the icon file without the .svg extension
|
||||
* @param string $class to be added to the svg string
|
||||
* @return string svg contents
|
||||
*/
|
||||
function icon($name, $class = null)
|
||||
{
|
||||
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
|
||||
if ($class) {
|
||||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
);
|
||||
}
|
||||
return $svg_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inline svg image
|
||||
*
|
||||
* @param string $name name of the image file without the .svg extension
|
||||
* @param string $class to be added to the svg string
|
||||
* @return string svg contents
|
||||
*/
|
||||
function svg($name, $class = null)
|
||||
{
|
||||
$svg_contents = file_get_contents('assets/images/' . $name . '.svg');
|
||||
if ($class) {
|
||||
$svg_contents = str_replace(
|
||||
'<svg',
|
||||
'<svg class="' . $class . '"',
|
||||
$svg_contents
|
||||
);
|
||||
}
|
||||
return $svg_contents;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
18
app/Language/en/AdminNavigation.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'dashboard' => 'Dashboard',
|
||||
'podcasts' => 'Podcasts',
|
||||
'users' => 'Users',
|
||||
'admin_home' => 'Home',
|
||||
'podcast_list' => 'All podcasts',
|
||||
'podcast_create' => 'New podcast',
|
||||
'user_list' => 'All users',
|
||||
'user_create' => 'New user',
|
||||
'go_to_website' => 'Go to website'
|
||||
];
|
@ -11,8 +11,9 @@ return [
|
||||
'create' => 'Create a Podcast',
|
||||
'new_episode' => 'New Episode',
|
||||
'feed' => 'RSS feed',
|
||||
'edit' => 'Edit',
|
||||
'delete' => 'Delete',
|
||||
'view' => 'View podcast',
|
||||
'edit' => 'Edit podcast',
|
||||
'delete' => 'Delete podcast',
|
||||
'see_episodes' => 'See episodes',
|
||||
'see_contributors' => 'See contributors',
|
||||
'goto_page' => 'Go to page',
|
||||
|
@ -125,6 +125,10 @@ class PodcastModel extends Model
|
||||
|
||||
$podcast_permissions = [
|
||||
'podcasts:' . $podcast->id => [
|
||||
[
|
||||
'name' => 'View',
|
||||
'description' => "View the $podcast->name podcast",
|
||||
],
|
||||
[
|
||||
'name' => 'edit',
|
||||
'description' => "Edit the $podcast->name podcast",
|
||||
|
11
app/Views/_assets/admin.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Dropdown from "./modules/Dropdown";
|
||||
import HTMLEditor from "./modules/HTMLEditor";
|
||||
import MarkdownEditor from "./modules/MarkdownEditor";
|
||||
import Slugify from "./modules/Slugify";
|
||||
import Tooltip from "./modules/Tooltip";
|
||||
|
||||
Dropdown();
|
||||
Tooltip();
|
||||
MarkdownEditor();
|
||||
HTMLEditor();
|
||||
Slugify();
|
6
app/Views/_assets/icons/add.svg
Normal 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="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 185 B |
6
app/Views/_assets/icons/caret-down.svg
Normal 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="M12 14l-4-4h8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 166 B |
6
app/Views/_assets/icons/dashboard.svg
Normal 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="M13 21V11h8v10h-8zM3 13V3h8v10H3zm6-2V5H5v6h4zM3 21v-6h8v6H3zm2-2h4v-2H5v2zm10 0h4v-6h-4v6zM13 3h8v6h-8V3zm2 2v2h4V5h-4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 272 B |
6
app/Views/_assets/icons/edit.svg
Normal 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="M6.414 16L16.556 5.858l-1.414-1.414L5 14.586V16h1.414zm.829 2H3v-4.243L14.435 2.322a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L7.243 18zM3 20h18v2H3v-2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 309 B |
6
app/Views/_assets/icons/external-link.svg
Normal 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="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 281 B |
6
app/Views/_assets/icons/eye.svg
Normal 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="M12 3c5.392 0 9.878 3.88 10.819 9-.94 5.12-5.427 9-10.819 9-5.392 0-9.878-3.88-10.819-9C2.121 6.88 6.608 3 12 3zm0 16a9.005 9.005 0 0 0 8.777-7 9.005 9.005 0 0 0-17.554 0A9.005 9.005 0 0 0 12 19zm0-2.5a4.5 4.5 0 1 1 0-9 4.5 4.5 0 0 1 0 9zm0-2a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 431 B |
6
app/Views/_assets/icons/group.svg
Normal 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="M2 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H2zm8-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 4zm8.284 3.703A8.002 8.002 0 0 1 23 22h-2a6.001 6.001 0 0 0-3.537-5.473l.82-1.824zm-.688-11.29A5.5 5.5 0 0 1 21 8.5a5.499 5.499 0 0 1-5 5.478v-2.013a3.5 3.5 0 0 0 1.041-6.609l.555-1.943z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 496 B |
6
app/Views/_assets/icons/mic.svg
Normal 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="M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zm0-2a5 5 0 0 1 5 5v6a5 5 0 0 1-10 0V6a5 5 0 0 1 5-5zM2.192 13.962l1.962-.393a8.003 8.003 0 0 0 15.692 0l1.962.393C20.896 18.545 16.85 22 12 22s-8.896-3.455-9.808-8.038z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 372 B |
6
app/Views/_assets/icons/more.svg
Normal 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="M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 388 B |
86
app/Views/_assets/images/logo-castopod.svg
Normal file
@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.0beta1 (ee59332, 2019-11-28)"
|
||||
sodipodi:docname="castopod.svg"
|
||||
id="svg839"
|
||||
version="1.1"
|
||||
viewBox="0 0 64 63.999998"
|
||||
height="64"
|
||||
width="64">
|
||||
<metadata
|
||||
id="metadata845">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs843" />
|
||||
<sodipodi:namedview
|
||||
inkscape:current-layer="svg839"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-x="0"
|
||||
inkscape:cy="33.560512"
|
||||
inkscape:cx="32"
|
||||
inkscape:zoom="8.9714173"
|
||||
showgrid="false"
|
||||
id="namedview841"
|
||||
inkscape:window-height="1035"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:document-rotation="0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" />
|
||||
<circle
|
||||
id="greencircle"
|
||||
fill="#37c837"
|
||||
cx="32"
|
||||
cy="32"
|
||||
r="31.684" />
|
||||
<g
|
||||
id="speak">
|
||||
<path
|
||||
d="M45.21 20.22H18.79c-6.473 0-11.74 5.266-11.74 11.74S12.317 43.7 18.79 43.7h10.756c1.08 0 1.957-.875 1.957-1.956 0-1.08-.877-1.957-1.957-1.957H18.79c-4.315 0-7.826-3.51-7.826-7.827 0-4.316 3.51-7.828 7.827-7.828h26.42c4.315 0 7.826 3.512 7.826 7.828 0 4.316-3.51 7.827-7.827 7.827H43.34v.002c-5.41.096-9.783 4.527-9.783 9.96 0 1.08.875 1.957 1.956 1.957 1.08 0 1.956-.876 1.956-1.957 0-3.336 2.714-6.05 6.05-6.05h1.687c6.473 0 11.74-5.266 11.74-11.74s-5.267-11.74-11.74-11.74"
|
||||
fill="#fff"
|
||||
id="phylactery" />
|
||||
<g
|
||||
id="threedots">
|
||||
<circle
|
||||
r="2"
|
||||
cy="32"
|
||||
cx="24.256159"
|
||||
id="leftdot"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;"
|
||||
id="middledot"
|
||||
cx="32"
|
||||
cy="32"
|
||||
r="2" />
|
||||
<circle
|
||||
r="2"
|
||||
cy="32"
|
||||
cx="39.743839"
|
||||
id="rightdot"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
app/Views/_assets/main.js
Normal file
@ -0,0 +1 @@
|
||||
console.log("main");
|
56
app/Views/_assets/modules/Dropdown.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { createPopper } from "@popperjs/core";
|
||||
|
||||
const Dropdown = () => {
|
||||
const dropdownContainers = document.querySelectorAll(
|
||||
"[data-toggle='dropdown']"
|
||||
);
|
||||
|
||||
for (let i = 0; i < dropdownContainers.length; i++) {
|
||||
const dropdownContainer = dropdownContainers[i];
|
||||
|
||||
const button = dropdownContainer.querySelector("[data-popper='button']");
|
||||
const menu = dropdownContainer.querySelector("[data-popper='menu']");
|
||||
|
||||
const popper = createPopper(button, menu, {
|
||||
placement: menu.dataset.popperPlacement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [menu.dataset.popperOffsetX, menu.dataset.popperOffsetY],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const dropdownToggle = () => {
|
||||
const isExpanded = !menu.classList.contains("hidden");
|
||||
|
||||
if (isExpanded) {
|
||||
menu.classList.add("hidden");
|
||||
menu.classList.remove("flex");
|
||||
} else {
|
||||
menu.classList.add("flex");
|
||||
menu.classList.remove("hidden");
|
||||
}
|
||||
|
||||
button.setAttribute("aria-expanded", isExpanded);
|
||||
popper.update();
|
||||
};
|
||||
|
||||
// Toggle dropdown menu on button click event
|
||||
button.addEventListener("click", dropdownToggle);
|
||||
|
||||
// Toggle off when clicking outside of dropdown
|
||||
document.addEventListener("click", function (event) {
|
||||
const isExpanded = !menu.classList.contains("hidden");
|
||||
const isClickOutside = !dropdownContainer.contains(event.target);
|
||||
|
||||
if (isExpanded && isClickOutside) {
|
||||
dropdownToggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default Dropdown;
|
19
app/Views/_assets/modules/HTMLEditor.js
Normal file
@ -0,0 +1,19 @@
|
||||
import CodeMirror from "codemirror";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
|
||||
const HTMLEditor = () => {
|
||||
const allHTMLEditors = 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;
|
143
app/Views/_assets/modules/MarkdownEditor.js
Normal file
@ -0,0 +1,143 @@
|
||||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import "prosemirror-example-setup/style/style.css";
|
||||
import {
|
||||
defaultMarkdownParser,
|
||||
defaultMarkdownSerializer,
|
||||
schema,
|
||||
} from "prosemirror-markdown";
|
||||
import "prosemirror-menu/style/menu.css";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import "prosemirror-view/style/prosemirror.css";
|
||||
|
||||
class MarkdownView {
|
||||
constructor(target) {
|
||||
this.textarea = target;
|
||||
this.textarea.classList.add("w-full", "h-full");
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.textarea.innerHTML;
|
||||
}
|
||||
focus() {
|
||||
this.textarea.focus();
|
||||
}
|
||||
show() {
|
||||
this.textarea.classList.remove("hidden");
|
||||
}
|
||||
hide() {
|
||||
this.textarea.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
class ProseMirrorView {
|
||||
constructor(target, content) {
|
||||
this.editorContainer = document.createElement("div");
|
||||
this.editorContainer.classList.add(
|
||||
"bg-white",
|
||||
"border",
|
||||
"px-2",
|
||||
"min-h-full"
|
||||
);
|
||||
this.editorContainer.style.minHeight = "200px";
|
||||
const editor = target.parentNode.insertBefore(
|
||||
this.editorContainer,
|
||||
target.nextSibling
|
||||
);
|
||||
|
||||
this.view = new EditorView(editor, {
|
||||
state: EditorState.create({
|
||||
doc: defaultMarkdownParser.parse(content),
|
||||
plugins: exampleSetup({ schema }),
|
||||
}),
|
||||
dispatchTransaction: (transaction) => {
|
||||
let newState = this.view.state.apply(transaction);
|
||||
this.view.updateState(newState);
|
||||
|
||||
if (transaction.docChanged) {
|
||||
target.innerHTML = this.content;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get content() {
|
||||
return defaultMarkdownSerializer.serialize(this.view.state.doc);
|
||||
}
|
||||
focus() {
|
||||
this.view.focus();
|
||||
}
|
||||
show() {
|
||||
this.editorContainer.classList.remove("hidden");
|
||||
}
|
||||
hide() {
|
||||
this.editorContainer.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
const MarkdownEditor = () => {
|
||||
const targets = document.querySelectorAll("textarea[data-editor='markdown']");
|
||||
const activeClass = ["font-bold"];
|
||||
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
const target = targets[i];
|
||||
|
||||
const wysiwygBtn = document.createElement("button");
|
||||
wysiwygBtn.classList.add(
|
||||
...activeClass,
|
||||
"py-1",
|
||||
"px-2",
|
||||
"bg-white",
|
||||
"border",
|
||||
"text-xs"
|
||||
);
|
||||
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.setAttribute("type", "button");
|
||||
markdownBtn.innerHTML = "Markdown";
|
||||
|
||||
const viewButtons = document.createElement("div");
|
||||
viewButtons.appendChild(wysiwygBtn);
|
||||
viewButtons.appendChild(markdownBtn);
|
||||
viewButtons.classList.add(
|
||||
"inline-flex",
|
||||
"absolute",
|
||||
"top-0",
|
||||
"right-0",
|
||||
"-mt-6"
|
||||
);
|
||||
|
||||
const markdownEditorContainer = document.createElement("div");
|
||||
markdownEditorContainer.classList.add("relative");
|
||||
markdownEditorContainer.style.minHeight = "200px";
|
||||
target.parentNode.appendChild(markdownEditorContainer);
|
||||
markdownEditorContainer.appendChild(target);
|
||||
|
||||
// show WYSIWYG editor by default
|
||||
target.classList.add("hidden");
|
||||
const markdownView = new MarkdownView(target);
|
||||
const wysiwygView = new ProseMirrorView(target, markdownView.content);
|
||||
|
||||
markdownEditorContainer.appendChild(viewButtons);
|
||||
|
||||
markdownBtn.addEventListener("click", () => {
|
||||
if (markdownBtn.classList.contains(...activeClass)) return;
|
||||
markdownBtn.classList.add(...activeClass);
|
||||
wysiwygBtn.classList.remove(...activeClass);
|
||||
wysiwygView.hide();
|
||||
markdownView.show();
|
||||
});
|
||||
|
||||
wysiwygBtn.addEventListener("click", () => {
|
||||
if (wysiwygBtn.classList.contains(...activeClass)) return;
|
||||
wysiwygBtn.classList.add(...activeClass);
|
||||
markdownBtn.classList.remove(...activeClass);
|
||||
markdownView.hide();
|
||||
wysiwygView.show();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
32
app/Views/_assets/modules/Slugify.js
Normal file
@ -0,0 +1,32 @@
|
||||
// Original code from: https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||
const slugify = (string) => {
|
||||
const a =
|
||||
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;";
|
||||
const b =
|
||||
"aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
|
||||
return string
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-") // Replace spaces with -
|
||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, "-and-") // Replace & with 'and'
|
||||
.replace(/[^\w-]+/g, "") // Remove all non-word characters
|
||||
.replace(/--+/g, "-") // Replace multiple - with single -
|
||||
.replace(/^-+/, "") // Trim - from start of text
|
||||
.replace(/-+$/, ""); // Trim - from end of text
|
||||
};
|
||||
|
||||
const Slugify = () => {
|
||||
const title = document.querySelector("input[data-slugify='title']");
|
||||
const slug = document.querySelector("input[data-slugify='slug']");
|
||||
|
||||
if (title && slug) {
|
||||
title.addEventListener("input", () => {
|
||||
slug.value = slugify(title.value);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default Slugify;
|
61
app/Views/_assets/modules/Tooltip.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { createPopper } from "@popperjs/core";
|
||||
|
||||
const Tooltip = () => {
|
||||
const tooltipContainers = document.querySelectorAll(
|
||||
"[data-toggle='tooltip']"
|
||||
);
|
||||
|
||||
for (let i = 0; i < tooltipContainers.length; i++) {
|
||||
const tooltipReference = tooltipContainers[i];
|
||||
const tooltipContent = tooltipReference.title;
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.setAttribute("id", "tooltip");
|
||||
tooltip.setAttribute(
|
||||
"class",
|
||||
"px-2 py-1 text-sm bg-gray-900 text-white rounded"
|
||||
);
|
||||
tooltip.innerHTML = tooltipContent;
|
||||
|
||||
const popper = createPopper(tooltipReference, tooltip, {
|
||||
placement: tooltipReference.dataset.placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const show = () => {
|
||||
tooltipReference.removeAttribute("title");
|
||||
tooltipReference.setAttribute("aria-describedby", "tooltip");
|
||||
document.body.appendChild(tooltip);
|
||||
popper.update();
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
const element = document.getElementById("tooltip");
|
||||
tooltipReference.removeAttribute("aria-describedby");
|
||||
tooltipReference.setAttribute("title", tooltipContent);
|
||||
if (element) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
};
|
||||
|
||||
const showEvents = ["mouseenter", "focus"];
|
||||
const hideEvents = ["mouseleave", "blur"];
|
||||
|
||||
showEvents.forEach((event) => {
|
||||
tooltipReference.addEventListener(event, show);
|
||||
});
|
||||
|
||||
hideEvents.forEach((event) => {
|
||||
tooltipReference.addEventListener(event, hide);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default Tooltip;
|
2
app/Views/_assets/styles/index.css
Normal file
@ -0,0 +1,2 @@
|
||||
@import "./tailwind.css";
|
||||
@import "./layout.css";
|
21
app/Views/_assets/styles/layout.css
Normal file
@ -0,0 +1,21 @@
|
||||
.holy-grail-grid {
|
||||
@apply grid;
|
||||
grid-template: auto 1fr auto / auto 1fr auto;
|
||||
|
||||
& .holy-grail-header {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
|
||||
& .holy-grail-sidenav {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 4;
|
||||
}
|
||||
|
||||
& .holy-grail-main {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
& .holy-grail-footer {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
}
|
9
app/Views/_assets/styles/tailwind.css
Normal file
@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
|
||||
/* Start purging... */
|
||||
@tailwind components;
|
||||
/* Stop purging. */
|
||||
|
||||
/* Start purging... */
|
||||
@tailwind utilities;
|
||||
/* Stop purging. */
|
@ -2,12 +2,12 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Castopod</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">
|
||||
<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="/index.css">
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen mx-auto">
|
||||
|
25
app/Views/admin/_header.php
Normal file
@ -0,0 +1,25 @@
|
||||
<header class="<?= $class ?>">
|
||||
<a href="<?= route_to(
|
||||
'admin_home'
|
||||
) ?>" class="inline-flex items-center text-xl">
|
||||
<?= svg('logo-castopod', 'text-3xl mr-2 -ml-2') ?>
|
||||
Admin
|
||||
</a>
|
||||
<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="myAccountDropdown" 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="myAccountDropdown" data-popper="menu" data-popper-placement="bottom-end">
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'myAccount'
|
||||
) ?>">My Account</a>
|
||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
||||
'myAccount_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,39 +1,33 @@
|
||||
<?php helper('html'); ?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<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">
|
||||
<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="/index.css">
|
||||
<link rel="stylesheet" href="/assets/admin.css"/>
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen mx-auto">
|
||||
<header class="text-white bg-gray-900 border-b">
|
||||
<div class="flex items-center px-4 py-4 mx-auto">
|
||||
<a href="<?= route_to('admin') ?>" class="text-xl">Castopod Admin</a>
|
||||
<a href="<?= route_to(
|
||||
'home'
|
||||
) ?>" class="ml-4 text-sm underline hover:no-underline">Go to website</a>
|
||||
<nav class="ml-auto">
|
||||
<span class="mr-2">Welcome, <?= user()->username ?></span>
|
||||
<a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to(
|
||||
'logout'
|
||||
) ?>">Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1">
|
||||
<?= view('admin/_sidenav') ?>
|
||||
<main class="container flex-1 px-4 py-6 mx-auto">
|
||||
<h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1>
|
||||
<?= view('_message_block') ?>
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
</div>
|
||||
<footer class="container px-2 py-4 mx-auto text-sm text-right border-t">
|
||||
<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') ?>
|
||||
</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>
|
||||
|
||||
<script src="/assets/admin.js"></script>
|
||||
</body>
|
||||
|
43
app/Views/admin/_partials/_episode-card.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php helper('html'); ?>
|
||||
|
||||
<article class="flex w-full max-w-lg mb-4 bg-white border rounded shadow">
|
||||
<img src="<?= $episode->image_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->id,
|
||||
$episode->slug
|
||||
) ?>"><?= lang('Episode.goto_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>
|
11
app/Views/admin/_partials/_episode-list.php
Normal file
@ -0,0 +1,11 @@
|
||||
<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>
|
26
app/Views/admin/_partials/_podcast-card.php
Normal file
@ -0,0 +1,26 @@
|
||||
<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
|
||||
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_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 bg-gray-100 rounded-full shadow-xs text-teal-gray 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,59 +1,39 @@
|
||||
<aside class="w-64 px-4 py-6">
|
||||
<nav>
|
||||
<a class="block px-2 py-1 mb-4 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'admin'
|
||||
) ?>">
|
||||
Dashboard
|
||||
</a>
|
||||
<div class="mb-4">
|
||||
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Podcasts</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'my_podcasts'
|
||||
) ?>">My podcasts</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'podcast_list'
|
||||
) ?>">All podcasts</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'podcast_create'
|
||||
) ?>">New podcast</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Users</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'user_list'
|
||||
) ?>">All Users</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'user_create'
|
||||
) ?>">New user</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">My Account</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'myAccount'
|
||||
) ?>">Account info</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'myAccount_change-password'
|
||||
) ?>">Change my password</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<?php
|
||||
$navigation = [
|
||||
'dashboard' => ['icon' => 'dashboard', 'items' => ['admin_home']],
|
||||
'podcasts' => [
|
||||
'icon' => 'mic',
|
||||
'items' => ['podcast_list', 'podcast_create'],
|
||||
],
|
||||
'users' => ['icon' => 'group', 'items' => ['user_list', 'user_create']],
|
||||
]; ?>
|
||||
|
||||
<nav class="<?= $class ?>">
|
||||
<?php foreach ($navigation as $section => $data): ?>
|
||||
<div class="mb-4">
|
||||
<button class="inline-flex items-center w-full px-4 py-1 outline-none focus:shadow-outline" type="button">
|
||||
<?= icon($data['icon'], 'text-gray-500') ?>
|
||||
<span class="ml-2"><?= lang('AdminNavigation.' . $section) ?></span>
|
||||
</button>
|
||||
<ul>
|
||||
<?php foreach ($data['items'] as $item): ?>
|
||||
<?php $isActive = base_url(route_to($item)) == current_url(); ?>
|
||||
<li>
|
||||
<a class="block py-1 pl-10 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) ?>"><?= lang(
|
||||
'AdminNavigation.' . $item
|
||||
) ?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</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>
|
||||
|
@ -1,15 +1,19 @@
|
||||
<?php helper('html'); ?>
|
||||
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $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('content') ?>
|
||||
<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
|
||||
'contributor_add',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Contributor.add') ?></a>
|
||||
|
||||
<table class="table-auto">
|
||||
<thead>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section(
|
||||
'title'
|
||||
) ?>Welcome to the admin dashboard!<?= $this->endSection() ?>
|
||||
<?= $this->section('title') ?>
|
||||
Welcome to the admin dashboard!
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
@ -20,23 +20,25 @@
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="title"><?= lang('Episode.form.title') ?></label>
|
||||
<input type="text" class="form-input" id="title" name="title" required value="<?= old(
|
||||
<input type="text" class="form-input" id="title" name="title" data-slugify="title" required value="<?= old(
|
||||
'title'
|
||||
) ?>" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="slug"><?= lang('Episode.form.slug') ?></label>
|
||||
<input type="text" class="form-input" id="slug" name="slug" required value="<?= old(
|
||||
<input type="text" class="form-input" id="slug" name="slug" data-slugify="slug" required value="<?= old(
|
||||
'slug'
|
||||
) ?>" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="description"><?= lang('Episode.form.description') ?></label>
|
||||
<textarea class="form-textarea" id="description" name="description" required><?= old(
|
||||
<textarea class="hidden form-textarea" id="description" name="description" required data-editor="markdown"><?= old(
|
||||
'description'
|
||||
) ?></textarea>
|
||||
<button type="button" data-editor-view="markdown">Markdown</button>
|
||||
<button type="button" data-editor-view="wysiwyg">WYSIWYG</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
|
@ -20,17 +20,17 @@
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="title"><?= lang('Episode.form.title') ?></label>
|
||||
<input type="text" class="form-input" id="title" name="title" value="<?= $episode->title ?>" required />
|
||||
<input type="text" class="form-input" id="title" name="title" data-slugify="title" value="<?= $episode->title ?>" required />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="slug"><?= lang('Episode.form.slug') ?></label>
|
||||
<input type="text" class="form-input" id="slug" name="slug" value="<?= $episode->slug ?>" required />
|
||||
<input type="text" class="form-input" id="slug" name="slug" data-slugify="slug" value="<?= $episode->slug ?>" required />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="description"><?= lang('Episode.form.description') ?></label>
|
||||
<textarea class="form-textarea" id="description" name="description" required><?= $episode->description ?></textarea>
|
||||
<textarea class="form-textarea" id="description" name="description" required data-editor="markdown"><?= $episode->description ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
|
@ -13,55 +13,10 @@
|
||||
'episode_create',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Episode.create') ?></a>
|
||||
<div class="flex flex-col py-4">
|
||||
<?php if ($podcast->episodes): ?>
|
||||
<?php foreach ($podcast->episodes as $episode): ?>
|
||||
<article class="flex-col w-full max-w-lg p-4 mb-4 border shadow">
|
||||
<div class="flex mb-2">
|
||||
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<a href="<?= route_to(
|
||||
'episode_edit',
|
||||
$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>
|
||||
<p><?= $episode->description ?></p>
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'episode_edit',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-gray-700 hover:bg-gray-800"><?= lang(
|
||||
'Episode.goto_page'
|
||||
) ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode_delete',
|
||||
$podcast->id,
|
||||
$episode->id
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
|
||||
'Episode.delete'
|
||||
) ?></a>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= view('admin/_partials/_episode-list.php', [
|
||||
'episodes' => $podcast->episodes,
|
||||
]) ?>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
|
40
app/Views/admin/episode/view.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<a class="underline hover:no-underline" href="<?= route_to(
|
||||
'podcast_view',
|
||||
$episode->podcast->id
|
||||
) ?>">< <?= lang('Episode.back_to_podcast') ?></a>
|
||||
<h1 class="text-2xl font-semibold"><?= $episode->title ?></h1>
|
||||
<img src="<?= $episode->image_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>
|
||||
|
||||
<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->id,
|
||||
$episode->slug
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-gray-700 hover:bg-gray-800"><?= lang(
|
||||
'Episode.goto_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>
|
||||
|
||||
<section class="prose">
|
||||
<?= $episode->description_html ?>
|
||||
</section>
|
||||
|
||||
<?= $this->endSection() ?>
|
@ -29,7 +29,7 @@
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="description"><?= lang('Podcast.form.description') ?></label>
|
||||
<textarea class="form-textarea" id="description" name="description" required><?= old(
|
||||
<textarea class="form-textarea" id="description" name="description" required data-editor="markdown"><?= old(
|
||||
'description'
|
||||
) ?></textarea>
|
||||
</div>
|
||||
@ -38,7 +38,7 @@
|
||||
<label for="episode_description_footer"><?= lang(
|
||||
'Podcast.form.episode_description_footer'
|
||||
) ?></label>
|
||||
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"><?= old(
|
||||
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer" data-editor="markdown"><?= old(
|
||||
'episode_description_footer'
|
||||
) ?></textarea>
|
||||
</div>
|
||||
@ -162,7 +162,7 @@
|
||||
<label for="custom_html_head"><?= esc(
|
||||
lang('Podcast.form.custom_html_head')
|
||||
) ?></label>
|
||||
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head"></textarea>
|
||||
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
||||
|
@ -25,14 +25,14 @@
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="description"><?= lang('Podcast.form.description') ?></label>
|
||||
<textarea class="form-textarea" id="description" name="description" required><?= $podcast->description ?></textarea>
|
||||
<textarea class="form-textarea" id="description" name="description" required data-editor="markdown"><?= $podcast->description ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
<label for="episode_description_footer"><?= lang(
|
||||
'Podcast.form.episode_description_footer'
|
||||
) ?></label>
|
||||
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"><?= $podcast->episode_description_footer ?></textarea>
|
||||
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer" data-editor="markdown"><?= $podcast->episode_description_footer ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col mb-4">
|
||||
@ -134,7 +134,7 @@
|
||||
<label for="custom_html_head"><?= esc(
|
||||
lang('Podcast.form.custom_html_head')
|
||||
) ?></label>
|
||||
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head"><?= $podcast->custom_html_head ?></textarea>
|
||||
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"><?= $podcast->custom_html_head ?></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
|
||||
|
@ -1,49 +1,25 @@
|
||||
<?php helper('html'); ?>
|
||||
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Podcast.all_podcasts') ?> (<?= count($all_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>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
|
||||
'podcast_create'
|
||||
) ?>"><?= lang('Podcast.create') ?></a>
|
||||
<div class="flex flex-wrap">
|
||||
<?php if ($all_podcasts): ?>
|
||||
<?php foreach ($all_podcasts as $podcast): ?>
|
||||
<article class="w-48 h-full p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
|
||||
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
|
||||
<a href="<?= route_to(
|
||||
'episode_list',
|
||||
$podcast->id
|
||||
) ?>" class="hover:underline">
|
||||
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
|
||||
</a>
|
||||
<p class="mb-4 text-gray-600">@<?= $podcast->name ?></p>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'podcast_edit',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Podcast.edit') ?></a>
|
||||
<a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to(
|
||||
'episode_list',
|
||||
$podcast->id
|
||||
) ?>"><?= lang('Podcast.see_episodes') ?></a>
|
||||
<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 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
|
||||
'podcast',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.goto_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>
|
||||
|
||||
</article>
|
||||
<?= view('admin/_partials/_podcast-card', [
|
||||
'podcast' => $podcast,
|
||||
]) ?>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<p class="italic"><?= lang('Podcast.no_podcast') ?></p>
|
||||
|
40
app/Views/admin/podcast/view.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php helper('html'); ?>
|
||||
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $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('content') ?>
|
||||
<img class="w-64 mb-4" src="<?= $podcast->image_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 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
|
||||
'podcast',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.goto_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,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
@ -2,12 +2,12 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Castopod Auth</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">
|
||||
<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="/index.css">
|
||||
<link rel="stylesheet" href="/assets/index.css"/>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center justify-center min-h-screen mx-auto bg-gray-100">
|
||||
|
@ -13,5 +13,13 @@
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
=======
|
||||
<section class="prose">
|
||||
<?= $episode->description_html ?>
|
||||
</section>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
>>>>>>> 240f1d4... feat: enhance ui using javascript in admin area
|
||||
|
@ -1 +1,3 @@
|
||||
/* eslint-disable */
|
||||
|
||||
module.exports = { extends: ["@commitlint/config-conventional"] };
|
||||
|
@ -10,7 +10,8 @@
|
||||
"james-heinrich/getid3": "~2.0.0-dev",
|
||||
"whichbrowser/parser": "^2.0",
|
||||
"geoip2/geoip2": "~2.0",
|
||||
"myth/auth": "1.0-beta.2"
|
||||
"myth/auth": "1.0-beta.2",
|
||||
"league/commonmark": "^1.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
|
97
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e494a281a4c6a239790ea930d05764e2",
|
||||
"content-hash": "df18ba9c8ecbb43a37d2a90ebd4316f6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
@ -427,6 +427,101 @@
|
||||
],
|
||||
"time": "2020-05-20T16:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/commonmark",
|
||||
"version": "1.5.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/commonmark.git",
|
||||
"reference": "2574454b97e4103dc4e36917bd783b25624aefcd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/2574454b97e4103dc4e36917bd783b25624aefcd",
|
||||
"reference": "2574454b97e4103dc4e36917bd783b25624aefcd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"scrutinizer/ocular": "1.7.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"cebe/markdown": "~1.0",
|
||||
"commonmark/commonmark.js": "0.29.1",
|
||||
"erusev/parsedown": "~1.0",
|
||||
"ext-json": "*",
|
||||
"github/gfm": "0.29.0",
|
||||
"michelf/php-markdown": "~1.4",
|
||||
"mikehaertl/php-shellcommand": "^1.4",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.2",
|
||||
"scrutinizer/ocular": "^1.5",
|
||||
"symfony/finder": "^4.2"
|
||||
},
|
||||
"bin": [
|
||||
"bin/commonmark"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\CommonMark\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Colin O'Dell",
|
||||
"email": "colinodell@gmail.com",
|
||||
"homepage": "https://www.colinodell.com",
|
||||
"role": "Lead Developer"
|
||||
}
|
||||
],
|
||||
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)",
|
||||
"homepage": "https://commonmark.thephpleague.com",
|
||||
"keywords": [
|
||||
"commonmark",
|
||||
"flavored",
|
||||
"gfm",
|
||||
"github",
|
||||
"github-flavored",
|
||||
"markdown",
|
||||
"md",
|
||||
"parser"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.colinodell.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.paypal.me/colinpodell/10.00",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/colinodell",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/colinodell",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-19T22:47:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maxmind-db/reader",
|
||||
"version": "v1.6.0",
|
||||
|
6738
package-lock.json
generated
33
package.json
@ -9,22 +9,51 @@
|
||||
"url": "https://code.podlibre.org/podlibre/castopod.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build:css": "postcss app/Javascript/assets/styles/index.css -o public/index.css",
|
||||
"watch:css": "postcss app/Javascript/assets/styles/index.css -o public/index.css -w",
|
||||
"watch:js": "parcel watch app/Views/_assets/*.js --out-dir public/assets",
|
||||
"build:js": "parcel build app/Views/_assets/*.js --out-dir public/assets",
|
||||
"watch:css": "postcss app/Views/_assets/styles/index.css -o public/assets/index.css -w",
|
||||
"build:css": "postcss app/Views/_assets/styles/index.css -o public/assets/index.css",
|
||||
"build:icons": "svgo -f app/Views/_assets/icons -o public/assets/icons --config=./.svgo.icons.yml",
|
||||
"build:svg": "svgo -f app/Views/_assets/images -o public/assets/images --config=./.svgo.yml",
|
||||
"build": "npm run build:js && cross-env NODE_ENV=production npm run build:css && npm run build:icons && npm run build:svg",
|
||||
"commit": "git-cz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"codemirror": "^5.55.0",
|
||||
"easymde": "^2.11.0",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.5.0",
|
||||
"prosemirror-state": "^1.3.3",
|
||||
"prosemirror-view": "^1.15.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^9.0.1",
|
||||
"@commitlint/config-conventional": "^9.0.1",
|
||||
"@prettier/plugin-php": "^0.14.2",
|
||||
"@tailwindcss/custom-forms": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cssnano": "^4.1.10",
|
||||
"cz-conventional-changelog": "^3.2.0",
|
||||
"eslint": "^7.5.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.11",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "2.0.5",
|
||||
"svgo": "^1.3.2",
|
||||
"tailwindcss": "^1.4.6"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
|
||||
|
@ -1,3 +1,16 @@
|
||||
/* eslint-disable */
|
||||
|
||||
module.exports = {
|
||||
plugins: [require("tailwindcss"), require("autoprefixer")],
|
||||
plugins: [
|
||||
require("postcss-import"),
|
||||
require("tailwindcss"),
|
||||
require("postcss-preset-env")({ stage: 1 }),
|
||||
...(process.env.NODE_ENV === "production"
|
||||
? [
|
||||
require("cssnano")({
|
||||
preset: "default",
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
|
@ -1,8 +1,13 @@
|
||||
/* eslint-disable */
|
||||
|
||||
module.exports = {
|
||||
purge: [],
|
||||
purge: ["./app/Views/**/*.php", "./app/Views/**/*.js"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [require("@tailwindcss/custom-forms")],
|
||||
plugins: [
|
||||
require("@tailwindcss/custom-forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
],
|
||||
};
|
||||
|