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
This commit is contained in:
Yassine Doghri 2020-07-27 09:35:34 +00:00
parent d58e51874a
commit c0e66d5f70
62 changed files with 7873 additions and 257 deletions

View File

@ -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
View 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
View File

@ -129,6 +129,9 @@ nb-configuration.xml
yarn.lock
node_modules
# JS
.cache
# public folder
public/*
!public/.htaccess

9
.svgo.icons.yml Normal file
View 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
View File

@ -0,0 +1,8 @@
plugins:
- removeXMLNS: true
- removeDimensions: true
- addAttributesToSVGElement:
attributes:
- width: "1em"
- height: "1em"
- sortAttrs: true

View File

@ -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))

View File

@ -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',
]);

View File

@ -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']);

View File

@ -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']);

View File

@ -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',

View File

@ -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']);
}
}

View 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;
}

View File

@ -1,3 +0,0 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

View 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'
];

View File

@ -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',

View File

@ -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",

View 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();

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="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 185 B

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="M12 14l-4-4h8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 166 B

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="M13 21V11h8v10h-8zM3 13V3h8v10H3zm6-2V5H5v6h4zM3 21v-6h8v6H3zm2-2h4v-2H5v2zm10 0h4v-6h-4v6zM13 3h8v6h-8V3zm2 2v2h4V5h-4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 272 B

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="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

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="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

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="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

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="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

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="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

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="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

View 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

View File

@ -0,0 +1 @@
console.log("main");

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,2 @@
@import "./tailwind.css";
@import "./layout.css";

View 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;
}
}

View File

@ -0,0 +1,9 @@
@tailwind base;
/* Start purging... */
@tailwind components;
/* Stop purging. */
/* Start purging... */
@tailwind utilities;
/* Stop purging. */

View File

@ -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">

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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() ?>

View File

@ -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">

View File

@ -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">

View File

@ -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()
?>

View 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() ?>

View File

@ -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(

View File

@ -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(

View File

@ -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>

View 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() ?>

View File

@ -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">

View File

@ -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

View File

@ -1 +1,3 @@
/* eslint-disable */
module.exports = { extends: ["@commitlint/config-conventional"] };

View File

@ -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
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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",
}),
]
: []),
],
};

View File

@ -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"),
],
};