Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f410b5d0e | |||
| e5aadb3b82 | |||
| 08b8009dec | |||
| 208534b5fb | |||
| 466d34c39f | |||
| a14df54cd9 | |||
| bba62180fe | |||
| 4c598340a8 | |||
| 6d7a7a5e9d | |||
| 1cdf4f1fe8 | |||
| 12783d351c | |||
| 39a14a759b | |||
| 869c3a8d6a | |||
| a459b86169 | |||
| fb5407a60b | |||
| 344786ee95 | |||
| 892110703b | |||
| 2f76c1ae35 | |||
| 4aebef12c8 | |||
| 2021ada52b | |||
| caca552cae | |||
| 8f462953b7 | |||
| 20d01d0d4c | |||
| 7b02960b46 | |||
| bedf615ad3 | |||
| 5d2281b713 | |||
| 7e877465a6 | |||
| 7360c279ae | |||
| 641fdb17c5 | |||
| 45e10dcacd | |||
| 28513d367d |
56
README.md
56
README.md
@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
# Novaconium PHP: A PHP Framework Built from the Past
|
# Novaconium PHP: A PHP Framework Built from the Past
|
||||||
|
|
||||||
@ -11,28 +11,48 @@ Master Repo: https://git.4lt.ca/4lt/novaconium
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Installation
|
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).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir project_name;
|
PROJECTNAME=novaproject;
|
||||||
cd project_name;
|
mkdir -p $PROJECTNAME/novaconium;
|
||||||
# Native
|
cd $PROJECTNAME;
|
||||||
composer require 4lt/novaconium
|
|
||||||
# Composer
|
|
||||||
docker run --rm --interactive --tty --volume $PWD:/app composer require 4lt/novaconium
|
|
||||||
cp -R vendor/4lt/novaconium/defaults/App/ .
|
|
||||||
cp -R vendor/4lt/novaconium/defaults/public/ .
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Compose install (debian)
|
docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest require 4lt/novaconium;
|
||||||
|
|
||||||
```bash
|
cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
|
||||||
sudo nala install curl php-cli php-mbstring git unzip
|
|
||||||
curl -sS https://getcomposer.org/installer -o composer-setup.php
|
# Edit .env
|
||||||
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
|
# pwgen -cnsB1v 12 root password
|
||||||
rm composer-setup.php
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Docker Setup](https://git.4lt.ca/4lt/novaconium)
|
* [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
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
BIN
_assets/novaconium-logo.png
Normal file
BIN
_assets/novaconium-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@ -1,29 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "4lt/novaconium",
|
"name": "4lt/novaconium",
|
||||||
"description": "A high-performance PHP framework built from the past.",
|
"description": "A high-performance PHP framework built from the past.",
|
||||||
"version": "1.0.3",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Nick Yeoman",
|
"name": "Nick Yeoman",
|
||||||
"email": "dev@4lt.ca",
|
"email": "dev@4lt.ca",
|
||||||
"homepage": "https://www.4lt.ca",
|
"homepage": "https://www.4lt.ca"
|
||||||
"role": "Consultant"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Novaconium\\\\": "src/"
|
"Novaconium\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"twig/twig": "*"
|
"php": "^8.1",
|
||||||
|
"twig/twig": "*",
|
||||||
|
"nickyeoman/php-validation-class": "^5.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"extra": {
|
"extra": {
|
||||||
"versioning": {
|
"versioning": {
|
||||||
"strategy": "semantic-versioning"
|
"strategy": "semantic-versioning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
50
config/routes.php
Normal file
50
config/routes.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?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'
|
||||||
|
],
|
||||||
|
];
|
||||||
4
controllers/404.php
Normal file
4
controllers/404.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
http_response_code('404');
|
||||||
|
header("Content-Type: text/html");
|
||||||
|
view('@novacore/404');
|
||||||
70
controllers/authenticate.php
Normal file
70
controllers/authenticate.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
60
controllers/create_admin.php
Normal file
60
controllers/create_admin.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?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');
|
||||||
13
controllers/dashboard.php
Normal file
13
controllers/dashboard.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?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);
|
||||||
84
controllers/editpage.php
Normal file
84
controllers/editpage.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?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);
|
||||||
201
controllers/init.php
Normal file
201
controllers/init.php
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
<?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);
|
||||||
9
controllers/login.php
Normal file
9
controllers/login.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?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');
|
||||||
5
controllers/logout.php
Normal file
5
controllers/logout.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
$session->kill();
|
||||||
|
$log->info("Logout - Logout Success - " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
$redirect->url('/');
|
||||||
|
makeitso();
|
||||||
15
controllers/message_delete.php
Normal file
15
controllers/message_delete.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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();
|
||||||
19
controllers/message_edit.php
Normal file
19
controllers/message_edit.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?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);
|
||||||
57
controllers/message_save.php
Normal file
57
controllers/message_save.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?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);
|
||||||
21
controllers/messages.php
Normal file
21
controllers/messages.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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);
|
||||||
20
controllers/pages.php
Normal file
20
controllers/pages.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?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);
|
||||||
34
controllers/samples.php
Normal file
34
controllers/samples.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
116
controllers/savepage.php
Normal file
116
controllers/savepage.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Nickyeoman\Validation;
|
||||||
|
use Novaconium\Services\TagManager;
|
||||||
|
|
||||||
|
$v = new Nickyeoman\Validation\Validate();
|
||||||
|
|
||||||
|
$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 CSRF 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'] ?? '[]';
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Decode & sanitize tags
|
||||||
|
// -------------------------
|
||||||
|
$tags = json_decode($tags_json, true);
|
||||||
|
if (!is_array($tags)) $tags = [];
|
||||||
|
$tags = array_map('trim', $tags);
|
||||||
|
$tags = array_filter($tags, fn($t) => $t !== '');
|
||||||
|
$tags = array_unique($tags);
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Validate required fields
|
||||||
|
// -------------------------
|
||||||
|
if (empty($title) || empty($slug) || empty($body)) {
|
||||||
|
$messages->error('Title, Slug, and Body are required.');
|
||||||
|
$redirect->url($url_error);
|
||||||
|
makeitso();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tagManager = new TagManager();
|
||||||
|
|
||||||
|
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;
|
||||||
|
$messages->notice('Page Created');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// -------------------------
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Save tags (for both new and existing pages)
|
||||||
|
// -------------------------
|
||||||
|
$tagManager->setTagsForPage($id, $tags);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$messages->error($e->getMessage());
|
||||||
|
$redirect->url($url_error);
|
||||||
|
makeitso();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect back to edit page
|
||||||
|
$redirect->url('/novaconium/page/edit/' . $id);
|
||||||
42
controllers/sitemap.php
Normal file
42
controllers/sitemap.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?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>";
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
$config = [
|
|
||||||
'database' => [
|
|
||||||
'host' => '',
|
|
||||||
'name' => '',
|
|
||||||
'user' => '',
|
|
||||||
'pass' => '',
|
|
||||||
'port' => 3306
|
|
||||||
],
|
|
||||||
'base_url' => 'http://localhost:8000'
|
|
||||||
];
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Define our status code and message
|
|
||||||
$status_code = 404;
|
|
||||||
$status_message = 'The requested resource could not be found.';
|
|
||||||
|
|
||||||
// Set the HTTP response code and message
|
|
||||||
http_response_code($status_code);
|
|
||||||
header("Content-Type: text/html");
|
|
||||||
?>
|
|
||||||
|
|
||||||
<h1>Error 404 Resource Not found</h1>
|
|
||||||
<p><?php echo $status_message; ?></p>
|
|
||||||
<p style="font-size:10px; margin-top:60px">Novaconium Default 404 page.</p>
|
|
||||||
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{% extends '@novaconium/master.html.twig' %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h1>This is twig</h1>
|
|
||||||
<p>Content Here</p>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
@ -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');
|
|
||||||
?>
|
|
||||||
6
docs/404.md
Normal file
6
docs/404.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# 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.
|
||||||
18
docs/Composer.md
Normal file
18
docs/Composer.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
52
docs/ConfigurationFile.md
Normal file
52
docs/ConfigurationFile.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# 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'
|
||||||
|
```
|
||||||
18
docs/Dev-Fake_autoload.md
Normal file
18
docs/Dev-Fake_autoload.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Fake autoload for dev
|
||||||
|
|
||||||
|
put this in index.php
|
||||||
|
|
||||||
|
```
|
||||||
|
// --- Dev-only autoloader for manually cloned vendor copy ---
|
||||||
|
spl_autoload_register(function ($class) {
|
||||||
|
if (str_starts_with($class, 'Novaconium\\')) {
|
||||||
|
$baseDir = BASEPATH . '/vendor/4lt/novaconium/src/';
|
||||||
|
$relativeClass = substr($class, strlen('Novaconium\\'));
|
||||||
|
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
19
docs/Logs.md
Normal file
19
docs/Logs.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 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'
|
||||||
|
|
||||||
3
docs/Messages.md
Normal file
3
docs/Messages.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Messages
|
||||||
|
|
||||||
|
Messages is $messages.
|
||||||
5
docs/Post.md
Normal file
5
docs/Post.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Post
|
||||||
|
|
||||||
|
There is a post class.
|
||||||
|
It cleans the post.
|
||||||
|
You can access it with $post.
|
||||||
6
docs/Redirect.md
Normal file
6
docs/Redirect.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# 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.
|
||||||
23
docs/Sass.md
Normal file
23
docs/Sass.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Sass
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
There is a dockerfile in the sass directory you can build an image with.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sass
|
||||||
|
docker build -t sass-container .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Sass
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
```bash
|
||||||
|
# Build Novaconium (compressed)
|
||||||
|
docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css
|
||||||
|
|
||||||
|
```
|
||||||
5
docs/Session.md
Normal file
5
docs/Session.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Sessions
|
||||||
|
|
||||||
|
There is a sessions handler built into Novaconium.
|
||||||
|
|
||||||
|
$session
|
||||||
7
docs/StyleSheets-sass.md
Normal file
7
docs/StyleSheets-sass.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
21
docs/Twig-Views.md
Normal file
21
docs/Twig-Views.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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);
|
||||||
|
```
|
||||||
@ -1,71 +1,8 @@
|
|||||||
# Getting Started With Docker
|
# Docker Cheatsheet (for Novaconium)
|
||||||
|
|
||||||
## Clone Docker Cookbooks
|
## Sample Docker Compose File
|
||||||
|
|
||||||
[Github Docker Compose Cookbooks](https://github.com/nickyeoman/docker-compose-cookbooks)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@github.com:nickyeoman/docker-compose-cookbooks.git /docker-compose-cookbooks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup Docker Compose File
|
|
||||||
|
|
||||||
Read the sample extends file of /docker-compose-cookbooks/phpcontainer/sample-extends.yml
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ensure docker-compose exists
|
|
||||||
[[ -f docker-compose.yml ]] || echo "services:" > docker-compose.yml
|
|
||||||
|
|
||||||
# PHP container
|
|
||||||
tail -n+2 /docker-compose-cookbooks/phpcontainer/sample-extends.yml >> docker-compose.yml
|
|
||||||
|
|
||||||
# PHP settings
|
|
||||||
cp -r /docker-compose-cookbooks/phpcontainer/config .
|
|
||||||
|
|
||||||
# Set project directory
|
|
||||||
sed -i 's|- "./project:/data"|- "./:/data"|' docker-compose.yml
|
|
||||||
|
|
||||||
# Mariadb container
|
|
||||||
tail -n +2 /docker-compose-cookbooks/mariadb/sample-extends.yml >> docker-compose.yml
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## ENV File
|
|
||||||
|
|
||||||
Then setup the .env file, which should look something like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
COOKBOOK=/docker-compose-cookbooks
|
|
||||||
COMPOSE_PROJECT_NAME=myProject
|
|
||||||
TZ=America/Vancouver
|
|
||||||
VOL_CONFIG_PATH=/data/myProject/config
|
|
||||||
VOL_PATH=/data/myProject/data
|
|
||||||
|
|
||||||
# PHP Container
|
|
||||||
PHPCONTAINER_IMAGE=4lights/phpcontainer:latest
|
|
||||||
|
|
||||||
# MariaDB
|
|
||||||
MARIADB_IMAGE=mariadb:latest
|
|
||||||
MARIADB_MARIADB_DATABASE=mydb
|
|
||||||
MARIADB_MARIADB_ROOT_PASSWORD=ChangeThisPassword0123456789ABCD
|
|
||||||
MARIADB_MARIADB_PASSWORD=AlsoChangeThisPassword0123456789
|
|
||||||
MARIADB_MARIADB_USER=dbuser
|
|
||||||
```
|
|
||||||
|
|
||||||
## APP Database config
|
|
||||||
|
|
||||||
Open the /App/config.php file and change the database section to match the above:
|
|
||||||
|
|
||||||
```php
|
|
||||||
'database' => [
|
|
||||||
'host' => 'mariadb',
|
|
||||||
'name' => 'mydb',
|
|
||||||
'user' => 'dbuser',
|
|
||||||
'pass' => 'AlsoChangeThisPassword0123456789',
|
|
||||||
'port' => 3306
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
|
See the skeleton directory for an example docker setup.
|
||||||
|
|
||||||
## Start Docker
|
## Start Docker
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
# Twig Overrides
|
|
||||||
|
|
||||||
You can override twig templates by creating the same file in the templates directory.
|
|
||||||
12
sass/Dockerfile
Normal file
12
sass/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Use an official Node.js image as a base
|
||||||
|
FROM node:16-slim
|
||||||
|
|
||||||
|
# Install Sass globally
|
||||||
|
RUN npm install -g sass \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
# Set working directory inside container
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Set entrypoint to Sass CLI
|
||||||
|
ENTRYPOINT ["sass"]
|
||||||
80
sass/abstracts/_variables.sass
Normal file
80
sass/abstracts/_variables.sass
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// 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}
|
||||||
1
sass/abstracts/index.sass
Normal file
1
sass/abstracts/index.sass
Normal file
@ -0,0 +1 @@
|
|||||||
|
@forward 'variables';
|
||||||
6
sass/base/_background.sass
Normal file
6
sass/base/_background.sass
Normal file
File diff suppressed because one or more lines are too long
178
sass/base/_reset.sass
Normal file
178
sass/base/_reset.sass
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// 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
|
||||||
2
sass/base/index.sass
Normal file
2
sass/base/index.sass
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@forward 'reset';
|
||||||
|
@forward 'background';
|
||||||
29
sass/framework/_ace.sass
Normal file
29
sass/framework/_ace.sass
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
// ACE Editor
|
||||||
|
.editor-container
|
||||||
|
width: 100%
|
||||||
|
min-height: 400px // ~10 rows
|
||||||
|
|
||||||
|
.ace-editor
|
||||||
|
height: 400px // Scrollable
|
||||||
|
border: 1px solid $accent-light
|
||||||
|
font-family: $mono-font // Your monospace
|
||||||
|
|
||||||
|
// Ace internals (dark theme tweaks)
|
||||||
|
.ace_gutter
|
||||||
|
background: $bg-darker !important
|
||||||
|
color: $text-muted !important
|
||||||
|
border-right: 1px solid $accent-light !important
|
||||||
|
|
||||||
|
.ace_scroller
|
||||||
|
background: $bg-dark !important
|
||||||
|
|
||||||
|
.ace_text-layer .ace_print-margin
|
||||||
|
background: transparent !important
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
.ace_marker-layer .ace_selection
|
||||||
|
background: color.adjust($accent-light, $alpha: -0.3) !important // Green tint
|
||||||
15
sass/framework/_edit_page.sass
Normal file
15
sass/framework/_edit_page.sass
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
@use 'sass:color' // Already there for adjusts
|
||||||
|
|
||||||
|
#edit-page-title
|
||||||
|
margin-top: 2rem
|
||||||
|
|
||||||
|
#title
|
||||||
|
width: 100%
|
||||||
|
font-size: 1.9rem
|
||||||
|
padding: 20px
|
||||||
|
|
||||||
|
&>div
|
||||||
|
font-size: 0.8rem
|
||||||
|
margin: 8px 0 0 20px
|
||||||
62
sass/framework/_forms.sass
Normal file
62
sass/framework/_forms.sass
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// pages/_forms.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
#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
|
||||||
53
sass/framework/_login_form.sass
Normal file
53
sass/framework/_login_form.sass
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// framework/_login_form.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
|
||||||
|
body.novaconium #login
|
||||||
|
// No background or border—use 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
|
||||||
79
sass/framework/_logo.sass
Normal file
79
sass/framework/_logo.sass
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
body.novaconium
|
||||||
|
#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)
|
||||||
62
sass/framework/_main.sass
Normal file
62
sass/framework/_main.sass
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// layout/_main.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
// 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
|
||||||
49
sass/framework/_tabs.sass
Normal file
49
sass/framework/_tabs.sass
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// framework/_tabs.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
// 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
|
||||||
109
sass/framework/_tags.sass
Normal file
109
sass/framework/_tags.sass
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use 'sass:color' // For color.adjust()—non-deprecated color tweaks
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
// 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
|
||||||
48
sass/framework/_tooltip.sass
Normal file
48
sass/framework/_tooltip.sass
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
@use 'sass:color' // Already there for adjusts
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
// 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
|
||||||
59
sass/framework/_ui.sass
Normal file
59
sass/framework/_ui.sass
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// components/_ui.sass
|
||||||
|
|
||||||
|
@use '../abstracts' as *
|
||||||
|
@use '../base' as *
|
||||||
|
|
||||||
|
body.novaconium
|
||||||
|
// 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
|
||||||
10
sass/framework/index.sass
Normal file
10
sass/framework/index.sass
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@forward 'main';
|
||||||
|
@forward 'ui';
|
||||||
|
@forward 'forms';
|
||||||
|
@forward 'login_form';
|
||||||
|
@forward 'logo';
|
||||||
|
@forward 'tabs';
|
||||||
|
@forward 'edit_page';
|
||||||
|
@forward 'tooltip';
|
||||||
|
@forward 'ace';
|
||||||
|
@forward 'tags';
|
||||||
4
sass/novaconium.sass
Normal file
4
sass/novaconium.sass
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// novaconium.sass
|
||||||
|
@use 'abstracts' as *
|
||||||
|
@use 'base' as *
|
||||||
|
@use 'framework' as *
|
||||||
2
skeleton/.env
Normal file
2
skeleton/.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
MYSQL_ROOT_PASSWORD=random
|
||||||
|
MYSQL_PASSWORD=random
|
||||||
3
skeleton/.gitignore
vendored
Normal file
3
skeleton/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
data/
|
||||||
|
novaconium/vendor/
|
||||||
|
novaconium/logs/
|
||||||
60
skeleton/docker-compose.yml
Normal file
60
skeleton/docker-compose.yml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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
|
||||||
|
- "./logs:/data/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
|
||||||
15
skeleton/novaconium/App/config.php
Normal file
15
skeleton/novaconium/App/config.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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'
|
||||||
|
];
|
||||||
@ -1,8 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
$routes = [
|
$routes = [
|
||||||
'/about' => [
|
|
||||||
'get' => 'about'
|
|
||||||
],
|
|
||||||
'/' => [
|
'/' => [
|
||||||
'get' => 'index'
|
'get' => 'index'
|
||||||
]
|
]
|
||||||
21
skeleton/novaconium/App/views/index.html.twig
Normal file
21
skeleton/novaconium/App/views/index.html.twig
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% 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 %}
|
||||||
1
skeleton/novaconium/public/css/novaconium.css
Normal file
1
skeleton/novaconium/public/css/novaconium.css
Normal file
File diff suppressed because one or more lines are too long
1
skeleton/novaconium/public/css/novaconium.css.map
Normal file
1
skeleton/novaconium/public/css/novaconium.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sourceRoot":"","sources":["../../../../sass/abstracts/_variables.sass","../../../../sass/base/_reset.sass","../../../../sass/base/_background.sass","../../../../sass/framework/_main.sass","../../../../sass/framework/_ui.sass","../../../../sass/framework/_forms.sass","../../../../sass/framework/_login_form.sass","../../../../sass/framework/_logo.sass","../../../../sass/framework/_tabs.sass","../../../../sass/framework/_edit_page.sass","../../../../sass/framework/_tooltip.sass","../../../../sass/framework/_ace.sass","../../../../sass/framework/_tags.sass"],"names":[],"mappings":"CAoEA,MACE,0BACA,8BACA,+BACA,mCACA,iCACA,oCACA,qCACA,qCACA,kCACA,yCACA,8CC1EF,EACE,sBAEF,KACE,eACA,gBACA,uBACA,iBDWQ,aCVR,MDYW,KCVb,KACE,SACA,UACA,YDgBW,kCCfX,iBDIQ,aCHR,MDKW,KCJX,iBAGF,kBACE,gBACA,gBACA,gBACA,qBAEF,GACE,iBAEF,GACE,eAEF,GACE,kBAEF,GACE,iBAEF,GACE,kBAEF,GACE,eAGF,EACE,eAEF,MACE,kBAEF,SACE,gBAEF,KACE,kBAGF,EACE,MDnCa,eCoCb,qBACA,sCACA,iCAEA,gBACE,oBDzCW,eC0CX,aAGJ,MACE,eACA,mBAEF,GACE,oBAGF,WACE,aACA,iBACA,qCACA,kCACA,MD7DW,KCgEb,cACE,YDvDU,uDCwDV,iBACA,oCACA,qBACA,kBAEF,IACE,aACA,YACA,cACA,oCACA,kBACA,SACE,gBACA,UAGJ,MACE,WACA,yBACA,aAEF,MACE,cACA,gBACA,2CAEF,GACE,gBAGF,6BACE,oBACA,kBACA,oCACA,MDpGW,KCqGX,oCACA,kBACA,aAEA,qDACE,aACA,aDxGW,eCyGX,wCAEJ,OACE,eACA,qCAEA,gBACE,WACA,mBAGJ,IACE,eACA,YACA,kBAEF,OACE,aAGF,GACE,YACA,WACA,oCACA,aAGF,gBACE,iCACA,mBAGF,aACE,KACE,2BACA,uBAEJ,YACE,8BACA,MDnJW,KCsJb,kBACE,8BACA,MDxJW,KEtBb,gBACI,sBACA,0uKCEF,iCACE,aACA,UACA,uBACA,uBACA,mBAGF,wBACE,aACA,cACA,aACA,SACA,iBHGM,aGFN,gCAEF,2BACE,YACA,cACA,gBACA,UACA,oBACA,iBHNM,aGON,gCAEF,4BACE,6CAEA,uCACE,mBAEJ,2BACE,cACA,mBACA,qBACA,MHjBS,KGmBT,kEACE,oCACA,MHlBS,eGmBT,kBAGJ,0BACE,iCACE,sBACA,mBAEF,iDACE,WACA,gBACA,cAEF,yBACE,gBCtDJ,qBACE,2EACA,kBACA,gCACA,MJcS,KIbT,qBACA,kBACA,sCACA,sCAGF,uBACE,kBAGF,+EACE,mBACA,YACA,eACA,kBACA,iBAEF,0BACE,oCACA,qBACA,4BAEF,2BACE,sCACA,uBACA,8BAEF,0BACE,gCACA,MJdS,iBIeT,kCACA,gBACA,mBAGF,6BACE,WACA,sCAEA,gEACE,sCACA,gBAEF,gCACE,qCACA,MJ/BS,iBIgCT,gBClDF,uDACE,mBAEA,0EACE,WACA,gBAEJ,2DACE,kBAGF,gOAIE,gCACA,sCACA,WACA,cACA,kBACA,2EAEA,wPACE,aLHO,eKIP,2CACA,aAEJ,gEACE,aLRS,eKWX,+DACE,iBLZS,eKaT,mBACA,YACA,mBACA,kBACA,eACA,2EAEA,0IACE,sBACA,sCAGJ,6CACE,ML3BO,iBK6BT,6CACE,ML7BS,eK+BT,mDACE,MLlCO,iBKmCP,0BCzDR,uBAEE,UAIA,+HAGE,WACA,gBACA,aACA,mBACA,sBACA,eACA,sCACA,kBACA,WNEM,aMDN,MNGS,6CMCT,uUACA,4BACA,gCACA,qBACA,kBAEF,4CACE,iVACA,4BACA,gCACA,qBACA,kBAEF,2CACE,WNZW,eMaX,aNbW,eMcX,eACA,0BAEA,iDACE,WNlBS,eMmBT,uCAGF,kDACE,aACA,kBCnDJ,yBACE,kBACA,qBACA,eACA,gBACA,gBAEA,iCACE,WACA,kBACA,MACA,OACA,QACA,SACA,gJACA,0BACA,WACA,oBAGF,gCACE,WACA,kBACA,MACA,OACA,QACA,WACA,qEACA,YACA,kCAEF,+BACE,cACA,kCACA,iBACA,gBACA,WACA,oBACA,gBACA,yBAGA,yFACA,mDAEF,8BACE,cACA,kCACA,iBACA,gBACA,WACA,qBACA,iBACA,0EACA,mDAIA,qCACE,8BAEN,gBACE,GACE,SACF,KACE,UAEJ,kBACE,QACE,uBACF,IACE,+BACF,IACE,gCACF,IACE,8BACF,IACE,gCCvEJ,+BACE,mBACA,WRcM,aQZR,yBACE,aACA,iBRWQ,eQVR,6CAEF,4BACE,mBACA,WRMQ,eQLR,sCACA,mBACA,eACA,eACA,gBACA,kBACA,WACA,YRSQ,uDQPR,kCACE,iCAEF,wCACE,iBAEF,mCACE,iBRZI,aQaJ,uCACA,MRTS,eQUT,iBAEJ,6BACE,aACA,eACA,iBRpBM,aQqBN,MRnBS,KQoBT,YAEA,oCACE,cC5CN,iBACI,gBAEA,wBACI,WACA,iBACA,aAEJ,qBACI,gBACA,oBCRN,yBACE,kBACA,qBACA,YACA,eACA,MViBW,eUhBX,mBACA,QAEA,sCACE,kBACA,YACA,iBVMM,eULN,MVMO,KULP,gBACA,gBACA,cACA,eACA,aACA,UACA,uBACA,eACA,oCACA,oBAEA,6CACE,WACA,kBACA,SACA,UACA,iBACA,mBACA,sEAEJ,4CACE,mBACA,UAGJ,8BACE,kBACA,aC1CJ,kBACE,WACA,iBAEF,YACE,aACA,gCACA,YXuBU,uDWpBV,wBACE,qCACA,kCACA,iDAEF,0BACE,mCAEF,8CACE,oCAGF,6CACE,yCCtBF,+BACE,kBACA,SACA,OACA,QACA,iBACA,gBACA,WZWQ,eYVR,sCACA,gBACA,0BACA,gBACA,SACA,UACA,WACA,UACA,kBACA,uBAEA,mDACE,UACA,mBAEF,kCACE,qBACA,eACA,MZPO,KYQP,eACA,sCAEA,mFAEE,8BACA,MZXO,eYaT,6CACE,mBAEN,4BACE,kBAGF,4BACE,aACA,eACA,WACA,mBACA,sCACA,eACA,WZ/BQ,eYgCR,gBACA,cAEA,kCACE,YACA,yBACA,MZrCO,KYsCP,YZ5BM,uDY6BN,eACA,OACA,gBACA,aAEA,+CACE,MZ3CK,iBY6CX,0BACE,oBACA,mBACA,8BACA,MZhDW,eYiDX,qBACA,kBACA,eACA,gBACA,gBACA,uBACA,mBAEA,sCACE,gBACA,YACA,cACA,eACA,mBACA,eACA,UACA,cAEA,4CACE,WAWN,2EACE","file":"novaconium.css"}
|
||||||
31
skeleton/novaconium/public/index.php
Normal file
31
skeleton/novaconium/public/index.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Novaconium Framework Entry Point
|
||||||
|
*
|
||||||
|
* This is the main entry point for the Novaconium framework.
|
||||||
|
* It sets up the environment, loads necessary components,
|
||||||
|
* and runs the application.
|
||||||
|
*
|
||||||
|
* @package Novaconium
|
||||||
|
* @author Nick Yeoman <dev@4lt.ca>
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Enable error reporting for development environments
|
||||||
|
// error_reporting(E_ALL);
|
||||||
|
// ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
// Define the base path where the website is running from
|
||||||
|
define('BASEPATH', dirname(__DIR__, 1));
|
||||||
|
|
||||||
|
// Load Composer's autoload file to handle class autoloading
|
||||||
|
require BASEPATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Define the framework path
|
||||||
|
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
|
||||||
|
|
||||||
|
// Bootstrap the Novaconium framework, which will create necessary objects like $session, $db, etc.
|
||||||
|
require FRAMEWORKPATH . '/src/novaconium.php';
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
makeitso();
|
||||||
1
skeleton/novaconium/sass/project.sass
Normal file
1
skeleton/novaconium/sass/project.sass
Normal file
@ -0,0 +1 @@
|
|||||||
|
@use '../vendor/4lt/novaconium/sass/novaconium'
|
||||||
160
src/Database.php
160
src/Database.php
@ -1,72 +1,124 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
private $host;
|
|
||||||
private $user;
|
|
||||||
private $pass;
|
|
||||||
private $dbname;
|
|
||||||
private $conn;
|
private $conn;
|
||||||
|
public $lastid;
|
||||||
|
|
||||||
public function __construct($dbinfo) {
|
public function __construct($dbinfo) {
|
||||||
$this->host = $dbinfo['host'];
|
$this->conn = new \mysqli($dbinfo['host'], $dbinfo['user'], $dbinfo['pass'], $dbinfo['name']);
|
||||||
$this->user = $dbinfo['user'];
|
|
||||||
$this->pass = $dbinfo['pass'];
|
|
||||||
$this->dbname = $dbinfo['name'];
|
|
||||||
|
|
||||||
$this->connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function connect() {
|
|
||||||
$this->conn = new mysqli($this->host, $this->user, $this->pass, $this->dbname);
|
|
||||||
|
|
||||||
if ($this->conn->connect_error) {
|
if ($this->conn->connect_error) {
|
||||||
die("Connection failed: " . $this->conn->connect_error);
|
die("Connection failed: " . $this->conn->connect_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query($query) {
|
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);
|
$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();
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result(); // Requires MySQL Native Driver (mysqlnd)
|
||||||
return $stmt->get_result();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRow($query) {
|
|
||||||
$result = $this->query($query);
|
|
||||||
return $result->fetch_assoc();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function debugGetRow($query) {
|
|
||||||
echo "<h1>Debug GetRow Query</h1>";
|
|
||||||
echo "<div class='debug-query'>Query: $query</div>";
|
|
||||||
$result = $this->query($query);
|
|
||||||
$row = $result->fetch_assoc();
|
|
||||||
|
|
||||||
echo "<pre>";
|
if ($result) {
|
||||||
print_r($row);
|
return $result->fetch_all(MYSQLI_ASSOC);
|
||||||
echo "</pre>";
|
} else {
|
||||||
|
return [];
|
||||||
die();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public function getRows($query) {
|
|
||||||
$result = $this->query($query);
|
|
||||||
return $result->fetch_all(MYSQLI_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function debugGetRows($query) {
|
|
||||||
echo "<h1>Debug GetRows Query</h1>";
|
|
||||||
echo "<div class='debug-query'>Query: $query</div>";
|
|
||||||
|
|
||||||
$result = $this->query($query);
|
|
||||||
$rows = $result->fetch_all(MYSQLI_ASSOC);
|
|
||||||
|
|
||||||
echo "<pre>";
|
|
||||||
print_r($rows);
|
|
||||||
echo "</pre>";
|
|
||||||
|
|
||||||
die();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function close() {
|
public function close() {
|
||||||
|
|||||||
54
src/Logger.php
Normal file
54
src/Logger.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/MessageHandler.php
Normal file
87
src/MessageHandler.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
|
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] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Post.php
Normal file
25
src/Post.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Redirect.php
Normal file
40
src/Redirect.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
class Router {
|
class Router {
|
||||||
public $routes = [];
|
public $routes = [];
|
||||||
public $query = [];
|
public $query = [];
|
||||||
@ -19,12 +19,13 @@ class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function loadRoutes() {
|
private function loadRoutes() {
|
||||||
|
require_once( \FRAMEWORKPATH . '/config/routes.php');
|
||||||
// Check if Path exists
|
// Check if Path exists
|
||||||
if (file_exists(BASEPATH . '/App/routes.php')) {
|
if (file_exists(\BASEPATH . '/App/routes.php')) {
|
||||||
require_once( BASEPATH . '/App/routes.php');
|
require_once( \BASEPATH . '/App/routes.php');
|
||||||
} else {
|
|
||||||
require_once(FRAMEWORKPATH . '/defaults/App/routes.php');
|
|
||||||
}
|
}
|
||||||
|
$routes = array_merge((array)$routes, (array)$framework_routes);
|
||||||
|
|
||||||
return $routes;
|
return $routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +68,9 @@ class Router {
|
|||||||
|
|
||||||
// one to one match
|
// one to one match
|
||||||
if (array_key_exists($this->path, $this->routes)) {
|
if (array_key_exists($this->path, $this->routes)) {
|
||||||
return $this->routes[$this->path][$this->requestType];
|
if (!empty($this->routes[$this->path][$this->requestType])) {
|
||||||
|
return $this->routes[$this->path][$this->requestType];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->routes as $key => $value) {
|
foreach ($this->routes as $key => $value) {
|
||||||
@ -103,35 +106,37 @@ class Router {
|
|||||||
|
|
||||||
// checks if the file exists, sets file path
|
// checks if the file exists, sets file path
|
||||||
private function setRouteFile() {
|
private function setRouteFile() {
|
||||||
|
|
||||||
$cp = BASEPATH . '/App/controllers/' . $this->controller . '.php';
|
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)) {
|
if (file_exists($cp)) {
|
||||||
return $cp;
|
return $cp;
|
||||||
} else {
|
} else {
|
||||||
//Check if 404 exits
|
//Check if 404 exits
|
||||||
if (file_exists(BASEPATH . '/App/controllers/404.php')) {
|
if (file_exists( \BASEPATH . '/App/controllers/404.php')) {
|
||||||
return BASEPATH . '/App/controllers/404.php';
|
return \BASEPATH . '/App/controllers/404.php';
|
||||||
} else {
|
} else {
|
||||||
return FRAMEWORKPATH . '/defaults/App/controllers/404.php';
|
return \FRAMEWORKPATH . '/controllers/404.php';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function debug() {
|
public function debug() {
|
||||||
echo '<h1>Debugging Router</h1>';
|
echo '<div id="router-debug-container" class="debug">';
|
||||||
echo '<h2>Url Path</h2>';
|
echo '<table border="1" cellpadding="10" cellspacing="0">';
|
||||||
echo $this->path . '<br>';
|
echo '<tr><th>Url Path</th><td>' . htmlspecialchars($this->path) . '</td></tr>';
|
||||||
echo '<h2>ControllerPath</h2>';
|
echo '<tr><th>Controller Path</th><td>' . htmlspecialchars($this->controllerPath) . '</td></tr>';
|
||||||
echo $this->controllerPath;
|
echo '<tr><th>Parameters</th><td><pre>' . print_r($this->parameters, true) . '</pre></td></tr>';
|
||||||
echo '<h2>Parameters</h2>';
|
echo '<tr><th>Routes</th><td><pre>' . print_r($this->routes, true) . '</pre></td></tr>';
|
||||||
echo '<pre>';
|
echo '</table></div>';
|
||||||
print_r($this->parameters);
|
|
||||||
echo '</pre>';
|
|
||||||
echo '<h2>Routes Variable</h2><pre>';
|
|
||||||
print_r($this->routes);
|
|
||||||
echo '</pre>';
|
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
10
src/Services/Auth.php
Normal file
10
src/Services/Auth.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace Novaconium\Services;
|
||||||
|
class Auth
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Services/TagManager.php
Normal file
53
src/Services/TagManager.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Novaconium\Services;
|
||||||
|
|
||||||
|
class TagManager
|
||||||
|
{
|
||||||
|
protected $db;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
global $db;
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign tags to a page.
|
||||||
|
*
|
||||||
|
* This will delete old links and insert new ones.
|
||||||
|
*
|
||||||
|
* @param int $pageId
|
||||||
|
* @param array $tags Array of tag names
|
||||||
|
*/
|
||||||
|
public function setTagsForPage(int $pageId, array $tags): void
|
||||||
|
{
|
||||||
|
// Remove existing links
|
||||||
|
$this->db->query("DELETE FROM page_tags WHERE page_id = ?", [$pageId]);
|
||||||
|
|
||||||
|
foreach ($tags as $tagName) {
|
||||||
|
$tagName = trim($tagName);
|
||||||
|
if ($tagName === '') continue;
|
||||||
|
|
||||||
|
// Check if tag exists
|
||||||
|
$stmt = $this->db->query("SELECT id FROM tags WHERE name = ?", [$tagName]);
|
||||||
|
$row = $stmt->fetch_assoc(); // mysqli_result -> assoc array
|
||||||
|
if ($row) {
|
||||||
|
$tagId = $row['id'];
|
||||||
|
} else {
|
||||||
|
// Insert new tag
|
||||||
|
$this->db->query(
|
||||||
|
"INSERT INTO tags (name, created) VALUES (?, NOW())",
|
||||||
|
[$tagName]
|
||||||
|
);
|
||||||
|
$tagId = $this->db->lastid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link page to tag
|
||||||
|
$this->db->query(
|
||||||
|
"INSERT INTO page_tags (page_id, tag_id) VALUES (?, ?)",
|
||||||
|
[$pageId, $tagId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
|
namespace Novaconium;
|
||||||
class Session {
|
class Session {
|
||||||
private $session;
|
private $session;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
if (!isset($_SESSION)) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
$this->session = $_SESSION;
|
|
||||||
} else {
|
|
||||||
$this->session = $_SESSION;
|
|
||||||
}
|
}
|
||||||
$this->setToken();
|
$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() {
|
public function setToken() {
|
||||||
if (!isset($this->session['token'])) {
|
$this->session['token'] = bin2hex(random_bytes(32));
|
||||||
$this->session['token'] = bin2hex(random_bytes(32));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function set($key, $value) {
|
public function set($key, $value) {
|
||||||
@ -34,7 +41,7 @@ class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function debug() {
|
public function debug() {
|
||||||
print_r($this->session);
|
return $this->session;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($key) {
|
public function delete($key) {
|
||||||
@ -44,8 +51,13 @@ class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function write() {
|
public function write() {
|
||||||
$_SESSION = $this->session;
|
// No need to assign to $_SESSION since $this->session is a reference
|
||||||
session_write_close();
|
session_write_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function kill() {
|
||||||
|
$this->session = [];
|
||||||
|
$_SESSION = [];
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
60
src/functions.php
Normal file
60
src/functions.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump and Die (debug function)
|
||||||
|
*
|
||||||
|
* @param mixed ...$vars Any number of variables to dump and then exit the script.
|
||||||
|
*/
|
||||||
|
function dd(...$vars): void {
|
||||||
|
echo "<pre style='background:#1e1e1e;color:#00ff00;padding:12px;border-radius:6px;font-size:14px;'>";
|
||||||
|
foreach ($vars as $var) {
|
||||||
|
var_dump($var);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
echo "</pre>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize the request lifecycle.
|
||||||
|
*
|
||||||
|
* This function safely writes session data, stores flash messages, closes the database
|
||||||
|
* connection if configured, and performs a redirect.
|
||||||
|
*/
|
||||||
|
function makeitso(): void {
|
||||||
|
global $session, $db, $redirect, $config, $messages, $log;
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Close database if configured
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
// Check if database configuration exists and the $db object is set
|
||||||
|
if (!empty($config['database']['host']) && isset($db)) {
|
||||||
|
// If the $db object has a close method, call it to close the connection
|
||||||
|
if (method_exists($db, 'close')) {
|
||||||
|
$db->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Save flash messages to session
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
// Set all messages in the session
|
||||||
|
$session->set('messages', $messages->getAllMessages());
|
||||||
|
|
||||||
|
// Write any buffered session data to persistent storage
|
||||||
|
$session->write();
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Perform redirect
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
// Execute the redirect operation
|
||||||
|
$redirect->execute();
|
||||||
|
|
||||||
|
// Exit the script after processing is complete
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@ -1,33 +1,53 @@
|
|||||||
<?php
|
<?php
|
||||||
define('FRAMEWORKPATH', BASEPATH . '/vendor/4lt/novaconium');
|
|
||||||
|
|
||||||
require_once(BASEPATH . '/vendor/autoload.php');
|
// --- Load Config ---
|
||||||
|
if (file_exists(\BASEPATH . '/App/config.php')) {
|
||||||
//Check if config file exists
|
require_once \BASEPATH . '/App/config.php';
|
||||||
if (file_exists(BASEPATH . '/App/config.php')) {
|
|
||||||
require_once(BASEPATH . '/App/config.php');
|
|
||||||
} else {
|
} else {
|
||||||
require_once(FRAMEWORKPATH . '/defaults/App/config.php');
|
require_once \FRAMEWORKPATH . '/skeleton/novaconium/App/config.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates twig and the view() function
|
require_once \FRAMEWORKPATH . '/src/functions.php';
|
||||||
require_once(FRAMEWORKPATH . '/src/twig.php');
|
require_once \FRAMEWORKPATH . '/src/twig.php';
|
||||||
|
|
||||||
// Start a Session
|
// --- Logging ---
|
||||||
require_once(FRAMEWORKPATH . '/src/Session.php');
|
use Novaconium\Logger;
|
||||||
|
$log = new Logger(\BASEPATH . $config['logfile'], $config['loglevel']);
|
||||||
|
|
||||||
|
// --- Twig Data Array ---
|
||||||
|
$data = [];
|
||||||
|
$data['fonts'] = $config['fonts'] ?? [];
|
||||||
|
|
||||||
|
// --- Session ---
|
||||||
|
use Novaconium\Session;
|
||||||
$session = new Session();
|
$session = new Session();
|
||||||
|
$data['token'] = $session->get('token');
|
||||||
|
$data['username'] = $session->get('username');
|
||||||
|
|
||||||
// Load Database Class
|
// --- Messages ---
|
||||||
|
use Novaconium\MessageHandler;
|
||||||
|
$messages = new MessageHandler($session->flash('messages'));
|
||||||
|
foreach (['error', 'notice'] as $key) {
|
||||||
|
$data[$key] = $messages->showMessages($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Database ---
|
||||||
|
use Novaconium\Database;
|
||||||
if (!empty($config['database']['host'])) {
|
if (!empty($config['database']['host'])) {
|
||||||
require_once(FRAMEWORKPATH . '/src/Database.php');
|
|
||||||
$db = new Database($config['database']);
|
$db = new Database($config['database']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a controller
|
// --- POST Wrapper ---
|
||||||
require_once(FRAMEWORKPATH . '/src/Router.php');
|
use Novaconium\Post;
|
||||||
$router = new Router();
|
if (!empty($_POST)) {
|
||||||
//$router->debug();
|
$post = new Post($_POST);
|
||||||
require_once($router->controllerPath);
|
}
|
||||||
|
|
||||||
//write the session
|
// --- Redirect Handler ---
|
||||||
$session->write();
|
use Novaconium\Redirect;
|
||||||
|
$redirect = new Redirect();
|
||||||
|
|
||||||
|
// --- Router ---
|
||||||
|
use Novaconium\Router;
|
||||||
|
$router = new Router();
|
||||||
|
require_once $router->controllerPath;
|
||||||
|
|||||||
75
src/twig.php
75
src/twig.php
@ -1,24 +1,69 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
//Twig
|
declare(strict_types=1);
|
||||||
function view($name = '', $data = []) {
|
|
||||||
global $config; // Use the globally included $config
|
|
||||||
|
|
||||||
$loader = new Twig\Loader\FilesystemLoader(BASEPATH . '/App/views/');
|
use Twig\Environment;
|
||||||
$loader->addPath(BASEPATH . '/vendor/4lt/novaconium/twig', 'novaconium');
|
use Twig\Loader\FilesystemLoader;
|
||||||
$loader->addPath(BASEPATH . '/App/templates', 'override');
|
|
||||||
|
|
||||||
$twig = new Twig\Environment($loader);
|
/**
|
||||||
|
* Render a Twig view.
|
||||||
|
*
|
||||||
|
* @param string $name Template name without extension (e.g. "index")
|
||||||
|
* @param array $moreData Additional variables to merge into template context
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function view(string $name = '', array $moreData = []): bool
|
||||||
|
{
|
||||||
|
global $config, $data;
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($moreData)) {
|
||||||
|
$data = array_merge($data, $moreData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Setup Twig
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
$loader = new FilesystemLoader(BASEPATH . '/App/views/');
|
||||||
|
|
||||||
|
// Add namespace paths
|
||||||
|
if (is_dir(FRAMEWORKPATH . '/twig')) {
|
||||||
|
$loader->addPath(FRAMEWORKPATH . '/twig', 'novaconium');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir(FRAMEWORKPATH . '/views')) {
|
||||||
|
$loader->addPath(FRAMEWORKPATH . '/views', 'novacore');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir(BASEPATH . '/App/templates')) {
|
||||||
|
$loader->addPath(BASEPATH . '/App/templates', 'override');
|
||||||
|
}
|
||||||
|
|
||||||
|
$twig = new Environment($loader);
|
||||||
|
|
||||||
// Add config to Twig globally
|
|
||||||
$twig->addGlobal('config', $config);
|
$twig->addGlobal('config', $config);
|
||||||
|
|
||||||
// Check if the template exists
|
// ----------------------------------------
|
||||||
if (file_exists(BASEPATH . '/App/views/' . $name . '.html.twig')) {
|
// Render template
|
||||||
echo $twig->render("$name.html.twig", $data);
|
// ----------------------------------------
|
||||||
|
|
||||||
|
$appTemplatePath = BASEPATH . '/App/views/' . $name . '.html.twig';
|
||||||
|
|
||||||
|
if (file_exists($appTemplatePath)) {
|
||||||
|
echo $twig->render($name . '.html.twig', $data);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
echo "Error: Twig Template ($name) Not Found.";
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (str_starts_with($name, '@')) {
|
||||||
|
echo $twig->render($name . '.html.twig', $data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Error: Twig Template ($name) Not Found.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -3,4 +3,9 @@
|
|||||||
right before the /body
|
right before the /body
|
||||||
like javascript
|
like javascript
|
||||||
or analytics
|
or analytics
|
||||||
-->
|
-->
|
||||||
|
{% include '@novaconium/javascript/page-edit.html.twig' %}
|
||||||
|
|
||||||
|
{% if editor == 'ace' %}
|
||||||
|
{% include '@novaconium/javascript/ace.html.twig' %}
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@ -1,26 +1,63 @@
|
|||||||
<meta charset="utf-8">
|
{# =============================================================================
|
||||||
<title>{{ title | default('Welcome') }}</title>
|
<HEAD>
|
||||||
<meta name="generator" content="Novaconium" />
|
=============================================================================
|
||||||
|
#}
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<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="description" content="{{ description | default('No description given') }}">
|
||||||
<meta name="keywords" content="{{ keywords | default('website') }}">
|
<meta name="keywords" content="{{ keywords | default('website') }}">
|
||||||
<meta name="author" content="{{ author | default('anonymous') }}">
|
<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="#000000">
|
||||||
|
|
||||||
|
{# OPEN GRAPH (OG) FOR SOCIAL SHARING #}
|
||||||
<meta property="og:title" content="{{ title | default('Welcome') }}">
|
<meta property="og:title" content="{{ title | default('Welcome') }}">
|
||||||
<meta property="og:type" content="">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="">
|
<meta property="og:url" content="{{ app_url | default(request.scheme ~ '://' ~ request.host) }}">
|
||||||
<meta property="og:image" content="">
|
<meta property="og:image" content="{{ og_image | default('/icon.png') }}">
|
||||||
|
|
||||||
|
{# PWA & FAVICONS #}
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="apple-touch-icon" href="/icon.png">
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
<!-- Place favicon.ico in the root directory -->
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
|
||||||
{# https://developers.google.com/fonts/docs/getting_started #}
|
{# GOOGLE FONTS (CDN VIA PRECONNECT) #}
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Material+Icons|Material+Icons+Outlined">
|
<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=IBM+Plex+Mono&family=Roboto+Mono&family=Source+Code+Pro&family=Lato&family=Poppins&family=Material+Icons&family=Material+Icons+Outlined&family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap') }}"
|
||||||
|
rel="stylesheet"
|
||||||
|
>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/main.css">
|
{# STYLESHEET #}
|
||||||
|
{% if pageclass == "novaconium" %}
|
||||||
|
<link rel="stylesheet" href="/css/novaconium.css">
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<meta name="theme-color" content="#000000">
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
{# highlight.js #}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/ir-black.min.css">
|
||||||
40
twig/javascript/ace.html.twig
Normal file
40
twig/javascript/ace.html.twig
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!-- Ace Editor -->
|
||||||
|
<script>
|
||||||
|
// Ace Editor Init (cleaned: removed duplicate setUseWorker, added null checks)
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const bodyTextarea = document.getElementById('body');
|
||||||
|
const bodyEditorEl = document.getElementById('body-editor');
|
||||||
|
|
||||||
|
if (bodyTextarea && bodyEditorEl && typeof ace !== 'undefined') { // Check ace loaded
|
||||||
|
try {
|
||||||
|
const editor = ace.edit(bodyEditorEl);
|
||||||
|
editor.session.setValue(bodyTextarea.value || ''); // Load initial HTML
|
||||||
|
editor.session.setMode('ace/mode/html'); // HTML syntax highlighting
|
||||||
|
editor.setTheme('ace/theme/tomorrow_night'); // Dark theme (black bg, green accents)
|
||||||
|
editor.setOptions({
|
||||||
|
fontSize: 14,
|
||||||
|
showPrintMargin: false,
|
||||||
|
wrap: true, // Line wrapping
|
||||||
|
useWorker: false // Disable worker for linting if not needed (faster)
|
||||||
|
});
|
||||||
|
editor.session.setUseWorker(false); // No JS linting in HTML mode
|
||||||
|
|
||||||
|
// Enable basic autocomplete
|
||||||
|
editor.setOptions({ enableBasicAutocompletion: true });
|
||||||
|
|
||||||
|
// Sync back to textarea on change
|
||||||
|
editor.session.on('change', function() {
|
||||||
|
bodyTextarea.value = editor.getValue(); // Full HTML string
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Ace loaded! Initial value:', editor.getValue().substring(0, 50) + '...'); // Debug
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ace init failed:', error); // Graceful error
|
||||||
|
bodyEditorEl.style.display = 'none'; // Hide div, show plain textarea if needed
|
||||||
|
bodyTextarea.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Ace elements or lib missing'); // Fallback to plain textarea
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
192
twig/javascript/page-edit.html.twig
Normal file
192
twig/javascript/page-edit.html.twig
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script>
|
||||||
|
// Tab switching JS (unchanged)
|
||||||
|
function switchTab(tabId, button) {
|
||||||
|
const contents = document.querySelectorAll('.tab-content');
|
||||||
|
contents.forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
const buttons = document.querySelectorAll('.tab-button');
|
||||||
|
buttons.forEach(b => b.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
button.classList.add('active');
|
||||||
|
|
||||||
|
if (tabId === 'content6') {
|
||||||
|
setTimeout(initTags, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags Init (with custom dropdown autocomplete)
|
||||||
|
let tagsListeners = [];
|
||||||
|
function initTags() {
|
||||||
|
const tagsInput = document.getElementById('tags-input');
|
||||||
|
const tagsField = document.getElementById('tags');
|
||||||
|
const hiddenTags = document.getElementById('tags_json');
|
||||||
|
const dropdown = document.getElementById('tags-dropdown');
|
||||||
|
|
||||||
|
if (!tagsInput || !tagsField || !hiddenTags || !dropdown) {
|
||||||
|
console.warn('Tags elements missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean old listeners
|
||||||
|
tagsListeners.forEach(ls => ls());
|
||||||
|
tagsListeners = [];
|
||||||
|
|
||||||
|
let tags = [];
|
||||||
|
let existingTags = [];
|
||||||
|
try {
|
||||||
|
tags = JSON.parse(tagsInput.dataset.tags || '[]');
|
||||||
|
existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('JSON error:', e);
|
||||||
|
tags = [];
|
||||||
|
existingTags = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedIndex = -1; // For keyboard nav
|
||||||
|
|
||||||
|
// Render chips (unchanged)
|
||||||
|
function renderTags() {
|
||||||
|
tagsInput.innerHTML = '';
|
||||||
|
tags.forEach((tag, index) => {
|
||||||
|
const chip = document.createElement('span');
|
||||||
|
chip.className = 'tag-chip';
|
||||||
|
chip.innerHTML = `${tag} <button type="button" class="tag-remove">×</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>
|
||||||
16
twig/left.html.twig
Normal file
16
twig/left.html.twig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="left">
|
||||||
|
{% if pageclass == "novaconium" %}
|
||||||
|
<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>
|
||||||
|
<li><a href="/novaconium/logout">Logout</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="/novaconium/login">Login</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@ -7,7 +7,7 @@
|
|||||||
{% include ['@override/head.html.twig', '@novaconium/head.html.twig'] %}
|
{% include ['@override/head.html.twig', '@novaconium/head.html.twig'] %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="{{ pageid | default('pageid') }}">
|
<body id="{{ pageid | default('pageid') }}" class="{{ pageclass | default('pageclass') }}" >
|
||||||
|
|
||||||
{# Page Header #}
|
{# Page Header #}
|
||||||
<header>
|
<header>
|
||||||
@ -20,6 +20,8 @@
|
|||||||
<div id="page">
|
<div id="page">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
|
{% include ['@override/left.html.twig','@novaconium/left.html.twig'] %}
|
||||||
|
|
||||||
<div class="middle">
|
<div class="middle">
|
||||||
{% if error|default is not empty %}
|
{% if error|default is not empty %}
|
||||||
{% for key, val in error %}
|
{% for key, val in error %}
|
||||||
@ -36,6 +38,8 @@
|
|||||||
<article>
|
<article>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{% include ['@override/right.html.twig','@novaconium/right.html.twig'] %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -47,6 +51,13 @@
|
|||||||
{% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %}
|
{% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %}
|
||||||
{% block footerafter %}{% endblock %}
|
{% block footerafter %}{% endblock %}
|
||||||
</footer>
|
</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/foot.html.twig', '@novaconium/foot.html.twig'] %}
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
@ -1,21 +1,5 @@
|
|||||||
<div class="container">
|
<div id="topnav">
|
||||||
<div id="logo"><a href="/">Logo Goes Here</a></div>
|
|
||||||
|
|
||||||
<nav>
|
<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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
twig/right.html.twig
Normal file
1
twig/right.html.twig
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- Right Col -->
|
||||||
7
views/404.html.twig
Normal file
7
views/404.html.twig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% 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 %}
|
||||||
8
views/dashboard.html.twig
Normal file
8
views/dashboard.html.twig
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% 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 %}
|
||||||
30
views/editmessage.html.twig
Normal file
30
views/editmessage.html.twig
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% 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 %}
|
||||||
62
views/editpage/index.html.twig
Normal file
62
views/editpage/index.html.twig
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{% extends '@novaconium/master.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Edit Page - {{ title }}</h2>
|
||||||
|
|
||||||
|
<form method="post" action="/novaconium/savePage" id="edit-page-form-novaconium">
|
||||||
|
|
||||||
|
<input type="hidden" name="id" value="{{ rows.id }}">
|
||||||
|
<input type="hidden" name="token" value="{{ token }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="edit-page-title" class="form-group">
|
||||||
|
<label for="title">
|
||||||
|
Title
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">This is the title for the cms, it's the default in twig, heading and metadata can be overwritten.</span></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="text" id="title" name="title" value="{{ rows.title }}" required>
|
||||||
|
<div id="edit-page-dates">
|
||||||
|
<strong>Last Updated:</strong> {{ rows.updated|date("Y-m-d H:i:s") }},
|
||||||
|
<strong>Created:</strong> {{ rows.created|date("Y-m-d H:i:s") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-container">
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<nav class="tab-nav">
|
||||||
|
<button type="button" class="tab-button active" onclick="switchTab('content1')">Basic Info</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content2')">SEO & Meta</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content3')">Sitemap</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content4')">Page Tweaks</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content5')">Page Notes</button>
|
||||||
|
<button type="button" class="tab-button" onclick="switchTab('content6', this)">Tags</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="content1" class="tab-content active">
|
||||||
|
{% include '@novacore/editpage/tab-main.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content2" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-metadata.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content3" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-other.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content4" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-tweaks.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content5" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-notes.html.twig' %}
|
||||||
|
</div>
|
||||||
|
<div id="content6" class="tab-content">
|
||||||
|
{% include '@novacore/editpage/tab-tags.html.twig' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
16
views/editpage/tab-main.html.twig
Normal file
16
views/editpage/tab-main.html.twig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label for="slug">
|
||||||
|
Slug: (<a href="/page/{{ rows.slug }}" target="_new">/page/{{ rows.slug }}</a>)
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">Slug is a human readable but uri friendly name for the page.</span></span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="slug" name="slug" value="{{ rows.slug }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ace Editor -->
|
||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="body">Body:</label>
|
||||||
|
<div class="editor-container">
|
||||||
|
<textarea id="body" name="body" rows="10" style="display: none;">{{ rows.body|default('')|e('html') }}</textarea>
|
||||||
|
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
17
views/editpage/tab-metadata.html.twig
Normal file
17
views/editpage/tab-metadata.html.twig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
<h2>Metadata</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description:</label>
|
||||||
|
<input type="text" id="description" name="description" value="{{ rows.description }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="keywords">Keywords:</label>
|
||||||
|
<input type="text" id="keywords" name="keywords" value="{{ rows.keywords }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="author">Author:</label>
|
||||||
|
<input type="text" id="author" name="author" value="{{ rows.author }}">
|
||||||
|
</div>
|
||||||
4
views/editpage/tab-notes.html.twig
Normal file
4
views/editpage/tab-notes.html.twig
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="notes">Notes:</label>
|
||||||
|
<textarea id="notes" name="notes" rows="5">{{ rows.notes }}</textarea>
|
||||||
|
</div>
|
||||||
25
views/editpage/tab-other.html.twig
Normal file
25
views/editpage/tab-other.html.twig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<h2>Sitemap</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="path">Path:</label>
|
||||||
|
<input type="text" id="path" name="path" value="{{ rows.path }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="changefreq">Change Frequency:</label>
|
||||||
|
<select id="changefreq" name="changefreq">
|
||||||
|
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
|
||||||
|
{% for freq in freqs %}
|
||||||
|
<option value="{{ freq }}" {% if rows.changefreq == freq %}selected{% endif %}>
|
||||||
|
{{ freq|capitalize }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="priority">Priority (0.0 - 1.0):</label>
|
||||||
|
<input type="number" id="priority" name="priority" value="{{ rows.priority }}" step="0.1" min="0" max="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
12
views/editpage/tab-tags.html.twig
Normal file
12
views/editpage/tab-tags.html.twig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<h1>Tags</h1>
|
||||||
|
<label for="tags">
|
||||||
|
Tags:
|
||||||
|
<span class="tooltip">?<span class="tooltiptext">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span><span class="tooltip-desc" id="desc-tags">Comma-separated keywords for categorizing this page (e.g., blog, tutorial). Click chips to remove; type to see suggestions.</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="tags-input" id="tags-input" data-tags="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}" data-existing-tags="{{ rows.existing_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
|
||||||
|
<input type="text" id="tags" name="tags" placeholder="e.g., seo, cms, php" aria-describedby="desc-tags">
|
||||||
|
<ul id="tags-dropdown" class="tags-dropdown" role="listbox" aria-expanded="false"></ul> {# Custom dropdown #}
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="tags_json" name="tags_json" value="{{ rows.page_tags|default('')|split(',')|filter('trim')|default([])|json_encode|e('html_attr') }}">
|
||||||
|
</div>
|
||||||
10
views/editpage/tab-tweaks.html.twig
Normal file
10
views/editpage/tab-tweaks.html.twig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<h2>Page Data</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="heading">Heading:</label>
|
||||||
|
<input type="text" id="heading" name="heading" value="{{ rows.heading }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group fullwidth">
|
||||||
|
<label for="intro">Intro:</label>
|
||||||
|
<textarea id="intro" name="intro" rows="5">{{ rows.intro }}</textarea>
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user