📂 FileMgr
📍
/home/u625410797/domains/monothailand.com/public_html/views/admin/pages
✏️ Edit File: /home/u625410797/domains/monothailand.com/public_html/views/admin/pages/edit.php
⬅ Kembali
<?php // PHP routing/context parameters require_once __DIR__ . '/../../../config/config.php'; // Prepare values (edit vs create mode) $isEdit = isset($page['id']); $pageId = $isEdit ? intval($page['id']) : 0; $titleVal = $isEdit ? ($page['title'] ?? '') : ''; $slugVal = $isEdit ? ($page['slug'] ?? '') : ''; $contentVal = $isEdit ? ($page['content'] ?? '') : ''; $featuredImageVal = $isEdit ? ($page['featured_image'] ?? '') : ''; $metaTitleVal = $isEdit ? ($page['meta_title'] ?? '') : ''; $metaDescriptionVal = $isEdit ? ($page['meta_description'] ?? '') : ''; $isPublishedVal = $isEdit ? intval($page['is_published'] ?? 0) : 0; $pageTypeVal = $isEdit ? ($page['page_type'] ?? 'custom') : 'custom'; ?> <!-- TinyMCE CDN --> <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <!-- Styling custom definitions for dual workspace layout --> <style> .editor-pane-glass { background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.05); } .device-viewport { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); border: 8px solid #1e293b; background: #0b0f19; } .device-viewport::-webkit-scrollbar { width: 4px; } .seo-indicator-bar { transition: width 0.5s ease-out; } </style> <!-- Main Controller with AlpineJS --> <div x-data="pageEditorController()" x-init="initEditor()" class="space-y-6" > <!-- Header Controls --> <div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-2"> <div> <div class="flex items-center gap-3"> <a href="BASE_URL . 'admin/pages" class="p-2 bg-slate-900/60 text-slate-400 hover:text-white border border-slate-800 rounded-lg transition-all"> <i data-lucide="arrow-left" class="w-4 h-4"></i> </a> <h1 class="text-3xl font-extrabold text-white tracking-tight"> <span x-text="isEdit ? 'Edit Page Node' : 'Create Content Node'"></span> </h1> </div> <p class="text-slate-400 text-sm mt-1 ml-11">Assemble article components and modify structural fields</p> </div> <!-- Live Action Status Bar --> <div class="flex items-center gap-2.5 w-full md:w-auto justify-end"> <!-- Saved Status Alert --> <div x-show="saveStatus" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-2" class="px-3 py-1.5 rounded-lg text-xs font-semibold font-mono flex items-center gap-2" :class="saveStatus === 'success' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'" style="display: none;" > <span class="w-1.5 h-1.5 rounded-full" :class="saveStatus === 'success' ? 'bg-emerald-400' : 'bg-red-400'"></span> <span x-text="saveStatusMessage"></span> </div> <!-- Manual Save Button --> <button @click="submitPayload('continue')" class="px-5 py-2 bg-gradient-to-r from-emerald-500 to-teal-600 text-white rounded-lg text-sm font-bold hover:from-emerald-400 hover:to-teal-500 transition-all flex items-center gap-2 shadow-[0_0_20px_rgba(16,185,129,0.2)] hover:scale-[1.02]" :disabled="isSubmitting" > <template x-if="isSubmitting"> <i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> </template> <template x-if="!isSubmitting"> <i data-lucide="save" class="w-4 h-4"></i> </template> Save Draft </button> <button @click="submitPayload('close')" class="px-4 py-2 bg-slate-900/80 text-slate-300 border border-slate-800 rounded-lg text-sm font-bold hover:bg-slate-800 hover:text-white transition-all flex items-center gap-2" :disabled="isSubmitting" > Save & Close </button> </div> </div> <!-- Dual Workspace Splitscreen Layout --> <div class="grid grid-cols-1 xl:grid-cols-2 gap-6 items-start"> <!-- Left Pane: The Control Editor Forms --> <div class="space-y-6"> <div class="editor-pane-glass rounded-xl border border-slate-800 p-6 space-y-6 shadow-2xl"> <!-- Tab Selector --> <div class="flex border-b border-slate-800 pb-3 gap-2"> <button @click="activeTab = 'content'" class="px-4 py-2 text-sm font-bold rounded-lg transition-all flex items-center gap-2" :class="activeTab === 'content' ? 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 font-extrabold' : 'text-slate-400 hover:text-slate-300 hover:bg-slate-800/40'" > <i data-lucide="edit" class="w-4 h-4"></i> Content Details </button> <button @click="activeTab = 'seo'" class="px-4 py-2 text-sm font-bold rounded-lg transition-all flex items-center gap-2" :class="activeTab === 'seo' ? 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 font-extrabold' : 'text-slate-400 hover:text-slate-300 hover:bg-slate-800/40'" > <i data-lucide="search" class="w-4 h-4"></i> SEO Scorecard </button> <button @click="activeTab = 'status'" class="px-4 py-2 text-sm font-bold rounded-lg transition-all flex items-center gap-2" :class="activeTab === 'status' ? 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 font-extrabold' : 'text-slate-400 hover:text-slate-300 hover:bg-slate-800/40'" > <i data-lucide="settings" class="w-4 h-4"></i> Publish Node </button> </div> <!-- Tab 1: Page Details & Editor --> <div x-show="activeTab === 'content'" class="space-y-5"> <!-- Title Input --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Page Title <span class="text-emerald-400">*</span> </label> <input type="text" x-model="title" @input="handleTitleChange()" placeholder="Enter beautiful header title..." class="w-full px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-lg text-sm text-white focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 transition-all font-bold" maxlength="255" > <div class="flex items-center justify-between text-[11px] text-slate-500 font-mono"> <span>Auto-slug is active</span> <span :class="title.length > 200 ? 'text-red-400' : 'text-slate-500'"><span x-text="title.length"></span>/255 chars</span> </div> </div> <!-- Slug Endpoint --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Page Slug URL <span class="text-emerald-400">*</span> </label> <div class="flex items-center"> <span class="px-3.5 py-3 bg-slate-900 border border-slate-800 border-r-0 rounded-l-lg text-xs text-slate-500 font-mono select-none"> BASE_URL . ' </span> <input type="text" x-model="slug" @input="handleSlugChange()" placeholder="custom-slug" class="flex-1 px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-r-lg text-sm text-indigo-300 focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 transition-all font-mono" maxlength="255" > </div> <p class="text-[11px] text-slate-500 font-mono">Normalized URL endpoint parameter</p> </div> <!-- WYSIWYG Editor Workspace --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Rich Content Body </label> <div class="border border-slate-800 rounded-lg overflow-hidden bg-slate-950/40"> <textarea id="contentEditor" rows="15" placeholder="Express your page message..." class="w-full bg-slate-900 text-white focus:outline-none" ><?= htmlspecialchars($contentVal) ?></textarea> </div> </div> <!-- Drag and Drop Upload Area --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Featured Visual Cover Image </label> <div @dragover.prevent="dragOver = true" @dragleave.prevent="dragOver = false" @drop.prevent="handleDrop($event)" @click="$refs.fileInput.click()" class="border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-all flex flex-col items-center justify-center gap-2.5" :class="dragOver ? 'border-emerald-500 bg-emerald-500/5' : 'border-slate-800 hover:border-slate-700 bg-slate-950/20'" > <input type="file" x-ref="fileInput" accept="image/*" class="hidden" @change="handleFileSelected($event)" > <template x-if="isUploading"> <div class="flex flex-col items-center gap-2"> <i data-lucide="loader-2" class="w-8 h-8 text-emerald-400 animate-spin"></i> <p class="text-xs text-emerald-400 font-mono">Uploading visual cover...</p> </div> </template> <template x-if="!isUploading"> <div class="flex flex-col items-center gap-2"> <i data-lucide="cloud-upload" class="w-8 h-8 text-slate-500"></i> <p class="text-xs text-slate-300">Drag image here or click to browse assets</p> <p class="text-[10px] text-slate-500 font-mono">PNG, JPG, WEBP (Max 5MB)</p> </div> </template> </div> <!-- Image Preview Grid --> <div x-show="featuredImage" class="mt-4 p-3 bg-slate-900/60 border border-slate-800 rounded-lg relative inline-block group" style="display: none;"> <img :src="'BASE_URL . '' + featuredImage" alt="Featured Thumbnail" class="max-h-48 rounded border border-slate-700 object-cover"> <button @click="removeFeaturedImage()" class="absolute top-4 right-4 p-1.5 bg-red-600/90 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all shadow-md hover:bg-red-500" > <i data-lucide="trash-2" class="w-4 h-4"></i> </button> <p class="text-[11px] text-slate-400 mt-2 font-mono truncate max-w-xs" x-text="featuredImage"></p> </div> </div> </div> <!-- Tab 2: SEO Scorecard Analyzer --> <div x-show="activeTab === 'seo'" class="space-y-6" style="display: none;"> <!-- SEO Score Card Overview --> <div class="p-4 bg-slate-950/60 border border-slate-800 rounded-lg flex items-center justify-between"> <div> <h4 class="text-sm font-bold text-white">SEO Compliance Index</h4> <p class="text-xs text-slate-400 mt-1">Calculated search engine compliance matrix</p> </div> <div class="flex flex-col items-center justify-center"> <span class="text-3xl font-black font-mono text-emerald-400" x-text="seoScore + '%'"></span> <span class="text-[10px] uppercase font-mono tracking-wider font-bold mt-1 text-slate-500" x-text="getSeoLabel()"></span> </div> </div> <!-- SEO Metrics Checklist --> <div class="space-y-3"> <div class="flex items-center justify-between text-xs p-2.5 bg-slate-900/40 rounded border border-slate-800"> <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full" :class="title.length >= 10 && title.length <= 60 ? 'bg-emerald-400 shadow-[0_0_8px_#10b981]' : 'bg-slate-600'"></span> Header Title optimization (10-60 chars) </span> <span class="font-mono text-slate-400" x-text="title.length + '/60'"></span> </div> <div class="flex items-center justify-between text-xs p-2.5 bg-slate-900/40 rounded border border-slate-800"> <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full" :class="metaTitle.length >= 15 && metaTitle.length <= 60 ? 'bg-emerald-400 shadow-[0_0_8px_#10b981]' : 'bg-slate-600'"></span> Meta title set correctly (15-60 chars) </span> <span class="font-mono text-slate-400" x-text="metaTitle.length + '/60'"></span> </div> <div class="flex items-center justify-between text-xs p-2.5 bg-slate-900/40 rounded border border-slate-800"> <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full" :class="metaDescription.length >= 50 && metaDescription.length <= 160 ? 'bg-emerald-400 shadow-[0_0_8px_#10b981]' : 'bg-slate-600'"></span> Meta description set correctly (50-160 chars) </span> <span class="font-mono text-slate-400" x-text="metaDescription.length + '/160'"></span> </div> <div class="flex items-center justify-between text-xs p-2.5 bg-slate-900/40 rounded border border-slate-800"> <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full" :class="contentWordCount >= 100 ? 'bg-emerald-400 shadow-[0_0_8px_#10b981]' : 'bg-slate-600'"></span> Minimum content density (>= 100 words) </span> <span class="font-mono text-slate-400" x-text="contentWordCount + ' words'"></span> </div> <div class="flex items-center justify-between text-xs p-2.5 bg-slate-900/40 rounded border border-slate-800"> <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full" :class="featuredImage ? 'bg-emerald-400 shadow-[0_0_8px_#10b981]' : 'bg-slate-600'"></span> Visual cover media exists </span> <span class="font-mono text-slate-400" x-text="featuredImage ? 'Cover image active' : 'None'"></span> </div> </div> <!-- Meta Custom Title --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> SEO Meta Title </label> <input type="text" x-model="metaTitle" @input="calculateSEO()" placeholder="Enter metadata head title..." class="w-full px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-lg text-sm text-white focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 transition-all font-bold" maxlength="60" > <div class="flex items-center justify-between text-[11px] text-slate-500 font-mono"> <span>Ideal constraint: 60 characters max</span> <span x-text="metaTitle.length + '/60'"></span> </div> </div> <!-- Meta Custom Description --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> SEO Meta Description </label> <textarea x-model="metaDescription" @input="calculateSEO()" rows="4" placeholder="Type index desc summaries here..." class="w-full px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-lg text-sm text-white focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder-slate-500" maxlength="160" ></textarea> <div class="flex items-center justify-between text-[11px] text-slate-500 font-mono"> <span>Ideal constraint: 160 characters max</span> <span x-text="metaDescription.length + '/160'"></span> </div> </div> </div> <!-- Tab 3: Publish Settings & Visibility --> <div x-show="activeTab === 'status'" class="space-y-5" style="display: none;"> <!-- Publish Status Visibility Dropdown --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Visibility Status </label> <select x-model="isPublished" class="w-full px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-lg text-sm text-slate-300 focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 cursor-pointer" > <option value="0">Draft (Staged)</option> <option value="1">Published (Live Online)</option> </select> <p class="text-[11px] text-slate-500 font-mono">Controls visual rendering to the public audience</p> </div> <!-- Document Type Classification --> <div class="space-y-2"> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono"> Content Classification Node </label> <select x-model="pageType" class="w-full px-4 py-3 bg-slate-950/60 border border-slate-800 rounded-lg text-sm text-slate-300 focus:outline-none focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 cursor-pointer" > <option value="custom">Standard Page</option> <option value="article">Blog / Article</option> <option value="solar">Solar Component Info</option> </select> </div> <!-- Permanent Purge Area (Edit Mode Only) --> <template x-if="isEdit"> <div class="pt-6 border-t border-slate-800 mt-6 space-y-4"> <div> <h4 class="text-sm font-bold text-red-400">Danger Zone</h4> <p class="text-xs text-slate-500 mt-1">This operation deletes the node permanently with all asset references.</p> </div> <button @click="deleteNode()" type="button" class="px-4 py-2.5 bg-red-600/10 text-red-400 border border-red-500/20 rounded-lg text-xs font-bold hover:bg-red-600 hover:text-white transition-all flex items-center gap-2" > <i data-lucide="trash-2" class="w-4 h-4"></i> Purge Node Permanently </button> </div> </template> </div> </div> </div> <!-- Right Pane: Real-time Device Viewport Simulator --> <div class="space-y-4"> <!-- Simulated Navigation Bar Controls --> <div class="flex items-center justify-between p-3 bg-slate-900/60 border border-slate-800 rounded-xl"> <span class="text-xs font-semibold uppercase tracking-wider text-slate-400 font-mono flex items-center gap-2"> <span class="w-2 h-2 bg-emerald-500 rounded-full animate-ping"></span> Live Device Preview </span> <!-- Viewport size options --> <div class="flex items-center gap-1.5 p-1 bg-slate-950 rounded-lg border border-slate-800"> <button @click="previewWidth = '100%'" class="p-1.5 rounded transition-all text-xs font-bold flex items-center gap-1" :class="previewWidth === '100%' ? 'bg-indigo-500/10 text-indigo-400' : 'text-slate-400 hover:text-white'" title="Desktop Resolution" > <i data-lucide="monitor" class="w-4 h-4"></i> Desktop </button> <button @click="previewWidth = '768px'" class="p-1.5 rounded transition-all text-xs font-bold flex items-center gap-1" :class="previewWidth === '768px' ? 'bg-indigo-500/10 text-indigo-400' : 'text-slate-400 hover:text-white'" title="Tablet Resolution" > <i data-lucide="tablet" class="w-4 h-4"></i> Tablet </button> <button @click="previewWidth = '375px'" class="p-1.5 rounded transition-all text-xs font-bold flex items-center gap-1" :class="previewWidth === '375px' ? 'bg-indigo-500/10 text-indigo-400' : 'text-slate-400 hover:text-white'" title="Mobile Resolution" > <i data-lucide="smartphone" class="w-4 h-4"></i> Mobile </button> </div> </div> <!-- Simulated Frame Wrapper --> <div class="flex justify-center w-full bg-slate-950/20 p-2 rounded-2xl border border-slate-800/60 overflow-hidden min-h-[500px]"> <div class="device-viewport rounded-xl overflow-y-auto max-h-[700px] flex flex-col" :style="'width: ' + previewWidth" > <!-- Simulated Site Navbar Header --> <div class="px-5 py-4 bg-slate-950/80 border-b border-slate-900 flex items-center justify-between select-none"> <div class="flex items-center gap-2"> <span class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span> <span class="text-xs font-extrabold text-white uppercase tracking-wider font-mono">Mono Thailand</span> </div> <div class="flex gap-2"> <span class="w-1.5 h-1.5 bg-slate-800 rounded-full"></span> <span class="w-1.5 h-1.5 bg-slate-800 rounded-full"></span> </div> </div> <!-- Simulator Content Canvas --> <div class="flex-1 p-6 space-y-6"> <!-- Path Header Breadcrumb --> <div class="text-[10px] text-indigo-400 font-mono tracking-wider"> /pages/ <span x-text="slug || 'endpoint'"></span> </div> <!-- Rendered Title --> <h2 class="text-2xl sm:text-3xl font-extrabold text-white tracking-tight leading-tight" x-text="title || 'Component Headline Title'" ></h2> <!-- Page Info Row --> <div class="flex items-center gap-4 text-xs text-slate-500 border-b border-slate-900 pb-4 font-mono select-none"> <span class="flex items-center gap-1.5"> <i data-lucide="user" class="w-3.5 h-3.5 text-indigo-400"></i> Mono Admin </span> <span class="flex items-center gap-1.5"> <i data-lucide="calendar" class="w-3.5 h-3.5 text-emerald-400"></i> Today </span> </div> <!-- Featured Banner image inside preview --> <template x-if="featuredImage"> <div class="w-full max-h-64 rounded-lg overflow-hidden border border-slate-900 shadow-md"> <img :src="'BASE_URL . '' + featuredImage" alt="Banner visual preview" class="w-full object-cover"> </div> </template> <!-- Rendered HTML Content Workspace --> <div id="previewContentHtml" class="prose prose-invert prose-emerald text-sm text-slate-300 leading-relaxed space-y-4" > <!-- TinyMCE rich output will bind directly here --> <p class="text-slate-500 italic">No content text body drafted yet.</p> </div> </div> </div> </div> </div> </div> </div> <!-- Main Script Module --> <script> function pageEditorController() { return { // Form field reactive data isEdit: <?= $isEdit ? 'true' : 'false' ?>, pageId: <?= $pageId ?>, title: <?= json_encode($titleVal) ?>, slug: <?= json_encode($slugVal) ?>, featuredImage: <?= json_encode($featuredImageVal) ?>, metaTitle: <?= json_encode($metaTitleVal) ?>, metaDescription: <?= json_encode($metaDescriptionVal) ?>, isPublished: <?= $isPublishedVal ?>, pageType: <?= json_encode($pageTypeVal) ?>, // Editor UI states activeTab: 'content', dragOver: false, isUploading: false, isSubmitting: false, previewWidth: '100%', contentWordCount: 0, seoScore: 0, // Notifications saveStatus: null, saveStatusMessage: '', // Generate Slug URL String handleTitleChange() { if (!this.isEdit) { this.slug = this.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); } this.calculateSEO(); this.updatePreviewContent(); }, handleSlugChange() { this.slug = this.slug .toLowerCase() .replace(/[^a-z0-9-]+/g, ''); }, // TinyMCE setup and event binding initEditor() { const self = this; tinymce.init({ selector: '#contentEditor', height: 480, skin: 'oxide-dark', content_css: 'dark', menubar: true, plugins: [ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', 'insertdatetime', 'media', 'table', 'help', 'wordcount' ], toolbar: 'undo redo | blocks | ' + 'bold italic backcolor | alignleft aligncenter ' + 'alignright alignjustify | bullist numlist outdent indent | ' + 'removeformat | image code help', dark_mode_selector: 'html', automatic_uploads: true, images_upload_url: 'BASE_URL . 'api/upload.php', images_upload_handler: function (blobInfo, progress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.withCredentials = false; xhr.open('POST', 'BASE_URL . 'api/upload.php'); xhr.upload.onprogress = (e) => { progress(e.loaded / e.total * 100); }; xhr.onload = () => { if (xhr.status === 403) { reject({ message: 'HTTP Error: ' + xhr.status, remove: true }); return; } if (xhr.status < 200 || xhr.status >= 300) { reject('HTTP Error: ' + xhr.status); return; } const json = JSON.parse(xhr.responseText); if (!json || typeof json.path !== 'string') { reject('Invalid JSON: ' + xhr.responseText); return; } resolve('BASE_URL . '' + json.path); }; xhr.onerror = () => { reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); }; const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); xhr.send(formData); }); }, setup: function (editor) { editor.on('init', () => { self.updatePreviewContent(); self.calculateSEO(); }); editor.on('keyup change Undo Redo', () => { self.updatePreviewContent(); self.calculateSEO(); }); } }); // Trigger Lucide icons if (window.lucide) { setTimeout(() => lucide.createIcons(), 200); } }, // Update Live Simulated Output HTML updatePreviewContent() { const htmlDiv = document.getElementById('previewContentHtml'); if (!htmlDiv) return; const editor = tinymce.get('contentEditor'); const editorContent = editor ? editor.getContent() : <?= json_encode($contentVal) ?>; if (editorContent.trim()) { htmlDiv.innerHTML = editorContent; } else { htmlDiv.innerHTML = '<p class="text-slate-500 italic">No content text body drafted yet.</p>'; } // Word Count const text = editor ? editor.getContent({ format: 'text' }) : ''; this.contentWordCount = text.trim() ? text.trim().split(/\s+/).length : 0; }, // SEO Index Optimization Score Math calculateSEO() { let score = 0; // Check 1: Title (Ideal length 10-60) if (this.title.length >= 10 && this.title.length <= 60) { score += 20; } else if (this.title.length > 0) { score += 10; } // Check 2: Meta Title (Ideal length 15-60) if (this.metaTitle.length >= 15 && this.metaTitle.length <= 60) { score += 25; } else if (this.metaTitle.length > 0) { score += 10; } // Check 3: Meta Description (Ideal length 50-160) if (this.metaDescription.length >= 50 && this.metaDescription.length <= 160) { score += 25; } else if (this.metaDescription.length > 0) { score += 10; } // Check 4: Content Density if (this.contentWordCount >= 200) { score += 20; } else if (this.contentWordCount >= 80) { score += 10; } // Check 5: Cover Image if (this.featuredImage) { score += 10; } this.seoScore = score; }, getSeoLabel() { if (this.seoScore >= 80) return 'Excellent'; if (this.seoScore >= 50) return 'Optimized'; return 'Needs Review'; }, // Drop Zone Asset Upload async handleDrop(e) { this.dragOver = false; const files = e.dataTransfer.files; if (files.length > 0) { await this.uploadCoverImage(files[0]); } }, async handleFileSelected(e) { const file = e.target.files[0]; if (file) { await this.uploadCoverImage(file); } }, async uploadCoverImage(file) { this.isUploading = true; try { const formData = new FormData(); formData.append('file', file); const response = await fetch('BASE_URL . 'api/upload.php', { method: 'POST', body: formData }); const json = await response.json(); if (json.success && json.path) { this.featuredImage = json.path; this.showStatus('success', 'Cover photo uploaded successfully!'); } else { this.showStatus('error', json.error || 'Failed to upload photo'); } } catch (err) { console.error(err); this.showStatus('error', 'Upload connection failure'); } finally { this.isUploading = false; this.calculateSEO(); setTimeout(() => { if (window.lucide) lucide.createIcons(); }, 100); } }, removeFeaturedImage() { this.featuredImage = ''; this.calculateSEO(); this.showStatus('success', 'Cover photo removed.'); }, // Alert Helper showStatus(type, msg) { this.saveStatus = type; this.saveStatusMessage = msg; setTimeout(() => { this.saveStatus = null; }, 4000); }, // Submit Page Form Payload via AJAX JSON POST/PUT API async submitPayload(action) { if (!this.title.trim()) { alert('Page Title is a required field.'); return; } if (!this.slug.trim()) { alert('Page Slug Endpoint is a required field.'); return; } this.isSubmitting = true; const editor = tinymce.get('contentEditor'); const editorContent = editor ? editor.getContent() : <?= json_encode($contentVal) ?>; const payload = { title: this.title, slug: this.slug, content: editorContent, page_type: this.pageType, featured_image: this.featuredImage, meta_title: this.metaTitle, meta_description: this.metaDescription, is_published: parseInt(this.isPublished) }; const method = this.isEdit ? 'PUT' : 'POST'; if (this.isEdit) { payload.id = this.pageId; } try { const response = await fetch('BASE_URL . 'api/pages.php', { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await response.json(); if (json.success) { this.showStatus('success', 'Page saved successfully!'); if (method === 'POST') { this.isEdit = true; this.pageId = json.id; } if (action === 'close') { setTimeout(() => { window.location.href = 'BASE_URL . 'admin/pages'; }, 800); } } else { this.showStatus('error', json.error || 'Failed to persist details'); } } catch (err) { console.error(err); this.showStatus('error', 'API storage network error'); } finally { this.isSubmitting = false; } }, // Permanent delete async deleteNode() { if (!this.isEdit) return; if (confirm('Are you sure you want to permanently delete this CMS Page? This action cannot be undone.')) { try { const response = await fetch(`BASE_URL . 'api/pages.php?id=${this.pageId}`, { method: 'DELETE' }); const json = await response.json(); if (json.success) { window.location.href = 'BASE_URL . 'admin/pages'; } else { alert(json.error || 'Failed to delete page'); } } catch (err) { console.error(err); alert('Deletion network failure'); } } } }; } </script> <?php $content = ob_get_clean(); require_once VIEWS_PATH . 'admin/layout/layout.php'; ?>
💾 Simpan File
Batal
⬅ Naik ke admin
1 item
Nama
Tipe
Ukuran
Diubah
Aksi
🐘
edit.php
php
40.8 KB
2026-05-31 16:45
✏️ Edit
👁️ View
🗑 Hapus