17 Commits

Author SHA1 Message Date
230476e8e5 Claude redid the router 2026-06-30 01:35:55 -07:00
d5871ed8e7 minor updates 2026-06-16 19:00:30 -07:00
ad6e07c76d fixed css and changed timezone 2026-05-04 11:30:14 -07:00
06310d9eb9 Updated readme and composer 2026-05-03 13:27:30 -07:00
f679d0b20e safety fix 2026-02-07 17:26:26 -08:00
9feccf9eaa tabs and draft working 2026-01-26 21:55:22 -08:00
14ec6b7e7a fixed matomo to be in the master twig template. 2026-01-22 15:40:46 -08:00
42a828a778 edit page template 2026-01-12 19:00:03 -08:00
81c239f043 added matomo 2026-01-09 12:41:55 -08:00
0d91b61bd0 control panel head changes 2025-12-22 18:32:35 -08:00
fd9a71c4d6 added page to the front end 2025-12-16 05:35:56 -08:00
71413283c8 added humans and robots 2025-12-15 09:54:57 -08:00
a32956b4a2 added coming soon page 2025-12-15 09:28:09 -08:00
7bf3dc6610 added coming soon page 2025-12-15 09:27:56 -08:00
7b064eb6da morning changes 2025-12-15 06:14:07 -08:00
934e134941 ready to test new layout 2025-12-14 03:45:07 -08:00
6f410b5d0e added logs to docker 2025-12-07 23:10:32 -08:00
56 changed files with 1053 additions and 162 deletions

View File

@@ -6,13 +6,20 @@ NovaconiumPHP is a high-performance PHP framework designed with inspiration from
Pronounced: Noh-vah-koh-nee-um Pronounced: Noh-vah-koh-nee-um
Packagist: https://packagist.org/packages/4lt/novaconium * Packagist: https://packagist.org/packages/4lt/novaconium
Master Repo: https://git.4lt.ca/4lt/novaconium * Master Repo: https://git.4lt.ca/4lt/novaconium
## Getting Started ## Getting Started
Novaconium is heavly influenced by docker, but you can use composer outside of docker. Novaconium is designed to be developed primarily using Docker. Instead of relying on tools installed directly on your system, common development tasks are executed inside containers:
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).
* Composer runs inside a Docker container rather than on your host machine
* Apache / PHP are served from containers instead of your local environment
* Sass compilation is also handled within a container
As long as you have Docker installed, you can use the full development environment without installing additional dependencies on your system.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Composer.md).
```bash ```bash
PROJECTNAME=novaproject; PROJECTNAME=novaproject;
@@ -24,9 +31,9 @@ docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest
cp -R novaconium/vendor/4lt/novaconium/skeleton/. .; cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
# Edit .env # Edit .env
# pwgen -cnsB1v 12 root password # pwgen -cnsB1v 12 # root password
# pwgen -cnsB1v 12 mysql user password (need in both config and env) # pwgen -cnsB1v 12 # mysql user password (need in both config and env)
# pwgen -cnsB1v 64 framework key (need in config) # pwgen -cnsB1v 64 # framework key (need in config)
# Edit novaconium/App/config.php # Edit novaconium/App/config.php
docker compose up -d docker compose up -d

View File

@@ -8,11 +8,14 @@ $framework_routes = [
], ],
'/novaconium/login' => [ '/novaconium/login' => [
'post' => 'NOVACONIUM/authenticate', 'post' => 'NOVACONIUM/authenticate',
'get' => 'NOVACONIUM/login' 'get' => 'NOVACONIUM/auth/login'
], ],
'/novaconium/dashboard' => [ '/novaconium/dashboard' => [
'get' => 'NOVACONIUM/dashboard' 'get' => 'NOVACONIUM/dashboard'
], ],
'/novaconium/settings' => [
'get' => 'NOVACONIUM/settings'
],
'/novaconium/pages' => [ '/novaconium/pages' => [
'get' => 'NOVACONIUM/pages' 'get' => 'NOVACONIUM/pages'
], ],
@@ -38,8 +41,8 @@ $framework_routes = [
'post' => 'NOVACONIUM/message_save' 'post' => 'NOVACONIUM/message_save'
], ],
'/novaconium/logout' => [ '/novaconium/logout' => [
'post' => 'NOVACONIUM/logout', 'post' => 'NOVACONIUM/auth/logout',
'get' => 'NOVACONIUM/logout' 'get' => 'NOVACONIUM/auth/logout'
], ],
'/novaconium/sitemap.xml' => [ '/novaconium/sitemap.xml' => [
'get' => 'NOVACONIUM/sitemap' 'get' => 'NOVACONIUM/sitemap'

View File

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

View File

@@ -0,0 +1,9 @@
<?php
$data = array_merge($data, [
'title' => 'Coming Soon',
'heading' => 'Coming Soon',
'countdown' => true,
'launch_date' => '2026-01-01T00:00:00'
]);
view('@novacore/coming-soon', $data);

View File

@@ -1,7 +1,8 @@
<?php <?php
$data = array_merge($data, [ $data = array_merge($data, [
'title' => 'Novaconium Dashboard Page', 'title' => 'Novaconium Dashboard Page',
'pageclass' => 'novaconium' 'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]); ]);
if ( empty($session->get('username'))) { if ( empty($session->get('username'))) {

View File

@@ -3,6 +3,7 @@
$data = array_merge($data, [ $data = array_merge($data, [
'title' => 'Novaconium Edit Page', 'title' => 'Novaconium Edit Page',
'pageclass' => 'novaconium', 'pageclass' => 'novaconium',
'pageid' => 'controlPanel',
'editor' => 'ace' 'editor' => 'ace'
]); ]);

View File

@@ -7,6 +7,7 @@ $data = [
'empty_users' => false, 'empty_users' => false,
'show_login' => false, 'show_login' => false,
'token' => $session->get('token'), 'token' => $session->get('token'),
'pageclass' => 'novaconium',
'title' => 'Novaconium Admin' 'title' => 'Novaconium Admin'
]; ];

View File

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

View File

@@ -2,7 +2,8 @@
$data = array_merge($data, [ $data = array_merge($data, [
'title' => 'Novaconium Messages', 'title' => 'Novaconium Messages',
'pageclass' => 'novaconium' 'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]); ]);
if ( empty($session->get('username'))) { if ( empty($session->get('username'))) {

View File

@@ -2,7 +2,8 @@
$data = array_merge($data, [ $data = array_merge($data, [
'title' => 'Novaconium Pages', 'title' => 'Novaconium Pages',
'pageclass' => 'novaconium' 'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]); ]);
if ( empty($session->get('username'))) { if ( empty($session->get('username'))) {

15
controllers/settings.php Normal file
View File

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

View File

@@ -8,7 +8,7 @@ Update novaconium with composer in docker: ```docker run --rm --interactive --tt
## Install Composer natively on Debian ## Install Composer natively on Debian
Assuming you have nala installed: Assuming you have nala installed (otherwise use apt-get):
```bash ```bash
sudo nala install curl php-cli php-mbstring git unzip sudo nala install curl php-cli php-mbstring git unzip

101
docs/Router.md Normal file
View File

@@ -0,0 +1,101 @@
# Router
`Novaconium\Router` resolves the current HTTP request to a controller file, based on a route table defined in PHP config files. It supports exact-match routes and simple parameterized routes (e.g. `/users/{id}`).
## How it works
On construction, the router runs through this pipeline:
1. **Load routes** — merges framework-level routes with app-level routes.
2. **Prepare path** — normalizes `REQUEST_URI` into a clean path string.
3. **Prepare query** — parses the query string into an array.
4. **Determine request type**`get` or `post`.
5. **Find controller** — matches the path against the route table.
6. **Resolve controller file** — turns the matched controller string into an actual file path, falling back to a 404 controller if needed.
## Properties
| Property | Type | Description |
|---|---|---|
| `$routes` | array | Combined route table (framework + app routes), keyed by path pattern. |
| `$query` | array | Parsed query string parameters. |
| `$path` | string | Normalized request path. |
| `$controller` | string | Resolved controller identifier (e.g. `Users/show` or `NOVACONIUM/Errors`). |
| `$controllerPath` | string | Absolute file path to the controller. |
| `$parameters` | array | Named parameters extracted from a parameterized route match. |
| `$requestType` | string | `'get'` or `'post'`. |
## Route table format
Routes are defined as an associative array, keyed by URL path. Each entry maps HTTP methods to a controller string:
```php
$routes = [
'/' => [
'get' => 'Home/index',
],
'/users/{id}' => [
'get' => 'Users/show',
],
'/login' => [
'get' => 'Auth/loginForm',
'post' => 'Auth/login',
],
];
```
Controller strings starting with `NOVACONIUM` are resolved against the framework's controller directory (e.g. `NOVACONIUM/Errors``FRAMEWORKPATH/controllers/Errors.php`); all other controller strings are resolved against the app's controller directory (`BASEPATH/App/controllers/`).
## Matching behavior
### 1. Exact match
The router first checks for an exact match between `$this->path` and a key in `$this->routes`. If found, and the current request type has a handler defined, that controller is returned immediately.
### 2. Parameterized match
If no exact match is found, the router scans the route table for patterns containing `{`. For each candidate:
- The **static prefix** of the pattern (everything before the first `{`) must prefix-match the current path.
- The pattern and the path must have the **same number of `/`-separated segments**.
The first route pattern satisfying both conditions is treated as the match — the router does not keep searching for a "better" match after this point, even if the matched route turns out to have no handler for the current request method (see [Known quirks](#known-quirks) below).
Once a candidate route is selected, the router walks its segments looking for the first `{param}` segment, extracts the corresponding path segment into `$this->parameters[paramName]`, and returns immediately with the controller for the current request type.
### 3. No match
If neither an exact nor a parameterized match is found, `$this->controller` is set to `'404'`, and `setRouteFile()` will fail to find a corresponding controller file, triggering the 404 fallback described below.
## Controller file resolution (`setRouteFile`)
Given the resolved `$this->controller` string:
1. If it starts with `NOVACONIUM`, look in `FRAMEWORKPATH/controllers/`.
2. Otherwise, look in `BASEPATH/App/controllers/`.
3. If the file exists, use it.
4. If not, fall back to `BASEPATH/App/controllers/404.php` if it exists.
5. Otherwise, fall back to the framework's built-in `FRAMEWORKPATH/controllers/404.php`.
## Known quirks
These behaviors exist in the current implementation and are preserved intentionally for backward compatibility — they're documented here so they aren't mistaken for bugs during future changes.
- **Only the first `{param}` in a route pattern is captured.** If a route pattern has multiple parameters (e.g. `/users/{id}/posts/{postId}`), only `id` is extracted into `$this->parameters`; `postId` is never processed, because the matching loop returns as soon as it finds the first parameter segment.
- **The first prefix+segment-count match wins, even without a method handler.** If a parameterized route's static prefix and segment count match the request, but that route has no handler defined for the current request type (e.g. the route only defines `get` but the request is `post`), the router still commits to that route and returns `null` as the controller (which then falls through to the 404 controller). It does **not** continue searching for another route that might have a valid handler.
- **No support for trailing wildcard or optional segments.** Segment counts must match exactly between the pattern and the path; there's no support for `*` or optional `{param?}` style segments.
## Debugging
Call `$router->debug()` to dump the resolved path, controller path, extracted parameters, and full route table as an HTML table, then halt execution via `die()`. Intended for development use only.
## Example
```php
$router = new \Novaconium\Router();
include $router->controllerPath;
// Inside the controller, access route parameters via:
$router->parameters['id'];
```

View File

@@ -21,3 +21,13 @@ 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 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
``` ```
Dev:
```bash
docker run --rm \
-v "$(pwd)/sass:/usr/src/sass" \
-v "/home/nick/tmp/novaproject/novaconium/public/css:/usr/src/css" \
-w /usr/src \
sass-container \
sass sass/novaconium.sass css/novaconium.css --no-source-map --style=compressed
```

View File

@@ -0,0 +1,94 @@
body#coming-soon
display: flex
justify-content: center
align-items: center
height: 100%
padding: 1rem
box-sizing: border-box
margin: 0
text-align: center
color: #eee
font-family: 'Segoe UI', Roboto, sans-serif
background-size: cover
background-repeat: no-repeat
background-position: center center
background-attachment: fixed
.container
max-width: 600px
background-color: rgba(30, 30, 30, 0.89)
padding: 2rem
border-radius: 8px
box-shadow: 0 0 20px rgba(0,0,0,0.5)
h1
font-size: 3rem
margin-bottom: 1rem
animation: pulse 1.5s infinite
p
font-size: 1.2rem
opacity: 0.85
margin-bottom: 1.5rem
.countdown
font-size: 2rem
font-weight: bold
margin: 1rem 0 2rem 0
form.newsletter
display: flex
flex-direction: column
gap: 0.5rem
margin-top: 1rem
input[type="email"]
padding: 0.5rem
font-size: 1rem
border: 1px solid #666
border-radius: 4px
background: rgba(255,255,255,0.05)
color: #eee
button
padding: 0.5rem
font-size: 1rem
border: 1px solid #1e90ff
border-radius: 4px
background: #1e90ff
color: #fff
cursor: pointer
transition: background 0.2s
&:hover
background: #1565c0
.social-icons
margin-top: 2rem
display: flex
justify-content: center
gap: 1rem
a
display: inline-flex
align-items: center
justify-content: center
width: 40px
height: 40px
font-size: 1.5rem
color: #eee
text-decoration: none
transition: color 0.2s
&:hover
color: #1e90ff
i
font-style: normal
@keyframes pulse
0%, 100%
opacity: 0.8
50%
opacity: 1

View File

@@ -0,0 +1,4 @@
body#controlPanel
footer
padding: 1rem

View File

@@ -0,0 +1,8 @@
body#controlPanel
header
padding: 0
h1#biglogo
padding: 10px 0 0 0
margin: 0

View File

@@ -0,0 +1,11 @@
body#controlPanel
div#panel
main#content
flex: 1
padding: 20px 40px
min-width: 0
background-color: #0b0b0bd6
border-left: 2px solid #104910

View File

@@ -0,0 +1,22 @@
@use '../abstracts' as *
body#controlPanel
div#panel
div#cp-menu
background-color: #000
width: 360px
padding: 0
white-space: nowrap
ul#cp-nav
list-style: none
margin: 0
padding: 20px 0 0 0
li a
display: block
border-bottom: 1px solid $border-light
padding: 4px 10px
&:hover
background-color: #222

View File

@@ -0,0 +1,5 @@
body#controlPanel
div#panel
display: flex
flex-direction: row

View File

@@ -0,0 +1,10 @@
@forward 'header';
@forward 'panel';
@forward 'menu';
@forward 'main';
@forward 'footer';
body#controlPanel
h1
font-family: "VT323", "Courier New", "SF Mono", "Fira Code", Consolas, monospace
font-size: 48px

View File

@@ -3,7 +3,7 @@
@use '../abstracts' as * @use '../abstracts' as *
@use 'sass:color' // For color.adjust()non-deprecated color tweaks @use 'sass:color' // For color.adjust()non-deprecated color tweaks
body.novaconium body#controlPanel
// Simplified tab styling: Square borders, no rounds // Simplified tab styling: Square borders, no rounds
.tab-container .tab-container
margin: space('lg') auto margin: space('lg') auto

View File

@@ -74,7 +74,7 @@ body.novaconium
display: inline-flex display: inline-flex
align-items: center align-items: center
background: color.adjust($accent-light, $alpha: -0.2) background: color.adjust($accent-light, $alpha: -0.2)
color: $accent-light color: #fff
padding: space('xs') space('sm') padding: space('xs') space('sm')
border-radius: 4px border-radius: 4px
font-size: 12px font-size: 12px

View File

@@ -2,3 +2,5 @@
@use 'abstracts' as * @use 'abstracts' as *
@use 'base' as * @use 'base' as *
@use 'framework' as * @use 'framework' as *
@use 'controlPanel' as *
@use 'coming-soon' as *

View File

@@ -1,14 +1,13 @@
# Sample Docker Compose # Sample Docker Compose
services: services:
corxn: corxn:
image: 4lights/corxn:6.0.0 image: 4lights/corxn:8.5.3
ports: ports:
- "8000:80" - "8000:80"
volumes: volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./novaconium:/data - ./novaconium:/data
- ./data/logs:/var/log/apache2 # Optional Logs - ./data/logs:/var/log/apache2 # Optional Logs
- "./logs:/data/logs"
restart: unless-stopped restart: unless-stopped
networks: networks:
- internal - internal
@@ -28,8 +27,6 @@ services:
MYSQL_USER: novaconium MYSQL_USER: novaconium
MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes: volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./data/db:/var/lib/mysql - ./data/db:/var/lib/mysql
networks: networks:
- internal - internal
@@ -48,9 +45,6 @@ services:
- PMA_USER=root - PMA_USER=root
- PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD}
- UPLOAD_LIMIT=200M - UPLOAD_LIMIT=200M
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks: networks:
proxy: proxy:

View File

@@ -10,6 +10,8 @@ $config = [
'base_url' => 'http://localhost:8000', 'base_url' => 'http://localhost:8000',
'secure_key' => '', //64 alphanumeric characters 'secure_key' => '', //64 alphanumeric characters
'logfile' => '/logs/novaconium.log', 'logfile' => '/logs/novaconium.log',
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE' 'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE',
'matomo_url' => 'matomo.4lt.ca',
'matomo_id' => '0',
'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' 'fonts' => 'https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap'
]; ];

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: public, max-age=604800');
echo <<<TXT
/*
* This humans.txt was generated by the framework.
* You may edit or remove it without affecting your site.
*/
/* FRAMEWORK */
Built With: Novaconium PHP
repo: https://git.4lt.ca/4lt/novaconium
Human: Nick Yeoman
url: https://www.nickyeoman.com/
Occupation: Linux Systems Administrator / Software Framework Author
Location: Canada
/* SITE */
Built with: Novaconium PHP
Runs on: CORXN Container
Optimized for: Humans
/* META */
There are four lights.
TXT;

View File

@@ -1,2 +1,7 @@
<?php <?php
$data = array_merge($data, [
'title' => 'Welcome to Novaconium Index Page',
'pageclass' => 'novaconium'
]);
view('index'); view('index');

View File

@@ -0,0 +1,33 @@
<?php
$slug = $router->parameters['slug'];
$query=<<<EOSQL
SELECT
id,
title,
heading,
description,
keywords,
author,
body,
created,
updated
FROM pages
WHERE slug = '$slug'
EOSQL;
//$db->debugGetRow($query);
$data = $db->getRow($query);
if(!$data) {
http_response_code('404');
header("Content-Type: text/html");
view('404');
exit;
}
$data = array_merge($data, [
'menuActive' => 'blog',
'pageid' => 'page',
'permissionsGroup' => $session->get('group')
]);
view('page', $data);

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
// Send proper headers
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: public, max-age=604800');
// Use $config['base_url'] as-is
$baseUrl = $config['base_url'];
echo <<<TXT
# robots.txt for sites powered by Novaconium framework
User-agent: *
Disallow: /novaconium/
Sitemap: {$baseUrl}/sitemap.xml
TXT;

View File

@@ -2,5 +2,14 @@
$routes = [ $routes = [
'/' => [ '/' => [
'get' => 'index' 'get' => 'index'
],
'/robots.txt' => [
'get' => 'robots'
],
'/humans.txt' => [
'get' => 'humans'
],
'/page/{slug}' => [
'get' => 'page'
] ]
]; ];

View File

@@ -0,0 +1,11 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{ title }}</h1>
<div class="page-meta">
{% if updated is not empty %}
<span class="page-updated">Last Updated: {{ updated | date("M\\. jS Y \\a\\t g:ia") }}</span>
{% endif %}
</div>
{{ body | raw}}
{% endblock %}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"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"}

View File

@@ -1,22 +1,10 @@
<?php <?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 // Enable error reporting for development environments
// error_reporting(E_ALL); // error_reporting(E_ALL);
// ini_set('display_errors', 1); // ini_set('display_errors', 1);
// Define the base path where the website is running from // Define the base path where the website is running from
define('BASEPATH', dirname(__DIR__, 1)); define('BASEPATH', dirname(__DIR__));
// Load Composer's autoload file to handle class autoloading // Load Composer's autoload file to handle class autoloading
require BASEPATH . '/vendor/autoload.php'; require BASEPATH . '/vendor/autoload.php';

View File

@@ -0,0 +1,185 @@
// /js/tabs.js
// Tab switching
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;
}
// Remove 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);
}
let selectedIndex = -1;
function renderTags() {
tagsInput.innerHTML = '';
tags.forEach((tag, index) => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `${tag} <button type="button" class="tag-remove">&times;</button>`;
chip.querySelector('.tag-remove').onclick = () => {
tags.splice(index, 1);
renderTags();
hiddenTags.value = JSON.stringify(tags);
};
tagsInput.appendChild(chip);
});
tagsInput.appendChild(tagsField);
tagsInput.appendChild(dropdown);
hiddenTags.value = JSON.stringify(tags);
}
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);
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');
}
function updateHighlight() {
dropdown.querySelectorAll('li').forEach((li, index) => {
li.classList.toggle('selected', index === selectedIndex);
});
}
function selectTag(tag) {
addTag(tag, true);
tagsField.value = '';
dropdown.setAttribute('aria-expanded', 'false');
}
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();
}
const keydownListener = (e) => {
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;
if (selected) selectTag(selected);
} else if (e.key === 'Escape') {
e.preventDefault();
tagsField.blur();
}
}
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);
}
}
};
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);
if (tagsField.value.trim()) addTag(tagsField.value);
};
tagsField.addEventListener('blur', blurListener);
tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener));
renderTags();
}
// Init on load and observe tab changes
document.addEventListener('DOMContentLoaded', () => {
const tagsTab = document.getElementById('content6');
if (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 });
});
// Export switchTab for external use if needed
window.switchTab = switchTab;

View File

@@ -1,5 +1,6 @@
<?php <?php
namespace Novaconium; namespace Novaconium;
class Router { class Router {
public $routes = []; public $routes = [];
public $query = []; public $query = [];
@@ -10,120 +11,185 @@ class Router {
public $requestType = 'get'; public $requestType = 'get';
public function __construct() { public function __construct() {
$this->routes = $this->loadRoutes(); $this->routes = $this->loadRoutes();
$this->path = $this->preparePath(); $this->path = $this->preparePath();
$this->query = $this->prepareQuery(); $this->query = $this->prepareQuery();
$this->requestType = $this->getRequestType(); $this->requestType = $this->getRequestType();
$this->controller = $this->findController(); $this->controller = $this->findController();
$this->controllerPath = $this->setRouteFile(); $this->controllerPath = $this->setRouteFile();
} }
/**
* Merge framework routes with app-defined routes (app routes can override framework routes).
*/
private function loadRoutes() { private function loadRoutes() {
require_once( \FRAMEWORKPATH . '/config/routes.php'); require_once(\FRAMEWORKPATH . '/config/routes.php');
// Check if Path exists
if (file_exists(\BASEPATH . '/App/routes.php')) {
require_once( \BASEPATH . '/App/routes.php');
}
$routes = array_merge((array)$routes, (array)$framework_routes);
return $routes; if (file_exists(\BASEPATH . '/App/routes.php')) {
require_once(\BASEPATH . '/App/routes.php');
}
return array_merge((array)$routes, (array)$framework_routes);
} }
/**
* Normalize the request path: strip query string, trailing slash, and anything after "&".
*/
private function preparePath() { private function preparePath() {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
//homepage
if ($path === '/') { if ($path === '/') {
return $path; return $path;
} }
// remove empty directory path $path = rtrim($path, '/');
$path = rtrim($path, '/'); // remove trailing slash
//remove anything after and including ampersand
$path = preg_replace('/&.+$/', '', $path); $path = preg_replace('/&.+$/', '', $path);
return $path; return $path;
} }
private function prepareQuery() { private function prepareQuery() {
$parsedUri = parse_url($_SERVER['REQUEST_URI']); $parsedUri = parse_url($_SERVER['REQUEST_URI']);
$queryArray = []; $queryArray = [];
if (isset($parsedUri['query'])) { if (isset($parsedUri['query'])) {
parse_str($parsedUri['query'], $queryArray); parse_str($parsedUri['query'], $queryArray);
} }
return $queryArray; return $queryArray;
} }
private function getRequestType() { private function getRequestType() {
// is the requewst a get or post? return empty($_POST) ? 'get' : 'post';
if (empty($_POST)) {
return 'get';
} else {
return 'post';
}
} }
/**
* Resolve the current path to a controller string.
* First tries an exact match, then falls back to matching parameterized
* routes like "/users/{id}".
*/
private function findController() { private function findController() {
$exactMatch = $this->findExactMatch();
// one to one match if ($exactMatch !== null) {
if (array_key_exists($this->path, $this->routes)) { return $exactMatch;
if (!empty($this->routes[$this->path][$this->requestType])) {
return $this->routes[$this->path][$this->requestType];
}
} }
foreach ($this->routes as $key => $value) { $paramMatch = $this->findParameterizedMatch();
// Check if key contains a curly bracket, if not continue. We already checked above. if ($paramMatch !== null) {
if (!strpos($key, '{')) continue; return $paramMatch;
// Remove everything after the curly bracket, from key
$keyPath = substr($key, 0, strpos($key, '{'));
//see if keyPath matches the first characters of $this->path, only the first characters have to match
if (strpos($this->path, $keyPath) === 0) {
// We have a potential match. Now check if the parameter count is equal
$keyParams = explode('/', $key);
$pathParams = explode('/', $this->path);
$keyParamCount = count($keyParams);
$pathParamCount = count($pathParams);
if ($keyParamCount === $pathParamCount) {
for ($i=0; $i < $pathParamCount; $i++) {
if (strpos($keyParams[$i], '{') !== false) {
$keyParams[$i] = substr($keyParams[$i], 1, -1);
$this->parameters[$keyParams[$i]] = $pathParams[$i];
return $this->routes[$key][$this->requestType];
}
}
}
}
} }
return '404'; return '404';
} }
// checks if the file exists, sets file path private function findExactMatch() {
private function setRouteFile() { if (array_key_exists($this->path, $this->routes)
&& !empty($this->routes[$this->path][$this->requestType])
if (str_starts_with($this->controller, 'NOVACONIUM')) { ) {
$trimmed = substr($this->controller, strlen('NOVACONIUM/')); return $this->routes[$this->path][$this->requestType];
$cp = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
} else {
$cp = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
} }
if (file_exists($cp)) { return null;
return $cp; }
} else {
//Check if 404 exits /**
if (file_exists( \BASEPATH . '/App/controllers/404.php')) { * Loop over parameterized routes (those containing "{") looking for the
return \BASEPATH . '/App/controllers/404.php'; * first one whose static prefix matches the path AND whose segment count
} else { * matches. As soon as such a route is found, control is handed off to
return \FRAMEWORKPATH . '/controllers/404.php'; * extractControllerFromMatch(), which returns immediately (even with a
* `null` controller) -- this mirrors the original code's behavior of
* stopping the search on the first prefix+segment-count match, rather
* than continuing on to try other candidate routes.
*/
private function findParameterizedMatch() {
foreach ($this->routes as $routePattern => $methods) {
// Only parameterized routes (containing "{") are relevant here;
// plain routes were already checked in findExactMatch().
if (strpos($routePattern, '{') === false) {
continue;
}
if (!$this->routeMatchesPath($routePattern)) {
continue;
}
if (!$this->segmentCountsMatch($routePattern)) {
continue;
}
// Original behavior: once we find a route with a matching prefix
// and segment count, we commit to it -- even if it turns out
// there's no handler for the current request method.
return $this->extractControllerFromMatch($routePattern, $methods);
}
return null;
}
/**
* Quick check: does the static portion of the route pattern (everything
* before the first "{") prefix-match the current path?
*/
private function routeMatchesPath($routePattern) {
$staticPrefix = substr($routePattern, 0, strpos($routePattern, '{'));
return strpos($this->path, $staticPrefix) === 0;
}
private function segmentCountsMatch($routePattern) {
$patternSegments = explode('/', $routePattern);
$pathSegments = explode('/', $this->path);
return count($patternSegments) === count($pathSegments);
}
/**
* Extracts named parameters into $this->parameters and returns the
* controller for the current request type (or null if that method
* isn't defined for this route).
*
* Matches the original quirk: only the FIRST "{param}" segment found
* gets added to $this->parameters, and the method returns right away --
* any additional "{param}" segments later in the pattern are never
* processed. This is preserved as-is rather than "fixed", since
* behavior should stay unchanged.
*/
private function extractControllerFromMatch($routePattern, $methods) {
$patternSegments = explode('/', $routePattern);
$pathSegments = explode('/', $this->path);
foreach ($patternSegments as $i => $segment) {
if (strpos($segment, '{') !== false) {
$paramName = substr($segment, 1, -1); // strip { }
$this->parameters[$paramName] = $pathSegments[$i];
return $methods[$this->requestType] ?? null;
} }
} }
return null;
}
/**
* Resolve the controller string to an actual file path, falling back to a 404 controller.
*/
private function setRouteFile() {
if (str_starts_with($this->controller, 'NOVACONIUM')) {
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
$controllerPath = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
} else {
$controllerPath = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
}
if (file_exists($controllerPath)) {
return $controllerPath;
}
if (file_exists(\BASEPATH . '/App/controllers/404.php')) {
return \BASEPATH . '/App/controllers/404.php';
}
return \FRAMEWORKPATH . '/controllers/404.php';
} }
public function debug() { public function debug() {
@@ -137,6 +203,4 @@ class Router {
die(); die();
} }
} }

View File

@@ -1,5 +1,16 @@
<?php <?php
use Novaconium\Logger;
use Novaconium\Session;
use Novaconium\MessageHandler;
use Novaconium\Database;
use Novaconium\Post;
use Novaconium\Redirect;
use Novaconium\Router;
$db = null;
$post = null;
// --- Load Config --- // --- Load Config ---
if (file_exists(\BASEPATH . '/App/config.php')) { if (file_exists(\BASEPATH . '/App/config.php')) {
require_once \BASEPATH . '/App/config.php'; require_once \BASEPATH . '/App/config.php';
@@ -11,43 +22,38 @@ require_once \FRAMEWORKPATH . '/src/functions.php';
require_once \FRAMEWORKPATH . '/src/twig.php'; require_once \FRAMEWORKPATH . '/src/twig.php';
// --- Logging --- // --- Logging ---
use Novaconium\Logger;
$log = new Logger(\BASEPATH . $config['logfile'], $config['loglevel']); $log = new Logger(\BASEPATH . $config['logfile'], $config['loglevel']);
// --- Twig Data Array --- // --- Twig Data Array ---
$data = []; $data = [];
$data['fonts'] = $config['fonts'] ?? []; $data['fonts'] = $config['fonts'] ?? [];
$data['matomo_url'] = $config['matomo_url'] ?? '';
$data['matomo_id'] = $config['matomo_id'] ?? '0';
// --- Session --- // --- Session ---
use Novaconium\Session;
$session = new Session(); $session = new Session();
$data['token'] = $session->get('token'); $data['token'] = $session->get('token');
$data['username'] = $session->get('username'); $data['username'] = $session->get('username');
// --- Messages --- // --- Messages ---
use Novaconium\MessageHandler;
$messages = new MessageHandler($session->flash('messages')); $messages = new MessageHandler($session->flash('messages'));
foreach (['error', 'notice'] as $key) { foreach (['error', 'notice'] as $key) {
$data[$key] = $messages->showMessages($key); $data[$key] = $messages->showMessages($key);
} }
// --- Database --- // --- Database ---
use Novaconium\Database;
if (!empty($config['database']['host'])) { if (!empty($config['database']['host'])) {
$db = new Database($config['database']); $db = new Database($config['database']);
} }
// --- POST Wrapper --- // --- POST Wrapper ---
use Novaconium\Post;
if (!empty($_POST)) { if (!empty($_POST)) {
$post = new Post($_POST); $post = new Post($_POST);
} }
// --- Redirect Handler --- // --- Redirect Handler ---
use Novaconium\Redirect;
$redirect = new Redirect(); $redirect = new Redirect();
// --- Router --- // --- Router ---
use Novaconium\Router;
$router = new Router(); $router = new Router();
require_once $router->controllerPath; require_once $router->controllerPath;

View File

@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title | default('Coming Soon') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/novaconium.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
background: url("{{ bg_image | default('https://w.wallhaven.cc/full/5y/wallhaven-5yymk9.jpg') }}") no-repeat center center fixed;
background-size: cover;
}
</style>
</head>
<body id="coming-soon">
<div class="container">
<h1>{{ heading | default('Coming Soon') }}</h1>
{% block content %}{% endblock %}
{% if countdown %}
<div class="countdown" id="countdown"></div>
{% endif %}
{# TODO: listmonk #}
<form class="newsletter" action="#" method="post">
<label for="email">Subscribe to our newsletter:</label>
<input type="email" id="email" name="email" placeholder="Your email address" required>
<button type="submit">Subscribe</button>
</form>
{# Social media icons using Font Awesome #}
<div class="social-icons">
{% block social %}{% endblock %}
</div>
</div>
{% if countdown %}
<script>
{% if countdown %}
{% set default_launch = "now"|date_modify("+30 days")|date("Y-m-d\TH:i:s") %}
const launchDate = new Date('{{ launch_date | default(default_launch) }}').getTime();
const countdownEl = document.getElementById('countdown');
function updateCountdown() {
const now = new Date().getTime();
const distance = launchDate - now;
if (distance <= 0) {
countdownEl.innerText = 'Launched!';
return;
}
const days = Math.floor(distance / (1000*60*60*24));
const hours = Math.floor((distance % (1000*60*60*24)) / (1000*60*60));
const mins = Math.floor((distance % (1000*60*60)) / (1000*60));
const secs = Math.floor((distance % (1000*60)) / 1000);
countdownEl.innerText = `${days}d ${hours}h ${mins}m ${secs}s`;
}
updateCountdown();
setInterval(updateCountdown, 1000);
{% endif %}
</script>
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,44 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
{% include ['@novaconium/cp/head.html.twig'] %}
</head>
<body id="{{ pageid | default('pageid') }}" class="{{ pageclass | default('pageclass') }}" >
{# Page Header #}
<header>
<h1 id="biglogo"><span class="main">Novaconium</span></h1>
</header>
<!-- Main Content Of The Page -->
<div id="panel" class="container">
{% include ['@novaconium/cp/menu.html.twig'] %}
<main id="content">
{% block content %}{% endblock %}
</main>
</div>
{# Page Footer #}
<footer>
<div class="copyright">&copy; {{ 'now' | date('Y') }} Novaconium</div>
</footer>
{% if editor == 'ace' %}
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}
{% if debug is not empty %}
<div id="debug">
<h2>Debugging Information</h2>
{{ debug|raw }}
</div>
{% endif %}
</body></html>

60
twig/cp/head.html.twig Normal file
View File

@@ -0,0 +1,60 @@
{# =============================================================================
<HEAD>
=============================================================================
#}
<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="keywords" content="{{ keywords | default('website') }}">
<meta name="author" content="{{ author | default('anonymous') }}">
{# 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:type" content="website">
<meta property="og:url" content="{{ app_url | default(request.scheme ~ '://' ~ request.host) }}">
<meta property="og:image" content="{{ og_image | default('/icon.png') }}">
{# PWA & FAVICONS #}
<link rel="manifest" href="site.webmanifest">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{# GOOGLE FONTS (CDN VIA PRECONNECT) #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="{{ fonts | default('https://fonts.googleapis.com/css2?family=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"
>
{% 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">
<script src="/js/tabs.js"></script>
{# STYLESHEET #}
<link rel="stylesheet" href="/css/novaconium.css">

9
twig/cp/menu.html.twig Normal file
View File

@@ -0,0 +1,9 @@
<div id="cp-menu">
<ul id="cp-nav">
<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/settings">Settings</a></li>
<li><a href="/novaconium/logout">Logout</a></li>
</ul>
</div>

View File

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

View File

@@ -1,3 +1 @@
<!-- <div class="copyright">&copy; {{ 'now' | date('Y') }} Novaconium</div>
What goes in the footer html tag
-->

View File

@@ -39,22 +39,7 @@
> >
{# STYLESHEET #} {# STYLESHEET #}
{% if pageclass == "novaconium" %} <link rel="stylesheet" href="/css/novaconium.css">
<link rel="stylesheet" href="/css/novaconium.css">
{% else %}
<link rel="stylesheet" href="/css/main.css">
{% endif %}
{% 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 #} {# highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>

View File

@@ -60,4 +60,32 @@
{% endif %} {% endif %}
{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %} {% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %}
{% if matomo_id > 0 %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
{% if is_404 %}
_paq.push([
'setDocumentTitle',
'404 / Not Found - ' + document.location.pathname
]);
{% endif %}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "//{{ matomo_url|trim('/') }}/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', {{ matomo_id }}]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</body></html> </body></html>

View File

@@ -1,3 +1,4 @@
{% set is_404 = status_code is defined and status_code == 404 %}
{% extends '@novaconium/master.html.twig' %} {% extends '@novaconium/master.html.twig' %}
{% block content %} {% block content %}

View File

@@ -0,0 +1,32 @@
{% extends '@novaconium/coming-soon/index.html.twig' %}
{% set bg_image = "https://i.4lt.ca/4lt/waterBubbles.webp" %}
{% set default_launch = "now"|date_modify("+30 days")|date("Y-m-d\TH:i:s") %}
{% block content %}
<p>Our website is under construction. Were working hard to bring you a better experience.</p>
<p>Stay tuned for updates!</p>
{% endblock %}
{% block social %}
<a href="mailto:you@example.com" title="Email">
<i class="fa-solid fa-envelope"></i>
</a>
<a href="tel:+1234567890" title="Phone">
<i class="fa-solid fa-phone"></i>
</a>
<a href="#" title="Twitter">
<i class="fab fa-twitter"></i>
</a>
<a href="#" title="Facebook">
<i class="fab fa-facebook-f"></i>
</a>
<a href="#" title="Instagram">
<i class="fab fa-instagram"></i>
</a>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %} {% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %} {% block content %}
<h1>{{title}}</h1> <h1>{{title}}</h1>

View File

@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %} {% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %} {% block content %}
<h2>Edit Page - {{ title }}</h2> <h2>Edit Page - {{ title }}</h2>

View File

@@ -14,3 +14,16 @@
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #} <div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
</div> </div>
</div> </div>
<div class="form-group">
<label for="draft">
<input
type="checkbox"
id="draft"
name="draft"
value="1"
{% if rows.draft|default(false) %}checked{% endif %}
>
Save as draft
</label>
</div>

View File

@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %} {% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %} {% block content %}
<h1>{{title}}</h1> <h1>{{title}}</h1>

View File

@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %} {% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %} {% block content %}
<h1>{{title}}</h1> <h1>{{title}}</h1>

6
views/settings.html.twig Normal file
View File

@@ -0,0 +1,6 @@
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
<p>Settings will go here.</p>
{% endblock %}