diff options
| author | George Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-04-26 21:43:02 +0300 |
|---|---|---|
| committer | George Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-04-26 21:43:02 +0300 |
| commit | 546daa78721e6abdbc89afcecc158118c2239865 (patch) | |
| tree | ae68219fb5ab69fd45d615cf059cc5d0ea279355 | |
| parent | 9cb35f2bf2127cc8ac077aae17514a57e576217d (diff) | |
| download | classroom-546daa78721e6abdbc89afcecc158118c2239865.tar.gz classroom-546daa78721e6abdbc89afcecc158118c2239865.tar.bz2 classroom-546daa78721e6abdbc89afcecc158118c2239865.zip | |
alfa.3 version; backend designer and front-end embeder enabled
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> Προεπισκόπηση + </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> Προσθήκη + </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> Νέα Σελίδα + </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"> +  <!-- 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>"; + + |
