Compare commits
17 Commits
1.0.7
...
refactor_r
| Author | SHA1 | Date | |
|---|---|---|---|
| 230476e8e5 | |||
| d5871ed8e7 | |||
| ad6e07c76d | |||
| 06310d9eb9 | |||
| f679d0b20e | |||
| 9feccf9eaa | |||
| 14ec6b7e7a | |||
| 42a828a778 | |||
| 81c239f043 | |||
| 0d91b61bd0 | |||
| fd9a71c4d6 | |||
| 71413283c8 | |||
| a32956b4a2 | |||
| 7bf3dc6610 | |||
| 7b064eb6da | |||
| 934e134941 | |||
| 6f410b5d0e |
21
README.md
21
README.md
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
11
controllers/auth/login.php
Normal file
11
controllers/auth/login.php
Normal 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');
|
||||||
9
controllers/coming-soon.php
Normal file
9
controllers/coming-soon.php
Normal 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);
|
||||||
@@ -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'))) {
|
||||||
|
|||||||
@@ -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'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
|
||||||
@@ -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'))) {
|
||||||
|
|||||||
@@ -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
15
controllers/settings.php
Normal 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);
|
||||||
@@ -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
101
docs/Router.md
Normal 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'];
|
||||||
|
```
|
||||||
10
docs/Sass.md
10
docs/Sass.md
@@ -20,4 +20,14 @@ Compressed:
|
|||||||
# Build Novaconium (compressed)
|
# 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
|
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
|
||||||
```
|
```
|
||||||
94
sass/coming-soon/index.sass
Normal file
94
sass/coming-soon/index.sass
Normal 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
|
||||||
4
sass/controlPanel/_footer.sass
Normal file
4
sass/controlPanel/_footer.sass
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
body#controlPanel
|
||||||
|
|
||||||
|
footer
|
||||||
|
padding: 1rem
|
||||||
8
sass/controlPanel/_header.sass
Normal file
8
sass/controlPanel/_header.sass
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
body#controlPanel
|
||||||
|
|
||||||
|
header
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
h1#biglogo
|
||||||
|
padding: 10px 0 0 0
|
||||||
|
margin: 0
|
||||||
11
sass/controlPanel/_main.sass
Normal file
11
sass/controlPanel/_main.sass
Normal 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
|
||||||
|
|
||||||
22
sass/controlPanel/_menu.sass
Normal file
22
sass/controlPanel/_menu.sass
Normal 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
|
||||||
5
sass/controlPanel/_panel.sass
Normal file
5
sass/controlPanel/_panel.sass
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
body#controlPanel
|
||||||
|
|
||||||
|
div#panel
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
10
sass/controlPanel/index.sass
Normal file
10
sass/controlPanel/index.sass
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 *
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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'
|
||||||
];
|
];
|
||||||
|
|||||||
28
skeleton/novaconium/App/controllers/humans.php
Normal file
28
skeleton/novaconium/App/controllers/humans.php
Normal 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;
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
$data = array_merge($data, [
|
||||||
|
'title' => 'Welcome to Novaconium Index Page',
|
||||||
|
'pageclass' => 'novaconium'
|
||||||
|
]);
|
||||||
|
|
||||||
view('index');
|
view('index');
|
||||||
|
|||||||
33
skeleton/novaconium/App/controllers/page.php
Normal file
33
skeleton/novaconium/App/controllers/page.php
Normal 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);
|
||||||
18
skeleton/novaconium/App/controllers/robots.php
Normal file
18
skeleton/novaconium/App/controllers/robots.php
Normal 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;
|
||||||
@@ -2,5 +2,14 @@
|
|||||||
$routes = [
|
$routes = [
|
||||||
'/' => [
|
'/' => [
|
||||||
'get' => 'index'
|
'get' => 'index'
|
||||||
|
],
|
||||||
|
'/robots.txt' => [
|
||||||
|
'get' => 'robots'
|
||||||
|
],
|
||||||
|
'/humans.txt' => [
|
||||||
|
'get' => 'humans'
|
||||||
|
],
|
||||||
|
'/page/{slug}' => [
|
||||||
|
'get' => 'page'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
11
skeleton/novaconium/App/views/page.html.twig
Normal file
11
skeleton/novaconium/App/views/page.html.twig
Normal 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
@@ -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"}
|
|
||||||
@@ -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';
|
||||||
|
|||||||
185
skeleton/novaconium/public/js/tabs.js
Normal file
185
skeleton/novaconium/public/js/tabs.js
Normal 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">×</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;
|
||||||
216
src/Router.php
216
src/Router.php
@@ -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() {
|
||||||
|
if (array_key_exists($this->path, $this->routes)
|
||||||
|
&& !empty($this->routes[$this->path][$this->requestType])
|
||||||
|
) {
|
||||||
|
return $this->routes[$this->path][$this->requestType];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop over parameterized routes (those containing "{") looking for the
|
||||||
|
* first one whose static prefix matches the path AND whose segment count
|
||||||
|
* matches. As soon as such a route is found, control is handed off to
|
||||||
|
* 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() {
|
private function setRouteFile() {
|
||||||
|
|
||||||
if (str_starts_with($this->controller, 'NOVACONIUM')) {
|
if (str_starts_with($this->controller, 'NOVACONIUM')) {
|
||||||
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
|
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
|
||||||
$cp = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
|
$controllerPath = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
|
||||||
} else {
|
} else {
|
||||||
$cp = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
|
$controllerPath = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_exists($cp)) {
|
if (file_exists($controllerPath)) {
|
||||||
return $cp;
|
return $controllerPath;
|
||||||
} else {
|
|
||||||
//Check if 404 exits
|
|
||||||
if (file_exists( \BASEPATH . '/App/controllers/404.php')) {
|
|
||||||
return \BASEPATH . '/App/controllers/404.php';
|
|
||||||
} else {
|
|
||||||
return \FRAMEWORKPATH . '/controllers/404.php';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
@@ -134,9 +200,7 @@ class Router {
|
|||||||
echo '<tr><th>Parameters</th><td><pre>' . print_r($this->parameters, true) . '</pre></td></tr>';
|
echo '<tr><th>Parameters</th><td><pre>' . print_r($this->parameters, true) . '</pre></td></tr>';
|
||||||
echo '<tr><th>Routes</th><td><pre>' . print_r($this->routes, true) . '</pre></td></tr>';
|
echo '<tr><th>Routes</th><td><pre>' . print_r($this->routes, true) . '</pre></td></tr>';
|
||||||
echo '</table></div>';
|
echo '</table></div>';
|
||||||
|
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
71
twig/coming-soon/index.html.twig
Normal file
71
twig/coming-soon/index.html.twig
Normal 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>
|
||||||
44
twig/cp/control-panel.html.twig
Normal file
44
twig/cp/control-panel.html.twig
Normal 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">© {{ '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
60
twig/cp/head.html.twig
Normal 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
9
twig/cp/menu.html.twig
Normal 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>
|
||||||
@@ -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 %}
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
<!--
|
<div class="copyright">© {{ 'now' | date('Y') }} Novaconium</div>
|
||||||
What goes in the footer html tag
|
|
||||||
-->
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
32
views/coming-soon.html.twig
Normal file
32
views/coming-soon.html.twig
Normal 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. We’re 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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -13,4 +13,17 @@
|
|||||||
<textarea id="body" name="body" rows="10" style="display: none;">{{ rows.body|default('')|e('html') }}</textarea>
|
<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 id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
|
||||||
</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>
|
</div>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
6
views/settings.html.twig
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{% extends '@novaconium/cp/control-panel.html.twig' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<p>Settings will go here.</p>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user