Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

104 changed files with 177 additions and 3604 deletions

View File

@ -1,4 +1,4 @@
![Novaconium PHP](/_assets/novaconium-logo.png)
![Novaconium PHP](/_assets/header.svg)
# Novaconium PHP: A PHP Framework Built from the Past
@ -6,53 +6,15 @@ NovaconiumPHP is a high-performance PHP framework designed with inspiration from
Pronounced: Noh-vah-koh-nee-um
Packagist: https://packagist.org/packages/4lt/novaconium
Master Repo: https://git.4lt.ca/4lt/novaconium
## Getting Started
Novaconium is heavly influenced by docker, but you can use composer outside of docker.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Install-Composer-On-Debian.md).
### Installation
```bash
PROJECTNAME=novaproject;
mkdir -p $PROJECTNAME/novaconium;
cd $PROJECTNAME;
mkdir project_name;
cd project_name;
mkdir -p App/controllers App/templates App/views public
touch App/controllers/404.php App/controllers/index.php App/views/index.html.twig public/index.php public/.htaccess App/routes.php
docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest require 4lt/novaconium;
cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
# Edit .env
# pwgen -cnsB1v 12 root password
# pwgen -cnsB1v 12 mysql user password (need in both config and env)
# pwgen -cnsB1v 64 framework key (need in config)
# Edit novaconium/App/config.php
docker compose up -d
composer require 4lt/novaconium
```
## Documentation
* [Novaconiumm Official Repo](https://git.4lt.ca/4lt/novaconium)
* [CORXN Apache and PHP Container for Novaconium](https://git.4lt.ca/4lt/CORXN)
### How it works
#### htaccess
htaccess ensures that all requests are sent to index.php
#### index.php
index.php does two things:
1. Allows you to turn on error reporting (off by default)
2. Loads the novaconium bootstrap file novaconium.php
#### novaconium.php
What happens here:
1. Autoload composer
1. Loads configurations

39
_assets/header.svg Normal file
View File

@ -0,0 +1,39 @@
<svg width="100%" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="black"/>
<defs>
<radialGradient id="star-gradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color: white; stop-opacity: 1" />
<stop offset="100%" style="stop-color: white; stop-opacity: 0" />
</radialGradient>
<g id="star">
<circle cx="0" cy="0" r="2" fill="white" />
</g>
</defs>
<!-- Lots of stars moving outward slower -->
<g>
<use href="#star" x="50%" y="50%" transform="translate(-300,-150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(300,-150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(-300,150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(300,150) scale(1)" />
<use href="#star" x="50%" y="50%" transform="translate(-150,-75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(150,-75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(-150,75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(150,75) scale(0.5)" />
<use href="#star" x="50%" y="50%" transform="translate(-75,-37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(75,-37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(-75,37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(75,37) scale(0.7)" />
<use href="#star" x="50%" y="50%" transform="translate(-350,-175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(350,-175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(-350,175) scale(0.4)" />
<use href="#star" x="50%" y="50%" transform="translate(350,175) scale(0.4)" />
<animateTransform attributeName="transform" type="scale" from="1" to="10" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" from="1" to="0" dur="2s" repeatCount="indefinite" />
</g>
<!-- Centered Title -->
<text x="50%" y="50%" fill="white" font-size="40" text-anchor="middle" font-family="Arial" dy=".3em">
Novaconium PHP
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,6 +1,7 @@
{
"name": "4lt/novaconium",
"description": "A high-performance PHP framework built from the past.",
"version": "1.0.0",
"license": "MIT",
"authors": [
{
@ -11,13 +12,12 @@
}
],
"autoload": {
"psr-4": {
"Novaconium\\": "src/"
}
"psr-4": {
"Novaconium\\\\": "src/"
}
},
"require": {
"twig/twig": "*",
"nickyeoman/php-validation-class": "^5.0"
"twig/twig": "*"
},
"minimum-stability": "stable",
"extra": {
@ -26,3 +26,4 @@
}
}
}

View File

@ -1,50 +0,0 @@
<?php
$framework_routes = [
'/novaconium' => [
'get' => 'NOVACONIUM/init'
],
'/novaconium/create_admin' => [
'post' => 'NOVACONIUM/create_admin'
],
'/novaconium/login' => [
'post' => 'NOVACONIUM/authenticate',
'get' => 'NOVACONIUM/login'
],
'/novaconium/dashboard' => [
'get' => 'NOVACONIUM/dashboard'
],
'/novaconium/pages' => [
'get' => 'NOVACONIUM/pages'
],
'/novaconium/page/edit/{id}' => [
'get' => 'NOVACONIUM/editpage'
],
'/novaconium/page/create' => [
'get' => 'NOVACONIUM/editpage'
],
'/novaconium/savePage' => [
'post' => 'NOVACONIUM/savepage'
],
'/novaconium/messages' => [
'get' => 'NOVACONIUM/messages'
],
'/novaconium/messages/delete/{id}' => [
'get' => 'NOVACONIUM/message_delete'
],
'/novaconium/messages/edit/{id}' => [
'get' => 'NOVACONIUM/message_edit'
],
'/novaconium/message_save' => [
'post' => 'NOVACONIUM/message_save'
],
'/novaconium/logout' => [
'post' => 'NOVACONIUM/logout',
'get' => 'NOVACONIUM/logout'
],
'/novaconium/sitemap.xml' => [
'get' => 'NOVACONIUM/sitemap'
],
'/novaconium/sample/{slug}' => [
'get' => 'NOVACONIUM/samples'
],
];

View File

@ -1,4 +0,0 @@
<?php
http_response_code('404');
header("Content-Type: text/html");
view('@novacore/404');

View File

@ -1,70 +0,0 @@
<?php
use Nickyeoman\Validation;
$v = new Nickyeoman\Validation\Validate();
$url_success = '/novaconium/dashboard';
$url_fail = '/novaconium/login';
// Don't go further if already logged in
if ( !empty($session->get('username')) ) {
$redirect->url($url_success);
makeitso();
}
// Make sure Session Token is correct
if ($session->get('token') != $post->get('token')) {
$messages->addMessage('error', "Invalid Session.");
$log->error("Login Authentication - Invalid Session Token");
}
// Handle Username
$rawUsername = $post->get('username', null);
$cleanUsername = $v->clean($rawUsername); // Clean the input
$username = strtolower($cleanUsername); // Convert to lowercase
if (!$username) {
$messages->addMessage('error', "No Username given.");
}
// Handle Password
$password = $v->clean($post->get('password', null));
if ( empty($password) ) {
$messages->addMessage('error', "Password Empty.");
}
/*************************************************************************************************************
* Query Database
************************************************************************************************************/
if ($messages->count('error') === 0) {
$query = "SELECT id, username, email, password, blocked FROM users WHERE username = ? OR email = ?";
$matched = $db->getRow($query, [$username, $username]);
if (empty($matched)) {
$messages->addMessage('error', "User or Password incorrect.");
$log->warning("Login Authentication - Login Error, user doesn't exist");
}
}
if ($messages->count('error') === 0) {
// Re-apply pepper
$peppered = hash_hmac('sha3-512', $password, $config['secure_key']);
// Verify hashed password
if (!password_verify($peppered, $matched['password'])) {
$messages->addMessage('error', "User or Password incorrect.");
$log->warning("Login Authentication - Login Error, password wrong");
}
}
// Process Login or Redirect
if ($messages->count('error') === 0) {
$query = "SELECT groupName FROM user_groups WHERE user_id = ?";
$groups = $db->getRow($query, [$matched['id']]);
$session->set('username', $cleanUsername);
$session->set('group', $groups['groupName']);
$redirect->url($url_success);
$log->info("Login Authentication - Login Success");
} else {
$redirect->url($url_fail);
}

View File

@ -1,60 +0,0 @@
<?php
// Create an admin user (POST)
use Nickyeoman\Validation;
$validate = new Validation\Validate();
$valid = true;
$p = $post->all();
// Check secure key
if (empty($p['secure_key']) || $p['secure_key'] !== $config['secure_key']) {
$valid = false;
}
// Username
$name = $validate->clean($p['username']);
if (!$validate->minLength($name, 1)) {
$valid = false;
}
// Email
if (empty($p['email'])) {
$valid = false;
} elseif (!$validate->isEmail($p['email'])) {
$valid = false;
}
// Password
if (empty($p['password'])) {
$valid = false;
} else {
// Use pepper + Argon2id
$peppered = hash_hmac('sha3-512', $p['password'], $config['secure_key']);
$hashed_password = password_hash($peppered, PASSWORD_ARGON2ID);
}
if ($valid) {
// Insert user
$query = <<<EOSQL
INSERT INTO `users`
(`username`, `password`, `email`, `validate`, `confirmationToken`, `reset`, `created`, `updated`, `confirmed`, `blocked`)
VALUES
(?, ?, ?, NULL, NULL, NULL, NOW(), NOW(), 1, 0);
EOSQL;
$params = [$name, $hashed_password, $p['email']];
$db->query($query, $params);
$userid = $db->lastid();
// Assign admin group
$groupInsertQuery = <<<EOSQL
INSERT INTO `user_groups` (`user_id`, `groupName`) VALUES (?, ?);
EOSQL;
$db->query($groupInsertQuery, [$userid, 'admin']);
}
// Always redirect at end
$redirect->url('/novaconium');

View File

@ -1,13 +0,0 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Dashboard Page',
'pageclass' => 'novaconium'
]);
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
view('@novacore/dashboard', $data);

View File

@ -1,84 +0,0 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Edit Page',
'pageclass' => 'novaconium',
'editor' => 'ace'
]);
// Check if logged in
if (empty($session->get('username'))) {
$messages->error('You are not logged in');
$redirect->url('/novaconium/login');
makeitso();
}
// Get page ID from router parameters
$pageid = $router->parameters['id'] ?? null;
if (!empty($pageid)) {
// Existing page: fetch from database
$query = <<<EOSQL
WITH all_tags AS (
SELECT GROUP_CONCAT(DISTINCT name ORDER BY name SEPARATOR ',') AS tags_list
FROM tags
)
SELECT
p.id,
p.title,
p.heading,
p.description,
p.keywords,
p.author,
p.slug,
p.path,
p.intro,
p.body,
p.notes,
p.draft,
p.changefreq,
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;
$data['rows'] = $db->getRow($query, [$pageid]);
// If no row is found, treat as new page
if (!$data['rows']) {
$pageid = null;
}
}
if (empty($pageid)) {
// New page: set default values for all fields
$data['rows'] = [
'id' => 'newpage',
'title' => '',
'heading' => '',
'description' => '',
'keywords' => '',
'author' => $session->get('username') ?? '',
'slug' => '',
'path' => '',
'intro' => '',
'body' => '',
'notes' => '',
'draft' => 0,
'changefreq' => 'monthly',
'priority' => 0.0,
'created' => date('Y-m-d H:i:s'),
'updated' => date('Y-m-d H:i:s')
];
}
// Render the edit page view
view('@novacore/editpage/index', $data);

View File

@ -1,201 +0,0 @@
<?php
$data = [
'secure_key' => false,
'gen_key' => NULL,
'users_created' => false,
'empty_users' => false,
'show_login' => false,
'token' => $session->get('token'),
'title' => 'Novaconium Admin'
];
// Check if SECURE KEY is Set in
if ($config['secure_key'] !== null && strlen($config['secure_key']) === 64) {
$data['secure_key'] = true;
} else {
$data['gen_key'] = substr(bin2hex(random_bytes(32)), 0, 64);
$log->warn('secure_key not detected');
}
// Check if user table exists
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'users';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(30) NOT NULL,
`password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`validate` varchar(32) DEFAULT NULL,
`confirmationToken` varchar(255) DEFAULT NULL,
`reset` varchar(32) DEFAULT NULL,
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`confirmed` tinyint(1) NOT NULL DEFAULT 0,
`blocked` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$data['users_created'] = true;
$log->info('Users Table Created');
}
// Check Usergroup
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'user_groups';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `user_groups` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) UNSIGNED NOT NULL,
`groupName` VARCHAR(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
EOSQL;
$db->query($query);
$log->info('User_groups Table Created');
}
// Check Pages Table
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'pages';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `pages` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`heading` varchar(255) NOT NULL,
`description` varchar(255) NOT NULL,
`keywords` varchar(255) NOT NULL,
`author` varchar(255) NOT NULL,
`slug` varchar(255) NOT NULL,
`path` varchar(255) DEFAULT NULL,
`intro` text DEFAULT NULL,
`body` text DEFAULT NULL,
`notes` text DEFAULT NULL,
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`draft` tinyint(1) NOT NULL DEFAULT 1,
`changefreq` varchar(7) NOT NULL DEFAULT 'monthly',
`priority` float(4,1) NOT NULL DEFAULT 0.0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$log->info('Pages Table Created');
}
// Check ContactForm Table
$query = <<<EOSQL
SELECT TABLE_NAME
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND TABLE_NAME = 'contactForm';
EOSQL;
$result = $db->query($query);
if ($result->num_rows === 0) {
$query = <<<EOSQL
CREATE TABLE `contactForm` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`message` text DEFAULT NULL,
`created` datetime NOT NULL DEFAULT current_timestamp(),
`unread` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Unread is true by default',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
EOSQL;
$db->query($query);
$log->info('ContactForm Table Created');
}
// Check if a user exists
$result = $db->query("SELECT COUNT(*) as total FROM users");
$row = $result->fetch_assoc();
if ($row['total'] < 1) {
$data['empty_users'] = true;
} else {
$log->info('Init Run complete, all sql tables exist with a user.');
// Everything is working, send them to login page
$redirect->url('/novaconium/login');
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 File

@ -1,9 +0,0 @@
<?php
$data['title'] = 'Novaconium Login Page';
// Don't come here if logged in
if ($session->get('username')) {
$redirect->url('/novaconium/dashboard');
makeitso();
}
view('@novacore/login');

View File

@ -1,5 +0,0 @@
<?php
$session->kill();
$log->info("Logout - Logout Success - " . $_SERVER['REMOTE_ADDR']);
$redirect->url('/');
makeitso();

View File

@ -1,15 +0,0 @@
<?php
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
$messageid = $router->parameters['id'];
$query="DELETE FROM contactForm WHERE `contactForm`.`id` = ?";
$db->query($query, [$messageid]);
$redirect->url('/novaconium/messages');
$messages->notice("Removed Message $messageid");
makeitso();

View File

@ -1,19 +0,0 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Message Page',
'pageclass' => 'novaconium'
]);
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
$messageid = $router->parameters['id'];
$query = "SELECT id, name, email, message, created, unread FROM contactForm WHERE id = '$messageid'";
$data['themessage'] = $db->getRow($query);
view('@novacore/editmessage', $data);

View File

@ -1,57 +0,0 @@
<?php
use Nickyeoman\Validation;
$v = new Nickyeoman\Validation\Validate();
$url_success = '/novaconium/messages';
$url_error = '/novaconium/messages/edit/' . $post->get('id'); // Redirect back to the message edit form on error
// Check if logged in
if (empty($session->get('username'))) {
$messages->error('You are not logged in');
$redirect->url('/novaconium/login');
makeitso();
}
// Check CSRF token
if ($session->get('token') != $post->get('token')) {
$messages->error('Invalid token');
$redirect->url($url_success);
makeitso();
}
// Get POST data
$id = $post->get('id');
$name = $post->get('name');
$email = $post->get('email');
$message = $post->get('message');
$unread = !empty($post->get('unread')) ? 1 : 0;
// Validate required fields
if (empty($id) || empty($message) || empty($email)) {
$messages->error('One of the required fields was empty.');
$redirect->url($url_error);
makeitso();
}
try {
// Prepare update query
$query = "UPDATE `contactForm`
SET `name` = ?, `email` = ?, `message` = ?, `unread` = ?
WHERE `id` = ?";
$params = [$name, $email, $message, $unread, $id];
$db->query($query, $params);
$messages->notice('Message updated successfully');
} catch (Exception $e) {
$messages->error('Error updating message: ' . $e->getMessage());
$redirect->url($url_error);
makeitso();
}
// Redirect to success page
$redirect->url($url_success);

View File

@ -1,21 +0,0 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Messages',
'pageclass' => 'novaconium'
]);
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
// Get the pages
$query = "SELECT id, name, email, LEFT(message, 40) AS message, created, unread FROM contactForm";
$matched = $db->getRows($query);
$data['messages'] = $matched;
view('@novacore/messages', $data);

View File

@ -1,20 +0,0 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Pages',
'pageclass' => 'novaconium'
]);
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
// Get the pages
$query = "SELECT id, title, created, updated, draft FROM pages";
$matched = $db->getRows($query);
$data['pages'] = $matched;
view('@novacore/pages', $data);

View File

@ -1,34 +0,0 @@
<?php
/**
* Pure Twig, no db example
*
* Replicate Hugo but with html and twig (not markdown)
**/
// Variables
$pt = '@novacore/samples'; //Define the view directory
//$pt = 'samples'; //drop the core for your project
//Grab the slug
$slug = $router->parameters['slug'];
//build path
$tmpl = $pt . '/' . $slug;
//Check if file exits
$baseDir = (strpos($pt, 'novacore') !== false) ? FRAMEWORKPATH : BASEPATH;
if (strpos($pt, '@novacore') !== false) {
$baseDir = str_replace('@novacore', FRAMEWORKPATH . '/views', $pt);
} else {
$baseDir = str_replace('@novacore', BASEPATH . '/views', $pt);
}
$possibleFile = $baseDir . '/' . $slug . '.html.twig'; // add .twig extension if needed
if (is_file($possibleFile) && is_readable($possibleFile)) {
view($tmpl, $data);
} else {
http_response_code('404');
header("Content-Type: text/html");
view('@novacore/404');
}

View File

@ -1,103 +0,0 @@
<?php
use Nickyeoman\Validation;
$v = new Nickyeoman\Validation\Validate();
use Novaconium\Services\TagManager; // Autoloads automatically
$url_error = '/novaconium/page/edit/' . $post->get('id'); // fallback for errors
// Check login
if (empty($session->get('username'))) {
$messages->error('You are not logged in');
$redirect->url('/novaconium/login');
makeitso();
}
// Check token
if ($session->get('token') != $post->get('token')) {
$messages->error('Invalid Token');
$redirect->url('/novaconium/pages');
makeitso();
}
// Gather POST data
$id = $post->get('id');
$title = $_POST['title'] ?? '';
$heading = $_POST['heading'] ?? '';
$description = $_POST['description'] ?? '';
$keywords = $_POST['keywords'] ?? '';
$author = $_POST['author'] ?? '';
$slug = $_POST['slug'] ?? '';
$path = $_POST['path'] ?? null;
$intro = $_POST['intro'] ?? '';
$body = $_POST['body'] ?? '';
$notes = $_POST['notes'] ?? '';
$draft = !empty($post->get('draft')) ? 1 : 0;
$changefreq = $_POST['changefreq'] ?? 'monthly';
$priority = $_POST['priority'] ?? 0.0;
$tags_json = $_POST['tags_json'] ?? '[]';
// Validate required fields
if (empty($title) || empty($slug) || empty($body)) {
$messages->error('Title, Slug, and Body are required.');
$redirect->url($url_error);
makeitso();
}
try {
if ($id == 'newpage') {
// Create new page
$query = "INSERT INTO `pages`
(`title`, `heading`, `description`, `keywords`, `author`,
`slug`, `path`, `intro`, `body`, `notes`,
`draft`, `changefreq`, `priority`, `created`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
$params = [
$title, $heading, $description, $keywords, $author,
$slug, $path, $intro, $body, $notes,
$draft, $changefreq, $priority
];
$db->query($query, $params);
$id = $db->lastid; // Get new page ID
$messages->notice('Page Created');
$newpage = true;
} else {
$newpage = false;
}
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
$query = "UPDATE `pages` SET
`title` = ?, `heading` = ?, `description` = ?, `keywords` = ?, `author` = ?,
`slug` = ?, `path` = ?, `intro` = ?, `body` = ?, `notes` = ?,
`draft` = ?, `changefreq` = ?, `priority` = ?, `updated` = NOW()
WHERE `id` = ?";
$params = [
$title, $heading, $description, $keywords, $author,
$slug, $path, $intro, $body, $notes,
$draft, $changefreq, $priority, $id
];
$db->query($query, $params);
$messages->notice('Page Updated');
}
} catch (Exception $e) {
$messages->error($e->getMessage());
$redirect->url($url_error);
makeitso();
}
// Redirect to edit page
$redirect->url('/novaconium/page/edit/' . $id);

View File

@ -1,42 +0,0 @@
<?php
header('Content-Type: text/xml');
// https://www.sitemaps.org/protocol.html
// Check it here: https://www.mysitemapgenerator.com/service/check.html
$query=<<<EOSQL
SELECT draft, slug, updated, changefreq, priority, path
FROM pages
WHERE priority > 0
AND draft = 0
ORDER BY updated DESC;
EOSQL;
$thepages = $db->getRows($query);
// Start the view
echo '<?xml version="1.0" encoding="UTF-8"?>';
echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
// Loop through the pages
if ( ! empty($thepages) ) {
foreach( $thepages as $v) {
$date = (new \DateTime($v['updated']))->format('Y-m-d');
echo "<url>";
if ( empty($v['path']) )
echo "<loc>" . $config['base_url'] . '/page/' . $v['slug'] . "</loc>";
else
echo "<loc>" . $config['base_url'] . $v['path'] . "</loc>";
echo "<lastmod>" . $date . "</lastmod>";
echo "<changefreq>" . $v['changefreq'] . "</changefreq>";
echo "<priority>" . sprintf("%.1f", $v['priority']) . "</priority>";
echo "</url>";
}
} else {
echo "no pages added yet";
}
echo "</urlset>";

View File

@ -1,6 +0,0 @@
# 404 Page
404 page is created like any other page.
Create a 404.php in your controllers and a 404.html.twig in your views.
anytime a resource is not found by the router, it will default to this controller.
if you do not have this controller in your app, it will default to the novaconium 404 page.

View File

@ -1,18 +0,0 @@
# PHP Composer Cheatsheet
Install novaconium with composer: ```composer require 4lt/novaconium```
Install novaconium with composer in docker: ```docker run --rm --interactive --tty --volume $PWD:/app composer:latest require 4lt/novaconium```
Update novaconium with composer in docker: ```docker run --rm --interactive --tty --volume $PWD:/app composer:latest update```
## Install Composer natively on Debian
Assuming you have nala installed:
```bash
sudo nala install curl php-cli php-mbstring git unzip
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php
```

View File

@ -1,52 +0,0 @@
# Configuration File
## App/config.php
The configuration file holds a php multi dimentional array for configuration.
#### database
This is the connection setting for mariadb.
```
'database' => [
'host' => 'ny-db',
'name' => 'nydb',
'user' => 'nydbu',
'pass' => 'as7!d5fLKJ2DLKJS5',
'port' => 3306
],
```
#### base_url
Defines the url to use
```
'base_url' => 'https://www.nickyeoman.com',
```
#### secure_key
The security key is used to verify admin account and salt encrpytion functions.
You can generate a key with ```pwgen -cnsB1v 64```
but if you don't set one, novaconium will generate one for you to use (you have to explicily set it though).
```
'secure_key' => '',
```
#### logfile
sets the path of the log file.
```
'logfile' => '/logs/novaconium.log',
```
#### loglevel
Sets the logging level for the app.
```
'loglevel' => 'ERROR' // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE'
```

View File

@ -1,19 +0,0 @@
# Logging
You can use the logging class to output to a file.
use ```$log->info(The Message');```
Logging levels are:
```
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
];
```
It's recommended that production is set to ERROR.
You set the log level in /App/config.php under 'loglevel' => 'ERROR'

View File

@ -1,3 +0,0 @@
# Messages
Messages is $messages.

View File

@ -1,5 +0,0 @@
# Post
There is a post class.
It cleans the post.
You can access it with $post.

View File

@ -1,6 +0,0 @@
# Redirect
How to use redirect class.
$redirect->url;
it's called on every page, if you set it more than once the last one is used.

View File

@ -1,12 +0,0 @@
# Sass
Running Sass
```
sudo docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container sass novaconium/sass/project.sass novaconium/public/css/novaconium.css
```
Compressed:
```
sudo docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container sass --style=compressed novaconium/sass/project.sass novaconium/public/css/novaconium.css
```

View File

@ -1,5 +0,0 @@
# Sessions
There is a sessions handler built into Novaconium.
$session

View File

@ -1,7 +0,0 @@
# Style Sheets
The idea is to use sass to generate only what you need for style sheets.
```bash
sudo docker run --rm -v $(pwd):/usr/src/app sass-container sass sass/project.sass public/css/main.css
```

View File

@ -1,21 +0,0 @@
# Twig
## Overrides
You can override twig templates by creating the same file in the templates directory.
## Calling View
There is a $data that the system uses to store arrays for twig you can save to this array:
```
$data['newinfo'] = 'stuff';
view('templatename');
```
and that will automotically go to twig.
or you can create a new array and pass it in:
```
$anotherArr['newinfo'] = 'stuff';
view('templatename',$anotherArr);
```

View File

@ -1,9 +0,0 @@
# Docker Cheatsheet (for Novaconium)
## Sample Docker Compose File
See the skeleton directory for an example docker setup.
## Start Docker
```docker compose up -d```

View File

@ -0,0 +1 @@
<h1>This is 404</h1>

View File

@ -0,0 +1,2 @@
<?php
echo $twig->render('index.html.twig');

View File

@ -1,4 +1,6 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?_uri=$1 [QSA,L]

View File

@ -0,0 +1,6 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
define('BASEPATH', dirname(__DIR__, 1));
require_once(BASEPATH . '/vendor/4lt/novaconium/src/bootstrap.php');
?>

9
examples/App/routes.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$routes = [
'/about' => [
'file' => 'about'
],
'/' => [
'file' => 'index'
]
];

View File

@ -0,0 +1,7 @@
{% extends '@nytwig/master.html.twig' %}
{% block content %}
<h1>This is twig</h1>
<p>Content Here</p>
{% endblock %}

View File

@ -1,80 +0,0 @@
// abstracts/_variables.sass (updated)
@use 'sass:map'
// Color palette (Sass map: HSL for modularity; all lightnesses darkened ~10-20%)
$colors: (
'bg-dark': hsl(0, 0%, 2%),
'bg-darker': hsl(210, 7%, 5%),
'text-light': hsla(0, 0%, 100%, 1.00),
'text-lighter': hsl(120, 52%, 15%),
'text-muted': hsl(120, 40%, 45%),
'accent-light': hsl(120, 100%, 35%),
'accent-success': hsl(120, 80%, 45%),
'accent-warning': hsl(60, 100%, 35%),
'accent-error': hsl(0, 100%, 45%),
'border-light': hsla(120, 60%, 70%, 0.2)
)
// Helper functions to pull from map
@function color($key)
@return map.get($colors, $key)
// Direct vars for common use
$bg-dark: color('bg-dark') !default
$bg-darker: color('bg-darker') !default
$text-light: color('text-light') !default
$text-lighter: color('text-lighter') !default
$text-muted: color('text-muted') !default
$accent-light: color('accent-light') !default
$accent-success: color('accent-success') !default
$accent-warning: color('accent-warning') !default
$accent-error: color('accent-error') !default
$border-light: color('border-light') !default
$font-stack: 'Courier New', 'SF Mono', monospace
$mono-font: 'Courier New', 'SF Mono', 'Fira Code', Consolas, monospace
$spacing: (
'xs': 0.25rem,
'sm': 0.5rem,
'md': 1rem,
'lg': 1.5rem,
'xl': 2.5rem,
'2xl': 4rem
)
@function space($key)
@return map.get($spacing, $key)
$breakpoints: (
'sm': 640px,
'md': 768px,
'lg': 1024px,
'xl': 1280px,
'2xl': 1536px
)
$z-index: (
'dropdown': 1000,
'sticky': 50,
'modal': 2000
)
@function z($key)
@return map.get($z-index, $key)
$dark-mode: true !default
:root
--bg-dark: #{color('bg-dark')}
--bg-darker: #{color('bg-darker')}
--text-light: #{color('text-light')}
--text-lighter: #{color('text-lighter')}
--text-muted: #{color('text-muted')}
--accent-light: #{color('accent-light')}
--accent-success: #{color('accent-success')}
--accent-warning: #{color('accent-warning')}
--accent-error: #{color('accent-error')}
--border-light: #{color('border-light')}
--font-stack: #{$font-stack}

View File

@ -1 +0,0 @@
@forward 'variables';

File diff suppressed because one or more lines are too long

View File

@ -1,178 +0,0 @@
// base/_reset.sass
@use '../abstracts' as *
// Global box-sizing and resets
*
box-sizing: border-box
html
font-size: 16px // Base font size
line-height: 1.5
scroll-behavior: smooth
background-color: $bg-dark // Dark bg
color: $text-light // Light text
body
margin: 0
padding: 0
font-family: $font-stack // e.g., -apple-system, sans-serif
background-color: $bg-dark
color: $text-light
min-height: 100vh
// Headings: Light, bold, with spacing
h1, h2, h3, h4, h5, h6
margin: 0 0 0.5em
font-weight: 600
line-height: 1.2
color: $accent-light
h1
font-size: 2.5rem
h2
font-size: 2rem
h3
font-size: 1.75rem
h4
font-size: 1.5rem
h5
font-size: 1.25rem
h6
font-size: 1rem
// Paragraphs and text
p
margin: 0 0 1em
small
font-size: 0.875rem
strong, b
font-weight: 700
em, i
font-style: italic
// Links: Light with hover underline
a
color: $accent-light
text-decoration: none
border-bottom: 1px solid transparent
transition: border-color 0.2s ease
&:hover, &:focus
border-bottom-color: $accent-light
outline: none
// Lists
ul, ol
margin: 0 0 1em
padding-left: 1.5em
li
margin-bottom: 0.25em
// Blockquote
blockquote
margin: 1em 0
padding: 0.5em 1em
border-left: 4px solid $accent-light
background-color: rgba($bg-dark, 0.5)
color: $text-light
// Code and pre
code, kbd, samp
font-family: $mono-font
font-size: 0.875em
background-color: rgba($text-light, 0.1)
padding: 0.125em 0.25em
border-radius: 3px
pre
margin: 1em 0
padding: 1em
overflow: auto
background-color: rgba($text-light, 0.1)
border-radius: 4px
code
background: none
padding: 0
// Tables
table
width: 100%
border-collapse: collapse
margin: 1em 0
th, td
padding: 0.75em
text-align: left
border-bottom: 1px solid rgba($text-light, 0.2)
th
font-weight: 600
// Forms
input, select, textarea, button
font-family: inherit
font-size: inherit
background-color: rgba($text-light, 0.1)
color: $text-light
border: 1px solid rgba($text-light, 0.3)
border-radius: 4px
padding: 0.5em
&:focus
outline: none
border-color: $accent-light
box-shadow: 0 0 0 2px rgba($accent-light, 0.2)
button
cursor: pointer
transition: background-color 0.2s ease
&:disabled
opacity: 0.5
cursor: not-allowed
// Images and media
img
max-width: 100%
height: auto
border-radius: 4px // Optional subtle rounding
figure
margin: 1em 0
// HR
hr
border: none
height: 1px
background-color: rgba($text-light, 0.2)
margin: 2em 0
// Accessibility: Focus visible for all interactive elements
*:focus-visible
outline: 2px solid $accent-light
outline-offset: 2px
// Print styles (optional)
@media print
body
background: white !important
color: black !important
::selection
background: rgba($accent-light, 0.3) // Subtle accent highlight for that terminal select vibe
color: $text-light // Crisp text contrast
// Optional: Vendor prefixes for broader support (though modern browsers are solid)
*::-moz-selection
background: rgba($accent-light, 0.3)
color: $text-light

View File

@ -1,2 +0,0 @@
@forward 'reset';
@forward 'background';

View File

@ -1,29 +0,0 @@
@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

@ -1,15 +0,0 @@
@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

View File

@ -1,61 +0,0 @@
// pages/_forms.sass
@use '../abstracts' as *
@use '../base' as *
#edit-page-form-novaconium
// Form groups: Spacing
.form-group
margin-bottom: space('md') // 1rem
&.fullwidth textarea
width: 100%
resize: vertical
.checkbox-group
margin-top: space('lg') // 1.5rem
// Inputs: Dark bg, green focus
input[type="text"],
input[type="number"],
textarea,
select
background-color: color('bg-darker') // Deeper green-black
border: 1px solid color('border-light')
color: $text-light
padding: space('sm')
border-radius: 4px
font-family: 'VT323', $mono-font // Matrix monospace
&:focus
border-color: $accent-light // Lime focus
box-shadow: 0 0 0 space('xs') rgba($accent-light, 0.3) // Glow ring
outline: none
input[type="checkbox"]
accent-color: $accent-light // Green checkbox
// Submit button: Green primary
button[type="submit"]
background-color: $accent-light
color: color('bg-dark') // Dark text on green
border: none
padding: space('sm') space('md')
border-radius: 4px
cursor: pointer
font-family: 'VT323', $mono-font
&:hover, &:focus
background-color: hsl(120, 100%, 30%) // Darker green (35% L -5%)
box-shadow: 0 0 4px rgba($accent-light, 0.5)
// Text/links: Muted with hover
p
color: $text-muted
a
color: $accent-light
&:hover
color: $text-lighter // Brighter green
text-decoration: underline

View File

@ -1,53 +0,0 @@
// framework/_login_form.sass
@use '../abstracts' as *
#login
// No background or borderuse parent's ambient styling
padding: 0 // Or keep minimal if needed; adjust to 2rem if you want internal space
// No centering: left-aligned by default
// Wrapper for inputs/button to align widths
input[type="text"],
input[type="password"],
button[type="submit"]
width: 100% // Full width of form (no max cap for flow)
max-width: 300px // Keep cap for consistency across fields
padding: 1rem
margin-bottom: 1rem
box-sizing: border-box // Include padding in width
font-size: 1rem
border: 1px solid $border-light
border-radius: 4px
background: $bg-dark
color: $text-light
// Icon backgrounds: Cyber icons via data URI, now in white for visibility
input[type="text"] // Username input
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E")
background-repeat: no-repeat
background-position: 1rem center
background-size: 20px
padding-left: 3rem // Space for icon
input[type="password"] // Password input
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E")
background-repeat: no-repeat
background-position: 1rem center
background-size: 20px
padding-left: 3rem // Space for icon
button[type="submit"]
background: $accent-light // Green accent for submit
border-color: $accent-light
cursor: pointer
transition: background 0.2s
&:hover
background: $accent-light
text-shadow: 0 0 5px rgba($accent-light, 0.5) // Cyber glow
// No icon needed for button, but add if you want (e.g., arrow)
&::after
content: '' // Simple arrow, or swap for Material Icon
margin-left: 0.5rem

View File

@ -1,78 +0,0 @@
#biglogo
position: relative
display: inline-block
padding: 20px 0
border-radius: 0 // Keep it sharp, no curves
overflow: hidden
&::before
content: ''
position: absolute
top: 0
left: 0
right: 0
bottom: 0
background-image: linear-gradient(rgba(0, 255, 0, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 0, 0.02) 1px, transparent 1px)
background-size: 20px 20px
opacity: 0.1 // Subtle grid/matrix rain hint
pointer-events: none
// Scan lines for extra flicker
&::after
content: ''
position: absolute
top: 0
left: 0
right: 0
height: 1px
background: linear-gradient(90deg, transparent, #00ff00, transparent)
opacity: 0.05
animation: scan 3s linear infinite
.main
display: block
font-family: 'Orbitron', sans-serif // Cyberpunk geometric punch
font-size: 4.5rem // Chunky figlet scale
font-weight: 900 // Black weight for max angular depth
color: #fff
letter-spacing: 0.1em // Slightly looser for Orbitron's geometry
line-height: 0.85 // Compact height like figlet blocks
text-transform: uppercase
// Multi-layer shadow to mimic ansishadow's depth: base shadow + edge glow
text-shadow: 4px 4px 0 #222, 5px 5px 0 #111, -1px -1px 0 #00ff00, 1px 1px 20px rgba(0, 255, 0, 0.3) // Main drop shadow, deeper offset, subtle green edge highlight for cyber pop, glow for security flair
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8)) // Browser shadow boost
.sub
display: block
font-family: 'Orbitron', sans-serif // Match for cohesion, or swap to monospace if you want contrast
font-size: 2.5rem
font-weight: 700 // Bold but not overpowering
color: #fff
letter-spacing: 0.15em // Wider for emphasis
margin-top: 0.5rem
text-shadow: 2px 2px 0 #222, -1px -1px 0 #00ff00, 1px 1px 10px rgba(0, 255, 0, 0.2) // Lighter shadow, subtle green edge, glow
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6))
// Optional: Glitch animation on hover for that hacked feel
&:hover
.main
animation: glitch 0.3s infinite
@keyframes scan
0%
top: -1px
100%
top: 100%
@keyframes glitch
0%, 100%
transform: translate(0)
20%
transform: translate(-2px, 2px)
40%
transform: translate(-2px, -2px)
60%
transform: translate(2px, 2px)
80%
transform: translate(2px, -2px)

View File

@ -1,61 +0,0 @@
// layout/_main.sass
@use '../abstracts' as *
@use '../base' as *
// Main page wrapper: Centered flex
#page .container
display: flex
padding: 0
justify-content: center
align-items: flex-start
padding-top: space('lg')
// Article content: Fixed width, no shrink
article
width: 1140px
flex-shrink: 0
padding: 3rem
margin: 0
background-color: $bg-dark
border: 1px solid $bg-darker
ul#leftnav
width: 320px
flex-shrink: 0
list-style: none
padding: 0
margin: 0 space('xl') 0 0 // Right margin preserved (overrides the 0 for other sides)
background-color: $bg-dark
border: 1px solid $bg-darker
#leftnav li
border-bottom: 1px solid color('border-light')
&:last-child
border-bottom: none
#leftnav a
display: block
padding: space('sm') space('md') // Consistent spacing
text-decoration: none
color: $text-light // Theme text
&:hover, &:focus
background-color: rgba($accent-light, 0.1) // Green hover tint
color: $accent-light // Accent on hover
border-radius: 4px
// Responsive: Stack on mobile (add to themes/ for media queries)
@media (max-width: 1280px) // Your xl breakpoint
#page .container
flex-direction: column
align-items: center
article, #leftnav
width: 100%
max-width: 900px
margin: space('md') 0
#leftnav
margin-right: 0

View File

@ -1,48 +0,0 @@
// 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

View File

@ -1,108 +0,0 @@
@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

@ -1,47 +0,0 @@
@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

@ -1,58 +0,0 @@
// components/_ui.sass
@use '../abstracts' as *
@use '../base' as *
// Code blocks: Monospace lock-in with darker green tint
code
font-family: 'VT323', $mono-font // Prioritize Matrix font
font-size: 0.875rem // Smaller for inline; reset has base
background-color: color('bg-darker') // Deeper green-black
color: $text-light // Lighter green text
padding: space('xs') space('sm')
border-radius: 4px
border: 1px solid color('border-light')
box-shadow: 0 0 2px rgba($accent-light, 0.1) // Subtle glow
// Utility: Small text (if still needed; consider rem-based)
.small
font-size: 0.625rem // 10px equiv; use sparingly
// Error/notice divs: Centered alerts with theme colors
div.error, div.notice, div#debug
margin: space('xl') auto // Top/bottom spacing from map
width: 900px // Fixed width; add media query for mobile later
padding: space('lg') // Generous padding
border-radius: 6px
border: 1px solid
div.error
background-color: rgba(color('accent-error'), 0.1) // Subtle red tint
color: color('accent-error') // Darker red text
border-color: color('accent-error')
div.notice
background-color: rgba(color('accent-success'), 0.1) // Green tint
color: color('accent-success') // Heavier green text
border-color: color('accent-success')
div#debug
background-color: color('bg-darker') // Deeper bg for debug
color: $text-muted // Muted green
border-color: color('border-light')
margin-top: space('2xl')
margin-bottom: space('2xl')
// Tables: Simplified; reset handles collapse/padding
.pages-table
width: 100%
border: 1px solid color('border-light') // Green border
th, td
border: 1px solid color('border-light')
text-align: left
th
background-color: rgba($accent-light, 0.05) // Subtle accent bg
color: $text-lighter // Header green
font-weight: 600 // Semi-bold; no bold in monospace

View File

@ -1,10 +0,0 @@
@forward 'main';
@forward 'ui';
@forward 'forms';
@forward 'login_form';
@forward 'logo';
@forward 'tabs';
@forward 'edit_page';
@forward 'tooltip';
@forward 'ace';
@forward 'tags';

View File

@ -1,4 +0,0 @@
// novaconium.sass
@use 'abstracts' as *
@use 'base' as *
@use 'framework' as *

View File

@ -1,2 +0,0 @@
MYSQL_ROOT_PASSWORD=random
MYSQL_PASSWORD=random

3
skeleton/.gitignore vendored
View File

@ -1,3 +0,0 @@
data/
novaconium/vendor/
novaconium/logs/

View File

@ -1,59 +0,0 @@
# Sample Docker Compose
services:
corxn:
image: 4lights/corxn:6.0.0
ports:
- "8000:80"
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./novaconium:/data
- ./data/logs:/var/log/apache2 # Optional Logs
restart: unless-stopped
networks:
- internal
- proxy
redis:
image: redis:latest
networks:
- internal
restart: unless-stopped
mariadb:
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: novadb
MYSQL_USER: novaconium
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./data/db:/var/lib/mysql
networks:
- internal
restart: unless-stopped
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
restart: unless-stopped
ports:
- "8001:80"
networks:
- internal
environment:
- PMA_ARBITRARY=-1
- PMA_HOST=mariadb
- PMA_USER=root
- PMA_PASSWORD=${MYSQL_ROOT_PASSWORD}
- UPLOAD_LIMIT=200M
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
proxy:
external: true
internal:
driver: bridge

View File

@ -1,15 +0,0 @@
<?php
$config = [
'database' => [
'host' => 'mariadb',
'name' => 'novadb',
'user' => 'novaconium',
'pass' => '',
'port' => 3306
],
'base_url' => 'http://localhost:8000',
'secure_key' => '', //64 alphanumeric characters
'logfile' => '/logs/novaconium.log',
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE'
'fonts' => 'https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap'
];

View File

@ -1,2 +0,0 @@
<?php
view('index');

View File

@ -1,6 +0,0 @@
<?php
$routes = [
'/' => [
'get' => 'index'
]
];

View File

@ -1 +0,0 @@
{# Overrides go here #}

View File

@ -1,21 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1 id="biglogo"><span class="main">Novaconium PHP</span></h1>
<h2>Minimalist PHP framework</h2>
<p>
Edit <code>App/routes.php</code> and <code>App/controllers/index.php</code><br>
to customize this page.
</p>
<p>Sign in to the <a href="/novaconium">Administration</a></p>
<h2>Documentation</h2>
<ul>
<li><a href="https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/StyleSheets-sass.md">Style Sheets</a></li>
<li><a href="https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Twig-Views.md">Twig overrides</a></li>
</ul>
<h2>Repository</h2>
<p class="small">Visit Source Control Repository for <a href="https://git.4lt.ca/4lt/novaconium">Novaconium</a></p>
{% endblock %}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
<?php
// error_reporting(E_ALL);
// ini_set('display_errors', 1);
define('BASEPATH', dirname(__DIR__, 1));
require_once(BASEPATH . '/vendor/4lt/novaconium/src/novaconium.php');
?>

View File

@ -1 +0,0 @@
@use '../vendor/4lt/novaconium/sass/novaconium'

View File

@ -1,126 +0,0 @@
<?php
class Database {
private $conn;
public $lastid;
public function __construct($dbinfo) {
$this->conn = new mysqli($dbinfo['host'], $dbinfo['user'], $dbinfo['pass'], $dbinfo['name']);
if ($this->conn->connect_error) {
die("Connection failed: " . $this->conn->connect_error);
}
}
public function query($query, $params = []) {
// Clean up pending results to avoid "commands out of sync"
while ($this->conn->more_results() && $this->conn->next_result()) {
if ($res = $this->conn->use_result()) {
$res->free();
}
}
// Prepare the SQL statement
$stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters if needed
if (!empty($params)) {
$types = str_repeat('s', count($params)); // Use 's' for all types, or detect types dynamically
$stmt->bind_param($types, ...$params);
}
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
}
// Save last insert id if it's an INSERT query
if (preg_match('/^\s*INSERT/i', $query)) {
$this->lastid = $this->conn->insert_id;
} else {
$this->lastid = 0;
}
// Decide what to return
// For SELECT/SHOW etc., return result set
if (preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)/i', $query)) {
$result = $stmt->get_result();
$stmt->close();
return $result;
}
// For INSERT/UPDATE/DELETE, return success status
$success = $stmt->affected_rows;
$stmt->close();
return $success;
}
public function lastid() {
return $this->lastid;
}
public function getRow($query, $params = []) {
try {
// Prepare the SQL statement
$stmt = $this->conn->prepare($query);
if (!$stmt) {
throw new Exception("Query preparation failed: " . $this->conn->error);
}
// Bind parameters
if (!empty($params)) {
$types = str_repeat('s', count($params)); // You may improve this with actual type detection
$stmt->bind_param($types, ...$params);
}
// Execute the statement
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Query execution failed: " . $stmt->error);
}
// Get result
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$stmt->close();
return $row;
} catch (Exception $e) {
echo "An error occurred: " . $e->getMessage();
return null;
}
}
public function getRows($query, $params = []) {
$stmt = $this->conn->prepare($query);
if (!$stmt) {
die("Query preparation failed: " . $this->conn->error);
}
// Bind parameters if provided
if (!empty($params)) {
$types = str_repeat('s', count($params)); // Assuming all are strings, adjust as needed
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
$result = $stmt->get_result(); // Requires MySQL Native Driver (mysqlnd)
if ($result) {
return $result->fetch_all(MYSQLI_ASSOC);
} else {
return [];
}
}
public function close() {
$this->conn->close();
}
}

View File

@ -1,54 +0,0 @@
<?php
class Logger {
protected string $logFile;
protected int $logLevelThreshold;
const LEVELS = [
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
'NONE' => 999
];
public function __construct(string $logFile, string $minLevel = 'DEBUG') {
$this->logFile = $logFile;
$minLevel = strtoupper($minLevel);
$this->logLevelThreshold = self::LEVELS[$minLevel] ?? 0;
}
protected function log(string $level, string $message): void {
$level = strtoupper($level);
if (!isset(self::LEVELS[$level]) || self::LEVELS[$level] < $this->logLevelThreshold) {
return;
}
$time = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'];
$logEntry = "[$time] [$ip] [$level] $message" . PHP_EOL;
file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
public function debug(string $msg): void {
$this->log('DEBUG', $msg);
}
public function info(string $msg): void {
$this->log('INFO', $msg);
}
public function warning(string $msg): void {
$this->log('WARNING', $msg);
}
// Alias
public function warn(string $msg): void {
$this->log('WARNING', $msg);
}
public function error(string $msg): void {
$this->log('ERROR', $msg);
}
}

View File

@ -1,87 +0,0 @@
<?php
class MessageHandler {
private $messages = [
'error' => [],
'warning' => [],
'notice' => [],
'success' => []
];
public function __construct(array $sessionMessages = [])
{
// Merge existing session messages into the default structure
foreach ($this->messages as $type => $_) {
if (isset($sessionMessages[$type]) && is_array($sessionMessages[$type])) {
$this->messages[$type] = $sessionMessages[$type];
}
}
}
// Add a message of a specific type
public function addMessage($type, $message) {
if (!isset($this->messages[$type])) {
throw new Exception("Invalid message type: $type");
}
$this->messages[$type][] = $message;
}
public function error($message){
$this->addMessage('error', $message);
}
public function notice($message){
$this->addMessage('notice', $message);
}
// Get all messages of a specific type
public function getMessages($type) {
return $this->messages[$type] ?? [];
}
// Get all messages of a specific type
public function showMessages($type) {
$result = $this->messages[$type] ?? [];
$this->messages[$type] = []; // Clear messages after showing
return $result;
}
// Get all messages of all types
public function getAllMessages() {
return $this->messages;
}
// Get the count of messages for a specific type
public function count($type) {
return isset($this->messages[$type]) ? count($this->messages[$type]) : 0;
}
// Get the total count of all messages
public function totalCount() {
return array_sum(array_map('count', $this->messages));
}
// Check if there are any messages of a specific type
public function hasMessages($type) {
return !empty($this->messages[$type]);
}
// Check if there are any messages at all
public function hasAnyMessages() {
return $this->totalCount() > 0;
}
// Clear messages of a specific type
public function clear($type) {
if (isset($this->messages[$type])) {
$this->messages[$type] = [];
}
}
// Clear all messages
public function clearAll() {
foreach ($this->messages as $type => $list) {
$this->messages[$type] = [];
}
}
}

View File

@ -1,25 +0,0 @@
<?php
class Post {
private $data = [];
public function __construct($post) {
$this->sanitize($post);
}
private function sanitize($post) {
foreach ($post as $key => $value) {
$this->data[$key] = is_array($value)
? filter_var_array($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS)
: filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
}
}
public function get($key, $default = null) {
return $this->data[$key] ?? $default;
}
public function all() {
return $this->data;
}
}

View File

@ -1,39 +0,0 @@
<?php
/**
* Use
* $redirect->url('/login');
* to trigger a redirect
*/
class Redirect {
private ?string $url = null;
private int $statusCode = 303;
public function url(string $relativeUrl, int $statusCode = 303): void {
$this->statusCode = $statusCode;
// Detect HTTPS
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
// Get Hostname
$host = $_SERVER['HTTP_HOST'];
// Get Base Directory
$basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
// Construct Absolute URL
$this->url = "$protocol://$host$basePath/" . ltrim($relativeUrl, '/');
}
public function isset(): bool {
return !is_null($this->url);
}
public function execute(): void {
if ($this->url) {
header("Location: " . $this->url, true, $this->statusCode);
exit();
}
}
}

View File

@ -4,46 +4,22 @@ class Router {
public $routes = [];
public $query = [];
public $path;
public $controller;
public $controllerPath;
public $parameters = [];
public $requestType = 'get';
public $controllerPath = BASEPATH . '/App/controllers/404.php';
public function __construct() {
$this->routes = $this->loadRoutes();
$this->path = $this->preparePath();
$this->query = $this->prepareQuery();
$this->requestType = $this->getRequestType();
$this->controller = $this->findController();
$this->controllerPath = $this->setRouteFile();
$this->loadRoutes();
$this->preparePath();
$this->prepareQuery();
$this->setRouteFile();
}
private function loadRoutes() {
require_once(FRAMEWORKPATH . '/config/routes.php');
// Check if Path exists
if (file_exists(BASEPATH . '/App/routes.php')) {
require_once( BASEPATH . '/App/routes.php');
}
$routes = array_merge((array)$routes, (array)$framework_routes);
return $routes;
require_once( BASEPATH . '/App/routes.php');
$this->routes = $routes;
}
private function preparePath() {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
//homepage
if ($path === '/') {
return $path;
}
// remove empty directory path
$path = rtrim($path, '/'); // remove trailing slash
//remove anything after and including ampersand
$path = preg_replace('/&.+$/', '', $path);
return $path;
$this->path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
}
private function prepareQuery() {
@ -52,91 +28,16 @@ class Router {
if (isset($parsedUri['query'])) {
parse_str($parsedUri['query'], $queryArray);
}
return $queryArray;
$this->query = $queryArray;
}
private function getRequestType() {
// is the requewst a get or post?
if (empty($_POST)) {
return 'get';
} else {
return 'post';
}
}
private function findController() {
// one to one match
if (array_key_exists($this->path, $this->routes)) {
if (!empty($this->routes[$this->path][$this->requestType])) {
return $this->routes[$this->path][$this->requestType];
}
}
foreach ($this->routes as $key => $value) {
// Check if key contains a curly bracket, if not continue. We already checked above.
if (!strpos($key, '{')) continue;
// Remove everything after the curly bracket, from key
$keyPath = substr($key, 0, strpos($key, '{'));
//see if keyPath matches the first characters of $this->path, only the first characters have to match
if (strpos($this->path, $keyPath) === 0) {
// We have a potential match. Now check if the parameter count is equal
$keyParams = explode('/', $key);
$pathParams = explode('/', $this->path);
$keyParamCount = count($keyParams);
$pathParamCount = count($pathParams);
if ($keyParamCount === $pathParamCount) {
for ($i=0; $i < $pathParamCount; $i++) {
if (strpos($keyParams[$i], '{') !== false) {
$keyParams[$i] = substr($keyParams[$i], 1, -1);
$this->parameters[$keyParams[$i]] = $pathParams[$i];
return $this->routes[$key][$this->requestType];
}
}
}
}
}
return '404';
}
// checks if the file exists, sets file path
private function setRouteFile() {
if (str_starts_with($this->controller, 'NOVACONIUM')) {
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
$cp = FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
} else {
$cp = BASEPATH . '/App/controllers/' . $this->controller . '.php';
}
if (file_exists($cp)) {
return $cp;
} else {
//Check if 404 exits
if (file_exists(BASEPATH . '/App/controllers/404.php')) {
return BASEPATH . '/App/controllers/404.php';
} else {
return FRAMEWORKPATH . '/controllers/404.php';
foreach ($this->routes as $key => $value) {
if ( $this->path == $key) {
$this->controllerPath = BASEPATH . '/App/controllers/' . $value['file'] . '.php';
break;
}
}
}
public function debug() {
echo '<div id="router-debug-container" class="debug">';
echo '<table border="1" cellpadding="10" cellspacing="0">';
echo '<tr><th>Url Path</th><td>' . htmlspecialchars($this->path) . '</td></tr>';
echo '<tr><th>Controller Path</th><td>' . htmlspecialchars($this->controllerPath) . '</td></tr>';
echo '<tr><th>Parameters</th><td><pre>' . print_r($this->parameters, true) . '</pre></td></tr>';
echo '<tr><th>Routes</th><td><pre>' . print_r($this->routes, true) . '</pre></td></tr>';
echo '</table></div>';
die();
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Novaconium\Services;
class Auth
{
public function __construct()
{
}
}

View File

@ -1,14 +0,0 @@
<?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,63 +0,0 @@
<?php
class Session {
private $session;
public function __construct() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$this->session = &$_SESSION; // Reference $_SESSION to keep them in sync
if (!isset($this->session['token'])) {
$this->setToken();
}
if (!isset($this->session['messages'])) {
$this->session['messages'] = []; // Always ensure messages is an array
}
if (!isset($this->session['formData'])) {
$this->session['formData'] = []; // Initialize formData
}
if (!isset($this->session['errors'])) {
$this->session['errors'] = []; // Initialize errors
}
}
public function setToken() {
$this->session['token'] = bin2hex(random_bytes(32));
}
public function set($key, $value) {
$this->session[$key] = $value;
}
public function get($key) {
return isset($this->session[$key]) ? $this->session[$key] : null;
}
public function flash($key) {
$return = $this->get($key);
$this->delete($key);
return $return;
}
public function debug() {
return $this->session;
}
public function delete($key) {
if (isset($this->session[$key])) {
unset($this->session[$key]);
}
}
public function write() {
// No need to assign to $_SESSION since $this->session is a reference
session_write_close();
}
public function kill() {
$this->session = [];
$_SESSION = [];
session_destroy();
}
}

13
src/bootstrap.php Normal file
View File

@ -0,0 +1,13 @@
<?php
require_once(BASEPATH . '/vendor/autoload.php');
//Twig
$loader = new Twig\Loader\FilesystemLoader(BASEPATH . '/App/views/');
$loader->addPath(BASEPATH . '/vendor/4lt/novaconium/twig', 'novaconium');
$loader->addPath(BASEPATH . '/App/templates', 'override');
$twig = new Twig\Environment($loader);
// Load a controller
require_once('Router.php');
$router = new Router();
require_once($router->controllerPath);

View File

@ -1,29 +0,0 @@
<?php
/**
* Dump and Die
*/
function dd(...$vars) {
echo "<pre style='background:#222;color:#0f0;padding:10px;border-radius:5px;'>";
foreach ($vars as $var) {
var_dump($var);
echo "\n";
}
echo "</pre>";
die();
}
function makeitso() {
global $session, $db, $redirect, $config, $messages, $log;
if (!empty($config['database']['host'])) {
$db->close();
}
$session->set('messages', $messages->getAllMessages());
$session->write();
$redirect->execute();
exit();
}

View File

@ -1,63 +0,0 @@
<?php
require_once(BASEPATH . '/vendor/autoload.php');
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
//Check if config file exists
if (file_exists(BASEPATH . '/App/config.php')) {
require_once(BASEPATH . '/App/config.php');
} else {
require_once(FRAMEWORKPATH . '/skeleton/novaconium/App/config.php');
}
// Logging
require_once(FRAMEWORKPATH . '/src/Logger.php');
$log = new Logger(BASEPATH . $config['logfile'], $config['loglevel']);
// Global Functions
require_once(FRAMEWORKPATH . '/src/functions.php');
// Creates the view() function using twig
$data = array();
require_once(FRAMEWORKPATH . '/src/twig.php');
$data['fonts'] = $config['fonts'];
// Start a Session
require_once(FRAMEWORKPATH . '/src/Session.php');
$session = new Session();
$data['token'] = $session->get('token');
$data['username'] = $session->get('username');
if ($config['loglevel'] == 'DEBUG') {
$data['debug'] = nl2br(print_r($session->debug(), true));
}
// Messages
require_once(FRAMEWORKPATH . '/src/MessageHandler.php');
$messages = new MessageHandler($session->flash('messages'));
foreach (['error','notice'] as $key){
$data[$key] = $messages->showMessages($key);
}
// Load Database Class
if (!empty($config['database']['host'])) {
require_once(FRAMEWORKPATH . '/src/Database.php');
$db = new Database($config['database']);
}
// Sanatize POST Data
if (!empty($_POST)) {
require_once(FRAMEWORKPATH . '/src/Post.php');
$post = new Post($_POST);
}
// Start a Redirect
require_once(FRAMEWORKPATH . '/src/Redirect.php');
$redirect = new Redirect();
// Load a controller
require_once(FRAMEWORKPATH . '/src/Router.php');
$router = new Router();
//$router->debug();
require_once($router->controllerPath);
makeitso();

View File

@ -1,32 +0,0 @@
<?php
//Twig
function view($name = '', $moreData = []) {
global $config, $data; // Use the globally included $config
if (!empty($moreData)){
$data = array_merge($data, $moreData);
}
$loader = new Twig\Loader\FilesystemLoader(BASEPATH . '/App/views/');
$loader->addPath(FRAMEWORKPATH . '/twig', 'novaconium');
$loader->addPath(FRAMEWORKPATH . '/views', 'novacore');
$loader->addPath(BASEPATH . '/App/templates', 'override');
$twig = new Twig\Environment($loader);
// Add config to Twig globally
$twig->addGlobal('config', $config);
// Check if the template exists
if (file_exists(BASEPATH . '/App/views/' . $name . '.html.twig')) {
echo $twig->render($name . '.html.twig', $data);
return true;
} elseif (str_starts_with($name, '@')) { // Check if using framework
echo $twig->render($name . '.html.twig', $data);
return true;
} else {
echo "Error: Twig Template ($name) Not Found.";
return false;
}
}

View File

@ -1,11 +0,0 @@
<!--
What goes very last on the page.
right before the /body
like javascript
or analytics
-->
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% if editor == 'ace' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}

View File

@ -1,3 +0,0 @@
<!--
What goes in the footer html tag
-->

View File

@ -1,50 +1,26 @@
{# =============================================================================
<HEAD>
=============================================================================
#}
<meta charset="utf-8">
<title>{{ title | default('Welcome') }}</title>
<meta name="generator" content="nickyeoman/phpframework" />
<title>{{ title | default('Welcome To Novaconium') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{# SEO & METADATA #}
<meta name="generator" content="Novaconium" />
<meta name="description" content="{{ description | default('No description given') }}">
<meta name="keywords" content="{{ keywords | default('website') }}">
<meta name="author" content="{{ author | default('anonymous') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# DARK MODE & THEME HINTS #}
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#FFFFFF">
{# OPEN GRAPH (OG) FOR SOCIAL SHARING #}
<meta property="og:title" content="{{ title | default('Welcome') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app_url | default(request.scheme ~ '://' ~ request.host) }}">
<meta property="og:image" content="{{ og_image | default('/icon.png') }}">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
{# PWA & FAVICONS #}
<link rel="manifest" href="site.webmanifest">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><rect width='32' height='32' fill='%23000'/><text x='16' y='22' font-family='Orbitron,monospace' font-size='20' fill='%2300ff00' text-anchor='middle' dominant-baseline='middle'>N</text><circle cx='16' cy='16' r='2' fill='%2300ff00'/></svg>">
<link rel="icon" type="image/png" sizes="32x32" href="/path/to/favicon-32x32.png"> <!-- Fallback if needed -->
{# GOOGLE FONTS (CDN VIA PRECONNECT) #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="{{ fonts | default('https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap') }}" rel="stylesheet">
<!-- Place favicon.ico in the root directory -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{# STYLESHEET #}
<link rel="stylesheet" href="/css/novaconium.css">
{# https://developers.google.com/fonts/docs/getting_started #}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Material+Icons|Material+Icons+Outlined">
{% 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 %}
<link rel="stylesheet" href="/css/main.css">
<meta name="theme-color" content="#000000">

View File

@ -1,40 +0,0 @@
<!-- 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

@ -1,192 +0,0 @@
<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,15 +0,0 @@
<div class="left">
<ul id="leftnav">
<li><a href="/">Home</a></li>
{% if username is not empty %}
<li><a href="/novaconium/dashboard">Dashboard</a></li>
<li><a href="/novaconium/pages">Pages</a></li>
<li><a href="/novaconium/messages">Messages</a></li>
{% endif %}
{% if username is not empty %}
<li><a href="/novaconium/logout">Logout</a></li>
{% else %}
<li><a href="/novaconium/login">Login</a></li>
{% endif %}
</ul>
</div>

View File

@ -1,63 +1,54 @@
<!doctype html>
<html class="no-js" lang="en">
{% include '@override/above_head.html.twig' ignore missing %}
{% include '@override/mod_above_head.html.twig' ignore missing %}
<head>
{% include ['@override/head.html.twig', '@novaconium/head.html.twig'] %}
{% include ['@override/mod_head.html.twig', '@novaconium/head.html.twig'] %}
</head>
<body id="{{ pageid | default('pageid') }}" class="{{ pageclass | default('pageclass') }}" >
<body id="{{ pageid | default('pageid') }}">
{# Page Header #}
<header>
{% block headerbefore %}{% endblock %}
{% include ['@override/nav.html.twig', '@novaconium/nav.html.twig'] %}
{% block headerafter %}{% endblock %}
</header>
{# Page Header #}
<header>
{% block headerbefore %}{% endblock %}
{% include ['@override/mod_nav.html.twig', '@novaconium/nav.html.twig'] %}
{% block headerafter %}{% endblock %}
</header>
<!-- Main Content Of The Page -->
<div id="page">
<div class="container">
<!-- Main Content Of The Page -->
<div id="page">
<div class="container">
{% include ['@override/left.html.twig','@novaconium/left.html.twig'] %}
<div class="middle">
{% if error|default is not empty %}
{% for key, val in error %}
<div class="error">{{ val }}</div>
{% endfor %}
{% endif %}
<div class="middle">
{% if error|default is not empty %}
{% for key, val in error %}
<div class="error">{{ val }}</div>
{% endfor %}
{% endif %}
{% if notice|default is not empty %}
{% for key, val in notice %}
<div class="notice">{{ val }}</div>
{% endfor %}
{% endif %}
<article>
{% block content %}{% endblock %}
</article>
{% include ['@override/right.html.twig','@novaconium/right.html.twig'] %}
</div>
{% if notice|default is not empty %}
{% for key, val in notice %}
<div class="notice">{{ val }}</div>
{% endfor %}
{% endif %}
<article>
{% include 'cms/mod_alex.html.twig' ignore missing %}
{% block content %}{% endblock %}
{% include 'cms/mod_simon.html.twig' ignore missing %}
</article>
</div>
</div>
</div>
{# Page Footer #}
<footer>
{% block footerbefore %}{% endblock %}
{% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %}
{% block footerafter %}{% endblock %}
</footer>
{# Page Footer #}
<footer>
{% block footerbefore %}{% endblock %}
{% include ['@override/mod_footer.html.twig', '@novaconium/footer.html.twig'] %}
{% block footerafter %}{% endblock %}
</footer>
{% if debug is not empty %}
<div id="debug">
<h2>Debugging Information</h2>
{{ debug|raw }}
</div>
{% endif %}
{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %}
{% include ['@override/mod_foot.html.twig', '@novaconium/foot.html.twig'] %}
</body></html>

View File

@ -1,5 +1,21 @@
<div id="topnav">
<div class="container">
<div id="logo"><a href="/">Logo Goes Here</a></div>
<nav>
<! -- Navigation Goes Here -->
<ul>
<li><a href="/">Home</a></li>
<li><a href="/contact">Contact Us</a></li>
{% if loggedin|default(false) %}
<li><a href="/logout/">Logout</a></li>
{% else %}
<li><a href="/login/">Login</a></li>
{% endif %}
{% if admin|default(false) == 'admin' %}
<li><a href="/admin">Admin</a></li>
{% endif %}
</ul>
</nav>
</div>

View File

@ -1 +0,0 @@
<!-- Right Col -->

View File

@ -1,7 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>404 File Not Found</h1>
<p style="font-size:10px; margin-top:60px">Novaconium Default 404 page.</p>
<p><a href="/">Return Home</a></p>
{% endblock %}

View File

@ -1,8 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
<p>Dashboard page</p>
<p><a href="/">Homepage</a></p>
<p><a href="/novaconium/logout">logout</p>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h2>Edit Message - {{ title }}</h2>
<p><a href="/novaconium/messages/delete/{{ themessage.id }}">Delete</a></p>
<form method="post" action="/novaconium/message_save">
<input type="hidden" name="id" value="{{ themessage.id }}">
<input type="hidden" name="token" value="{{ token }}">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="{{ themessage.name }}" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" value="{{ themessage.email }}" required>
<label for="message">Message:</label>
<textarea id="message" name="message" rows="10" required>{{ themessage.message }}</textarea>
<label for="unread">
<input type="checkbox" id="unread" name="unread" value="1" {% if themessage.unread %}checked{% endif %}>
Unread
</label>
<p><strong>Created:</strong> {{ themessage.created|date("Y-m-d H:i:s") }}</p>
<button type="submit">Save Changes</button>
</form>
{% endblock %}

View File

@ -1,62 +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">
<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

@ -1,16 +0,0 @@
<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

@ -1,17 +0,0 @@
<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

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

View File

@ -1,25 +0,0 @@
<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

@ -1,12 +0,0 @@
<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

@ -1,10 +0,0 @@
<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>

View File

@ -1,67 +0,0 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
{% if not secure_key %}
<div id="secure_key">
<h2>Secure Key</h2>
<p>Please set the <code>secure_key</code> in <code>App/config.php</code> to an alphanumeric code that is 64 characters in length.</p>
<p>You can generate a secure key like this:</p>
<pre><code>pwgen -sB 64 1</code></pre>
<p>Or use this one:</p>
<pre><code>{{gen_key}}</code></pre>
</div>
{% endif %}
{% if users_created %}
<div id="users_created">
<h2>Users Table Created</h2>
<p>There was no users table in the database. One was created.</p>
</div>
{% endif %}
{% if empty_users and secure_key %}
<div id="users_created">
<h2>Create Admin</h2>
<p>No admin users exist, make an admin user now.</p>
<form method="post" action="/novaconium/create_admin">
<input type="hidden" name="token" value="{{ token }}" />
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="email">Email:</label><br>
<input type="text" id="email" name="email" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<label for="secure_key">Secure Key <i>The <code>secure_key</code> from your config.php file</i>:</label><br>
<input type="text" id="secure_key" name="secure_key" required><br><br>
<button type="submit">Create User</button>
</form>
</div>
{% endif %}
{% if show_login %}
<div id="users_created">
<h2>Administrator Login</h2>
<form method="post" action="/novaconium/login">
<input type="hidden" name="token" value="{{ token }}" />
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br><br>
<button type="submit">Login</button>
</form>
</div>
{% endif %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More