Browse Source

Added page system

Toby Chui 1 day ago
parent
commit
091fdfb519

+ 1 - 0
sd_card/www/admin/post.html

@@ -81,6 +81,7 @@
       </div>
       <script>
         var editingPost = ""; //The name of the post being edited, if empty = new post
+        var editingPage = "";
         $("#postengine_tab").load("posteng/all.html");
 
         // Add event listener to menu items with target attribute

+ 143 - 0
sd_card/www/admin/posteng/newpg.html

@@ -0,0 +1,143 @@
+<br>
+<style>
+    #editorFrame {
+        width: 100%;
+        border: none;
+        height: calc(100vh - 1em);
+    }
+</style>
+<div class="ui text container" style="margin-top:20px;">
+    <h2>Create New Page</h2>
+    <form class="ui form">
+        <div class="field">
+            <label for="pageTitle">Page Title</label>
+            <input type="text" id="pageTitle" name="pageTitle" placeholder="Enter page title">
+        </div>
+        <div class="field">
+            <label for="saveFilename">Save Filename</label>
+            <div class="ui right labeled input">
+                <input type="text" id="saveFilename" name="saveFilename" placeholder="Enter filename">
+                <div class="ui basic label">
+                    .html
+                </div>
+            </div>
+            <small>The page will be saved to <code>/site/pages/</code></small>
+        </div>
+    </form>
+    <br>
+</div>
+<script>
+    //Set this to non null for the editor to start a new page
+    var inputFiles = null;
+    if (editingPage && editingPage != ""){
+        inputFiles = "/site/pages/" + editingPage;
+        
+
+        let filenameInput = document.getElementById("saveFilename");
+        filenameInput.value = editingPage;
+        filenameInput.disabled = true;
+
+        editingPage = ""; //Reset it
+    }
+    
+    //URL encode value of the template
+    var saveTemplate = "%3Chtml%3E%0A%20%20%20%20%3Chead%3E%0A%20%20%20%20%20%20%20%20%3Ctitle%3E%7B%7Btitle%7D%7D%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%3E%0A%20%20%20%20%20%20%20%20%3Clink%20rel%3D%22stylesheet%22%20href%3D%22https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Ftocas-ui%2F5.0.2%2Ftocas.min.css%22%3E%0A%20%20%20%20%20%20%20%20%3Cscript%20src%3D%22https%3A%2F%2Fcdnjs.cloudflare.com%2Fajax%2Flibs%2Ftocas-ui%2F5.0.2%2Ftocas.min.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%20%20%20%20%3Clink%20rel%3D%22preconnect%22%20href%3D%22https%3A%2F%2Ffonts.googleapis.com%22%3E%0A%20%20%20%20%20%20%20%20%3Clink%20rel%3D%22preconnect%22%20href%3D%22https%3A%2F%2Ffonts.gstatic.com%22%20crossorigin%3E%0A%20%20%20%20%20%20%20%20%3Clink%20href%3D%22https%3A%2F%2Ffonts.googleapis.com%2Fcss2%3Ffamily%3DNoto%2BSans%2BTC%3Awght%40400%3B500%3B700%26display%3Dswap%22%20rel%3D%22stylesheet%22%3E%0A%20%20%20%20%3C%2Fhead%3E%0A%20%20%20%20%3Cbody%3E%0A%20%20%20%20%20%20%20%20%3Cdiv%20id%3D%22context%22%20class%3D%22ts-content%20is-padded%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bcontent%7D%7D%0A%20%20%20%20%20%20%20%20%3C%2Fdiv%3E%0A%20%20%20%20%3C%2Fbody%3E%0A%20%20%20%20%3C%2Fhtml%3E";
+    saveTemplate = decodeURIComponent(saveTemplate);
+    function handleSavefile(content){
+            //Creating new file
+            let title = document.getElementById("pageTitle").value;
+            if (title.length > 27) {
+                msgbox(`<i class="ui red times icon"></i> The title cannot be longer than 27 characters.`);
+                return;
+            }
+
+            if (title.trim() === "") {
+                msgbox(`<i class="ui red times icon"></i> The page title cannot be empty.`);
+                return;
+            }
+
+            //Replace the title and content to the template
+            let editingTemplate = JSON.parse(JSON.stringify(saveTemplate)); //deepcopy
+            let filledTemplate = editingTemplate.replace("{{title}}", title).replace("{{content}}", content);
+            let filename = document.getElementById("saveFilename").value.trim();
+            if (inputFiles != null){
+                //Replace the filename with the base name of inputFiles
+                filename = inputFiles.split("/").pop();
+            }
+            if (filename.trim() === "") {
+                msgbox(`<i class="ui red times icon"></i> The filename cannot be empty.`);
+                return;
+            }
+            if (!filename.endsWith(".html")) {
+                filename += ".html";
+            }
+
+            //This will just overwrite the org file
+            createNewPage(filename, filledTemplate, function(){
+                if (inputFiles == null){
+                    msgbox(`<i class="ui green check icon"></i> Page created`, 3000);
+                    switchToTab("allpages"); // Switch to the all posts tab
+                }else{
+                    msgbox(`<i class="ui green check icon"></i> Page updated`, 3000);
+                }
+            });
+        }
+    
+
+    //Upload the new content html to disk
+    function createNewPage(filename, content, callback=undefined){
+        if (filename.trim() === "" && content.trim() === "") {
+            alert("Cannot create an empty page.");
+            return;
+        }
+
+        //Create the markdown file at the /blog/posts folder
+        const blob = new Blob([content], { type: 'text/plain' });
+        const file = new File([blob], filename);
+        handleFile(file, "/site/pages", function(){
+            if (callback){
+                callback();
+            }
+        });
+    }
+
+    // Error handler for AJAX requests
+    function errorHandler(event) {
+        msgbox("Failed to create post: " + event.target.responseText, 3000);
+        $("#saveButton").removeClass("loading disabled");
+    }
+
+    //This is use for pedit to set page title from iframe
+    function setPageTitle(title){
+        let titleInput = document.getElementById("pageTitle");
+        titleInput.value = title;
+        titleInput.disabled = true;
+    }
+
+    // Function to handle file upload
+    function handleFile(file, dir, callback=undefined) {
+        // Perform actions with the selected file
+        var formdata = new FormData();
+        formdata.append("file1", file);
+        var ajax = new XMLHttpRequest();
+        ajax.addEventListener("load", function(event){
+            let responseText = event.target.responseText;
+            try{
+                responseText = JSON.parse(responseText);
+                if (responseText.error != undefined){
+                    alert(responseText.error);
+                }
+            }catch(ex){
+
+            }
+            if (callback != undefined){
+                callback();
+            }
+        }, false); // doesnt appear to ever get called even upon success
+        ajax.addEventListener("error", errorHandler, false);
+        //ajax.addEventListener("abort", abortHandler, false);
+        ajax.open("POST", "/upload?dir=" + dir);
+        ajax.send(formdata);
+    }
+</script>
+<iframe id="editorFrame" src="posteng/pedit.html"></iframe>

+ 142 - 0
sd_card/www/admin/posteng/pages.html

@@ -0,0 +1,142 @@
+<br>
+<div class="ui container">
+    <div class="ui basic segment">
+        <h2 class="ui header">All Pages</h2>
+        <table class="ui single line table">
+            <thead>
+                <tr>
+                    <th>Page Name</th>
+                    <th>Edit</th>
+                    <th>Delete</th>
+                </tr>
+            </thead>
+            <tbody id="pageTableBody">
+                <tr id="loadingRow">
+                    <td colspan="4" style="text-align: center;">
+                        <div class="ui active inline loader"></div>
+                    </td>
+                </tr>
+                <!-- Rows will be dynamically loaded here -->
+            </tbody>
+        </table>
+        <button class="ui green basic button" id="refreshButton">
+            <i class="ui refresh icon"></i> Refresh
+        </button>
+    </div>
+
+    <div class="ui basic small modal" id="deleteConfirmModal">
+        <div class="ui icon header">
+            <i class="trash alternate outline icon"></i>
+            Confirm Delete
+        </div>
+        <div class="content" align="center">
+            <p>Are you sure you want to delete the page <span id="deletePageName"></span>?</p>
+        </div>
+        <div class="actions">
+            <div class="ui red ok inverted button" onclick="confirmDelete()" deletingPageID="">
+                <i class="trash icon"></i>
+                Yes
+            </div>
+            <div class="ui basic cancel inverted button">
+                <i class="remove icon"></i>
+                Cancel
+            </div>
+        </div>
+    </div>
+
+    <div class="ui active dimmer" style="display:none;" id="loadingDimmer">
+        <div class="ui text loader">Loading page contents</div>
+    </div>
+</div>
+
+<script>
+    /*
+        Page List
+
+        This script will fetch the list of pages from /www/site/pages/*.html
+        and display them in a table format.
+    */
+
+    function getPages() {
+        // Show loading animation
+        $("#pageTableBody").html(`<tr id="loadingRow">
+            <td colspan="4" style="text-align: center;">
+                <div class="ui active inline loader"></div>
+            </td>
+        </tr>`);
+        // Fetch the list of pages from the server
+        $.get("/api/fs/list?dir=/site/pages/", function(data) {
+            console.log(data);
+            let pageList = data.filter(file => file.Filename.endsWith(".html") && !file.IsDir);
+            pageList.sort((a, b) => a.Filename.localeCompare(b.Filename)); // Sort alphabetically by filename
+
+            // Render the pages in the table
+            console.log("Page list loaded: ", pageList);
+            $("#pageTableBody").empty(); // Clear the table body
+
+            pageList.forEach(page => {
+                const pageName = page.Filename.split('/').pop().replace('.html', ''); // Extract the page name
+                $("#pageTableBody").append(`
+                    <tr>
+                        <td><a href="/site/pages/${page.Filename}" target="_blank">${pageName}</a></td>
+                        <td><button class="ui basic button edit-button" onclick="editPage('${page.Filename}');"><i class="ui blue edit icon"></i> Edit</button></td>
+                        <td><button class="ui red basic button delete-button" onclick="deletePage('${page.Filename}');"><i class="ui trash icon"></i> Delete</button></td>
+                    </tr>
+                `);
+            });
+
+            if (pageList.length == 0) {
+                $("#pageTableBody").append(`
+                    <tr id="nopage">
+                        <td colspan="3" style="text-align: center;">No pages available</td>
+                    </tr>
+                `);
+            }
+        });
+    }
+
+    // Open page editor
+    function editPage(pageID) {
+        $("#loadingDimmer").show(); // Show loading dimmer
+        editingPage = pageID; // Store the pageID for editing
+        $("#postengine_tab").load("posteng/newpg.html");
+    }
+
+    // Delete page
+    function deletePage(pageID) {
+        const pageName = pageID.replace(/^\d+_/, ''); // Trim the timestamp prefix
+        $('#deletePageName').text(pageName); // Populate the page name in the modal
+        $('#deleteConfirmModal .ok.button').attr('deletingPageID', pageID); // Store the pageID in the confirm button
+        $('#deleteConfirmModal').modal('show'); // Show the confirmation modal
+    }
+
+    function confirmDelete() {
+        const pageID = $('#deleteConfirmModal .ok.button').attr('deletingPageID'); // Retrieve the pageID
+        if (!pageID) return;
+
+        // Send a delete request to the server
+        $.ajax({
+            url: `/api/fs/del?target=/site/pages/${pageID}`,
+            type: 'POST',
+            success: function(response) {
+                console.log('Page deleted:', response);
+                $('#deleteConfirmModal').modal('hide'); // Hide the modal
+                getPages(); // Refresh the page list
+                msgbox(`Page "${pageID.replace(/^\d+_/, '')}" deleted successfully`, 2000); // Show success message
+            },
+            error: function(error) {
+                console.error('Error deleting page:', error);
+                msgbox('Failed to delete the page', 2000); // Show error message
+            }
+        });
+    }
+
+    // Initial call to load pages
+    getPages();
+
+    // Event listener for the Refresh button
+    $('#refreshButton').on('click', function() {
+        getPages(); // Reload the table content
+        msgbox("Page list refreshed", 2000); // Show a message box
+    });
+</script>

+ 145 - 0
sd_card/www/admin/posteng/pedit.html

@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="apple-mobile-web-app-capable" content="yes" />
+		<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
+		<meta name="theme-color" content="#ff9224">
+		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
+		<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+		<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
+		
+		<!-- Editor -->
+		<script src=" https://cdn.jsdelivr.net/npm/[email protected]/dist/suneditor.min.js "></script>
+		<link href=" https://cdn.jsdelivr.net/npm/[email protected]/dist/css/suneditor.min.css " rel="stylesheet">
+		<script src="src/en.js"></script>
+
+		<style>
+			body{
+				margin: 0px;
+			}
+			#editorWrapper{
+				position: fixed;
+				left: 0px;
+				top: 0px;
+				width: 100%;
+				height: 100%;
+			}
+
+			#maineditor{
+				width: 100%;
+				height: calc(100vh - 0.4em);
+			}
+		</style>
+	</head>
+	<body>
+		<div id="editorWrapper">
+			<textarea id="maineditor"></textarea>
+		</div>
+		
+		<script>
+			//Start a new editor if there is no file ported in
+			var editor;
+			if (parent.inputFiles == null){
+				//New Editor
+				initEditor();
+			}else{
+				//Get the filepath and load it
+				var filepath = parent.inputFiles;
+				//Load the given file
+				$.get("/api/fs/download?file=" + filepath, function(data) {
+					if (data.error == undefined){
+
+						var titleMatch = data.match(/<title>(.*?)<\/title>/i);
+						if (titleMatch && titleMatch[1]) {
+							var title = titleMatch[1];
+							console.log("Title found: " + title);
+							parent.setPageTitle(title);
+						}
+
+						var contentWithoutHead = data.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '');
+						var contentWithoutBody = contentWithoutHead.replace(/<body[^>]*>|<\/body>/gi, '');
+						$("#maineditor").html(contentWithoutBody);
+						initEditor();
+					} else {
+						alert("Failed to load post content: " + data.error);
+					}
+				}).fail(function() {
+					alert("Error loading post content.");
+				});
+				
+			}
+			
+			$(window).on("resize", function(){
+				$("#suneditor_maineditor").css("width", window.innerWidth + "px");
+			});
+			
+			function getContent(){
+				return editor.getContents();
+			}
+
+			function initEditor(){
+				editor = SUNEDITOR.create(document.getElementById('maineditor'),
+					{	
+						lang: SUNEDITOR_LANG['en'],
+						buttonList: [
+							['undo', 'redo', 'save',  'preview'],
+							['font', 'fontSize', 'formatBlock'],
+							['paragraphStyle', 'blockquote'],
+							['bold', 'underline', 'italic', 'strike', 'subscript', 'superscript'],
+							['fontColor', 'hiliteColor'],
+							['removeFormat'],
+							'/', // Line break
+							['outdent', 'indent'],
+							['align', 'horizontalRule', 'list', 'lineHeight'],
+							['table', 'link', 'image', 'video', 'audio' /** ,'math' */], // You must add the 'katex' library at options to use the 'math' plugin.
+							/** ['imageGallery'] */ // You must add the "imageGalleryUrl".
+							['showBlocks', 'codeView']
+						],
+						paragraphStyles : [
+							{
+								name: 'Segment',
+								class: '__se__ ui segment',
+							},
+							{
+								name: 'Red Segment',
+								class: '__se__ ui red segment',
+							},
+							{
+								name: 'Blue Segment',
+								class: '__se__ ui blue segment',
+							},
+							{
+								name: 'Green Segment',
+								class: '__se__ ui green segment',
+							},
+							{
+								name: 'Message',
+								class: '__se__ ui message',
+							},
+							{
+								name: 'Red Message',
+								class: '__se__ ui red message',
+							},
+							{
+								name: 'Blue Message',
+								class: '__se__ ui blue message',
+							},
+							{
+								name: 'Green Message',
+								class: '__se__ ui green message',
+							}
+						],
+						"resizingBar": false,
+						callBackSave : function (contents, isChanged) {
+							parent.handleSavefile(contents);
+							return false;
+						}
+					}
+				);
+				return editor;
+			}
+
+		</script>
+	</body>
+</html>

+ 186 - 0
sd_card/www/admin/posteng/src/en.js

@@ -0,0 +1,186 @@
+/*
+ * wysiwyg web editor
+ *
+ * suneditor.js
+ * Copyright 2017 JiHong Lee.
+ * MIT license.
+ */
+'use strict';
+
+(function (global, factory) {
+    if (typeof module === 'object' && typeof module.exports === 'object') {
+        module.exports = global.document ?
+            factory(global, true) :
+            function (w) {
+                if (!w.document) {
+                    throw new Error('SUNEDITOR_LANG a window with a document');
+                }
+                return factory(w);
+            };
+    } else {
+        factory(global);
+    }
+}(typeof window !== 'undefined' ? window : this, function (window, noGlobal) {
+    const lang = {
+        code: 'en',
+        toolbar: {
+            default: 'Default',
+            save: 'Save',
+            font: 'Font',
+            formats: 'Formats',
+            fontSize: 'Size',
+            bold: 'Bold',
+            underline: 'Underline',
+            italic: 'Italic',
+            strike: 'Strike',
+            subscript: 'Subscript',
+            superscript: 'Superscript',
+            removeFormat: 'Remove Format',
+            fontColor: 'Font Color',
+            hiliteColor: 'Highlight Color',
+            indent: 'Indent',
+            outdent: 'Outdent',
+            align: 'Align',
+            alignLeft: 'Align left',
+            alignRight: 'Align right',
+            alignCenter: 'Align center',
+            alignJustify: 'Align justify',
+            list: 'List',
+            orderList: 'Ordered list',
+            unorderList: 'Unordered list',
+            horizontalRule: 'Horizontal line',
+            hr_solid: 'Solid',
+            hr_dotted: 'Dotted',
+            hr_dashed: 'Dashed',
+            table: 'Table',
+            link: 'Link',
+            math: 'Math',
+            image: 'Image',
+            video: 'Video',
+            audio: 'Audio',
+            fullScreen: 'Full screen',
+            showBlocks: 'Show blocks',
+            codeView: 'Code view',
+            undo: 'Undo',
+            redo: 'Redo',
+            preview: 'Preview',
+            print: 'print',
+            tag_p: 'Paragraph',
+            tag_div: 'Normal (DIV)',
+            tag_h: 'Header',
+            tag_blockquote: 'Quote',
+            tag_pre: 'Code',
+            template: 'Template',
+            lineHeight: 'Line height',
+            paragraphStyle: 'Paragraph style',
+            textStyle: 'Text style',
+            imageGallery: 'Image gallery',
+            mention: 'Mention'
+        },
+        dialogBox: {
+            linkBox: {
+                title: 'Insert Link',
+                url: 'URL to link',
+                text: 'Text to display',
+                newWindowCheck: 'Open in new window',
+                downloadLinkCheck: 'Download link',
+                bookmark: 'Bookmark'
+            },
+            mathBox: {
+                title: 'Math',
+                inputLabel: 'Mathematical Notation',
+                fontSizeLabel: 'Font Size',
+                previewLabel: 'Preview'
+            },
+            imageBox: {
+                title: 'Insert image',
+                file: 'Select from files',
+                url: 'Image URL',
+                altText: 'Alternative text'
+            },
+            videoBox: {
+                title: 'Insert Video',
+                file: 'Select from files',
+                url: 'Media embed URL, YouTube/Vimeo'
+            },
+            audioBox: {
+                title: 'Insert Audio',
+                file: 'Select from files',
+                url: 'Audio URL'
+            },
+            browser: {
+                tags: 'Tags',
+                search: 'Search',
+            },
+            caption: 'Insert description',
+            close: 'Close',
+            submitButton: 'Submit',
+            revertButton: 'Revert',
+            proportion: 'Constrain proportions',
+            basic: 'Basic',
+            left: 'Left',
+            right: 'Right',
+            center: 'Center',
+            width: 'Width',
+            height: 'Height',
+            size: 'Size',
+            ratio: 'Ratio'
+        },
+        controller: {
+            edit: 'Edit',
+            unlink: 'Unlink',
+            remove: 'Remove',
+            insertRowAbove: 'Insert row above',
+            insertRowBelow: 'Insert row below',
+            deleteRow: 'Delete row',
+            insertColumnBefore: 'Insert column before',
+            insertColumnAfter: 'Insert column after',
+            deleteColumn: 'Delete column',
+            fixedColumnWidth: 'Fixed column width',
+            resize100: 'Resize 100%',
+            resize75: 'Resize 75%',
+            resize50: 'Resize 50%',
+            resize25: 'Resize 25%',
+            autoSize: 'Auto size',
+            mirrorHorizontal: 'Mirror, Horizontal',
+            mirrorVertical: 'Mirror, Vertical',
+            rotateLeft: 'Rotate left',
+            rotateRight: 'Rotate right',
+            maxSize: 'Max size',
+            minSize: 'Min size',
+            tableHeader: 'Table header',
+            mergeCells: 'Merge cells',
+            splitCells: 'Split Cells',
+            HorizontalSplit: 'Horizontal split',
+            VerticalSplit: 'Vertical split'
+        },
+        menu: {
+            spaced: 'Spaced',
+            bordered: 'Bordered',
+            neon: 'Neon',
+            translucent: 'Translucent',
+            shadow: 'Shadow',
+            code: 'Code'
+        }
+    };
+
+    if (typeof noGlobal === typeof undefined) {
+        if (!window.SUNEDITOR_LANG) {
+            Object.defineProperty(window, 'SUNEDITOR_LANG', {
+                enumerable: true,
+                writable: false,
+                configurable: false,
+                value: {}
+            });
+        }
+
+        Object.defineProperty(window.SUNEDITOR_LANG, 'en', {
+            enumerable: true,
+            writable: true,
+            configurable: true,
+            value: lang
+        });
+    }
+
+    return lang;
+}));

BIN
sd_card/www/favicon.psd


BIN
sd_card/www/img/logo.psd