summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/app/controllers/admin/Course_admin.php267
-rw-r--r--public/app/controllers/cms/Course.php38
-rw-r--r--public/app/extends/Cache_service.php33
-rw-r--r--public/app/models/cms/Course_model.php22
-rw-r--r--public/app/routes/backend.php85
-rw-r--r--public/app/routes/frontend.php8
-rw-r--r--public/app/views/admin/admin-menu.php2
-rw-r--r--public/app/views/admin/edit_page.php139
-rw-r--r--public/app/views/admin/pages.php109
-rw-r--r--public/app/views/components/footer.php240
-rw-r--r--public/app/views/components/lessons_list.php49
-rw-r--r--public/app/views/error/general.php3
-rw-r--r--public/app/views/templates/course.php11
-rw-r--r--public/app/views/templates/lesson.php10
-rw-r--r--public/app/views/templates/page.php64
-rw-r--r--public/app/views/welcome.php11
-rw-r--r--public/assets/css/class.css34
-rw-r--r--public/assets/css/overides.css27
-rw-r--r--public/assets/js/admin/edit_lesson.js149
-rw-r--r--public/assets/js/admin/edit_page.js382
-rw-r--r--public/assets/js/admin/pages.js240
-rw-r--r--public/assets/js/lesson-features.js55
-rw-r--r--tests/code/menu.php42
23 files changed, 1849 insertions, 171 deletions
diff --git a/public/app/controllers/admin/Course_admin.php b/public/app/controllers/admin/Course_admin.php
index 72ddc79..f68ead2 100644
--- a/public/app/controllers/admin/Course_admin.php
+++ b/public/app/controllers/admin/Course_admin.php
@@ -168,11 +168,12 @@ class Course_admin {
);
if (isset($post['media'])) {
- // update media's privileges
+ // update media's privileges; NOTE: media table
self::update_medias_privileges($post['media'], $post['privilege_id']);
- // set lesson's media files
- self::create_medias_for_lesson($post['media'], $id, $post['privilege_id']);
+ // set lesson's media files; NOTE: lesson_media table
+ // ERROR: self::create_medias_for_lesson($post['media'], $id, $post['privilege_id']);
+ self::create_medias_for_lesson($post['media'], $id);
}
return true;
@@ -226,41 +227,13 @@ class Course_admin {
if (isset($post['media'])) {
// update lesson's media files
- self::create_medias_for_lesson($post['media'], $post['id'], $post['privilege_id']);
+ // ERROR: self::create_medias_for_lesson($post['media'], $post['id'], $post['privilege_id']);
+ self::create_medias_for_lesson($post['media'], $post['id']);
}
return true;
}
- /** update medias privileges
- *
- * @param $medias (array) : array of media id(s)
- * @param $privilege (int) : privilege id
- * @return true (always)
- */
- private static function update_medias_privileges($medias, $privilege)
- {
- // create WHERE IN (LIST) holders and params for prepare statement
- $params = [ ':privilege' => $privilege];
- $holders = [];
- foreach($medias as $key => $media) {
- $params[':id'.$key] = $media;
- $holders[] = ':id'.$key;
- }
- $list = '('. implode(',', $holders) .')';
-
- // print_r(['params' => $params, 'list' => $list]); die();
-
- Registry::use('database')->runQuery(
- "UPDATE media SET privilege_id = :privilege
- WHERE id IN {$list}",
- $params
- );
-
- return true;
- }
-
-
## -------------------------------------------------------------------------
##
@@ -271,6 +244,7 @@ class Course_admin {
/** files
* echo all files
+ *
* @return (array)
*/
public static function files()
@@ -434,11 +408,12 @@ class Course_admin {
}
- /** create media for post
+ /** create media for lesson
+ *
+ * links lesson to each media-file of the media `id`s array
*
- * links post to each media-file of the media `id`s array
* @param $media (array): a list of media-file `id`s
- * @param $post_id (int)
+ * @param $lesson_id (int)
*/
private static function create_medias_for_lesson($medias, $lesson_id)
{
@@ -449,6 +424,22 @@ class Course_admin {
}
+ /** create media for page
+ *
+ * links page to each media-file of the media `id`s array
+ *
+ * @param $media (array): a list of media-file `id`s
+ * @param $page_id (int)
+ */
+ private static function create_medias_for_page($medias, $page_id)
+ {
+ foreach($medias as $key => $medi) {
+ self::link_media_to_page($medi, $page_id); // link to page
+ }
+ return true;
+ }
+
+
/** link one media-file to a specific post
*
* NOTE:
@@ -471,5 +462,209 @@ class Course_admin {
return true;
}
+ /** link one media-file to a specific page
+ *
+ * NOTE:
+ * the method does not check if media is linked already
+ * so be sure that the pair of (media_id, page_id) not exist
+ *
+ * @param $media_id (int)
+ * @param $page_id (int)
+ */
+ private static function link_media_to_page( $media_id, $page_id )
+ {
+ Registry::use('database')->runQuery(
+ "INSERT INTO page_media (page_id, media_id)
+ VALUES (:page, :media)",
+ [
+ 'page' => $page_id,
+ 'media' => $media_id,
+ ]
+ );
+ return true;
+ }
+
+
+ /** update medias privileges
+ *
+ * UPDATE media SET privige WHERE IN {list}
+ *
+ * @param $medias (array) : array of media id(s)
+ * @param $privilege (int) : privilege id
+ * @return true (always)
+ */
+ private static function update_medias_privileges($medias, $privilege)
+ {
+ // create WHERE IN (LIST) holders and params for prepare statement
+ $params = [ ':privilege' => $privilege];
+ $holders = [];
+ foreach($medias as $key => $media) {
+ $params[':id'.$key] = $media;
+ $holders[] = ':id'.$key;
+ }
+ $list = '('. implode(',', $holders) .')';
+
+ // print_r(['params' => $params, 'list' => $list]); die();
+
+ Registry::use('database')->runQuery(
+ "UPDATE media SET privilege_id = :privilege
+ WHERE id IN {$list}",
+ $params
+ );
+
+ return true;
+ }
+
+
+
+
+
+
+ ## -------------------------------------------------------------------------
+ ##
+ ## PAGE METHODS
+ ##
+ ## -------------------------------------------------------------------------
+
+
+ /** all pages
+ *
+ */
+ public static function all_pages()
+ {
+ Render::json(Registry::use('database')->runQuery(
+ "SELECT * FROM page", []
+ ));
+ }
+
+
+ /** get_page
+ *
+ * @param $id (int) : page's id
+ */
+ public static function get_page($id)
+ {
+ // get (first) page with id = $id; render as json
+ Render::json(Registry::use('database')->query(
+ "SELECT page.*,
+ ( SELECT
+ CONCAT('[', GROUP_CONCAT(JSON_OBJECT(
+ 'id', media.id,
+ 'label', media.label,
+ 'type', media.type,
+ 'path', media.path )),
+ ']')
+ FROM media
+ LEFT JOIN page_media ON page_media.media_id = media.id
+ WHERE page_media.page_id = :id
+ ) AS medias_json
+ FROM page
+ WHERE page.id = :id",
+ [ ':id' => $id]
+ )->getFirst());
+ }
+
+
+ /** add_page
+ * insert a new page
+ *
+ * all parametres are passed via $_POST array
+ *
+ * POST @param title
+ * POST @param body
+ * POST @param status (int): 0 = draft , 1 = published
+ */
+ public static function add_page()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // insert page content into daabase
+ $id = Registry::use('database')->query(
+ "INSERT INTO page (title, `body`, `status`)
+ VALUES (:title, :body, :status)",
+ [
+ ':title' => $post['title'],
+ ':body' => $post['body'],
+ 'status' => $post['status']
+ ]
+ )->lastInsertID();
+
+
+ if (isset($post['media'])) {
+ // update media's privileges;
+ // NOTE: media table; pages have public files (privilege_id=0)
+ self::update_medias_privileges($post['media'], 0);
+
+ // set page's media files
+ // NOTE: page_media table
+ self::create_medias_for_page($post['media'], $id);
+ }
+
+ self::update_pages_cache(); // update pages cache
+
+ return true;
+ }
+
+
+ /** update_page
+ *
+ * POST @param id (int)
+ * POST @param title
+ * POST @param body
+ * POST @param status (int): 0 = draft , 1 = published
+ */
+ public static function update_page()
+ {
+ $post = Registry::get('REQUEST')->POST;
+
+ // print_r($post); die();
+
+ // update page's content
+ Registry::use('database')->query(
+ "UPDATE page
+ SET title = :title, `body` = :body, `status` = :status
+ WHERE id = :id",
+ [
+ ':id' => $post['id'],
+ ':title' => $post['title'],
+ ':body' => $post['body'],
+ ':status' => $post['status']
+ ]
+ );
+
+ if (isset($post['media'])) {
+ // update media's privileges
+ self::update_medias_privileges($post['media'], 0);
+ }
+
+ // remove all old page's links to media files
+ Registry::use('database')->runQuery(
+ "DELETE from page_media WHERE page_id = :page",
+ [ ':page' => $post['id'] ]
+ );
+
+ if (isset($post['media'])) {
+ // update page's media files
+ self::create_medias_for_page($post['media'], $post['id']);
+ }
+
+ self::update_pages_cache(); // update pages cache
+
+ return true;
+ }
+
+
+ /** update pages cache
+ * --- -- -- - - -
+ * Forces re-caching of pages tree
+ * Shall run if anything changes to courses
+ *
+ * @param void
+ * @return (array): ['tree' => ..., 'breadcrumbs' => ... ]
+ */
+ public static function update_pages_cache()
+ {
+ return Cache_service::pages_list(PROXY_IGNORE_CACHE);
+ }
} \ No newline at end of file
diff --git a/public/app/controllers/cms/Course.php b/public/app/controllers/cms/Course.php
index a1de9c1..c795ab9 100644
--- a/public/app/controllers/cms/Course.php
+++ b/public/app/controllers/cms/Course.php
@@ -131,7 +131,9 @@ class Course {
'categories' => $tree,
'breadcrumbs' => $breadcrumbs,
'lessons' => $lessons,
- 'course_path' => $course_path
+ 'course_path' => $course_path,
+ // needed by footer
+ 'entity' => Cache_service::pages_list()
]);
}
@@ -174,6 +176,12 @@ class Course {
}
$course_path[] = intval($course_id);
+ // NOTE: CRITICAL:
+ // do not pass Class::method directly to the eported variables
+ // $entity = Cache_service::pages_list();
+
+ // var_dump($entity); die();
+
// then Render
// --- -- -- - - -
Render::view('templates/lesson', [
@@ -185,7 +193,9 @@ class Course {
// needed for side panel and breadcrunbs
'categories' => $tree,
'breadcrumbs' => $breadcrumbs,
- 'course_path' => $course_path
+ 'course_path' => $course_path,
+ // needed by footer
+ 'entity' => Cache_service::pages_list()
]);
} else { // user is not authorized
@@ -202,7 +212,31 @@ class Course {
}
+ /** lesson
+ *
+ * check if user is authorized to view the content;
+ * if so, prepare and render the lesson view
+ *
+ * @param $id : lesson id
+ */
+ public static function page($id)
+ {
+ $id = intval($id); // lesson id
+
+ $pages = Cache_service::pages_list();
+ $page = $pages[$id];
+
+ // then Render
+ // --- -- -- - - -
+ Render::view('templates/page', [
+ // main content
+ 'id' => $id,
+ 'page' => $page,
+ // needed by footer
+ 'entity' => $pages
+ ]);
+ }
diff --git a/public/app/extends/Cache_service.php b/public/app/extends/Cache_service.php
index 9aac1d0..52311f3 100644
--- a/public/app/extends/Cache_service.php
+++ b/public/app/extends/Cache_service.php
@@ -45,5 +45,38 @@ class Cache_service
);
}
+ public static function pages_list( $options = 0 )
+ {
+ return proxy(
+ [\app\models\cms\Course_model::class, 'pages'],
+ [], CACHE_ROOT_TTL,
+ $options
+ );
+ }
+
+
+ /** entity
+ *
+ * create and retrieve a cached array of some entity
+ *
+ * @param $entiry (string|method): method of the main CMS model
+ * NOTE: CRITICAL: method must exist in the main CMS model
+ */
+ public static function entity( $entity, $options = 0 )
+ {
+ $allowed_methods = [ // to make sure method existence
+ 'page',
+ 'course',
+ 'lesson',
+ ];
+
+ // TODO: needs testing before release
+ // return proxy(
+ // [\app\models\admin\Course_model::class, $entity],
+ // [], CACHE_ROOT_TTL,
+ // $options
+ // );
+ }
+
} \ No newline at end of file
diff --git a/public/app/models/cms/Course_model.php b/public/app/models/cms/Course_model.php
index 54f49cf..6614993 100644
--- a/public/app/models/cms/Course_model.php
+++ b/public/app/models/cms/Course_model.php
@@ -288,5 +288,27 @@ class Course_model {
}
+ /** pages
+ *
+ * return all pages as array;
+ * array key of each item shall be the page-id
+ *
+ * @param void
+ * @return array
+ */
+ public static function pages()
+ {
+ $result = [];
+ $pages = Registry::use('database')->runQuery("SELECT * FROM page", []);
+ foreach($pages as $key => $page) {
+ $result[ $page['id'] ] = [
+ 'title' => $page['title'],
+ 'body' => $page['body'],
+ 'status' => $page['status']
+ ];
+ }
+ // print_r($result); die();
+ return $result;
+ }
}
diff --git a/public/app/routes/backend.php b/public/app/routes/backend.php
index ed02295..2bbd056 100644
--- a/public/app/routes/backend.php
+++ b/public/app/routes/backend.php
@@ -31,18 +31,6 @@ Route::add('/admin/panel', function () { // admin panel #2
});
-// admin lessons
-////////////////////////////////////////////////////////////////////////////////
-
-Route::add('/admin/lessons', function () { // request admin-lessons page
- Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
- Render::view('admin/skeleton', [
- 'title' => 'Διαχείριση Μαθημάτων',
- 'action' => 'lessons',
- ]);
-});
-
-
// admin categories (=courses)
////////////////////////////////////////////////////////////////////////////////
@@ -55,8 +43,6 @@ Route::add('/admin/categories', function () { // manage categories page
]);
});
-
-
Route::add('/admin/api/categories', function () { // ajax: get all courses; with breadcrumbs
Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
Course_admin::breadcrumbs();
@@ -99,11 +85,17 @@ Route::add('/admin/api/privileges', function () {
-// admin lesson
+// admin lessons
////////////////////////////////////////////////////////////////////////////////
+Route::add('/admin/lessons', function () { // request admin-lessons page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Μαθημάτων',
+ 'action' => 'lessons',
+ ]);
+});
-// ---
Route::add('/admin/edit_lesson', function () { // new lesson form
Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
Render::view('admin/skeleton', [
@@ -130,13 +122,13 @@ Route::add('/admin/api/lesson/([0-9]*)', function ($id) { // ajax: get les
Route::add('/admin/api/lesson/add', function () { // ajax: add lesson
Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
- Course_admin::add_lesson();
+ Render::json(['success' => Course_admin::add_lesson()]);
}, 'post');
Route::add('/admin/api/lesson/update', function () { // ajax: update lesson
Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
- Course_admin::update_lesson();
+ Render::json(['success' => Course_admin::update_lesson()]);
}, 'post');
@@ -146,6 +138,7 @@ Route::add('/admin/api/lessons', function () { // ajax: get all lessons
});
+
// admin files
////////////////////////////////////////////////////////////////////////////////
@@ -170,3 +163,59 @@ Route::add('/admin/api/file_upload', function () {
Render::json(Course_admin::upload_file());
}, 'post');
+
+
+
+// admin page
+////////////////////////////////////////////////////////////////////////////////
+
+Route::add('/admin/pages', function () { // request admin-lessons page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Διαχείριση Σελίδων',
+ 'action' => 'pages',
+ ]);
+});
+
+
+Route::add('/admin/edit_page', function () { // new page form
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::view('admin/skeleton', [
+ 'title' => 'Νέα Σελίδα',
+ 'action' => 'edit_page',
+ ]);
+});
+
+Route::add('/admin/edit_page/([0-9]*)', function ($id) { // edit page form
+ Auth::allowRoles([1, 2, 3]);
+ Render::view('admin/skeleton', [
+ 'title' => 'Επεξεργασία Σελίδας',
+ 'action' => 'edit_page',
+ 'id' => intval($id)
+ ]);
+});
+
+
+Route::add('/admin/api/page/([0-9]*)', function ($id) { // ajax: get page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::get_page($id);
+});
+
+
+Route::add('/admin/api/page/add', function () { // ajax: add page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::add_page()]);
+}, 'post');
+
+
+Route::add('/admin/api/page/update', function () { // ajax: update page
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Render::json(['success' => Course_admin::update_page()]);
+}, 'post');
+
+
+Route::add('/admin/api/pages', function () { // ajax: get all pages
+ Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles
+ Course_admin::all_pages();
+});
+
diff --git a/public/app/routes/frontend.php b/public/app/routes/frontend.php
index 33fe401..601641f 100644
--- a/public/app/routes/frontend.php
+++ b/public/app/routes/frontend.php
@@ -12,7 +12,8 @@ use app\extends\Cache_service;
Route::add('/', function() {
Render::view('welcome', [
- 'categories' => Cache_service::courses_struct()['tree']
+ 'categories' => Cache_service::courses_struct()['tree'],
+ 'entity' => Cache_service::pages_list()
]);
// need redirect?... header('Location: /some/default/url', true, 302);
});
@@ -40,6 +41,11 @@ Route::add('/lesson/([0-9]*)', // request lesson by lesson-id
function($id) { Course::lesson($id); }
);
+Route::add('/page/([0-9]*)', // request lesson by lesson-id
+ function($id) { Course::page($id); }
+);
+
+
Route::add('/serve/file/([0-9a-zA-Z-_\.\/]*)', // serve file by path
function ($path) { Course::serve_file($path); } // (also ?type= is sent)
);
diff --git a/public/app/views/admin/admin-menu.php b/public/app/views/admin/admin-menu.php
index b783b39..52e8522 100644
--- a/public/app/views/admin/admin-menu.php
+++ b/public/app/views/admin/admin-menu.php
@@ -21,7 +21,7 @@ $admin_options = [
'edit_lesson' => 'Νέο Μάθημα',
'lessons' => 'Μαθήματα',
'categories' => 'Κεφάλαια',
- // 'pages' => 'Σελίδες',
+ 'pages' => 'Σελίδες',
'files' => 'Αρχεία',
'privileges' => 'Πρόσβαση',
'users' => 'Χρήστες'
diff --git a/public/app/views/admin/edit_page.php b/public/app/views/admin/edit_page.php
new file mode 100644
index 0000000..46c47bc
--- /dev/null
+++ b/public/app/views/admin/edit_page.php
@@ -0,0 +1,139 @@
+<?php
+ // some labels (if editing new or existing record)
+ // --- -- -- - - -
+ if ($id == 0) {
+ $action_title = "Δημιουργία Σελίδας";
+ $action_button = "Δημιουργία";
+
+ } else {
+ $action_title = "Επεξεργασία Σελίδας";
+ $action_button = "Αποθήκευση Αλλαγών";
+ }
+?>
+
+<!-- title bar -->
+<div class="title-bar">
+ <div><h5><?=$action_title?></h5></div>
+ <div>
+ <button type="button" class="btn pull-right"
+ data-bs-toggle="modal" data-bs-target="#editorModal" data-action="preview">
+ <i class="fas fa-search"></i> &nbsp; Προεπισκόπηση
+ </button>
+ </div>
+</div>
+
+<!-- manage -->
+<div class="manage edit-page">
+ <form>
+ <div class="row">
+
+
+ <div class="col col-8">
+
+ <!-- id (hidden) -->
+ <input type="hidden" name="id" value="0"><!-- action = (0) ? 'new' : 'edit' -->
+
+ <!--
+ <div class="row form-group">
+ <input type="date" name="date" value="" readonly="">
+ </div>
+ -->
+
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="title">Τίτλος</label>
+ <div class="col-sm-12">
+ <input class="form-control form-control-sm" type="text" name="title" value="" required="">
+ </div>
+ </div>
+
+ <!-- Post body -->
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="body">Κείμενο</label>
+ <div class="col-sm-12">
+ <textarea class="form-control form-control-sm" type="text" name="body" value="" required=""></textarea>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="col">
+
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="status">Κατάσταση</label>
+ <div class="col-sm-12">
+ <select class="form-control form-control-sm form-select form-select-sm" name="status" onchange="this.dataset.chosen = this.value;">
+ <option value="0">Αδημοσίευτο</option>
+ <option value="one">ΔΗΜΟΣΙΕΥΜΕΝΟ</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="media">Αρχεία</label>
+ <div class="col-sm-12 post-media">
+
+ <!-- Button trigger modal -->
+ <button type="button" class="btn btn-sm btn-success pull-right over-bar"
+ data-bs-toggle="modal" data-bs-target="#editorModal" data-action="upload_file">
+ <i class="fas fa-plus"></i> &nbsp; Προσθήκη
+ </button>
+
+ <ul id="files_list">
+ </ul>
+
+ </div>
+ </div>
+
+ <?php if ($id != 0) : ?>
+ <!--
+ <div class="row form-group">
+ <label class="control-label col-sm-12">Πληροφορίες</label>
+ <div class="col-sm-12" id="post-info">
+ </div>
+ </div>
+ -->
+ <?php endif; ?>
+
+ </div>
+
+ </div>
+
+ <div class="row">
+
+ <div class="col">
+ <hr/>
+ <!-- close | submit buttons -->
+
+ <div class="form-actions row justify-content-md-center">
+
+ <div class="col-3">
+ <button type="button" id="go-back" class="btn btn-light col-sm-12">
+ Επιστροφή
+ </button>
+ </div>
+
+ <div class="col-4">
+ <button class="btn btn-primary col-sm-12" type="submit">
+ <?=$action_button?>
+ </button>
+ </div>
+
+ </div>
+
+ </div>
+
+ </form>
+
+ </div>
+</div><!-- /manage -->
+
+
+<!-- MODAL for file uploads and preview -->
+<div class="modal fade" id="editorModal" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-dialog modal-lg" role="document">
+ <div class="modal-content">
+ <!-- content will be dynamic -->
+ </div>
+ </div>
+</div>
+
diff --git a/public/app/views/admin/pages.php b/public/app/views/admin/pages.php
new file mode 100644
index 0000000..dd8f954
--- /dev/null
+++ b/public/app/views/admin/pages.php
@@ -0,0 +1,109 @@
+ <!-- title bar -->
+ <div class="title-bar">
+ <div><h5>Διαχείριση Μαθημάτων</h5></div>
+ <div>
+ <!-- Button trigger modal -->
+ <a type="button" class="btn pull-right" href="/admin/edit_page">
+ <i class="fas fa-plus"></i> &nbsp; Νέα Σελίδα
+ </a>
+ </div>
+</div>
+
+<div class="manage">
+ <div class="row">
+ <div class="col-md-12">
+
+ <div id="pages">
+
+ <table id="dt-pages" class="table table-responsive dt-table" style="width:100%">
+ <thead>
+ <th class="dt-id">α/α</th>
+ <th class="dt-title">Τίτλος</th>
+ <th class="dt-actions"></th>
+ </thead>
+ </table>
+
+ </div>
+
+ </div>
+ </div>
+</div><!-- /manage -->
+
+
+
+<!-- MODAL for new/edit category -->
+<!-- NOTE: tabindex="-1" removed, ref:https://github.com/select2/select2-bootstrap-theme/issues/41 -->
+<div class="modal fade" id="managePages" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-dialog modal-lg" role="document">
+ <div class="modal-content">
+
+ <form method="post" action="">
+
+ <div class="modal-header">
+ <h4 class="modal-title" id="myModalLabel">Παράμετροι Μαθήματος</h4>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div><!-- /modal-header -->
+
+ <!-- modal-body -->
+ <div class="modal-body">
+
+ <!-- form fields -->
+
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="label">Τίτλος</label>
+ <div class="col-sm-12">
+ <input class="form-control form-control-sm" type="text" name="title" value="" required="">
+ </div>
+ </div>
+
+
+ <!-- one line / two parts; status: pull right -->
+ <div class="row">
+ <div class="col-md-8">
+ &nbsp<!-- nothing -->
+ </div>
+ <div class="col-md-4">
+
+ <!-- status: published|unpublished -->
+ <div class="row form-group">
+ <label class="control-label col-sm-12" for="status">Κατάσταση</label>
+ <div class="col-sm-12">
+ <select class="form-control form-control-sm form-select form-select-sm" name="status" onchange="this.dataset.chosen = this.value;">
+ <option value="0">Αδημοσίευτο</option>
+ <option value="one">ΔΗΜΟΣΙΕΥΜΕΝΟ</option>
+ </select>
+ </div>
+ </div>
+
+
+ </div>
+ </div>
+
+
+
+
+ <div class="row form-group">
+ <input type="hidden" name="id" value="0"><!-- page-id -->
+ </div>
+
+
+ </div><!-- /modal body -->
+
+ <div class="modal-footer">
+ <div class="row" style="width: 50%;">
+ <div class="form-actions">
+ <div class="col-4">
+ <button type="button" class="btn btn-default js-modal-close col-sm-12" data-bs-dismiss="modal">Κλείσιμο</button>
+ </div>
+ <div class="col-8">
+ <button class="btn btn-success col-sm-12" type="submit">Καταχώριση</button>
+ </div>
+ </div>
+ </div>
+ </div><!-- /modal-footer -->
+
+ </form><!-- /form -->
+
+ </div>
+ </div>
+</div>
diff --git a/public/app/views/components/footer.php b/public/app/views/components/footer.php
new file mode 100644
index 0000000..7cd4a5a
--- /dev/null
+++ b/public/app/views/components/footer.php
@@ -0,0 +1,240 @@
+<?php
+
+use app\extends\Cache_service;
+
+
+/** DESIGNERS
+ * -----------------------------------------------------------------------------
+ *
+ * structures that combine several design information
+ * in a json structrure and parses inta a quite complicated view;
+ *
+ * co-operate with pages
+ *
+ * can be used to rended menus, sections like header or footer etc;
+ *
+ * TODO:
+ * + embed it as a core \Render method
+ * + dynamic designers
+ * + recursive designers
+ *
+ * Here we implement a simplified version of the consept
+ * where entities shall always be `pages`
+ *
+ *
+ * PARAMETRES
+ * --- -- -- - - -
+ *
+ * primary properties of designer structure are 2 properties
+ *
+ * @var class (string): container class name
+ * @var struct (json array): representatoion of the "to-be-designed" structure
+ *
+ * TODO: support more complx `class` strings ex. `.container>#footer>row`
+ *
+ *
+ * Each sub-structure has 2-5 properties:
+ *
+ * @var title (string) : title of the sub-section
+ * @var entity (string) : type of entity (ex: page/lesson/etc; TODO: method of CMS_model?)
+ * @var list (array) : list of entity id(s)
+ * @var template (string) : template that is used to draw the listed pages' info
+ * @var width (string) : class that relates to sub-structure's width
+ *
+ * NOTE:
+ * designer parsing and rendering can be a quite expensive proccess
+ * thus it is strongly recommented to use cached entities
+ *
+ *
+ * Example structure:
+ {
+ "class" : "row", # container class
+ "struct" : [
+ { # block 1
+ "title" : "",
+ "entity" : "page",
+ "list" : [2, 1, 3], # pages to be listed
+ "template" : "list", # list page titles with link to the pages
+ "width" : "col-md-4"
+ },
+ {
+ "template" : "null", # list nothing
+ "width" : "col-md-3"
+ },
+ {
+ "entity" : "page",
+ "list" : [4], # list content of page 4;
+ "template" : "content", # set title to page-title
+ "width" : "col-md-5"
+ }
+
+ TODO: (wish)
+ ,{
+ "template": "struct",
+ "struct" : [
+ {
+ title, entity, list, template, width
+ },
+ ...
+ ]
+ }
+ ]
+ }
+ *
+ * brainstormin: (TODO:)
+ * use some document-describe stucture like the one on the pdfmake js-library
+ * -----------------------------------------------------------------------------
+ */
+
+
+/** check imported variables / data
+ * -----------------------------------------------------------------------------
+ */
+if (!isset($designer)) {
+
+ // default designer (and first case-study)
+ $designer = '{
+ "class" : "row",
+ "struct" : [
+ {
+ "title" : "Πληροφορίες",
+ "entity" : "page",
+ "list" : [2, 1, 3, 4],
+ "template" : "list",
+ "width" : "col-md-4 no-list-mark"
+ },
+ {
+ "entity" : "page",
+ "list" : [6],
+ "template" : "content",
+ "width" : "col-md-3 no-list-mark"
+ },
+ {
+ "entity" : "page",
+ "list" : [5],
+ "template" : "content",
+ "width" : "col-md-5 no-list-mark"
+ }
+ ]
+ }';
+}
+
+// $entity list shall be passed
+// entity link url shall be passed
+
+
+/** templating functions
+ * -----------------------------------------------------------------------------
+ */
+
+/** List template
+ * --- -- -- - - -
+ *
+ * lists entity titles with links
+ * in a `ul>li` html-structure
+ *
+ * @param $title
+ * @param $list
+ * @param $container
+ */
+function list_template($title, $list, $container, $entity) {
+
+ $result = '<div class="'. $container .'">
+ <h4>'. $title .'</h4>
+ <ul>';
+
+ foreach($list as $id) {
+
+ if ($entity[$id]['status'] == 1) { // if entity is published
+ $result .= '
+ <li>
+ <a href="/page/'. $id . '">
+ ' . $entity[$id]['title'] .'
+ </a>
+ </li>';
+ }
+
+ }
+ return $result . '</ul></div>';
+}
+
+/** content_template
+ *
+ * lists whole title and content of entity
+ *
+ * @param $list
+ * @param $container
+ */
+function content_template($list, $container, $entity) {
+
+ $parsedown = new Parsedown();
+
+ $result = '<div class="'. $container .'"><ul>';
+
+ foreach($list as $id) {
+
+ if ($entity[$id]['status'] == 1) { // if entity is published
+
+ $result .= '
+ <li>
+ <h4>'. $entity[$id]['title'] .'</h4>
+ <div>'. $parsedown->text($entity[$id]['body']) .'</div>
+ </li>';
+ }
+
+ }
+ return $result .'</ul></div>';
+}
+
+
+/** ready to start the proccessing
+ * -----------------------------------------------------------------------------
+ */
+
+// decode ...
+$parse = json_decode($designer);
+
+/** then start parse-proccessing ...
+ * -----------------------------------------------------------------------------
+ */
+?>
+
+
+
+<div class="<?=$parse->class?>">
+
+ <?php
+ foreach( $parse->struct as $section ) {
+
+ switch ($section->template) {
+
+ case 'list':
+ echo list_template(
+ $section->title,
+ $section->list,
+ $section->width,
+ $entity
+ );
+ break;
+
+ case 'content':
+ echo content_template(
+ $section->list,
+ $section->width,
+ $entity
+ );
+ break;
+
+ default: // ex. 'null'
+
+ echo '<div class="'. $section->width .'">
+ <h4>'. $section->title.'</h4>
+ </div>';
+ }
+
+ }
+ ?>
+
+</div>
+
+
diff --git a/public/app/views/components/lessons_list.php b/public/app/views/components/lessons_list.php
index 46c1f7e..a778d59 100644
--- a/public/app/views/components/lessons_list.php
+++ b/public/app/views/components/lessons_list.php
@@ -16,40 +16,39 @@
* -----------------------------------------------------------------------------
*/
?>
-<div class="row">
- <?php foreach($lessons as $post) : ?>
- <!-- <div class="col-xl-4 col-lg-6"> -->
- <div class="post-card">
+<?php foreach($lessons as $post) : ?>
- <?php if (in_array('date', $fieldset)) : ?>
- <?=$post['date']?>
- <?php endif; ?>
+ <!-- <div class="col-xl-4 col-lg-6"> -->
+ <div class="post-card">
- <div class="post-card--content">
+ <?php if (in_array('date', $fieldset)) : ?>
+ <?=$post['date']?>
+ <?php endif; ?>
- <h3>
- <a href="/lesson/<?=$post['id']?>"><?=$post['title']?></a>
- </h3>
+ <div class="post-card--content">
- <?php if (in_array('level', $fieldset)) : ?>
- <p class="getegory">(επίπεδο πρόσβασης <?=$post['privilege_id']?>)</p>
- <?php endif; ?>
+ <h3>
+ <a href="/lesson/<?=$post['id']?>"><?=$post['title']?></a>
+ </h3>
- <?php if (in_array('category', $fieldset)) : ?>
- <p class="category"><?=$post['course_id']?></p>
- <?php endif; ?>
+ <?php if (in_array('level', $fieldset)) : ?>
+ <p class="getegory">(επίπεδο πρόσβασης <?=$post['privilege_id']?>)</p>
+ <?php endif; ?>
- <?php if (in_array('intro', $fieldset) && $post['intro']) : ?>
- <p class="intro"><?=$post['intro']?></p>
- <?php endif; ?>
+ <?php if (in_array('category', $fieldset)) : ?>
+ <p class="category"><?=$post['course_id']?></p>
+ <?php endif; ?>
+
+ <?php if (in_array('intro', $fieldset) && $post['intro']) : ?>
+ <p class="intro"><?=$post['intro']?></p>
+ <?php endif; ?>
- </div>
+ </div>
- </div>
- <!-- </div> -->
+ </div>
+ <!-- </div> -->
- <?php endforeach; ?>
+<?php endforeach; ?>
-</div> \ No newline at end of file
diff --git a/public/app/views/error/general.php b/public/app/views/error/general.php
index 0a27e56..6ed984b 100644
--- a/public/app/views/error/general.php
+++ b/public/app/views/error/general.php
@@ -55,8 +55,7 @@
<div class='container'>
<div class='content'>
<h3><?= isset($title) ? $title : PAGE_404_TITLE ?></h3>
- Oops!<br />
- —
+ <br />
<br />
<span><?= $message ?? "" ?></span>
</div>
diff --git a/public/app/views/templates/course.php b/public/app/views/templates/course.php
index 39e177f..1d212b8 100644
--- a/public/app/views/templates/course.php
+++ b/public/app/views/templates/course.php
@@ -76,6 +76,17 @@
</div>
</div>
+ <footer>
+ <div class="container">
+
+ <?php // render footer
+ ////////////////////////////////////////////////////////////////
+ Render::view('components/footer', [ 'entity' => $entity ]);
+ ?>
+
+ </div>
+ </footer>
+
<script src="/assets/js/classroom.js"></script>
</body>
diff --git a/public/app/views/templates/lesson.php b/public/app/views/templates/lesson.php
index 3baafd0..0867cd3 100644
--- a/public/app/views/templates/lesson.php
+++ b/public/app/views/templates/lesson.php
@@ -65,6 +65,16 @@
</div>
</div>
+ <footer>
+ <div class="container">
+
+ <?php // render footer
+ ////////////////////////////////////////////////////////////////
+ Render::view('components/footer', [ 'entity' => $entity ]);
+ ?>
+
+ </div>
+ </footer>
<script src="/assets/js/classroom.js"></script>
<script src="/assets/js/lesson-features.js"></script>
diff --git a/public/app/views/templates/page.php b/public/app/views/templates/page.php
new file mode 100644
index 0000000..6afe45e
--- /dev/null
+++ b/public/app/views/templates/page.php
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title><?=SITE_TITLE?> - <?=$page['title']?></title>
+
+ <?php // header includes
+ ////////////////////////////////////////////////////////////////////////
+ Render::view('components/header_includes');
+ ?>
+
+</head>
+<body class="classroom">
+
+ <?php // top-bar
+ ////////////////////////////////////////////////////////////////////////
+ Render::view('components/top_bar', [
+ 'id' => 0,
+ 'label' => '',
+ 'parents' => []
+ ]);
+ ?>
+
+
+ <div class="main-content container page-container">
+
+
+ <div class="lesson-view">
+
+ <!-- page Title -->
+ <h3>
+ <?=$page['title']?>
+ </h3>
+
+ <!-- page body -->
+ <div class="article-body">
+ <?php
+
+ $parsedown = new Parsedown();
+ echo $parsedown->text($page['body']);
+
+ ?>
+ </div>
+
+
+ </div>
+
+ </div>
+
+ <footer>
+ <div class="container">
+
+ <?php // render footer
+ ////////////////////////////////////////////////////////////////
+ Render::view('components/footer', [ 'entity' => $entity ]);
+ ?>
+
+ </div>
+ </footer>
+
+ <script src="/assets/js/classroom.js"></script>
+ <script src="/assets/js/lesson-features.js"></script>
+</body>
+</html>
diff --git a/public/app/views/welcome.php b/public/app/views/welcome.php
index 08be0a2..c5552a8 100644
--- a/public/app/views/welcome.php
+++ b/public/app/views/welcome.php
@@ -54,6 +54,17 @@
--- */ ?>
+ <footer>
+ <div class="container">
+
+ <?php // render footer
+ ////////////////////////////////////////////////////////////////
+ Render::view('components/footer', [ 'entity' => $entity ]);
+ ?>
+
+ </div>
+ </footer>
+
<script src="/assets/js/classroom.js"></script>
</body>
diff --git a/public/assets/css/class.css b/public/assets/css/class.css
index cae509d..5ad26c4 100644
--- a/public/assets/css/class.css
+++ b/public/assets/css/class.css
@@ -57,7 +57,11 @@
-.classroom .main-content { padding: 1.5em; max-width: 1440px; }
+.classroom .main-content {
+ padding: 1.5em;
+ max-width: 1440px;
+ min-height: calc(100vh - 345px);
+}
/* category list menu
@@ -267,8 +271,8 @@
* -----------------------------------------------------------------------------
*/
.classroom .post-card {
- padding: .5em;
- margin: 1em;
+ padding-top: .75em;
+ margin: 0;
border-bottom: 1px solid #7773;
display: flex;
}
@@ -363,4 +367,26 @@
.classroom .article-body table tbody td {
padding: 6px 8px;
border-bottom: 1px solid var(--class-grey-background);
-} \ No newline at end of file
+}
+
+
+
+.classroom footer {
+ margin: 2em 0 0 0;
+ padding: 2.5em 0;
+ font-size: .87em;
+ background: #f5f6f7;
+}
+.classroom footer a {
+ color: #024;
+ text-decoration: none;
+ padding: 2px 36px 2px 4px;
+ border-radius: 3px;
+}
+.classroom footer a:hover { color: #fff; background: #369 ; }
+.classroom footer h4 { font-size: 20px; }
+.classroom footer .no-list-mark ul { padding-left: 0; margin-left: 0; list-style: none; }
+.classroom footer .no-list-mark ul li { margin: 0 0 4px 0; }
+
+
+.classroom .page-container { max-width: 1024px; } \ No newline at end of file
diff --git a/public/assets/css/overides.css b/public/assets/css/overides.css
index c0f6bd9..f9d351c 100644
--- a/public/assets/css/overides.css
+++ b/public/assets/css/overides.css
@@ -105,18 +105,28 @@ ul.select2-results__options li { font-size: 14px; padding: 4px 8px; }
/* ------------------------------------------------------------------------- end
*/
-.admin-container .edit-post textarea[name=body] { height: calc(100vh - 420px); } /* almost all avialable height */
+.admin-container .edit-post textarea[name=body] {
+ height: calc(100vh - 420px); /* almost all avialable height */
+}
+
+.admin-container .edit-page textarea[name=body] {
+ height: calc(100vh - 380px); /* almost all avialable height */
+}
+
.admin-container .edit-post textarea[name=intro] { height: 94px ;} /* almost 3 (half-)lines */
+.admin-container .edit-post textarea[name=body] {
+ height: calc(100vh - 420px); /* almost all avialable height */
+}
-.admin-container .edit-post .post-media button.over-bar { margin: -26px 0 0 0; float: right; }
-.admin-container .edit-post .post-media ul {
+.admin-container .post-media button.over-bar { margin: -26px 0 0 0; float: right; }
+.admin-container .post-media ul {
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px;
}
-.admin-container .edit-post .post-media ul li {
+.admin-container .post-media ul li {
border-bottom: 1px solid #ddd;
list-style-type: none;
padding: 3px 6px;
@@ -125,12 +135,13 @@ ul.select2-results__options li { font-size: 14px; padding: 4px 8px; }
justify-content: space-between;
align-items: center;
}
-.admin-container .edit-post .post-media ul li:last-child { border-bottom: none ;}
-.admin-container .edit-post .post-media ul li:hover { background: #eee; }
+.admin-container .post-media ul li:last-child { border-bottom: none ;}
+.admin-container .post-media ul li:hover { background: #eee; }
-.edit-post .post-media li .btn-xs { font-size: 14px; width: 24px; height: 24px; padding:0; }
+.admin-container .post-media li .btn-xs { font-size: 14px; width: 24px; height: 24px; padding:0; }
-.classroom .edit-post textarea[name=body] {
+.classroom .edit-post textarea[name=body],
+.classroom .edit-page textarea[name=body] {
min-height:320px;
font-family: Consolas, 'Ubuntu Mono','Courier New', Courier, monospace;
color: #333;
diff --git a/public/assets/js/admin/edit_lesson.js b/public/assets/js/admin/edit_lesson.js
index ee1b537..367c3b7 100644
--- a/public/assets/js/admin/edit_lesson.js
+++ b/public/assets/js/admin/edit_lesson.js
@@ -1,3 +1,9 @@
+/** NOTE:
+ * depricated code for tag-system is commented-out and kept in the source-code
+ * as case-study; the code is operational;
+ */
+
+
// Globals
// -----------------------------------------------------------------------------
@@ -23,9 +29,6 @@ var files = []; // array of { id:.., title:.., type:.., path:.. } records
// var tags = []; // array of integers
-const files_root = '/media/'; // files root firectory
-
-
// an alternative--id when a lesson_id not exists (needed for file-upload)
// ... consist of a 6-digit date string + random number up to 999
const yymmdd = new Date().toISOString().slice(2, 10).replaceAll('-','');
@@ -166,22 +169,27 @@ $(document).ready(function() {
});
- // // init tags (select2)
- // // ...
- // $('#tags').select2({
- // placeholder: 'Ετικέτες (tags/keywords)',
- // width: '100%',
- // tags: true
- // });
-
+ /** DEPRICATED: (tag system)
+ *
+ * // init tags (select2)
+ * // ...
+ * $('#tags').select2({
+ * placeholder: 'Ετικέτες (tags/keywords)',
+ * width: '100%',
+ * tags: true
+ * });
+ */
- // init editor (tiny MCE)
- // tinymce.init({
- // selector: 'form textarea[name=body]',
- // language: 'el',
- // menubar: true
- // });
- // --- -- -- - - -
+ /** DEPRICATED:
+ *
+ * // TINY-MCE EDITOR
+ * init editor (tiny MCE)
+ * tinymce.init({
+ * selector: 'form textarea[name=body]',
+ * language: 'el',
+ * menubar: true
+ * });
+ */
/** preload content
@@ -252,15 +260,18 @@ $(document).ready(function() {
});
categories.sort( compare_label ); // sort categories by breadcrumb
- // construct tags data
- // ---
- /// Object.keys(tags_response[0]).forEach( key => {
- /// tag = tags_response[0][key];
- /// tags.push({
- /// id: tag.id,
- /// label: tag.name
- /// });
- /// });
+ /** DEPRICATED: (tag system)
+ *
+ * // construct tags data
+ * // ---
+ * Object.keys(tags_response[0]).forEach( key => {
+ * tag = tags_response[0][key];
+ * tags.push({
+ * id: tag.id,
+ * label: tag.name
+ * });
+ * });
+ */
// construct provileges data
// ---
@@ -283,17 +294,21 @@ $(document).ready(function() {
// setup category_id options; select post's chosen category-id
set_parent_options( $('#course_id'), categories, lesson.course_id );
- // parse selected tags
- // ---
- /// var selected_tag_ids = [];
- /// if (post.tags_json != null) {
- /// var selected_tags = JSON.parse(post.tags_json);
- /// selected_tags.forEach( itag => { selected_tag_ids.push(itag.id); })
- /// }
+ /*** DEPRICATED: (tag system)
+ *
+ * // parse selected tags
+ * // ---
+ * var selected_tag_ids = [];
+ * if (post.tags_json != null) {
+ * var selected_tags = JSON.parse(post.tags_json);
+ * selected_tags.forEach( itag => { selected_tag_ids.push(itag.id); })
+ * }
+ */
+
// setup tags options; select post's chosen tags
set_parent_options( $('#privilege_id'), privileges, lesson.privilege_id );
- // ALSO: if tags selectd, show tags field
+ // DEPRICATED: ALSO: if tags selectd, show tags field
// if (selected_tag_ids.length != 0) { $('.optional label[for=tags]').addClass('show'); }
}
@@ -374,16 +389,20 @@ $(document).ready(function() {
$('.edit-post form').submit( event => {
event.preventDefault();
- // // rearrange tags to old and new ones
- // // ---
- // var old_tags = [];
- // var new_tags = [];
- // var tags = $('#tags').select2('data');
- // tags.forEach( t => {
- // if (!parseInt(t.id)) {
- // new_tags.push(t.text);
- // } else { old_tags.push(t.id); }
- // })
+ /** DEPRICATED:
+ *
+ ### TAG SYSTEM
+ # // rearrange tags to old and new ones
+ # // ---
+ # var old_tags = [];
+ # var new_tags = [];
+ # var tags = $('#tags').select2('data');
+ # tags.forEach( t => {
+ # if (!parseInt(t.id)) {
+ # new_tags.push(t.text);
+ # } else { old_tags.push(t.id); }
+ # })
+ */
var attachments = [];
// attachments (array) of "`media_id`;`reference`" strings
@@ -471,25 +490,27 @@ $(document).ready(function() {
-
- // show|hide file as referece button
- // --- -- -- - - -
- $('.admin-container').on('click', 'button.js-reference', function (e) {
- var id = $(this).data('id');
-
- // toggle reference attribute of item with `id` = id
- // ---
- var temp = [];
- files.forEach( it => {
- if (it.id == id) {
- it.reference = (it.reference==0) ? 1 : 0;
- }
- temp.push(it)
- });
-
- files = temp; // re-define files
- draw_files_list(); // then re-draw files list
- });
+ /** DEPRICATED:
+ *
+ # // show|hide file as referece button
+ # // --- -- -- - - -
+ # $('.admin-container').on('click', 'button.js-reference', function (e) {
+ # var id = $(this).data('id');
+ #
+ # // toggle reference attribute of item with `id` = id
+ # // ---
+ # var temp = [];
+ # files.forEach( it => {
+ # if (it.id == id) {
+ # it.reference = (it.reference==0) ? 1 : 0;
+ # }
+ # temp.push(it)
+ # });
+ #
+ # files = temp; // re-define files
+ # draw_files_list(); // then re-draw files list
+ # });
+ */
diff --git a/public/assets/js/admin/edit_page.js b/public/assets/js/admin/edit_page.js
new file mode 100644
index 0000000..ec0e84e
--- /dev/null
+++ b/public/assets/js/admin/edit_page.js
@@ -0,0 +1,382 @@
+// Globals
+// -----------------------------------------------------------------------------
+
+// NOTE:
+// Variable `entity_id` is defined before evaluating this script
+// id (int) : the id of edited post (or 0 if new post)
+
+var page;
+
+var id = (entity_id == 0) ? false : entity_id;
+
+var files = []; // array of { id:.., title:.., type:.., path:.. } records
+
+// TODO: support tags
+// var tags = []; // array of integers
+
+
+
+// an alternative--id when a page_id not exists (needed for file-upload)
+// ... consist of a 6-digit date string + random number up to 999
+const yymmdd = new Date().toISOString().slice(2, 10).replaceAll('-','');
+var rnd999 = Math.floor(Math.random() * 1000);
+var altID = (id == 0) ? ( yymmdd +'-'+ rnd999.toString() ) : id;
+
+
+
+
+// Supplamentary functions
+// -----------------------------------------------------------------------------
+
+// function for sorting an array by 'label' attribute
+// --- -- -- - - -
+function compare_label (a,b) {
+ if ( a.label < b.label ) { return -1; }
+ if ( a.label > b.label ) { return 1; }
+ return 0;
+}
+
+// return course record by course-id
+// --- -- -- - - -
+function course_record(id) {
+ var record = 0;
+ categories.forEach(el => { if (el.id == id) record = el; });
+
+ return record;
+}
+
+
+
+
+// Modal HTML Creators
+// -----------------------------------------------------------------------------
+
+const modal_header = (title) => {
+ return [
+ '<div class="modal-header">',
+ '<h4 class="modal-title" id="myModalLabel">',
+ title,
+ '</h4>',
+ '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">',
+ '<span aria-hidden="true"></span>',
+ '</button>',
+ '</div>'
+ ].join('\n');
+}
+
+
+const upload_file_form = () => {
+ return [
+ '<form method="POST" id="file_upload_form" name="upload_file_form">',
+
+ '<div class="modal-body">',
+
+ '<input type="hidden" name="folder" value="'+ altID +'">',
+
+ '<div class="form-group">',
+ '<label for="title">Τίτλος/Περιγραφή για το Αρχείο</label>',
+ '<input type="text" class="form-control " value="" name="title" id="title" placeholder="Τίτλος για το Αρχείο" required="">',
+ '</div>',
+
+ '<div class="form-group">',
+ '<label class="control-label" for="file">Εισαγωγή Αρχείου</label>',
+ '<input class="form-control" type="file" id="file" name="file" required="">',
+ '</div>',
+
+ '</div>',
+
+ '<div class="modal-footer">',
+ '<div class="row" style="width: 50%;">',
+ '<div class="form-actions">',
+ '<div class="col col-4">',
+ '<buton type="button" class="btn btn-default js-modal-close col-12" data-bs-dismiss="modal">',
+ 'Κλείσιμο',
+ '</button>',
+ '</div>',
+ '<div class="col-8">',
+ '<button class="btn btn-success col-12" type="submit">Καταχώριση</button>',
+ '</div>',
+ '</div>',
+ '</div>',
+ '</div>',
+
+ '</form>'
+ ].join('\n');
+}
+
+
+const preview_article = (title, body) => {
+ return [
+ '<div class="modal-body">',
+ '<div class="post">',
+
+ '<h2>'+ title +'</h2>',
+
+ '<div class="article-body">',
+ marked.parse(body, { sanitize: true }),
+ '</div>',
+
+ '</div>',
+ '</div>',
+ '<div class="modal-footer">',
+ '<div class="row" style="width: 50%;">',
+ '<div class="form-actions">',
+ '<div class="col-md-4 col-md-offset-4 col-6 col-offset-3">',
+ '<buton type="button" class="btn btn-default js-modal-close col-12" data-bs-dismiss="modal">',
+ 'Κλείσιμο',
+ '</button>',
+ '</div>',
+ '</div>',
+ '</div>',
+ '</div>'
+ ].join('\n');
+}
+
+
+$(document).ready(function() {
+
+ // When document is ready
+ // -------------------------------------------------------------------------
+
+ /** preload content
+ * --- -- -- - - -
+ * get page
+ * update content (form-elements)
+ */
+ if (id) { // if an `id` is given, load post .. then init_form
+
+ $.getJSON( "/admin/api/page/" + id )
+ .done( function (data) {
+ page = data; // keep post
+
+ // update fields' values
+ $('input[name=id]').val(page.id); // id
+ $('input[name=title]').val(page.title); // title
+ $('textarea[name=body]').val(page.body); // body
+ $('select[name=status]').val(
+ (page.status == 0) ? '0' : "one"
+ ).trigger('change');
+
+ // $('#post-info').html('Δημιουργία: '+post.creation_date+'<br>Τελευταία Ενημέρωση: '+ post.update_date);
+
+ // parse media files; then draw
+ if (page.medias_json != null) {
+ files = JSON.parse(page.medias_json);
+ }
+ draw_files_list(); // then draw the files list
+
+ });
+
+ }
+
+
+ /** draw_files_list
+ * --- -- -- - - -
+ */
+ function draw_files_list() {
+ var li_list = [];
+ var ref_icon, ref_class;
+
+ files.forEach( item => {
+ var copy = [ // copy button
+ '<button type="button" class="btn btn-xs btn-default js-copy"',
+ ' data-url="' + item.path +'"',
+ ' data-type="' + item.type + '"',
+ '>',
+ '<i class="far fa-clone"></i>',
+ '</button>'
+ ].join('');
+
+ var del = [ // delete button
+ '<button type="button" class="btn btn-xs btn-danger js-delete"',
+ 'data-id="' + item.id + '">',
+ '<i class="fas fa-trash-alt"></i>',
+ '</button>'
+ ].join('');
+ var option = [ // li html
+ '<li>',
+ '<div class="text">' + item.label + '</div>',
+ '<div class="actions">',
+ copy, ' ', del,
+ '</div>',
+ '</li>'
+ ].join('');
+ li_list.push(option); // push to li_list
+ });
+ $('#files_list').html( li_list.join('\n') );
+
+ }
+
+
+
+ // When form's action-buttons are clicked
+ // -------------------------------------------------------------------------
+
+
+ // form submit
+ // --- -- -- - - -
+ $('.edit-page form').submit( event => {
+ event.preventDefault();
+
+ var attachments = [];
+ // attachments (array) of "`media_id`;`reference`" strings
+ files.forEach( f => { attachments.push( f.id ) });
+
+ // prepare data to POST
+ // ---
+ var data = {
+ id: parseInt($('.edit-page form input[name=id]').val()),
+ title: $('.edit-page form input[name=title]').val(),
+ body: $('.edit-page form textarea[name=body]').val(),
+ status: ( ($('select[name=status]').val() == "0") ? 0 : 1 ),
+ media: attachments
+ };
+
+ // select action url (add or update)
+ // ---
+ var request = '/admin/api/page/' + ((data.id == 0) ? 'add' : 'update');
+
+ // send POST (ajax) request
+ $.post(request, data)
+ .done(function( data ) {
+ console.log(data);
+ window.location.href = '/admin/pages'; // seems ok; bach to articles
+ });
+
+ });
+
+
+ // go-back (to pages)
+ // --- -- -- - - -
+ $('#go-back').click( event => {
+ if (confirm('Θέλετε να ακυρώσετε τις όποιες αλλαγές και να φύγτε από τη σελίδα;')) {
+ window.location.href = '/admin/pages';
+ }
+ });
+
+
+ // copy url button
+ // --- -- -- - - -
+ $('.admin-container').on('click', 'button.js-copy', function (e) {
+ var copytext = ''; // default: copy nothing
+
+ if ($(this).data('id') !== undefined) {
+ copytext = window.location.origin +'/page/' + $(this).data('id');
+ }
+
+ if ($(this).data('url') !== undefined) {
+ copytext = window.location.origin + '/serve/file/' + $(this).data('url') + '?type=' + $(this).data('type');
+ }
+
+ navigator.clipboard
+ .writeText(copytext)
+ .then(() => { // notify the copy event
+ $(this).addClass('copied');
+ setTimeout( () => { $(this).removeClass('copied'); }, 700);
+ console.log(copytext, ' copied!');
+ })
+ .catch(() => {
+ console.log("error on coping text");
+ });
+ });
+
+
+
+ // delete (remove) file button
+ // --- -- -- - - -
+ $('.admin-container').on('click', 'button.js-delete', function (e) {
+ console.log('delete!');
+ var media_id = $(this).data('id');
+
+ // remove from files array the one with id == media_id
+ // ---
+ var temp = [];
+ files.forEach( it => { if (it.id != media_id) temp.push(it); });
+
+ files = temp; // re-define files
+ draw_files_list(); // then re-draw files list
+ });
+
+
+
+ /** When modal show-up
+ * -------------------------------------------------------------------------
+ * ... prepare the manage-category form
+ * ... update select2 with possible parents
+ */
+ $('#editorModal').on('show.bs.modal', event => {
+ let button = $(event.relatedTarget); // Button that triggered the modal
+ let action = button.data('action'); // action
+ // var modal = $(this); // modal object
+
+ // update modal literature
+
+ switch (action) {
+ case 'upload_file':
+ $("#editorModal .modal-content").html(
+ modal_header('Εισαγωγή Αρχείου') + upload_file_form()
+ );
+ break;
+
+ case 'preview':
+ let preview = preview_article(
+ $('.edit-page form input[name=title]').val(),
+ $('.edit-page form textarea[name=body]').val()
+ );
+ $("#editorModal .modal-content").html( modal_header('Προεπισκόπηση') + preview);
+ break;
+
+ default:
+ // do nothing
+ }
+
+ });
+
+
+
+ /** submit upload file event
+ * -------------------------------------------------------------------------
+ */
+ $('#editorModal').on('submit', 'form[name=upload_file_form]', function (event) {
+ event.preventDefault();
+
+ // create the FormData object
+ // --- -- -- - - -
+ var fd = new FormData(document.getElementById('file_upload_form'));
+
+ // post the form (via ajax)
+ $.ajax({
+ url: '/admin/api/file_upload',
+ type: 'POST',
+ data: fd,
+ contentType: false,
+ processData: false,
+ dataType: 'json',
+ success: function(response) { // on success
+
+ if (response.success) {
+ files.push({ // ... update files array
+ id: response.id,
+ label: response.title,
+ path: response.path,
+ type: response.type
+ });
+ draw_files_list();
+
+ $('#editorModal').modal('hide');
+
+ } else {
+ console.log('File not uploaded');
+ }
+ }
+ });
+ });
+
+
+ // TODO:
+ // disable links while previewing the article
+ // OR add a target="_blank"
+
+
+});
+
diff --git a/public/assets/js/admin/pages.js b/public/assets/js/admin/pages.js
new file mode 100644
index 0000000..4aa02ca
--- /dev/null
+++ b/public/assets/js/admin/pages.js
@@ -0,0 +1,240 @@
+// Globals
+// -----------------------------------------------------------------------------
+var categories = [];
+var privileges = [ { id: 0, label: 'Δημόσιο' } ];
+var pages = [];
+var table;
+
+
+
+// Supplamentary functions
+// -----------------------------------------------------------------------------
+
+// array.indexOf polyfill
+// --- -- -- - - -
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt /*, from*/)
+ {
+ var len = this.length >>> 0;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0)
+ ? Math.ceil(from)
+ : Math.floor(from);
+ if (from < 0)
+ from += len;
+
+ for (; from < len; from++)
+ {
+ if (from in this &&
+ this[from] === elt)
+ return from;
+ }
+ return -1;
+ };
+}
+
+
+// function for sorting an array by 'label' attribute
+// --- -- -- - - -
+function compare_label (a,b) {
+ if ( a.label < b.label ) { return -1; }
+ if ( a.label > b.label ) { return 1; }
+ return 0;
+}
+
+
+// function for sorting an array by 'breadcrumb' attribute
+// --- -- -- - - -
+function compare_breadcrumb (a,b) {
+ if ( a.breadcrumb < b.breadcrumb ) { return -1; }
+ if ( a.breadcrumb > b.breadcrumb ) { return 1; }
+ return 0;
+}
+
+// return category record by category-id
+// --- -- -- - - -
+function page_record(id) {
+ var record = 0;
+ pages.forEach(el => { if (el.id == id) record = el; });
+
+ return record;
+}
+
+
+// create actions html (edit and delete buttons)
+// --- -- -- - - -
+function create_actions_html(el) {
+
+ var pub, del, edit;
+
+ // preapre [show|hide]-file as reference
+ if (el.status == 1) {
+ pub = '<span class="btn btn-sm"><i class="fas fa-eye"></i></span>';
+
+ } else {
+ pub = '<span class="btn btn-sm"><i class="fas fa-eye-slash"></i></span>';
+ }
+
+ del = "";
+
+ fast_edit = [
+ '<button type="button" class="btn btn-sm btn-light"',
+ 'data-bs-toggle="modal" data-bs-target="#managePages"',
+ 'data-id="'+ el.id +'">',
+ '<i class="fa-solid fa-bolt"></i>',
+ '</button>'
+ ].join('\n');
+
+ edit = [
+ '<a type="button" class="btn btn-sm btn-warning "',
+ 'href="/admin/edit_page/'+ el.id +'">',
+ '<i class="fas fa-pencil-alt"></i>',
+ '</a>'
+ ].join('\n');
+
+ return pub +' '+ fast_edit +' '+ edit +' '+ del;
+}
+
+
+
+$(document).ready(function() {
+
+ // When document is ready
+ // -------------------------------------------------------------------------
+
+
+ /** init_table
+ * --- -- -- - - -
+ * get categories
+ * constuct categories array
+ * render dataTable
+ */
+ function init_table() {
+
+ // get all categories from server (ajax)
+ $.getJSON( "/admin/api/pages")
+ .done(function( json ) {
+ // then ...
+
+ // reset categories
+ pages.length = 0;
+
+ // construct (new) categories data
+ Object.keys(json).forEach( key => {
+ el = json[key];
+ pages.push({
+ id: parseInt(el.id),
+ title: el.title,
+ actions: create_actions_html(el)
+ });
+ });
+
+ // sort by breadcrumb
+ // categories.sort( compare_breadcrumb );
+
+ // destroy previous datatable table instances
+ $('#dt-pages').dataTable().fnClearTable();
+ $('#dt-pages').dataTable().fnDestroy();
+
+ // (re-)create categories datatable
+ table = $('#dt-pages').DataTable({
+ language: { url: '/libs/DataTables/localization/Greek.json' },
+ data: pages,
+ ordering: false,
+ columns: [
+ { data: 'id' },
+ { data: 'title' },
+ { data: 'actions' }
+ ]
+ });
+
+ })
+ .fail(function( jqxhr, textStatus, error ) {
+ console.log( "Request Failed (" + error +")" );
+ });
+
+ }
+ init_table(); // init table on first run
+
+
+
+ /** When modal show-up
+ * -------------------------------------------------------------------------
+ * ... prepare the manage-category form
+ * ... update select2 with possible parents
+ */
+ $('#managePages').on('show.bs.modal', event => {
+ var button = $(event.relatedTarget); // Button that triggered the modal
+ var id = parseInt( button.data('id') ); // category_id from data-id
+ // var modal = $(this); // modal object
+
+ //// update modal literature
+ //// ---
+ //$('#manageCategory .modal-title').html( // modal title
+ // (id) ? 'Ενημέρωση Κατηγορίας' : 'Δημιουργία Κατηγορίας'
+ //);
+ //$('#manageCategory form input[name=id]').val( id ); // category id (hidden)
+ //$('#manageCategory form input[name=label]').val( // category label
+ // (id) ? category_record(id).label : ""
+ //);
+ //prepare_parents_element(id); // prepare <select> for parents
+ })
+
+
+ /** set_parent_options()
+ *
+ * -> create options for parents <select>
+ * -> choose selected parent
+ *
+ * NOTE:
+ * the targer <select> should be a select2 control
+ *
+ * @param parents (array): array of parents
+ * @param selected (int): selected parent
+ */
+ function set_parent_options(selector, list, selected_value = false) {
+
+ list.forEach( li => { // attach options into <select> control
+ selector.append(`<option value="${li.id}">${li.label}</option>`);
+ });
+
+ if (selected_value !== false) { // mark selected option(s)
+ selector.val(selected_value).trigger('change');
+ }
+ else { selector.val(null).trigger('change'); }
+ }
+
+
+
+
+
+ // When form is submited
+ // -------------------------------------------------------------------------
+
+ $('#manageCategory form').submit( event => {
+ event.preventDefault();
+
+ // prepare data to POST
+ var data = {
+ id: parseInt($('#manageCategory form input[name=id]').val()),
+ label: $('#manageCategory form input[name=label]').val(),
+ parent_id: parseInt($('#parent_id').select2('data')[0].id)
+ };
+
+ // select action url (add or update)
+ var request = '/admin/api/categories/' + ((data.id == 0) ? 'add' : 'update');
+
+ // send POST request
+ $.post(request, data)
+ .done(function( data ) {
+
+ $('#manageCategory').modal('hide'); // when done, close modal
+
+ init_table(); // reload table of categories
+ });
+
+ });
+
+});
diff --git a/public/assets/js/lesson-features.js b/public/assets/js/lesson-features.js
index 369c8f7..3634aa3 100644
--- a/public/assets/js/lesson-features.js
+++ b/public/assets/js/lesson-features.js
@@ -1,5 +1,8 @@
const valid_codeblocks = [ // supported sort-code keys
- 'youtube'
+ 'youtube',
+ 'spotify',
+ 'spotify-track',
+ 'world-data'
];
@@ -16,17 +19,21 @@ const symbol_escapes = [
var youtube_template = ( id => {
- return '<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/'+ id +'?rel=0&controls=0" title="YouTube video player" frameborder="0" allow="clipboard-write; encrypted-media; picture-in-picture; web-share" allowfullscreen></iframe>'
+ return '<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/'+ id +'?rel=0&controls=0" title="YouTube video player" frameborder="0" allow="clipboard-write; encrypted-media; picture-in-picture; web-share" allowfullscreen></iframe>';
});
-/*
-
-
-<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/6EA-MIYY1bg?controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
+var spotify_template = ( id => {
+ return '<iframe style="border-radius:12px" src="https://open.spotify.com/embed/episode/'+ id +'?utm_source=generator" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>';
+});
+var spotify_track = ( id => {
+ return '<iframe style="border-radius:12px" src="https://open.spotify.com/embed/track/'+ id +'?utm_source=generator" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>';
+});
-*/
+var owid_template = ( id => { // our world in data (explorer)
+ return '<iframe src="https://ourworldindata.org/explorers/'+ id +'?zoomToSelection=true&hideControls=true" loading="lazy" style="width: 100%; height: 480px; border: 0px none;"></iframe>'
+});
/** inject_codeblocks
@@ -51,6 +58,8 @@ function inject_codeblocks(code) {
// console.log(matches);
var output = code; // use a copy of (source) code
+
+ console.log(matches);
matches.forEach( match => {
// parse (every) match
@@ -59,15 +68,41 @@ function inject_codeblocks(code) {
// match[0] : `{{youtube=6EA-MIYY1bg}}`
// match[1] : `youtube=6EA-MIYY1bg`
// match[3] : the (source) code string
- let blockparts = unescaped_str( match[1] ).trim().split('=');
+
+ // let blockparts = unescaped_str( match[1] ).trim().split('=');
+ let blockparts = match[1].trim().split('=');
let target = blockparts[0].toLowerCase();
let id = blockparts[1];
// let label = escaped_str(blockparts[1]);
if (valid_codeblocks.includes(target)) {
- if (target == 'youtube') {
- var codeblock = youtube_template(id);
+ var codeblock = '';
+
+ console.log(target, id);
+
+ switch (target) {
+
+ case 'youtube':
+ codeblock = youtube_template(id);
+ break;
+
+ case 'spotify':
+ codeblock = spotify_template(id);
+ break;
+
+ case 'spotify-track':
+ codeblock = spotify_track(id);
+ break;
+
+ case 'world-data':
+ codeblock = owid_template(id);
+ break;
+
+ default:
+ codeblock = id;
+
}
+
output = output.replaceAll(match[0], codeblock);
}
diff --git a/tests/code/menu.php b/tests/code/menu.php
new file mode 100644
index 0000000..8236f91
--- /dev/null
+++ b/tests/code/menu.php
@@ -0,0 +1,42 @@
+<?php
+
+/* menus
+ * --------------------------------
+ */
+$menu_json = '{
+ "class" : "row",
+ "struct" : [
+ {
+ "title" : "Σχετικά με το site",
+ "entity" : "page",
+ "list" : [2, 1, 3],
+ "template" : "list",
+ "width" : "col-md-4"
+ },
+ {
+ "title" : "",
+ "template" : "null",
+ "width" : "col-md-3"
+ },
+ {
+ "title" : "Eπικοινωνία",
+ "entity" : "page",
+ "content" : "Διεύθυνση: Κάποια Οδός 123 \nΤηλ. 6976.543.210",
+ "template" : "static",
+ "width" : "col-md-5"
+ }
+ ]
+}';
+
+
+
+
+$menu = json_decode($menu_json);
+
+echo "<pre>";
+
+ print_r($menu);
+
+echo "</pre>";
+
+