aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Lange <code@nerdmind.de>2021-07-01 20:11:34 +0200
committerThomas Lange <code@nerdmind.de>2021-07-01 20:11:34 +0200
commite6cef37e0c782fe770db20888d99c17d10e2c479 (patch)
treeaed0790a0fa8eb7cceef7bb32aef4f69b724d619
parentf0ea19767d502ec7b5afff7c66c2681292175a3b (diff)
downloadblog-e6cef37e0c782fe770db20888d99c17d10e2c479.tar.gz
blog-e6cef37e0c782fe770db20888d99c17d10e2c479.tar.xz
blog-e6cef37e0c782fe770db20888d99c17d10e2c479.zip
Add category system to categorize posts (readme)
This commit implements a new category system to categorize posts. Each category can have an unlimited number of nested children categories. A single post don't necessarily need to be in a category, but it can. Each category can have a full content body like posts or pages, so you have enough space to describe the content of your categories. Please note that you need to have at least the following MySQL/MariaDB versions to use the category system, because it uses "WITH RECURSIVE" database queries, the so-called "Common-Table-Expressions (CTE)". MariaDB: 10.2.2 MySQL: 8.0 See: https://mariadb.com/kb/en/with/ See: https://dev.mysql.com/doc/refman/8.0/en/with.html
-rw-r--r--admin/category/delete.php56
-rw-r--r--admin/category/index.php72
-rw-r--r--admin/category/insert.php80
-rw-r--r--admin/category/update.php84
-rw-r--r--admin/post/insert.php17
-rw-r--r--admin/post/update.php17
-rw-r--r--core/application.php7
-rw-r--r--core/functions.php64
-rw-r--r--core/include/category/list.php69
-rw-r--r--core/include/category/main.php134
-rw-r--r--core/include/post/main.php17
-rw-r--r--core/namespace/Application.php10
-rw-r--r--core/namespace/ORM/Entities/Category.php14
-rw-r--r--core/namespace/ORM/Entities/Post.php13
-rw-r--r--core/namespace/ORM/Repositories/Category.php138
-rw-r--r--core/namespace/ORM/Repositories/Post.php12
-rw-r--r--index.php5
-rw-r--r--theme/admin/html/category/delete.php6
-rw-r--r--theme/admin/html/category/form.php112
-rw-r--r--theme/admin/html/category/index.php15
-rw-r--r--theme/admin/html/category/insert.php6
-rw-r--r--theme/admin/html/category/item.php33
-rw-r--r--theme/admin/html/category/update.php6
-rw-r--r--theme/admin/html/main.php1
-rw-r--r--theme/admin/html/post/form.php27
-rw-r--r--theme/admin/html/post/item.php3
-rw-r--r--theme/default/html/category/item.php27
-rw-r--r--theme/default/html/category/list.php23
-rw-r--r--theme/default/html/category/main.php44
-rw-r--r--theme/default/html/main.php5
-rw-r--r--theme/default/html/post/main.php8
-rw-r--r--theme/default/rsrc/css/main.css48
-rw-r--r--theme/default/rsrc/css/main.scss50
33 files changed, 1211 insertions, 12 deletions
diff --git a/admin/category/delete.php b/admin/category/delete.php
new file mode 100644
index 0000000..e92387c
--- /dev/null
+++ b/admin/category/delete.php
@@ -0,0 +1,56 @@
+<?php
+#===============================================================================
+# DEFINE: Administration
+#===============================================================================
+const ADMINISTRATION = TRUE;
+const AUTHENTICATION = TRUE;
+
+#===============================================================================
+# INCLUDE: Initialization
+#===============================================================================
+require '../../core/application.php';
+
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+
+#===============================================================================
+# Throw 404 error if category could not be found
+#===============================================================================
+if(!$Category = $CategoryRepository->find(HTTP::GET('id'))) {
+ Application::error404();
+}
+
+#===============================================================================
+# Check for delete request
+#===============================================================================
+if(HTTP::issetPOST(['token' => Application::getSecurityToken()], 'delete')) {
+ try {
+ if($CategoryRepository->delete($Category)) {
+ HTTP::redirect(Application::getAdminURL('category/'));
+ }
+ } catch(PDOException $Exception) {
+ $messages[] = $Exception->getMessage();
+ }
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$FormTemplate = Template\Factory::build('category/form');
+$FormTemplate->set('HTML', parseEntityContent($Category));
+$FormTemplate->set('FORM', [
+ 'TYPE' => 'DELETE',
+ 'INFO' => $messages ?? [],
+ 'DATA' => array_change_key_case($Category->getAll(), CASE_UPPER),
+ 'TOKEN' => Application::getSecurityToken()
+]);
+
+$DeleteTemplate = Template\Factory::build('category/delete');
+$DeleteTemplate->set('HTML', $FormTemplate);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('NAME', $Language->text('title_category_delete'));
+$MainTemplate->set('HTML', $DeleteTemplate);
+echo $MainTemplate;
diff --git a/admin/category/index.php b/admin/category/index.php
new file mode 100644
index 0000000..429435a
--- /dev/null
+++ b/admin/category/index.php
@@ -0,0 +1,72 @@
+<?php
+#===============================================================================
+# DEFINE: Administration
+#===============================================================================
+const ADMINISTRATION = TRUE;
+const AUTHENTICATION = TRUE;
+
+#===============================================================================
+# INCLUDE: Initialization
+#===============================================================================
+require '../../core/application.php';
+
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+
+#===============================================================================
+# Pagination
+#===============================================================================
+$site_size = Application::get('ADMIN.CATEGORY.LIST_SIZE');
+
+$count = $CategoryRepository->getCount();
+$lastSite = ceil($count / $site_size);
+
+$currentSite = HTTP::GET('site') ?? 1;
+$currentSite = intval($currentSite);
+
+#===============================================================================
+# Redirect to category create form if no category exists
+#===============================================================================
+if($count === 0) {
+ HTTP::redirect(Application::getAdminURL('category/insert.php'));
+}
+
+if($currentSite < 1 OR ($currentSite > $lastSite AND $lastSite > 0)) {
+ Application::error404();
+}
+
+#===============================================================================
+# Get paginated category list
+#===============================================================================
+$categories = $CategoryRepository->getPaginatedTree(
+ $site_size, ($currentSite-1) * $site_size);
+
+foreach($categories as $Category) {
+ $templates[] = generateCategoryItemTemplate($Category, TRUE);
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$PaginationTemplate = Template\Factory::build('pagination');
+$PaginationTemplate->set('THIS', $currentSite);
+$PaginationTemplate->set('LAST', $lastSite);
+$PaginationTemplate->set('HREF', Application::getAdminURL('category/?site=%d'));
+
+$ListTemplate = Template\Factory::build('category/index');
+$ListTemplate->set('LIST', [
+ 'CATEGORIES' => $templates ?? []
+]);
+
+$ListTemplate->set('PAGINATION', [
+ 'THIS' => $currentSite,
+ 'LAST' => $lastSite,
+ 'HTML' => $PaginationTemplate
+]);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('NAME', $Language->text('title_category_overview', $currentSite));
+$MainTemplate->set('HTML', $ListTemplate);
+echo $MainTemplate;
diff --git a/admin/category/insert.php b/admin/category/insert.php
new file mode 100644
index 0000000..f6f193e
--- /dev/null
+++ b/admin/category/insert.php
@@ -0,0 +1,80 @@
+<?php
+#===============================================================================
+# DEFINE: Administration
+#===============================================================================
+const ADMINISTRATION = TRUE;
+const AUTHENTICATION = TRUE;
+
+#===============================================================================
+# INCLUDE: Initialization
+#===============================================================================
+require '../../core/application.php';
+
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+
+#===============================================================================
+# Instantiate new Category entity
+#===============================================================================
+$Category = new ORM\Entities\Category;
+
+#===============================================================================
+# Check for insert request
+#===============================================================================
+if(HTTP::issetPOST('parent', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'insert')) {
+ $Category->set('parent', HTTP::POST('parent') ?: NULL);
+ $Category->set('slug', HTTP::POST('slug') ?: generateSlug(HTTP::POST('name')));
+ $Category->set('name', HTTP::POST('name') ?: NULL);
+ $Category->set('body', HTTP::POST('body') ?: NULL);
+ $Category->set('argv', HTTP::POST('argv') ?: NULL);
+ $Category->set('time_insert', HTTP::POST('time_insert') ?: date('Y-m-d H:i:s'));
+ $Category->set('time_update', HTTP::POST('time_update') ?: date('Y-m-d H:i:s'));
+
+ if(HTTP::issetPOST(['token' => Application::getSecurityToken()])) {
+ try {
+ if($CategoryRepository->insert($Category)) {
+ HTTP::redirect(Application::getAdminURL('category/'));
+ }
+ } catch(PDOException $Exception) {
+ $messages[] = $Exception->getMessage();
+ }
+ }
+
+ else {
+ $messages[] = $Language->text('error_security_csrf');
+ }
+}
+
+#===============================================================================
+# Generate category list
+#===============================================================================
+foreach($CategoryRepository->getAll([], 'name ASC') as $_Category) {
+ $categoryList[] = [
+ 'ID' => $_Category->getID(),
+ 'NAME' => $_Category->get('name'),
+ 'PARENT' => $_Category->get('parent'),
+ ];
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$FormTemplate = Template\Factory::build('category/form');
+$FormTemplate->set('FORM', [
+ 'TYPE' => 'INSERT',
+ 'INFO' => $messages ?? [],
+ 'DATA' => array_change_key_case($Category->getAll(), CASE_UPPER),
+ 'CATEGORY_LIST' => $categoryList ?? [],
+ 'CATEGORY_TREE' => generateCategoryDataTree($categoryList ?? []),
+ 'TOKEN' => Application::getSecurityToken()
+]);
+
+$InsertTemplate = Template\Factory::build('category/insert');
+$InsertTemplate->set('HTML', $FormTemplate);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('NAME', $Language->text('title_category_insert'));
+$MainTemplate->set('HTML', $InsertTemplate);
+echo $MainTemplate;
diff --git a/admin/category/update.php b/admin/category/update.php
new file mode 100644
index 0000000..7cee587
--- /dev/null
+++ b/admin/category/update.php
@@ -0,0 +1,84 @@
+<?php
+#===============================================================================
+# DEFINE: Administration
+#===============================================================================
+const ADMINISTRATION = TRUE;
+const AUTHENTICATION = TRUE;
+
+#===============================================================================
+# INCLUDE: Initialization
+#===============================================================================
+require '../../core/application.php';
+
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+
+#===============================================================================
+# Throw 404 error if category could not be found
+#===============================================================================
+if(!$Category = $CategoryRepository->find(HTTP::GET('id'))) {
+ Application::error404();
+}
+
+#===============================================================================
+# Check for update request
+#===============================================================================
+if(HTTP::issetPOST('parent', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'update')) {
+ $Category->set('slug', HTTP::POST('slug') ?: generateSlug(HTTP::POST('name')));
+ $Category->set('name', HTTP::POST('name') ?: NULL);
+ $Category->set('body', HTTP::POST('body') ?: NULL);
+ $Category->set('argv', HTTP::POST('argv') ?: NULL);
+ $Category->set('time_insert', HTTP::POST('time_insert') ?: date('Y-m-d H:i:s'));
+ $Category->set('time_update', HTTP::POST('time_update') ?: date('Y-m-d H:i:s'));
+
+ # Modify parent field only if it is not a self-reference
+ if(HTTP::POST('parent') != $Category->getID()) {
+ $Category->set('parent', HTTP::POST('parent') ?: NULL);
+ }
+
+ if(HTTP::issetPOST(['token' => Application::getSecurityToken()])) {
+ try {
+ $CategoryRepository->update($Category);
+ } catch(PDOException $Exception) {
+ $messages[] = $Exception->getMessage();
+ }
+ }
+
+ else {
+ $messages[] = $Language->text('error_security_csrf');
+ }
+}
+
+#===============================================================================
+# Generate category list
+#===============================================================================
+foreach($CategoryRepository->getAll([], 'name ASC') as $_Category) {
+ $categoryList[] = [
+ 'ID' => $_Category->getID(),
+ 'NAME' => $_Category->get('name'),
+ 'PARENT' => $_Category->get('parent'),
+ ];
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$FormTemplate = Template\Factory::build('category/form');
+$FormTemplate->set('FORM', [
+ 'TYPE' => 'UPDATE',
+ 'INFO' => $messages ?? [],
+ 'DATA' => array_change_key_case($Category->getAll(), CASE_UPPER),
+ 'CATEGORY_LIST' => $categoryList ?? [],
+ 'CATEGORY_TREE' => generateCategoryDataTree($categoryList ?? []),
+ 'TOKEN' => Application::getSecurityToken()
+]);
+
+$InsertTemplate = Template\Factory::build('category/update');
+$InsertTemplate->set('HTML', $FormTemplate);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('NAME', $Language->text('title_category_update'));
+$MainTemplate->set('HTML', $InsertTemplate);
+echo $MainTemplate;
diff --git a/admin/post/insert.php b/admin/post/insert.php
index b4404fa..bad55ae 100644
--- a/admin/post/insert.php
+++ b/admin/post/insert.php
@@ -13,6 +13,7 @@ require '../../core/application.php';
#===============================================================================
# Get repositories
#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
$PostRepository = Application::getRepository('Post');
$UserRepository = Application::getRepository('User');
@@ -24,7 +25,8 @@ $Post = new ORM\Entities\Post;
#===============================================================================
# Check for insert request
#===============================================================================
-if(HTTP::issetPOST('user', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'insert')) {
+if(HTTP::issetPOST('category', 'user', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'insert')) {
+ $Post->set('category', HTTP::POST('category') ?: NULL);
$Post->set('user', HTTP::POST('user'));
$Post->set('slug', HTTP::POST('slug') ?: generateSlug(HTTP::POST('name')));
$Post->set('name', HTTP::POST('name') ?: NULL);
@@ -60,6 +62,17 @@ foreach($UserRepository->getAll([], 'fullname ASC') as $User) {
}
#===============================================================================
+# Generate category list
+#===============================================================================
+foreach($CategoryRepository->getAll([], 'name ASC') as $Category) {
+ $categoryList[] = [
+ 'ID' => $Category->getID(),
+ 'NAME' => $Category->get('name'),
+ 'PARENT' => $Category->get('parent'),
+ ];
+}
+
+#===============================================================================
# Build document
#===============================================================================
$FormTemplate = Template\Factory::build('post/form');
@@ -68,6 +81,8 @@ $FormTemplate->set('FORM', [
'INFO' => $messages ?? [],
'DATA' => array_change_key_case($Post->getAll(), CASE_UPPER),
'USER_LIST' => $userList ?? [],
+ 'CATEGORY_LIST' => $categoryList ?? [],
+ 'CATEGORY_TREE' => generateCategoryDataTree($categoryList ?? []),
'TOKEN' => Application::getSecurityToken()
]);
diff --git a/admin/post/update.php b/admin/post/update.php
index 3e4d0ab..e68b3ec 100644
--- a/admin/post/update.php
+++ b/admin/post/update.php
@@ -13,6 +13,7 @@ require '../../core/application.php';
#===============================================================================
# Get repositories
#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
$PostRepository = Application::getRepository('Post');
$UserRepository = Application::getRepository('User');
@@ -26,7 +27,8 @@ if(!$Post = $PostRepository->find(HTTP::GET('id'))) {
#===============================================================================
# Check for update request
#===============================================================================
-if(HTTP::issetPOST('user', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'update')) {
+if(HTTP::issetPOST('category', 'user', 'slug', 'name', 'body', 'argv', 'time_insert', 'time_update', 'update')) {
+ $Post->set('category', HTTP::POST('category') ?: NULL);
$Post->set('user', HTTP::POST('user'));
$Post->set('slug', HTTP::POST('slug') ?: generateSlug(HTTP::POST('name')));
$Post->set('name', HTTP::POST('name') ?: NULL);
@@ -60,6 +62,17 @@ foreach($UserRepository->getAll([], 'fullname ASC') as $User) {
}
#===============================================================================
+# Generate category list
+#===============================================================================
+foreach($CategoryRepository->getAll([], 'name ASC') as $Category) {
+ $categoryList[] = [
+ 'ID' => $Category->getID(),
+ 'NAME' => $Category->get('name'),
+ 'PARENT' => $Category->get('parent'),
+ ];
+}
+
+#===============================================================================
# Build document
#===============================================================================
$FormTemplate = Template\Factory::build('post/form');
@@ -68,6 +81,8 @@ $FormTemplate->set('FORM', [
'INFO' => $messages ?? [],
'DATA' => array_change_key_case($Post->getAll(), CASE_UPPER),
'USER_LIST' => $userList ?? [],
+ 'CATEGORY_LIST' => $categoryList ?? [],
+ 'CATEGORY_TREE' => generateCategoryDataTree($categoryList ?? []),
'TOKEN' => Application::getSecurityToken()
]);
diff --git a/core/application.php b/core/application.php
index 5bbc5d1..0e1324a 100644
--- a/core/application.php
+++ b/core/application.php
@@ -63,23 +63,29 @@ foreach([
'PATHINFO.PROT' => $_SERVER['REQUEST_SCHEME'] ?? 'https',
'PATHINFO.HOST' => $_SERVER['HTTP_HOST'] ?? 'localhost',
'PATHINFO.BASE' => '',
+ 'CATEGORY.DIRECTORY' => 'category',
'PAGE.DIRECTORY' => 'page',
'POST.DIRECTORY' => 'post',
'USER.DIRECTORY' => 'user',
+ 'CATEGORY.SLUG_URLS' => TRUE,
'PAGE.SLUG_URLS' => TRUE,
'POST.SLUG_URLS' => TRUE,
'USER.SLUG_URLS' => TRUE,
+ 'CATEGORY.EMOTICONS' => TRUE,
'PAGE.EMOTICONS' => TRUE,
'POST.EMOTICONS' => TRUE,
'USER.EMOTICONS' => TRUE,
+ 'CATEGORY.LIST_SIZE' => 10,
'PAGE.LIST_SIZE' => 10,
'POST.LIST_SIZE' => 10,
'USER.LIST_SIZE' => 10,
'PAGE.FEED_SIZE' => 25,
'POST.FEED_SIZE' => 25,
+ 'CATEGORY.DESCRIPTION_SIZE' => 200,
'PAGE.DESCRIPTION_SIZE' => 200,
'POST.DESCRIPTION_SIZE' => 200,
'USER.DESCRIPTION_SIZE' => 200,
+ 'CATEGORY.SINGLE_REDIRECT' => FALSE,
'PAGE.SINGLE_REDIRECT' => FALSE,
'POST.SINGLE_REDIRECT' => FALSE,
'USER.SINGLE_REDIRECT' => FALSE,
@@ -98,6 +104,7 @@ foreach([
# Set default configuration (for admin prefixes)
#===============================================================================
foreach([
+ 'ADMIN.CATEGORY.LIST_SIZE' => 12, # for 1/2/3-column grid layout
'ADMIN.PAGE.LIST_SIZE' => 12, # for 1/2/3-column grid layout
'ADMIN.POST.LIST_SIZE' => 12, # for 1/2/3-column grid layout
'ADMIN.USER.LIST_SIZE' => Application::get('USER.LIST_SIZE'),
diff --git a/core/functions.php b/core/functions.php
index dadaab8..ea4b7eb 100644
--- a/core/functions.php
+++ b/core/functions.php
@@ -1,8 +1,9 @@
<?php
+use ORM\EntityInterface;
+use ORM\Entities\Category;
use ORM\Entities\Page;
use ORM\Entities\Post;
use ORM\Entities\User;
-use ORM\EntityInterface;
use Template\Template as Template;
use Template\Factory as TemplateFactory;
@@ -48,6 +49,37 @@ function generateUserNaviTemplate($current): Template {
#===============================================================================
# Helper function to reduce duplicate code
#===============================================================================
+function generateCategoryNaviTemplate($current): Template {
+ return generateNaviTemplate($current, Application::getCategoryURL(), 'Category');
+}
+
+#===============================================================================
+# Helper function to reduce duplicate code
+#===============================================================================
+function generateCategoryItemTemplate(Category $Category, bool $is_root = FALSE): Template {
+ $CategoryRepository = Application::getRepository('Category');
+ $PostRepository = Application::getRepository('Post');
+
+ foreach($CategoryRepository->findWithParents($Category->getID()) as $Category) {
+ $category_data = generateItemTemplateData($Category);
+ $category_list[] = $category_data;
+ }
+
+ $Template = TemplateFactory::build('category/item');
+ $Template->set('IS_ROOT', $is_root);
+ $Template->set('CATEGORY', $category_data ?? []);
+ $Template->set('CATEGORIES', $category_list ?? []);
+ $Template->set('COUNT', [
+ 'POST' => $PostRepository->getCountByCategory($Category),
+ 'CHILDREN' => $CategoryRepository->getChildrenCount($Category)
+ ]);
+
+ return $Template;
+}
+
+#===============================================================================
+# Helper function to reduce duplicate code
+#===============================================================================
function generatePageItemTemplate(Page $Page, User $User): Template {
$Template = TemplateFactory::build('page/item');
$Template->set('PAGE', generateItemTemplateData($Page));
@@ -60,9 +92,18 @@ function generatePageItemTemplate(Page $Page, User $User): Template {
# Helper function to reduce duplicate code
#===============================================================================
function generatePostItemTemplate(Post $Post, User $User): Template {
+ $CategoryRepository = Application::getRepository('Category');
+
+ foreach($CategoryRepository->findWithParents($Post->get('category')) as $Category) {
+ $category_data = generateItemTemplateData($Category);
+ $categories[] = $category_data;
+ }
+
$Template = TemplateFactory::build('post/item');
$Template->set('POST', generateItemTemplateData($Post));
$Template->set('USER', generateItemTemplateData($User));
+ $Template->set('CATEGORY', $category_data ?? []);
+ $Template->set('CATEGORIES', $categories ?? []);
return $Template;
}
@@ -112,6 +153,24 @@ function generateItemTemplateData(EntityInterface $Entity): array {
}
#===============================================================================
+# Generate a nested tree from a category data array
+#===============================================================================
+function generateCategoryDataTree(array $category_data, $root = 0): array {
+ foreach($category_data as &$category){
+ $tree[intval($category['PARENT'])][] = &$category;
+ unset($category['PARENT']);
+ }
+
+ foreach($category_data as &$category){
+ if (isset($tree[$category['ID']])){
+ $category['CHILDS'] = $tree[$category['ID']];
+ }
+ }
+
+ return $tree[$root] ?? [];
+}
+
+#===============================================================================
# Generate pseudo GUID for entity
#===============================================================================
function generatePseudoGUID(EntityInterface $Entity) {
@@ -166,6 +225,9 @@ function parseContentTags(string $text): string {
#===============================================================================
function parseEntityContent(EntityInterface $Entity): string {
switch($class = get_class($Entity)) {
+ case 'ORM\Entities\Category':
+ $prefix = 'CATEGORY';
+ break;
case 'ORM\Entities\Page':
$prefix = 'PAGE';
break;
diff --git a/core/include/category/list.php b/core/include/category/list.php
new file mode 100644
index 0000000..9bd2a68
--- /dev/null
+++ b/core/include/category/list.php
@@ -0,0 +1,69 @@
+<?php
+#===============================================================================
+# Get instances
+#===============================================================================
+$Language = Application::getLanguage();
+
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+$PostRepository = Application::getRepository('Post');
+
+#===============================================================================
+# Pagination
+#===============================================================================
+$site_size = Application::get('CATEGORY.LIST_SIZE');
+$site_sort = Application::get('CATEGORY.LIST_SORT');
+
+#$count = $CategoryRepository->getCount(['parent' => NULL]);
+$count = $CategoryRepository->getCount();
+$lastSite = ceil($count / $site_size);
+
+$currentSite = HTTP::GET('site') ?? 1;
+$currentSite = intval($currentSite);
+
+if($currentSite < 1 OR ($currentSite > $lastSite AND $lastSite > 0)) {
+ Application::error404();
+}
+
+#===============================================================================
+# Single redirect
+#===============================================================================
+if(Application::get('CATEGORY.SINGLE_REDIRECT') === TRUE AND $count === 1) {
+ $Category = $CategoryRepository->getLast();
+ HTTP::redirect(Application::getEntityURL($Category));
+}
+
+#===============================================================================
+# Get paginated category list
+#===============================================================================
+$categories = $CategoryRepository->getPaginatedTree(
+ $site_size,
+ ($currentSite-1) * $site_size
+);
+
+foreach($categories as $Category) {
+ $templates[] = generateCategoryItemTemplate($Category, TRUE);
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$ListTemplate = Template\Factory::build('category/list');
+$ListTemplate->set('PAGINATION', [
+ 'THIS' => $currentSite,
+ 'LAST' => $lastSite,
+ 'HTML' => generateCategoryNaviTemplate($currentSite)
+]);
+$ListTemplate->set('LIST', [
+ 'CATEGORIES' => $templates ?? []
+]);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('HTML', $ListTemplate);
+$MainTemplate->set('HEAD', [
+ 'NAME' => $Language->text('title_category_overview', $currentSite)
+]);
+
+echo $MainTemplate;
diff --git a/core/include/category/main.php b/core/include/category/main.php
new file mode 100644
index 0000000..57f8625
--- /dev/null
+++ b/core/include/category/main.php
@@ -0,0 +1,134 @@
+<?php
+#===============================================================================
+# Get repositories
+#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
+$PostRepository = Application::getRepository('Post');
+$UserRepository = Application::getRepository('User');
+
+#===============================================================================
+# Try to find category (with parents) by slug URL or unique ID
+#===============================================================================
+if(Application::get('CATEGORY.SLUG_URLS')) {
+ if(!$categories = $CategoryRepository->findWithParentsBy('slug', $param)) {
+ if($categories = $CategoryRepository->findWithParents($param)) {
+ $redirect_scheduled = TRUE;
+ }
+ }
+}
+
+else {
+ if(!$categories = $CategoryRepository->findWithParents($param)) {
+ if($categories = $CategoryRepository->findWithParentsBy('slug', $param)) {
+ $redirect_scheduled = TRUE;
+ }
+ }
+}
+
+#===============================================================================
+# Throw 404 error if category (with parents) could not be found
+#===============================================================================
+if(!isset($categories)) {
+ Application::error404();
+}
+
+#===============================================================================
+# The last element represents the current category
+#===============================================================================
+$Category = $categories[array_key_last($categories)];
+
+#===============================================================================
+# If category with parents was found by alternative, redirect to correct URL
+#===============================================================================
+if(isset($redirect_scheduled)) {
+ HTTP::redirect(Application::getEntityURL($Category));
+}
+
+#===============================================================================
+# Generate category template data (including parents)
+#===============================================================================
+foreach($categories as $_Category) {
+ $category_list[] = generateItemTemplateData($_Category);
+}
+
+#===============================================================================
+# Define data variable for current category
+#===============================================================================
+$category_data = $category_list[array_key_last($category_list)];
+
+#===============================================================================
+# Generate category children list
+#===============================================================================
+$child_categories = $CategoryRepository->getAll(
+ ['parent' => $Category->getID()],
+ Application::get('CATEGORY.LIST_SORT')
+);
+
+foreach($child_categories as $ChildCategory) {
+ $child_templates[] = generateCategoryItemTemplate($ChildCategory);
+}
+
+#===============================================================================
+# Pagination (for posts in this category)
+#===============================================================================
+$site_size = Application::get('POST.LIST_SIZE');
+$site_sort = Application::get('POST.LIST_SORT');
+
+$count = $PostRepository->getCountByCategory($Category);
+$lastSite = ceil($count / $site_size);
+
+$currentSite = HTTP::GET('site') ?? 1;
+$currentSite = intval($currentSite);
+
+if($currentSite < 1 OR ($currentSite > $lastSite AND $lastSite > 0)) {
+ Application::error404();
+}
+
+#===============================================================================
+# Get paginated post list for this category
+#===============================================================================
+$posts = $PostRepository->getAll(
+ ['category' => $Category->getID()],
+ $site_sort,
+ ($currentSite-1) * $site_size.','.$site_size
+);
+
+foreach($posts as $Post) {
+ $User = $UserRepository->find($Post->get('user'));
+ $post_templates[] = generatePostItemTemplate($Post, $User);
+}
+
+#===============================================================================
+# Build document
+#===============================================================================
+$CategoryTemplate = Template\Factory::build('category/main');
+$CategoryTemplate->set('CATEGORY', $category_data);
+$CategoryTemplate->set('CATEGORIES', $category_list ?? []);
+$CategoryTemplate->set('COUNT', [
+ 'POST' => $PostRepository->getCountByCategory($Category),
+ 'CHILDREN' => $CategoryRepository->getChildrenCount($Category)
+]);
+$CategoryTemplate->set('PAGINATION', [
+ 'THIS' => $currentSite,
+ 'LAST' => $lastSite,
+ 'HTML' => generatePostNaviTemplate($currentSite)
+]);
+$CategoryTemplate->set('LIST', [
+ 'POSTS' => $post_templates ?? [],
+ 'CATEGORIES' => $child_templates ?? []
+]);
+
+$MainTemplate = Template\Factory::build('main');
+$MainTemplate->set('TYPE', 'CATEGORY');
+$MainTemplate->set('CATEGORY', $category_data);
+$MainTemplate->set('CATEGORIES', $category_list ?? []);
+$MainTemplate->set('HTML', $CategoryTemplate);
+$MainTemplate->set('HEAD', [
+ 'NAME' => $category_data['ATTR']['NAME'],
+ 'DESC' => description($category_data['BODY']['HTML'](),
+ Application::get('CATEGORY.DESCRIPTION_SIZE')),
+ 'PERM' => $category_data['URL'],
+ 'OG_IMAGES' => $category_data['FILE']['LIST']
+]);
+
+echo $MainTemplate;
diff --git a/core/include/post/main.php b/core/include/post/main.php
index 86008f6..bfccc7b 100644
--- a/core/include/post/main.php
+++ b/core/include/post/main.php
@@ -2,6 +2,7 @@
#===============================================================================
# Get repositories
#===============================================================================
+$CategoryRepository = Application::getRepository('Category');
$PostRepository = Application::getRepository('Post');
$UserRepository = Application::getRepository('User');
@@ -50,11 +51,27 @@ if($NextPost = $PostRepository->findNext($Post)) {
}
#===============================================================================
+# Generate category template data (including parents)
+#===============================================================================
+foreach($CategoryRepository->findWithParents($Post->get('category')) as $Category) {
+ $category_list[] = generateItemTemplateData($Category);
+}
+
+#===============================================================================
+# Define data variable for current category
+#===============================================================================
+if(isset($category_list)) {
+ $category_data = $category_list[array_key_last($category_list)];
+}
+
+#===============================================================================
# Build document
#===============================================================================
$PostTemplate = Template\Factory::build('post/main');
$PostTemplate->set('POST', $post_data);
$PostTemplate->set('USER', $user_data);
+$PostTemplate->set('CATEGORY', $category_data ?? []);
+$PostTemplate->set('CATEGORIES', $category_list ?? []);
$MainTemplate = Template\Factory::build('main');
$MainTemplate->set('TYPE', 'POST');
diff --git a/core/namespace/Application.php b/core/namespace/Application.php
index 2329253..5690841 100644
--- a/core/namespace/Application.php
+++ b/core/namespace/Application.php
@@ -141,6 +141,13 @@ class Application {
}
#===============================================================================
+ # Return absolute category URL
+ #===============================================================================
+ public static function getCategoryURL($more = ''): string {
+ return self::getURL(self::get('CATEGORY.DIRECTORY')."/{$more}");
+ }
+
+ #===============================================================================
# Return absolute post URL
#===============================================================================
public static function getPostURL($more = ''): string {
@@ -188,6 +195,9 @@ class Application {
#===============================================================================
public static function getEntityURL(EntityInterface $Entity) {
switch($class = get_class($Entity)) {
+ case 'ORM\Entities\Category':
+ $attr = self::get('CATEGORY.SLUG_URLS') ? 'slug' : 'id';
+ return self::getCategoryURL($Entity->get($attr).'/');
case 'ORM\Entities\Page':
$attr = self::get('PAGE.SLUG_URLS') ? 'slug' : 'id';
return self::getPageURL($Entity->get($attr).'/');
diff --git a/core/namespace/ORM/Entities/Category.php b/core/namespace/ORM/Entities/Category.php
new file mode 100644
index 0000000..17560a7
--- /dev/null
+++ b/core/namespace/ORM/Entities/Category.php
@@ -0,0 +1,14 @@
+<?php
+namespace ORM\Entities;
+use ORM\Entity;
+
+class Category extends Entity {
+ protected $id = FALSE;
+ protected $parent = FALSE;
+ protected $slug = FALSE;
+ protected $name = FALSE;
+ protected $body = FALSE;
+ protected $argv = FALSE;
+ protected $time_insert = FALSE;
+ protected $time_update = FALSE;
+}
diff --git a/core/namespace/ORM/Entities/Post.php b/core/namespace/ORM/Entities/Post.php
index e6bff36..080f01f 100644
--- a/core/namespace/ORM/Entities/Post.php
+++ b/core/namespace/ORM/Entities/Post.php
@@ -3,12 +3,13 @@ namespace ORM\Entities;
use ORM\Entity;
class Post extends Entity {
- protected $id = FALSE;
- protected $user = FALSE;
- protected $slug = FALSE;
- protected $name = FALSE;
- protected $body = FALSE;
- protected $argv = FALSE;
+ protected $id = FALSE;
+ protected $user = FALSE;
+ protected $category = FALSE;
+ protected $slug = FALSE;
+ protected $name = FALSE;
+ protected $body = FALSE;
+ protected $argv = FALSE;
protected $time_insert = FALSE;
protected $time_update = FALSE;
}
diff --git a/core/namespace/ORM/Repositories/Category.php b/core/namespace/ORM/Repositories/Category.php
new file mode 100644
index 0000000..bd7d060
--- /dev/null
+++ b/core/namespace/ORM/Repositories/Category.php
@@ -0,0 +1,138 @@
+<?php
+namespace ORM\Repositories;
+use ORM\Repository;
+use ORM\EntityInterface;
+use ORM\Entities\Category as CategoryEntity;
+
+class Category extends Repository {
+ public static function getTableName(): string { return 'category'; }
+ public static function getClassName(): string { return 'ORM\Entities\Category'; }
+
+ #===============================================================================
+ # Find category with parents based on primary key
+ #===============================================================================
+ public function findWithParents($id): array {
+ return $this->findWithParentsBy('id', $id);
+ }
+
+ #===============================================================================
+ # Find category with parents based on specific field comparison
+ #===============================================================================
+ public function findWithParentsBy(string $field, $value): array {
+ $query = 'WITH RECURSIVE tree AS (
+ SELECT *, 0 AS _depth FROM %s WHERE %s %s UNION
+ SELECT c.*, _depth+1 FROM %s c, tree WHERE tree.parent = c.id
+ ) SELECT * FROM tree ORDER BY _depth DESC';
+
+ $table = static::getTableName();
+ $check = is_null($value) ? 'IS NULL': '= ?';
+ $query = sprintf($query, $table, $field, $check, $table);
+
+ $Statement = $this->Database->prepare($query);
+ $Statement->execute([$value]);
+
+ # TODO: Virtual column _depth shall not be fetched into the entity class
+ if($entities = $Statement->fetchAll($this->Database::FETCH_CLASS, static::getClassName())) {
+ $this->storeMultipleInstances($entities);
+ return $entities;
+ }
+
+ return [];
+ }
+
+ #===============================================================================
+ # Get paginated category tree list
+ #===============================================================================
+ public function getPaginatedTree(int $limit, int $offset = 0): array {
+ $query = 'WITH RECURSIVE tree AS (
+ SELECT *, name AS _depth FROM %s WHERE parent IS NULL UNION
+ SELECT c.*, CONCAT(_depth, "/", c.name) AS _depth FROM %s c INNER JOIN tree ON tree.id = c.parent
+ ) SELECT * FROM tree ORDER BY _depth %s';
+
+ $_limit = "LIMIT $limit";
+
+ if($offset) {
+ $_limit = "LIMIT $offset,$limit";
+ }
+
+ $table = static::getTableName();
+ $query = sprintf($query, $table, $table, $_limit);
+
+ $Statement = $this->Database->prepare($query);
+ $Statement->execute();
+
+ if($entities = $Statement->fetchAll($this->Database::FETCH_CLASS, static::getClassName())) {
+ $this->storeMultipleInstances($entities);
+ return $entities;
+ }
+
+ return [];
+ }
+
+ #===============================================================================
+ # Get children count of $Category
+ #===============================================================================
+ public function getChildrenCount(CategoryEntity $Category): int {
+ $query = 'WITH RECURSIVE tree AS (
+ SELECT * FROM %s WHERE id = ? UNION
+ SELECT c.* FROM %s c, tree WHERE tree.id = c.parent
+ ) SELECT COUNT(id) FROM tree WHERE id != ?';
+
+ $query = sprintf($query,
+ static::getTableName(),
+ static::getTableName()
+ );
+
+ $Statement = $this->Database->prepare($query);
+ $Statement->execute([$Category->getID(), $Category->getID()]);
+
+ return $Statement->fetchColumn();
+ }
+
+ #===============================================================================
+ # Update category (and check for parent/child circular loops)
+ #===============================================================================
+ public function update(EntityInterface $Entity): bool {
+ # Entity parent might have changed *in memory*, so we re-fetch the original
+ # parent of the entity from the database and save it in a variable.
+ # TODO: Repository/Entity class should have a mechanism to detect changes!
+ $query = 'SELECT parent FROM %s WHERE id = ?';
+ $query = sprintf($query, static::getTableName());
+
+ $Statement = $this->Database->prepare($query);
+ $Statement->execute([$Entity->getID()]);
+
+ $parent = $Statement->fetchColumn();
+
+ # If parent is unchanged, circular loop check is not needed.
+ if($parent === $Entity->get('parent')) {
+ return parent::update($Entity);
+ }
+
+ $query = 'SELECT parent FROM %s WHERE id = ?';
+ $query = sprintf($query, static::getTableName());
+
+ $Statement = $this->Database->prepare($query);
+ $_parent = $Entity->get('parent');
+
+ # Fetch the parent of the *new* parent category and let the while loop run through
+ # the tree until either a parent of "NULL" was found or if the new parent category
+ # is a *child* of the *current* category which would cause a circular loop.
+ while($Statement->execute([$_parent]) && $_parent = $Statement->fetchColumn()) {
+ if($_parent == $Entity->get('id')) {
+ # Set parent of the *new* parent category to the *original* parent category
+ # of the *current* category (one level up) to prevent a circular loop.
+ $query = 'UPDATE %s SET parent = ? WHERE id = ?';
+ $query = sprintf($query, static::getTableName());
+
+ $UpdateStatement = $this->Database->prepare($query);
+ $UpdateStatement->execute([$parent, $Entity->get('parent')]);
+ break;
+ } else if($_parent === NULL) {
+ break;
+ }
+ }
+
+ return parent::update($Entity);
+ }
+}
diff --git a/core/namespace/ORM/Repositories/Post.php b/core/namespace/ORM/Repositories/Post.php
index 8eac12f..d6328e6 100644
--- a/core/namespace/ORM/Repositories/Post.php
+++ b/core/namespace/ORM/Repositories/Post.php
@@ -2,6 +2,7 @@
namespace ORM\Repositories;
use ORM\Repository;
use ORM\Entities\User;
+use ORM\Entities\Category;
class Post extends Repository {
public static function getTableName(): string { return 'post'; }
@@ -16,4 +17,15 @@ class Post extends Repository {
return $Statement->fetchColumn();
}
+
+ # TODO: This only gets the count of the direct category, not its children
+ public function getCountByCategory(Category $Category): int {
+ $query = 'SELECT COUNT(id) FROM %s WHERE category = ?';
+ $query = sprintf($query, static::getTableName());
+
+ $Statement = $this->Database->prepare($query);
+ $Statement->execute([$Category->getID()]);
+
+ return $Statement->fetchColumn();
+ }
}
diff --git a/index.php b/index.php
index 70cd831..977ed35 100644
--- a/index.php
+++ b/index.php
@@ -7,6 +7,7 @@ require 'core/application.php';
#===============================================================================
# Item base directory paths
#===============================================================================
+$CATEGORYPATH = Application::get('CATEGORY.DIRECTORY');
$PAGEPATH = Application::get('PAGE.DIRECTORY');
$POSTPATH = Application::get('POST.DIRECTORY');
$USERPATH = Application::get('USER.DIRECTORY');
@@ -14,6 +15,7 @@ $USERPATH = Application::get('USER.DIRECTORY');
#===============================================================================
# ROUTE: Item
#===============================================================================
+Router::add("{$CATEGORYPATH}/([^/]+)/", function($param) { require 'core/include/category/main.php'; });
Router::add("{$PAGEPATH}/([^/]+)/", function($param) { require 'core/include/page/main.php'; });
Router::add("{$POSTPATH}/([^/]+)/", function($param) { require 'core/include/post/main.php'; });
Router::add("{$USERPATH}/([^/]+)/", function($param) { require 'core/include/user/main.php'; });
@@ -21,6 +23,7 @@ Router::add("{$USERPATH}/([^/]+)/", function($param) { require 'core/include/use
#===============================================================================
# ROUTE: Item overview
#===============================================================================
+Router::add("{$CATEGORYPATH}/", function() { require 'core/include/category/list.php'; });
Router::add("{$PAGEPATH}/", function() { require 'core/include/page/list.php'; });
Router::add("{$POSTPATH}/", function() { require 'core/include/post/list.php'; });
Router::add("{$USERPATH}/", function() { require 'core/include/user/list.php'; });
@@ -28,6 +31,7 @@ Router::add("{$USERPATH}/", function() { require 'core/include/user/list.php'; }
#===============================================================================
# REDIRECT: Item (trailing slash)
#===============================================================================
+Router::addRedirect("{$CATEGORYPATH}/([^/]+)", Application::getCategoryURL('$1/'));
Router::addRedirect("{$PAGEPATH}/([^/]+)", Application::getPageURL('$1/'));
Router::addRedirect("{$POSTPATH}/([^/]+)", Application::getPostURL('$1/'));
Router::addRedirect("{$USERPATH}/([^/]+)", Application::getUserURL('$1/'));
@@ -35,6 +39,7 @@ Router::addRedirect("{$USERPATH}/([^/]+)", Application::getUserURL('$1/'));
#===============================================================================
# REDIRECT: Item overview (trailing slash)
#===============================================================================
+Router::addRedirect("{$CATEGORYPATH}", Application::getCategoryURL());
Router::addRedirect("{$PAGEPATH}", Application::getPageURL());
Router::addRedirect("{$POSTPATH}", Application::getPostURL());
Router::addRedirect("{$USERPATH}", Application::getUserURL());
diff --git a/theme/admin/html/category/delete.php b/theme/admin/html/category/delete.php
new file mode 100644
index 0000000..635e9a8
--- /dev/null
+++ b/theme/admin/html/category/delete.php
@@ -0,0 +1,6 @@
+<main id="main-content">
+<h1><i class="fa fa-trash-o"></i><?=$Language->text('delete_category')?></h1>
+<p><?=$Language->text('delete_category_desc')?></p>
+
+<?=$HTML?>
+</main>
diff --git a/theme/admin/html/category/form.php b/theme/admin/html/category/form.php
new file mode 100644
index 0000000..2cb7b20
--- /dev/null
+++ b/theme/admin/html/category/form.php
@@ -0,0 +1,112 @@
+<?php
+function categorySelectList($category_tree, $selected = NULL, $current = NULL, $prefix = '') {
+ foreach($category_tree as $category) {
+ $option = '<option value="%s"%s>%s%s [%d]</option>';
+ $select = ($category['ID'] == $selected) ? ' selected' : '';
+ $select = ($category['ID'] == $current) ? ' disabled' : $select;
+
+ printf($option, $category['ID'], $select, $prefix, escapeHTML($category['NAME']), $category['ID']);
+
+ if(isset($category['CHILDS'])) {
+ # If there are children, call self and pass children array.
+ (__FUNCTION__)($category['CHILDS'], $selected, $current, $prefix.'– ');
+ }
+ }
+}
+?>
+<?php if($FORM['INFO']): ?>
+ <div id="message-list-wrapper">
+ <ul id="message-list">
+ <?php foreach($FORM['INFO'] as $message): ?>
+ <li><?=$message?></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+<?php endif ?>
+
+<form action="" method="POST">
+ <input type="hidden" name="token" value="<?=$FORM['TOKEN']?>" />
+
+<?php if($FORM['TYPE'] !== 'DELETE'): ?>
+ <div class="form-grid">
+ <label for="form_name">
+ <i class="fa fa-tag"></i><?=$Language->text('label_name')?></label>
+
+ <div class="form-grid-item first">
+ <input id="form_name" name="name" value="<?=escapeHTML($FORM['DATA']['NAME'])?>" />
+ </div>
+
+ <label for="form_slug">
+ <i class="fa fa-link"></i><?=$Language->text('label_slug')?></label>
+
+ <div class="form-grid-item">
+ <input id="form_slug" name="slug" value="<?=escapeHTML($FORM['DATA']['SLUG'])?>" />
+ </div>
+
+ <label for="form_category_parent">
+ <i class="fa fa-tags"></i><?=$Language->text('label_category_parent')?></label>
+
+ <div class="form-grid-item">
+ <select id="form_category_parent" name="parent">
+ <option value="">[ –– <?=$Language->text('label_category')?> –– ]</option>
+ <?=categorySelectList($FORM['CATEGORY_TREE'], $FORM['DATA']['PARENT'], $FORM['DATA']['ID']);?>
+ </select>
+ </div>
+
+ <label for="form_time_insert">
+ <i class="fa fa-clock-o"></i><?=$Language->text('label_insert')?></label>
+
+ <div class="form-grid-item">
+ <input id="form_time_insert" name="time_insert" placeholder="YYYY-MM-DD HH:II:SS" value="<?=escapeHTML($FORM['DATA']['TIME_INSERT'])?>" />
+ </div>
+
+ <label for="form_time_update">
+ <i class="fa fa-pencil"></i><?=$Language->text('label_update')?></label>
+
+ <div class="form-grid-item">
+ <input id="form_time_update" name="time_update" placeholder="<?=escapeHTML($FORM['DATA']['TIME_UPDATE'] ?: 'CURRENT_TIMESTAMP')?>" value="" />
+ </div>
+ </div>
+
+ <div id="content-editor-wrapper" class="form-border-box">
+ <div id="button-list-wrapper">
+ <ul id="markdown-list" class="button-list markdown">
+ <li data-markdown="bold" class="fa fa-bold" title="<?=$Language->text('markdown_bold')?>"></li>
+ <li data-markdown="italic" class="fa fa-italic" title="<?=$Language->text('markdown_italic')?>"></li>
+ <li data-markdown="heading" class="fa fa-header" title="<?=$Language->text('markdown_heading')?>"></li>
+ <li data-markdown="link" class="fa fa-link" title="<?=$Language->text('markdown_link')?>"></li>
+ <li data-markdown="image" class="fa fa-picture-o" title="<?=$Language->text('markdown_image')?>"></li>
+ <li data-markdown="code" class="fa fa-code" title="<?=$Language->text('markdown_code')?>"></li>
+ <li data-markdown="quote" class="fa fa-quote-right" title="<?=$Language->text('markdown_quote')?>"></li>
+ <li data-markdown="list_ul" class="fa fa-list-ul" title="<?=$Language->text('markdown_list_ul')?>"></li>
+ <li data-markdown="list_ol" class="fa fa-list-ol" title="<?=$Language->text('markdown_list_ol')?>"></li>
+ </ul>
+ </div>
+ <textarea id="content-editor" name="body" placeholder="[…]"><?=escapeHTML($FORM['DATA']['BODY'])?></textarea>
+ </div>
+ <div id="emoticon-list-wrapper" class="form-border-box background padding">
+ <ul id="emoticon-list" class="button-list emoticons">
+ <?php foreach(getUnicodeEmoticons() as $emoticon => $explanation):?>
+ <li data-emoticon="<?=$emoticon?>" title="<?=$explanation?>"><?=$emoticon?></li>
+ <?php endforeach; ?>
+ </ul>
+ </div>
+ <div class="form-border-box background padding">
+ <input id="form_argv" name="argv" maxlength="250" placeholder="[ARGUMENT_ONE=foo|ARGUMENT_TWO=bar …]" value="<?=escapeHTML($FORM['DATA']['ARGV'])?>" />
+ </div>
+<?php else: ?>
+ <div class="form-border-box background padding">
+ <?=$HTML?>
+ </div>
+<?php endif; ?>
+
+ <div class="form-border-box background padding">
+ <?php if($FORM['TYPE'] === 'INSERT'): ?>
+ <input id="insert-button" type="submit" name="insert" value="<?=$Language->text('insert')?>" />
+ <?php elseif($FORM['TYPE'] === 'UPDATE'): ?>
+ <input id="update-button" type="submit" name="update" value="<?=$Language->text('update')?>" />
+ <?php elseif($FORM['TYPE'] === 'DELETE'): ?>
+ <input id="delete-button" type="submit" name="delete" value="<?=$Language->text('delete')?>" data-text="<?=$Language->text('sure')?>" />
+ <?php endif; ?>
+ </div>
+</form>
diff --git a/theme/admin/html/category/index.php b/theme/admin/html/category/index.php
new file mode 100644
index 0000000..e2c35a4
--- /dev/null
+++ b/theme/admin/html/category/index.php
@@ -0,0 +1,15 @@
+<main id="main-content" class="wide">
+<h1><i class="fa fa-tags"></i><?=$Language->text('category_overview')?></h1>
+<p class="actions-before"><?=$Language->text('overview_category_desc')?></p>
+<ul class="actions">
+ <li><a href="<?=Application::getAdminURL('category/insert.php')?>" title="<?=$Language->text('insert_category')?>"><i class="fa fa-plus"></i><?=$Language->text('insert')?></a></li>
+</ul>
+
+<div class="item-container category grid">
+ <?php foreach($LIST['CATEGORIES'] as $category): ?>
+ <?php echo $category; ?>
+ <?php endforeach; ?>
+</div>
+
+<?=$PAGINATION['HTML']?>
+</main>
diff --git a/theme/admin/html/category/insert.php b/theme/admin/html/category/insert.php
new file mode 100644
index 0000000..be9fdfe
--- /dev/null
+++ b/theme/admin/html/category/insert.php
@@ -0,0 +1,6 @@
+<main id="main-content">
+<h1><i class="fa fa-pencil-square-o"></i><?=$Language->text('insert_category')?></h1>
+<p><?=$Language->text('insert_category_desc')?></p>
+
+<?=$HTML?>
+</main>
diff --git a/theme/admin/html/category/item.php b/theme/admin/html/category/item.php
new file mode 100644
index 0000000..05212e7
--- /dev/null
+++ b/theme/admin/html/category/item.php
@@ -0,0 +1,33 @@
+<article class="item">
+ <header>
+ <h2><i class="fa fa-tag"></i><?=escapeHTML($CATEGORY['ATTR']['NAME'])?></h2>
+ <div>
+ <span class="brackets item-id">#<?=$CATEGORY['ATTR']['ID']?></span>
+ <time class="brackets" datetime="<?=$CATEGORY['ATTR']['TIME_INSERT']?>"><?=parseDatetime($CATEGORY['ATTR']['TIME_INSERT'], $Language->text('date_format'))?></time>
+ <span class="brackets"><?=$Language->text('posts')?>: <?=$COUNT['POST']?></span>
+ <span class="brackets"><?=$Language->text('categories')?>: <?=$COUNT['CHILDREN']?></span>
+ </div>
+ </header>
+ <blockquote cite="<?=$CATEGORY['URL']?>">
+ <?php if(isset($CATEGORY['FILE']['LIST'][0])): ?>
+ <img class="item-image" src="<?=$CATEGORY['FILE']['LIST'][0]?>" alt="" />
+ <?php endif; ?>
+ <p><?=excerpt($CATEGORY['BODY']['HTML']())?></p>
+ </blockquote>
+
+ <?php if($CATEGORY['ARGV']): ?>
+ <ul class="arguments">
+ <?php foreach($CATEGORY['ARGV'] as $argument => $value): ?>
+ <li><strong><?=$argument?>:</strong> <span><?=escapeHTML($value)?></span></li>
+ <?php endforeach; ?>
+ </ul>
+ <?php endif; ?>
+
+ <footer>
+ <ul>
+ <li><a href="<?=$CATEGORY['URL']?>" target="_blank" title="<?=$Language->text('select_category')?>"><i class="fa fa-external-link"></i><span class="hidden"><?=$Language->text('select_category')?></span></a></li>
+ <li><a href="<?=Application::getAdminURL("category/update.php?id={$CATEGORY['ATTR']['ID']}")?>" title="<?=$Language->text('update_category')?>"><i class="fa fa-pencil-square-o"></i><span class="hidden"><?=$Language->text('update_category')?></span></a></li>
+ <li><a href="<?=Application::getAdminURL("category/delete.php?id={$CATEGORY['ATTR']['ID']}")?>" title="<?=$Language->text('delete_category')?>"><i class="fa fa-trash-o"></i><span class="hidden"><?=$Language->text('delete_category')?></span></a></li>
+ </ul>
+ </footer>
+</article>
diff --git a/theme/admin/html/category/update.php b/theme/admin/html/category/update.php
new file mode 100644
index 0000000..fefaa18
--- /dev/null
+++ b/theme/admin/html/category/update.php
@@ -0,0 +1,6 @@
+<main id="main-content">
+<h1><i class="fa fa-pencil-square-o"></i><?=$Language->text('update_category')?></h1>
+<p><?=$Language->text('update_category_desc')?></p>
+
+<?=$HTML?>
+</main>
diff --git a/theme/admin/html/main.php b/theme/admin/html/main.php
index 9a5de23..45cdc41 100644
--- a/theme/admin/html/main.php
+++ b/theme/admin/html/main.php
@@ -26,6 +26,7 @@
<?php if(Application::isAuthenticated()): ?>
<li><a href="<?=Application::getAdminURL()?>" title="<?=$Language->text('overview_dashboard_text')?>"><i class="fa fa-dashboard"></i><span>Dashboard</span></a></li>
<li><a href="<?=Application::getAdminURL('post/')?>" title="<?=$Language->text('post_overview')?>"><i class="fa fa-newspaper-o"></i><span><?=$Language->text('posts')?></span></a></li>
+ <li><a href="<?=Application::getAdminURL('category/')?>" title="<?=$Language->text('category_overview')?>"><i class="fa fa-tags"></i><span><?=$Language->text('categories')?></span></a></li>
<li><a href="<?=Application::getAdminURL('page/')?>" title="<?=$Language->text('page_overview')?>"><i class="fa fa-folder-open"></i><span><?=$Language->text('pages')?></span></a></li>
<li><a href="<?=Application::getAdminURL('user/')?>" title="<?=$Language->text('user_overview')?>"><i class="fa fa-users"></i><span><?=$Language->text('users')?></span></a></li>
<li><a href="<?=Application::getAdminURL('database.php')?>" title="<?=$Language->text('overview_database_text')?>"><i class="fa fa-database"></i><span><?=$Language->text('overview_database_text')?></span></a></li>
diff --git a/theme/admin/html/post/form.php b/theme/admin/html/post/form.php
index 346d6f4..4ae07bd 100644
--- a/theme/admin/html/post/form.php
+++ b/theme/admin/html/post/form.php
@@ -1,3 +1,18 @@
+<?php
+function categorySelectList($category_tree, $selected = NULL, $prefix = '') {
+ foreach($category_tree as $category) {
+ $option = '<option value="%s"%s>%s%s [%d]</option>';
+ $select = ($category['ID'] == $selected) ? ' selected' : '';
+
+ printf($option, $category['ID'], $select, $prefix, escapeHTML($category['NAME']), $category['ID']);
+
+ if(isset($category['CHILDS'])) {
+ # If there are children, call self and pass children array.
+ (__FUNCTION__)($category['CHILDS'], $selected, $prefix.'– ');
+ }
+ }
+}
+?>
<?php if($FORM['INFO']): ?>
<div id="message-list-wrapper">
<ul id="message-list">
@@ -16,7 +31,7 @@
<label for="form_name">
<i class="fa fa-newspaper-o"></i><?=$Language->text('label_name')?></label>
- <div class="form-grid-item first">
+ <div class="form-grid-item">
<input id="form_name" name="name" value="<?=escapeHTML($FORM['DATA']['NAME'])?>" />
</div>
@@ -27,6 +42,16 @@
<input id="form_slug" name="slug" value="<?=escapeHTML($FORM['DATA']['SLUG'])?>" />
</div>
+ <label for="form_category">
+ <i class="fa fa-tag"></i><?=$Language->text('label_category')?></label>
+
+ <div class="form-grid-item">
+ <select id="form_category" name="category">
+ <option value="">[ –– <?=$Language->text('label_category')?> –– ]</option>
+ <?=categorySelectList($FORM['CATEGORY_TREE'], $FORM['DATA']['CATEGORY']);?>
+ </select>
+ </div>
+
<label for="form_user">
<i class="fa fa-user"></i><?=$Language->text('label_user')?></label>
diff --git a/theme/admin/html/post/item.php b/theme/admin/html/post/item.php
index ae06752..41991d5 100644
--- a/theme/admin/html/post/item.php
+++ b/theme/admin/html/post/item.php
@@ -4,6 +4,9 @@
<div>
<span class="brackets item-id">#<?=$POST['ATTR']['ID']?></span>
<a class="brackets" href="<?=Application::getAdminURL("user/update.php?id={$USER['ATTR']['ID']}")?>" title="<?=$Language->text('update_user')?>"><?=escapeHTML($USER['ATTR']['FULLNAME'])?></a>
+ <?php if($CATEGORY): ?>
+ <a class="brackets" href="<?=Application::getAdminURL("category/update.php?id={$CATEGORY['ATTR']['ID']}")?>" title="<?=$Language->text('update_category')?>"><?=escapeHTML($CATEGORY['ATTR']['NAME'])?></a>
+ <?php endif ?>
<time class="brackets" datetime="<?=$POST['ATTR']['TIME_INSERT']?>"><?=parseDatetime($POST['ATTR']['TIME_INSERT'], $Language->text('date_format'))?></time>
</div>
</header>
diff --git a/theme/default/html/category/item.php b/theme/default/html/category/item.php
new file mode 100644
index 0000000..9435581
--- /dev/null
+++ b/theme/default/html/category/item.php
@@ -0,0 +1,27 @@
+<?php
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# Category Item Template [Thomas Lange <code@nerdmind.de>] #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# #
+# [see documentation] #
+# #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+?>
+<article class="item">
+ <header>
+ <h2 class="fa fa-tag">
+ <a title="<?=$Language->text('select_category')?>: »<?=escapeHTML($CATEGORY['ATTR']['NAME'])?>«" href="<?=$CATEGORY['URL']?>">
+ <?=escapeHTML($CATEGORY['ATTR']['NAME'])?>
+ </a>
+ </h2>
+ <span class="brackets info">
+ <?=$Language->text('posts')?>: <?=$COUNT['POST']?> –
+ <?=$Language->text('categories')?>: <?=$COUNT['CHILDREN']?>
+ </span>
+ </header>
+ <?php if($IS_ROOT): ?>
+ <blockquote cite="<?=$CATEGORY['URL']?>">
+ <?=$CATEGORY['BODY']['HTML']()?>
+ </blockquote>
+ <?php endif ?>
+</article>
diff --git a/theme/default/html/category/list.php b/theme/default/html/category/list.php
new file mode 100644
index 0000000..9fffb2b
--- /dev/null
+++ b/theme/default/html/category/list.php
@@ -0,0 +1,23 @@
+<?php
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# Category List Template [Thomas Lange <code@nerdmind.de>] #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# #
+# [see documentation] #
+# #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+?>
+<h1><i class="fa fa-tags"></i><?=$Language->text('category_overview')?></h1>
+<p><?=$Language->text('category_list_title', $PAGINATION['THIS'])?></p>
+
+<?php if($LIST['CATEGORIES']): ?>
+ <div class="item-container category">
+ <?php foreach($LIST['CATEGORIES'] as $category): ?>
+ <?php echo $category; ?>
+ <?php endforeach; ?>
+ </div>
+<?php else: ?>
+ <p><?=$Language->text('category_list_empty')?></p>
+<?php endif ?>
+
+<?=$PAGINATION['HTML']?>
diff --git a/theme/default/html/category/main.php b/theme/default/html/category/main.php
new file mode 100644
index 0000000..86b50e0
--- /dev/null
+++ b/theme/default/html/category/main.php
@@ -0,0 +1,44 @@
+<?php
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# Category Main Template [Thomas Lange <code@nerdmind.de>] #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+# #
+# [see documentation] #
+# #
+#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
+?>
+<h1 id="category-heading"><i class="fa fa-tag"></i><?=escapeHTML($CATEGORY['ATTR']['NAME'])?></h1>
+<?php if($CATEGORIES && array_pop($CATEGORIES)): ?>
+ <ul class="category-heading-list">
+ <?php foreach($CATEGORIES as $category): ?>
+ <li><a href="<?=$category['URL']?>"><?=escapeHTML($category['ATTR']['NAME'])?></a></li>
+ <?php endforeach ?>
+ </ul>
+<?php endif ?>
+
+<div id="category-body">
+ <?=$CATEGORY['BODY']['HTML']()?>
+</div>
+
+<?php if(!empty($LIST['CATEGORIES'])): ?>
+ <div class="item-container category">
+ <?php foreach($LIST['CATEGORIES'] as $category): ?>
+ <?php echo $category; ?>
+ <?php endforeach; ?>
+ </div>
+<?php endif ?>
+
+<h2 id="category-posts-heading"><i class="fa fa-newspaper-o"></i><?=$Language->text('posts')?></h2>
+<div id="category-posts-page"><?=$Language->text('category_posts_page', $PAGINATION['THIS'])?></div>
+<?php if($LIST['POSTS']): ?>
+ <div class="item-container post">
+ <?php foreach($LIST['POSTS'] as $post): ?>
+ <?php echo $post; ?>
+ <?php endforeach; ?>
+ </div>
+ <?=$PAGINATION['HTML']?>
+<?php else: ?>
+ <div class="item-container post">
+ <?=$Language->text('category_empty')?>
+ </div>
+<?php endif ?>
diff --git a/theme/default/html/main.php b/theme/default/html/main.php
index e84618b..5a067ef 100644
--- a/theme/default/html/main.php
+++ b/theme/default/html/main.php
@@ -74,6 +74,11 @@ $BLOGMETA_DESC = escapeHTML($BLOGMETA['DESC']);
</a>
</li>
<li>
+ <a href="<?=Application::getCategoryURL()?>" title="<?=$Language->text('category_overview')?>">
+ <i class="fa fa-tags"></i><?=$Language->text('categories')?>
+ </a>
+ </li>
+ <li>
<a href="<?=Application::getPageURL()?>" title="<?=$Language->text('page_overview')?>">
<i class="fa fa-file-text-o"></i><?=$Language->text('pages')?>
</a>
diff --git a/theme/default/html/post/main.php b/theme/default/html/post/main.php
index 4b84ed9..328a614 100644
--- a/theme/default/html/post/main.php
+++ b/theme/default/html/post/main.php
@@ -17,6 +17,14 @@ $time = "<time datetime=\"{$POST['ATTR']['TIME_INSERT']}\" title=\"".parseDateti
<?=$POST['BODY']['HTML']()?>
</div>
+<?php if($CATEGORIES): ?>
+ <ul class="category-list fa fa-tag">
+ <?php foreach($CATEGORIES as $category): ?>
+ <li><a href="<?=$category['URL']?>"><?=escapeHTML($category['ATTR']['NAME'])?></a></li>
+ <?php endforeach ?>
+ </ul>
+<?php endif ?>
+
<section id="site-navi">
<?php if($POST['PREV']): ?>
diff --git a/theme/default/rsrc/css/main.css b/theme/default/rsrc/css/main.css
index 11cae12..e49412b 100644
--- a/theme/default/rsrc/css/main.css
+++ b/theme/default/rsrc/css/main.css
@@ -281,9 +281,11 @@ a.brackets:before, a.brackets:after {
overflow: hidden;
margin: 0.5rem 0;
}
+.item-container {
+ clear: both;
+}
.item header {
padding: 0.25rem 1rem;
- border-bottom: 0.05rem solid #AAA;
overflow: hidden;
text-transform: uppercase;
}
@@ -303,6 +305,7 @@ a.brackets:before, a.brackets:after {
margin: 0;
padding: 0 1rem;
font-family: inherit;
+ border-top: 0.05rem solid #AAA;
}
.item blockquote img {
display: block;
@@ -322,6 +325,49 @@ a.brackets:before, a.brackets:after {
}
/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+# Category list
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
+#category-heading,
+#category-posts-heading {
+ float: left;
+}
+
+#category-posts-page {
+ float: right;
+ font-size: 0.6rem;
+}
+
+#category-body {
+ clear: both;
+}
+
+.category-list,
+.category-heading-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font-size: 0.6rem;
+ text-align: center;
+}
+.category-list li,
+.category-heading-list li {
+ display: inline-block;
+}
+.category-list li + li:before,
+.category-heading-list li + li:before {
+ content: " ➜ ";
+}
+.category-list a,
+.category-heading-list a {
+ text-decoration: underline;
+}
+
+.category-heading-list {
+ text-align: left;
+ float: right;
+}
+
+/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Responsive
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
#toogle-nav {
diff --git a/theme/default/rsrc/css/main.scss b/theme/default/rsrc/css/main.scss
index ba6e823..4ea6a2b 100644
--- a/theme/default/rsrc/css/main.scss
+++ b/theme/default/rsrc/css/main.scss
@@ -303,9 +303,12 @@ a.brackets:before, a.brackets:after {
overflow: hidden;
margin: 0.5rem 0;
+ &-container {
+ clear: both;
+ }
+
header {
padding: 0.25rem 1rem;
- border-bottom: 0.05rem solid #AAA;
overflow: hidden;
text-transform: uppercase;
@@ -329,6 +332,7 @@ a.brackets:before, a.brackets:after {
margin: 0;
padding: 0 1rem;
font-family: inherit;
+ border-top: 0.05rem solid #AAA;
img {
display: block;
@@ -351,6 +355,50 @@ a.brackets:before, a.brackets:after {
}
/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+# Categories
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
+#category-heading,
+#category-posts-heading {
+ float: left;
+}
+
+#category-posts-page {
+ float: right;
+ font-size: 0.6rem;
+}
+
+#category-body {
+ clear: both;
+}
+
+.category-list,
+.category-heading-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font-size: 0.6rem;
+ text-align: center;
+
+ li {
+ display: inline-block;
+
+ + li:before {
+ content: ' ➜ ';
+ }
+ }
+
+ a {
+ text-decoration: underline;
+ }
+}
+
+.category-heading-list {
+
+ text-align: left;
+ float: right;
+}
+
+/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Responsive
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
#toogle-nav {