Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions resources/config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,43 @@ routes:
controller: Neuron\Cms\Controllers\Admin\Tags@destroy
filter: auth

# Page Management
admin_pages:
method: GET
route: /admin/pages
controller: Neuron\Cms\Controllers\Admin\Pages@index
filter: auth

admin_pages_create:
method: GET
route: /admin/pages/create
controller: Neuron\Cms\Controllers\Admin\Pages@create
filter: auth

admin_pages_store:
method: POST
route: /admin/pages
controller: Neuron\Cms\Controllers\Admin\Pages@store
filter: auth

admin_pages_edit:
method: GET
route: /admin/pages/:id/edit
controller: Neuron\Cms\Controllers\Admin\Pages@edit
filter: auth

admin_pages_update:
method: PUT
route: /admin/pages/:id
controller: Neuron\Cms\Controllers\Admin\Pages@update
filter: auth

admin_pages_destroy:
method: DELETE
route: /admin/pages/:id
controller: Neuron\Cms\Controllers\Admin\Pages@destroy
filter: auth

# Homepage
home:
method: GET
Expand Down Expand Up @@ -251,6 +288,12 @@ routes:
route: /rss
controller: Neuron\Cms\Controllers\Blog@feed

# Public Pages
page_show:
method: GET
route: /pages/:slug
controller: Neuron\Cms\Controllers\Pages@show

# Member Registration Routes
register:
method: GET
Expand Down
37 changes: 37 additions & 0 deletions resources/database/migrate/20250113000000_create_pages_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use Phinx\Migration\AbstractMigration;

/**
* Create pages table for CMS static/dynamic pages
*/
class CreatePagesTable extends AbstractMigration
{
/**
* Create pages table
*/
public function change()
{
$table = $this->table( 'pages' );

$table->addColumn( 'title', 'string', [ 'limit' => 255, 'null' => false ] )
->addColumn( 'slug', 'string', [ 'limit' => 255, 'null' => false ] )
->addColumn( 'content', 'text', [ 'null' => false, 'comment' => 'Editor.js JSON content' ] )
->addColumn( 'template', 'string', [ 'limit' => 50, 'default' => 'default' ] )
->addColumn( 'meta_title', 'string', [ 'limit' => 255, 'null' => true ] )
->addColumn( 'meta_description', 'text', [ 'null' => true ] )
->addColumn( 'meta_keywords', 'string', [ 'limit' => 255, 'null' => true ] )
->addColumn( 'author_id', 'integer', [ 'null' => false ] )
->addColumn( 'status', 'string', [ 'limit' => 20, 'default' => 'draft' ] )
->addColumn( 'published_at', 'timestamp', [ 'null' => true ] )
->addColumn( 'view_count', 'integer', [ 'default' => 0 ] )
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
->addColumn( 'updated_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP' ] )
->addIndex( [ 'slug' ], [ 'unique' => true ] )
->addIndex( [ 'author_id' ] )
->addIndex( [ 'status' ] )
->addIndex( [ 'published_at' ] )
->addForeignKey( 'author_id', 'users', 'id', [ 'delete' => 'CASCADE', 'update' => 'CASCADE' ] )
->create();
}
}
195 changes: 195 additions & 0 deletions resources/views/admin/pages/create.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Create Page</h2>
<a href="<?= route_path('admin_pages') ?>" class="btn btn-secondary">Back to Pages</a>
</div>

<form method="POST" action="<?= route_path('admin_pages_store') ?>" id="page-form">
<?= csrf_field() ?>

<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Page Content</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="title" class="form-label">Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" required>
<small class="form-text text-muted">The main heading of your page</small>
</div>

<div class="mb-3">
<label for="slug" class="form-label">Slug</label>
<div class="input-group">
<span class="input-group-text">/pages/</span>
<input type="text" class="form-control" id="slug" name="slug" pattern="[a-z0-9-]+">
</div>
<small class="form-text text-muted">URL-friendly version. Leave blank to auto-generate from title.</small>
</div>

<div class="mb-3">
<label class="form-label">Content</label>
<div id="editorjs" style="border: 1px solid #ddd; border-radius: 0.25rem; padding: 20px; min-height: 400px; background: #fff;"></div>
<input type="hidden" name="content" id="content-json">
<small class="form-text text-muted">
Use shortcodes for dynamic content: <code>[latest-posts limit="5"]</code> or <code>[contact-form]</code>
</small>
</div>
</div>
</div>
</div>

<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Publish Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="draft" selected>Draft</option>
<option value="published">Published</option>
</select>
<small class="form-text text-muted">Only published pages are visible to visitors</small>
</div>

<div class="mb-3">
<label for="template" class="form-label">Template</label>
<select class="form-select" id="template" name="template">
<option value="default" selected>Default</option>
<option value="full-width">Full Width</option>
<option value="sidebar">With Sidebar</option>
<option value="landing">Landing Page</option>
</select>
</div>
</div>
</div>

<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">SEO</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="meta_title" class="form-label">Meta Title</label>
<input type="text" class="form-control" id="meta_title" name="meta_title" maxlength="60">
<small class="form-text text-muted">60 chars max. Leave blank to use page title.</small>
</div>

<div class="mb-3">
<label for="meta_description" class="form-label">Meta Description</label>
<textarea class="form-control" id="meta_description" name="meta_description" rows="3" maxlength="160"></textarea>
<small class="form-text text-muted">160 chars max. Appears in search results.</small>
</div>

<div class="mb-3">
<label for="meta_keywords" class="form-label">Meta Keywords</label>
<input type="text" class="form-control" id="meta_keywords" name="meta_keywords">
<small class="form-text text-muted">Comma-separated</small>
</div>
</div>
</div>

<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-check-circle"></i> Create Page
</button>
<a href="<?= route_path('admin_pages') ?>" class="btn btn-outline-secondary w-100">
Cancel
</a>
</div>
</div>
</form>
</div>

<!-- Load Editor.js -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/raw@latest"></script>

<script>
const editor = new EditorJS({
holder: 'editorjs',

placeholder: 'Start writing your page content...',

tools: {
header: {
class: Header,
config: {
levels: [2, 3, 4],
defaultLevel: 2
}
},
list: {
class: List,
inlineToolbar: true
},
image: {
class: ImageTool,
config: {
endpoints: {
byFile: '/admin/upload/image'
}
}
},
quote: {
class: Quote,
inlineToolbar: true
},
code: CodeTool,
delimiter: Delimiter,
raw: {
class: RawTool,
config: {
placeholder: 'Enter HTML or shortcodes like [latest-posts limit="5"]'
}
}
},

onChange: async () => {
const savedData = await editor.save();
document.getElementById('content-json').value = JSON.stringify(savedData);
}
});

// Auto-generate slug from title
document.getElementById('title').addEventListener('input', function() {
const slugInput = document.getElementById('slug');
if (!slugInput.value || slugInput.dataset.autoGenerated === 'true') {
const slug = this.value.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
slugInput.value = slug;
slugInput.dataset.autoGenerated = 'true';
}
});

// Mark slug as manually edited
document.getElementById('slug').addEventListener('input', function() {
if (this.value) {
this.dataset.autoGenerated = 'false';
}
});

// Save content before submit
document.getElementById('page-form').addEventListener('submit', async (e) => {
e.preventDefault();

try {
const savedData = await editor.save();
document.getElementById('content-json').value = JSON.stringify(savedData);
e.target.submit();
} catch (error) {
console.error('Error saving editor content:', error);
alert('Error preparing content. Please try again.');
}
});
</script>
Loading