Added Tabs to edit page

This commit is contained in:
Nick Yeoman 2025-11-16 21:57:10 -08:00
parent bba62180fe
commit a14df54cd9
29 changed files with 771 additions and 130 deletions

View File

@ -12,7 +12,7 @@
], ],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Novaconium\\\\": "src/" "Novaconium\\": "src/"
} }
}, },
"require": { "require": {
@ -26,4 +26,3 @@
} }
} }
} }

View File

@ -1,5 +1,7 @@
<?php <?php
// Create an admin user (POST)
use Nickyeoman\Validation; use Nickyeoman\Validation;
$validate = new Validation\Validate(); $validate = new Validation\Validate();

View File

@ -2,7 +2,8 @@
$data = array_merge($data, [ $data = array_merge($data, [
'title' => 'Novaconium Edit Page', 'title' => 'Novaconium Edit Page',
'pageclass' => 'novaconium' 'pageclass' => 'novaconium',
'editor' => 'ace'
]); ]);
// Check if logged in // Check if logged in
@ -18,25 +19,35 @@ $pageid = $router->parameters['id'] ?? null;
if (!empty($pageid)) { if (!empty($pageid)) {
// Existing page: fetch from database // Existing page: fetch from database
$query = <<<EOSQL $query = <<<EOSQL
WITH all_tags AS (
SELECT GROUP_CONCAT(DISTINCT name ORDER BY name SEPARATOR ',') AS tags_list
FROM tags
)
SELECT SELECT
id, p.id,
title, p.title,
heading, p.heading,
description, p.description,
keywords, p.keywords,
author, p.author,
slug, p.slug,
path, p.path,
intro, p.intro,
body, p.body,
notes, p.notes,
draft, p.draft,
changefreq, p.changefreq,
priority, p.priority,
created, p.created,
updated p.updated,
FROM pages COALESCE(GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ','), '') AS page_tags,
WHERE id = ? at.tags_list AS existing_tags
FROM pages p
LEFT JOIN page_tags pt ON p.id = pt.page_id
LEFT JOIN tags t ON pt.tag_id = t.id
CROSS JOIN all_tags at -- Zero-cost join for scalar
WHERE p.id = ?
GROUP BY p.id;
EOSQL; EOSQL;
$data['rows'] = $db->getRow($query, [$pageid]); $data['rows'] = $db->getRow($query, [$pageid]);
@ -70,4 +81,4 @@ if (empty($pageid)) {
} }
// Render the edit page view // Render the edit page view
view('@novacore/editpage', $data); view('@novacore/editpage/index', $data);

View File

@ -151,4 +151,51 @@ if ($row['total'] < 1) {
makeitso(); makeitso();
} }
// Check Tags Table
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'tags';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE IF NOT EXISTS `tags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL UNIQUE,
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$log->info('Tags Table Created');
}
// Check Page Tags Junction Table (after tags table)
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'page_tags';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE IF NOT EXISTS `page_tags` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`page_id` int(11) NOT NULL,
`tag_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `page_id` (`page_id`),
KEY `tag_id` (`tag_id`),
FOREIGN KEY (`page_id`) REFERENCES `pages` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$log->info('Page Tags Junction Table Created');
}
view('@novacore/init', $data); view('@novacore/init', $data);

View File

@ -2,6 +2,7 @@
use Nickyeoman\Validation; use Nickyeoman\Validation;
$v = new Nickyeoman\Validation\Validate(); $v = new Nickyeoman\Validation\Validate();
use Novaconium\Services\TagManager; // Autoloads automatically
$url_error = '/novaconium/page/edit/' . $post->get('id'); // fallback for errors $url_error = '/novaconium/page/edit/' . $post->get('id'); // fallback for errors
@ -34,6 +35,7 @@ $notes = $_POST['notes'] ?? '';
$draft = !empty($post->get('draft')) ? 1 : 0; $draft = !empty($post->get('draft')) ? 1 : 0;
$changefreq = $_POST['changefreq'] ?? 'monthly'; $changefreq = $_POST['changefreq'] ?? 'monthly';
$priority = $_POST['priority'] ?? 0.0; $priority = $_POST['priority'] ?? 0.0;
$tags_json = $_POST['tags_json'] ?? '[]';
// Validate required fields // Validate required fields
if (empty($title) || empty($slug) || empty($body)) { if (empty($title) || empty($slug) || empty($body)) {
@ -64,6 +66,19 @@ try {
} }
if (!empty($id) && !$newpage) { if (!empty($id) && !$newpage) {
/** Work in Progress
// Delete old tag links for this page (cleanup)
$deleteQuery = <<<EOSQL
DELETE FROM page_tags WHERE page_id = ?
EOSQL;
$db->query($deleteQuery, [$id]);
$tagManager = new TagManager();
dd($tags_json);
**/
// Update existing page // Update existing page
$query = "UPDATE `pages` SET $query = "UPDATE `pages` SET
`title` = ?, `heading` = ?, `description` = ?, `keywords` = ?, `author` = ?, `title` = ?, `heading` = ?, `description` = ?, `keywords` = ?, `author` = ?,

29
sass/framework/_ace.sass Normal file
View File

@ -0,0 +1,29 @@
@use '../abstracts' as *
@use 'sass:color' // For color.adjust()non-deprecated color tweaks
@use '../base' as *
// ACE Editor
.editor-container
width: 100%
min-height: 400px // ~10 rows
.ace-editor
height: 400px // Scrollable
border: 1px solid $accent-light
font-family: $mono-font // Your monospace
// Ace internals (dark theme tweaks)
.ace_gutter
background: $bg-darker !important
color: $text-muted !important
border-right: 1px solid $accent-light !important
.ace_scroller
background: $bg-dark !important
.ace_text-layer .ace_print-margin
background: transparent !important
// Selection
.ace_marker-layer .ace_selection
background: color.adjust($accent-light, $alpha: -0.3) !important // Green tint

View File

@ -0,0 +1,15 @@
@use '../abstracts' as *
@use '../base' as *
@use 'sass:color' // Already there for adjusts
#edit-page-title
margin-top: 2rem
#title
width: 100%
font-size: 1.9rem
padding: 20px
&>div
font-size: 0.8rem
margin: 8px 0 0 20px

48
sass/framework/_tabs.sass Normal file
View File

@ -0,0 +1,48 @@
// framework/_tabs.sass
@use '../abstracts' as *
@use 'sass:color' // For color.adjust()non-deprecated color tweaks
// Simplified tab styling: Square borders, no rounds
.tab-container
margin: space('lg') auto
background: $bg-dark
.tab-nav
display: flex
background-color: $bg-darker
border-bottom: 1px solid $border-light
.tab-button
padding: space('sm') space('md')
background: $bg-darker
border: 1px solid $border-light
border-bottom: none
cursor: pointer
font-size: 14px
min-width: 120px // Makes them more square/equal
text-align: center
color: $text-light
font-family: $mono-font // From vars
&:hover
background-color: color.adjust($bg-darker, $lightness: 5%) // Modern, non-deprecated lighten equivalent
+ .tab-button
border-left: none // Shared borders
&.active
background-color: $bg-dark
border-bottom: 2px solid $accent-light
color: $accent-light
font-weight: bold
.tab-content
display: none
padding: space('lg')
background-color: $bg-dark
color: $text-light
border: none
&.active
display: block

108
sass/framework/_tags.sass Normal file
View File

@ -0,0 +1,108 @@
@use '../abstracts' as *
@use 'sass:color' // For color.adjust()non-deprecated color tweaks
@use '../base' as *
// Tags Dropdown
.tags-dropdown
position: absolute
top: 100%
left: 0
right: 0
max-height: 200px
overflow-y: auto
background: $bg-darker
border: 1px solid $border-light
border-top: none
border-radius: 0 0 4px 4px
list-style: none
margin: 0
padding: 0
z-index: 10
opacity: 0
visibility: hidden
transition: opacity 0.2s
&[aria-expanded="true"]
opacity: 1
visibility: visible
li
padding: space('xs') space('sm')
cursor: pointer
color: $text-light
font-size: 14px
border-bottom: 1px solid transparent
&:hover,
&.selected
background: color.adjust($accent-light, $alpha: -0.1)
color: $accent-light
&:last-child
border-bottom: none
.tags-input
position: relative // For absolute dropdown
// Tags Input
.tags-input
display: flex
flex-wrap: wrap
gap: space('xs')
align-items: center
border: 1px solid $border-light
padding: space('xs')
background: $bg-darker
min-height: 40px
grid-column: 2 // Spans input area in .form-row grid
input
border: none
background: transparent
color: $text-light
font-family: $mono-font
font-size: 14px
flex: 1
min-width: 100px
outline: none
&::placeholder
color: $text-muted
.tag-chip
display: inline-flex
align-items: center
background: color.adjust($accent-light, $alpha: -0.2)
color: $accent-light
padding: space('xs') space('sm')
border-radius: 4px
font-size: 12px
max-width: 150px
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.tag-remove
background: none
border: none
color: inherit
font-size: 16px
margin-left: space('xs')
cursor: pointer
padding: 0
line-height: 1
&:hover
opacity: 0.7
// Tags Autocomplete (native datalist styling)
.tags-input input[list]
// Existing input styles apply...
#tag-suggestions
// Native datalist not directly stylable, but options can be influenced via input focus
// For custom polyfill, add JS-generated <ul>, but native is fine for now
// Browser-specific tweaks (Chrome/Firefox)
.tags-input input[list]::-webkit-calendar-picker-indicator
display: none // Hide arrow if interfering

View File

@ -0,0 +1,47 @@
@use '../abstracts' as *
@use '../base' as *
@use 'sass:color' // Already there for adjusts
// Tooltip styling (mouse-follow version)
.tooltip
position: relative
display: inline-block
cursor: help
font-size: 16px
color: $accent-light // Green accent for ?
margin-left: space('xs') // 0.25rem
top: 2px
.tooltiptext
visibility: hidden
width: 200px
background-color: $bg-darker // Darker bg
color: $text-light // White text
text-align: left // Changed to left-align for better readability
border-radius: 0 // Square
padding: space('sm') // 0.5rem
position: fixed // Fixed to viewport, updated by JS
z-index: z('dropdown') // High z-index
opacity: 0
transition: opacity 0.3s
font-size: 14px
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) // Subtle shadow
pointer-events: none // Prevents mouse interference with tooltip
&::after
content: ""
position: absolute
top: -5px // Arrow above tooltip (points up to mouse)
left: 10px // Slight indent from left edge
border-width: 5px
border-style: solid
border-color: $bg-darker transparent transparent transparent // Upward arrow
&:hover .tooltiptext
visibility: visible
opacity: 1
// Accessibility: Hidden tooltip text for screen readers
.tooltip-desc
position: absolute
left: -9999px

View File

@ -3,3 +3,8 @@
@forward 'forms'; @forward 'forms';
@forward 'login_form'; @forward 'login_form';
@forward 'logo'; @forward 'logo';
@forward 'tabs';
@forward 'edit_page';
@forward 'tooltip';
@forward 'ace';
@forward 'tags';

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
<?php <?php
class Logger { class Logger {
protected string $logFile; protected string $logFile;
protected int $logLevelThreshold; protected int $logLevelThreshold;

View File

@ -1,5 +1,4 @@
<?php <?php
/** /**
* Use * Use
* $redirect->url('/login'); * $redirect->url('/login');

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Novaconium\Services;
class Auth class Auth
{ {

View File

@ -0,0 +1,14 @@
<?php
namespace Novaconium\Services;
/**
* TagManager Class
* Handles tag preparation, insertion, and linking for pages.
* Cleans up controller by encapsulating tag logic.
*/
class TagManager
{
public function __construct()
{
echo "class access";
}
}

View File

@ -1,7 +1,6 @@
<?php <?php
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
require_once(BASEPATH . '/vendor/autoload.php'); require_once(BASEPATH . '/vendor/autoload.php');
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
//Check if config file exists //Check if config file exists
if (file_exists(BASEPATH . '/App/config.php')) { if (file_exists(BASEPATH . '/App/config.php')) {
@ -48,7 +47,7 @@ if (!empty($config['database']['host'])) {
// Sanatize POST Data // Sanatize POST Data
if (!empty($_POST)) { if (!empty($_POST)) {
require_once(FRAMEWORKPATH . '/src/Post.php'); require_once(FRAMEWORKPATH . '/src/Post.php');
$post = new POST($_POST); $post = new Post($_POST);
} }
// Start a Redirect // Start a Redirect

View File

@ -4,3 +4,8 @@
like javascript like javascript
or analytics or analytics
--> -->
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% if editor == 'ace' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}

View File

@ -38,3 +38,13 @@
{# STYLESHEET #} {# STYLESHEET #}
<link rel="stylesheet" href="/css/novaconium.css"> <link rel="stylesheet" href="/css/novaconium.css">
{% if editor == 'ace' %}
<!-- ACE Editor -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
<!-- END ACE Editor -->
{% endif %}

View File

@ -0,0 +1,40 @@
<!-- Ace Editor -->
<script>
// Ace Editor Init (cleaned: removed duplicate setUseWorker, added null checks)
document.addEventListener('DOMContentLoaded', function() {
const bodyTextarea = document.getElementById('body');
const bodyEditorEl = document.getElementById('body-editor');
if (bodyTextarea && bodyEditorEl && typeof ace !== 'undefined') { // Check ace loaded
try {
const editor = ace.edit(bodyEditorEl);
editor.session.setValue(bodyTextarea.value || ''); // Load initial HTML
editor.session.setMode('ace/mode/html'); // HTML syntax highlighting
editor.setTheme('ace/theme/tomorrow_night'); // Dark theme (black bg, green accents)
editor.setOptions({
fontSize: 14,
showPrintMargin: false,
wrap: true, // Line wrapping
useWorker: false // Disable worker for linting if not needed (faster)
});
editor.session.setUseWorker(false); // No JS linting in HTML mode
// Enable basic autocomplete
editor.setOptions({ enableBasicAutocompletion: true });
// Sync back to textarea on change
editor.session.on('change', function() {
bodyTextarea.value = editor.getValue(); // Full HTML string
});
console.log('Ace loaded! Initial value:', editor.getValue().substring(0, 50) + '...'); // Debug
} catch (error) {
console.error('Ace init failed:', error); // Graceful error
bodyEditorEl.style.display = 'none'; // Hide div, show plain textarea if needed
bodyTextarea.style.display = 'block';
}
} else {
console.warn('Ace elements or lib missing'); // Fallback to plain textarea
}
});
</script>

View File

@ -0,0 +1,192 @@
<script>
// Tab switching JS (unchanged)
function switchTab(tabId, button) {
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => content.classList.remove('active'));
const buttons = document.querySelectorAll('.tab-button');
buttons.forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
button.classList.add('active');
if (tabId === 'content6') {
setTimeout(initTags, 0);
}
}
// Tags Init (with custom dropdown autocomplete)
let tagsListeners = [];
function initTags() {
const tagsInput = document.getElementById('tags-input');
const tagsField = document.getElementById('tags');
const hiddenTags = document.getElementById('tags_json');
const dropdown = document.getElementById('tags-dropdown');
if (!tagsInput || !tagsField || !hiddenTags || !dropdown) {
console.warn('Tags elements missing');
return;
}
// Clean old listeners
tagsListeners.forEach(ls => ls());
tagsListeners = [];
let tags = [];
let existingTags = [];
try {
tags = JSON.parse(tagsInput.dataset.tags || '[]');
existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]');
} catch (e) {
console.warn('JSON error:', e);
tags = [];
existingTags = [];
}
let selectedIndex = -1; // For keyboard nav
// Render chips (unchanged)
function renderTags() {
tagsInput.innerHTML = '';
tags.forEach((tag, index) => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `${tag} <button type="button" class="tag-remove">&times;</button>`;
chip.querySelector('.tag-remove').onclick = () => {
tags.splice(index, 1);
renderTags();
hiddenTags.value = JSON.stringify(tags);
};
tagsInput.appendChild(chip);
});
tagsInput.appendChild(tagsField);
tagsInput.appendChild(dropdown); // Re-add dropdown
hiddenTags.value = JSON.stringify(tags);
}
// Filter and render dropdown
function updateDropdown() {
const value = tagsField.value.toLowerCase().trim();
dropdown.innerHTML = '';
dropdown.setAttribute('aria-expanded', value ? 'true' : 'false');
selectedIndex = -1;
if (!value) return;
const matches = existingTags.filter(tag =>
tag.toLowerCase().startsWith(value) && !tags.includes(tag.toLowerCase())
).slice(0, 10); // Top 10 matches, exclude existing
matches.forEach((tag, index) => {
const li = document.createElement('li');
li.textContent = tag;
li.setAttribute('role', 'option');
li.onclick = () => selectTag(tag);
li.onmouseover = () => { selectedIndex = index; updateHighlight(); };
dropdown.appendChild(li);
});
if (matches.length) dropdown.parentElement.classList.add('has-dropdown');
else dropdown.parentElement.classList.remove('has-dropdown');
}
// Highlight selected in dropdown
function updateHighlight() {
dropdown.querySelectorAll('li').forEach((li, index) => {
li.classList.toggle('selected', index === selectedIndex);
});
}
// Select tag from dropdown
function selectTag(tag) {
addTag(tag, true); // Add as chip, refocus
tagsField.value = ''; // Clear input
dropdown.setAttribute('aria-expanded', 'false');
}
// Add tag (unchanged)
function addTag(inputValue, refocus = false) {
const tag = inputValue.trim().toLowerCase().replace(/[^\w-]/g, '');
if (tag && tag.length > 0 && tag.length <= 50 && !tags.includes(tag)) {
tags.push(tag);
renderTags();
if (refocus) tagsField.focus();
}
tagsField.value = '';
updateDropdown(); // Hide dropdown after add
}
// Events: Enter/comma/TAB adds tag (no form submit)
const keydownListener = (e) => {
console.log('Keydown:', e.key, 'Value:', tagsField.value, 'Selected:', selectedIndex);
if (dropdown.getAttribute('aria-expanded') === 'true') {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, dropdown.querySelectorAll('li').length - 1);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).scrollIntoView({ block: 'nearest' });
} else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIndex >= 0) {
e.preventDefault();
const selected = dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`).textContent;
selectTag(selected); // Adds chip, refocuses
} else if (e.key === 'Escape') {
e.preventDefault();
tagsField.blur();
}
}
// Fallback for no dropdown or non-selected Tab/Enter
if (!dropdown.getAttribute('aria-expanded') === 'true' || selectedIndex < 0) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag(tagsField.value, true);
} else if (e.key === 'Tab') {
e.preventDefault();
if (tagsField.value.trim()) addTag(tagsField.value, true); // Add typed value, refocus
}
}
};
tagsField.addEventListener('keydown', keydownListener);
tagsListeners.push(() => tagsField.removeEventListener('keydown', keydownListener));
const inputListener = () => updateDropdown();
tagsField.addEventListener('input', inputListener);
tagsListeners.push(() => tagsField.removeEventListener('input', inputListener));
const blurListener = () => {
setTimeout(() => dropdown.setAttribute('aria-expanded', 'false'), 150); // Hide after blur
if (tagsField.value.trim()) addTag(tagsField.value);
};
tagsField.addEventListener('blur', blurListener);
tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener));
// Initial render
renderTags();
console.log('Tags ready with autocomplete, loaded:', tags.length, 'tags');
}
// Init on load + observe (unchanged)
document.addEventListener('DOMContentLoaded', function() {
const tagsTab = document.getElementById('content6');
if (tagsTab && tagsTab.classList.contains('active')) {
initTags();
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
if (target.id === 'content6' && target.classList.contains('active')) {
initTags();
}
}
});
});
observer.observe(document.body, { subtree: true, attributes: true });
});
</script>

View File

@ -1,92 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h2>Edit Page - {{ title }}</h2>
<form method="post" action="/novaconium/savePage" id="edit-page-form-novaconium">
<input type="hidden" name="id" value="{{ rows.id }}">
<input type="hidden" name="token" value="{{ token }}">
<div class="form-group">
<label for="title">Title:</label>
<input type="text" id="title" name="title" value="{{ rows.title }}" required>
</div>
<div class="form-group">
<label for="slug">
Slug: (<a href="/page/{{ rows.slug }}" target="_new">/page/{{ rows.slug }}</a>)
</label>
<input type="text" id="slug" name="slug" value="{{ rows.slug }}" required>
</div>
<div class="form-group fullwidth">
<label for="body">Body:</label>
<textarea id="body" name="body" rows="10">{{ rows.body }}</textarea>
</div>
<h2>Metadata</h2>
<div class="form-group">
<label for="description">Description:</label>
<input type="text" id="description" name="description" value="{{ rows.description }}">
</div>
<div class="form-group">
<label for="keywords">Keywords:</label>
<input type="text" id="keywords" name="keywords" value="{{ rows.keywords }}">
</div>
<div class="form-group">
<label for="author">Author:</label>
<input type="text" id="author" name="author" value="{{ rows.author }}">
</div>
<h2>CMS Info</h2>
<div class="form-group">
<label for="path">Path:</label>
<input type="text" id="path" name="path" value="{{ rows.path }}">
</div>
<div class="form-group">
<label for="changefreq">Change Frequency:</label>
<select id="changefreq" name="changefreq">
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
{% for freq in freqs %}
<option value="{{ freq }}" {% if rows.changefreq == freq %}selected{% endif %}>
{{ freq|capitalize }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="priority">Priority (0.0 - 1.0):</label>
<input type="number" id="priority" name="priority" value="{{ rows.priority }}" step="0.1" min="0" max="1">
</div>
<div class="form-group fullwidth">
<label for="notes">Notes:</label>
<textarea id="notes" name="notes" rows="5">{{ rows.notes }}</textarea>
</div>
<h2>Page Data</h2>
<div class="form-group">
<label for="heading">Heading:</label>
<input type="text" id="heading" name="heading" value="{{ rows.heading }}">
</div>
<div class="form-group fullwidth">
<label for="intro">Intro:</label>
<textarea id="intro" name="intro" rows="5">{{ rows.intro }}</textarea>
</div>
<p><strong>Created:</strong> {{ rows.created|date("Y-m-d H:i:s") }}</p>
<p><strong>Last Updated:</strong> {{ rows.updated|date("Y-m-d H:i:s") }}</p>
<button type="submit">Save Changes</button>
</form>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h2>Edit Page - {{ title }}</h2>
<form method="post" action="/novaconium/savePage" id="edit-page-form-novaconium">
<input type="hidden" name="id" value="{{ rows.id }}">
<input type="hidden" name="token" value="{{ token }}">
<div class="form-group">
<button type="submit">Save Changes</button>
</div>
<div id="edit-page-title" class="form-group">
<label for="title">
Title
<span class="tooltip">?<span class="tooltiptext">This is the title for the cms, it's the default in twig, heading and metadata can be overwritten.</span></span>
</label>
<input type="text" id="title" name="title" value="{{ rows.title }}" required>
<div id="edit-page-dates">
<strong>Last Updated:</strong> {{ rows.updated|date("Y-m-d H:i:s") }},
<strong>Created:</strong> {{ rows.created|date("Y-m-d H:i:s") }}
</div>
</div>
<div class="tab-container">
<!-- Tab Navigation -->
<nav class="tab-nav">
<button type="button" class="tab-button active" onclick="switchTab('content1')">Basic Info</button>
<button type="button" class="tab-button" onclick="switchTab('content2')">SEO & Meta</button>
<button type="button" class="tab-button" onclick="switchTab('content3')">Sitemap</button>
<button type="button" class="tab-button" onclick="switchTab('content4')">Page Tweaks</button>
<button type="button" class="tab-button" onclick="switchTab('content5')">Page Notes</button>
<button type="button" class="tab-button" onclick="switchTab('content6', this)">Tags</button>
</nav>
<div id="content1" class="tab-content active">
{% include '@novacore/editpage/tab-main.html.twig' %}
</div>
<div id="content2" class="tab-content">
{% include '@novacore/editpage/tab-metadata.html.twig' %}
</div>
<div id="content3" class="tab-content">
{% include '@novacore/editpage/tab-other.html.twig' %}
</div>
<div id="content4" class="tab-content">
{% include '@novacore/editpage/tab-tweaks.html.twig' %}
</div>
<div id="content5" class="tab-content">
{% include '@novacore/editpage/tab-notes.html.twig' %}
</div>
<div id="content6" class="tab-content">
{% include '@novacore/editpage/tab-tags.html.twig' %}
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,16 @@
<div class="form-group">
<label for="slug">
Slug: (<a href="/page/{{ rows.slug }}" target="_new">/page/{{ rows.slug }}</a>)
<span class="tooltip">?<span class="tooltiptext">Slug is a human readable but uri friendly name for the page.</span></span>
</label>
<input type="text" id="slug" name="slug" value="{{ rows.slug }}" required>
</div>
<!-- Ace Editor -->
<div class="form-group fullwidth">
<label for="body">Body:</label>
<div class="editor-container">
<textarea id="body" name="body" rows="10" style="display: none;">{{ rows.body|default('')|e('html') }}</textarea>
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
</div>
</div>

View File

@ -0,0 +1,17 @@
<h2>Metadata</h2>
<div class="form-group">
<label for="description">Description:</label>
<input type="text" id="description" name="description" value="{{ rows.description }}">
</div>
<div class="form-group">
<label for="keywords">Keywords:</label>
<input type="text" id="keywords" name="keywords" value="{{ rows.keywords }}">
</div>
<div class="form-group">
<label for="author">Author:</label>
<input type="text" id="author" name="author" value="{{ rows.author }}">
</div>

View File

@ -0,0 +1,4 @@
<div class="form-group fullwidth">
<label for="notes">Notes:</label>
<textarea id="notes" name="notes" rows="5">{{ rows.notes }}</textarea>
</div>

View File

@ -0,0 +1,25 @@
<h2>Sitemap</h2>
<div class="form-group">
<label for="path">Path:</label>
<input type="text" id="path" name="path" value="{{ rows.path }}">
</div>
<div class="form-group">
<label for="changefreq">Change Frequency:</label>
<select id="changefreq" name="changefreq">
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
{% for freq in freqs %}
<option value="{{ freq }}" {% if rows.changefreq == freq %}selected{% endif %}>
{{ freq|capitalize }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="priority">Priority (0.0 - 1.0):</label>
<input type="number" id="priority" name="priority" value="{{ rows.priority }}" step="0.1" min="0" max="1">
</div>

View File

@ -0,0 +1,12 @@
<h1>Tags</h1>
<label for="tags">
Tags:
<span class="tooltip">?<span class="tooltiptext">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span><span class="tooltip-desc" id="desc-tags">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span></span>
</label>
<div class="form-row">
<div class="tags-input" id="tags-input" data-tags="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}" data-existing-tags="{{ rows.existing_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
<input type="text" id="tags" name="tags" placeholder="e.g., seo, cms, php" aria-describedby="desc-tags">
<ul id="tags-dropdown" class="tags-dropdown" role="listbox" aria-expanded="false"></ul> {# Custom dropdown #}
</div>
<input type="hidden" id="tags_json" name="tags_json" value="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
</div>

View File

@ -0,0 +1,10 @@
<h2>Page Data</h2>
<div class="form-group">
<label for="heading">Heading:</label>
<input type="text" id="heading" name="heading" value="{{ rows.heading }}">
</div>
<div class="form-group fullwidth">
<label for="intro">Intro:</label>
<textarea id="intro" name="intro" rows="5">{{ rows.intro }}</textarea>
</div>