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
SELECT WITH all_tags AS (
id, SELECT GROUP_CONCAT(DISTINCT name ORDER BY name SEPARATOR ',') AS tags_list
title, FROM tags
heading, )
description, SELECT
keywords, p.id,
author, p.title,
slug, p.heading,
path, p.description,
intro, p.keywords,
body, p.author,
notes, p.slug,
draft, p.path,
changefreq, p.intro,
priority, p.body,
created, p.notes,
updated p.draft,
FROM pages p.changefreq,
WHERE id = ? p.priority,
p.created,
p.updated,
COALESCE(GROUP_CONCAT(DISTINCT t.name ORDER BY t.name SEPARATOR ','), '') AS page_tags,
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>