diff --git a/public/index.php b/public/index.php
index e3ab58c..b11557f 100644
--- a/public/index.php
+++ b/public/index.php
@@ -75,18 +75,19 @@ if (str_starts_with(haystack: $route, needle: '/.well-known/lnurlp/')) {
}
// Use this controller for routes that include a model ID
-if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|product|subscription|cart)\/([\w-]+)$/', $route, $matches)) {
+if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|product|subscription|cart|admin\/categories\/edit)\/([\w-]+)$/', $route, $matches)) {
[$full, $type, $id] = $matches;
$controller = [
- 'address/edit' => fn($id) => address::edit($defaults, $id),
- 'address/delete' => fn($id) => address::delete($defaults, $id),
- 'transaction' => fn($id) => transaction::view($defaults, $id),
- 'user' => fn($id) => users::view($id),
- 'order' => fn($id) => orders::view($id),
- 'quote' => fn($id) => quotes::view($id),
- 'product' => fn($id) => products::view($id),
- 'subscription' => fn($id) => subscriptions::view($id),
- 'cart' => fn($id) => cart::index($id),
+ 'address/edit' => fn($id) => address::edit($defaults, $id),
+ 'address/delete' => fn($id) => address::delete($defaults, $id),
+ 'transaction' => fn($id) => transaction::view($defaults, $id),
+ 'user' => fn($id) => users::view($id),
+ 'order' => fn($id) => orders::view($id),
+ 'quote' => fn($id) => quotes::view($id),
+ 'product' => fn($id) => products::view($id),
+ 'subscription' => fn($id) => subscriptions::view($id),
+ 'cart' => fn($id) => cart::index($id),
+ 'admin/categories/edit' => fn($id) => $defaults['is_admin'] ? admin::categories_edit($defaults, $id) : lost::index($defaults),
];
if (isset($controller[$type])) {
@@ -112,8 +113,11 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
'/account/address/set-default-billing' => $defaults['is_user'] ? account::set_default_billing($defaults) : header('Location: /account/login'),
'/admin' => $defaults['is_admin'] ? admin::index($defaults) : lost::index($defaults),
'/admin/users' => $defaults['is_admin'] ? admin::users($defaults) : lost::index($defaults),
+ '/admin/products' => $defaults['is_admin'] ? admin::products($defaults) : lost::index($defaults),
'/admin/orders' => $defaults['is_admin'] ? admin::orders($defaults) : lost::index($defaults),
'/admin/emails' => $defaults['is_admin'] ? admin::emails($defaults) : lost::index($defaults),
+ '/admin/categories' => $defaults['is_admin'] ? admin::categories($defaults) : lost::index($defaults),
+ '/admin/categories/add' => $defaults['is_admin'] ? admin::categories_add($defaults) : lost::index($defaults),
'/admin/transactions' => $defaults['is_admin'] ? admin::transactions($defaults) : lost::index($defaults),
'/admin/transactions/add' => $defaults['is_admin'] ? admin::transactions_add($defaults) : lost::index($defaults),
'/admin/transactions/reset' => $defaults['is_admin'] ? admin::transactions_reset($defaults) : lost::index($defaults),
diff --git a/src/controllers/admin.php b/src/controllers/admin.php
index f94c1c1..bd8a793 100644
--- a/src/controllers/admin.php
+++ b/src/controllers/admin.php
@@ -1,6 +1,7 @@
render('lib/pages/index.twig', array_merge($defaults, [
- 'child_template' => 'admin/orders.twig',
- 'page_title' => 'Orders',
+ 'child_template' => 'admin/products/index.twig',
+ 'page_title' => 'Products',
+ 'categories' => categories::getTree(),
'breadcrumbs' => [
[
'url' => '/admin',
'title' => 'Admin',
],
[
- 'url' => '/admin/orders',
- 'title' => 'Orders',
+ 'url' => '/admin/products',
+ 'title' => 'Products',
+ ],
+ ],
+ ]));
+ }
+
+ public static function categories($defaults)
+ {
+ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
+ 'child_template' => 'admin/categories/index.twig',
+ 'page_title' => 'Categories',
+ 'categories' => categories::getTree(),
+ 'breadcrumbs' => [
+ [
+ 'url' => '/admin',
+ 'title' => 'Admin',
+ ],
+ [
+ 'url' => '/admin/categories',
+ 'title' => 'Categories',
+ ],
+ ],
+ ]));
+ }
+
+ public static function categories_add($defaults)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $parent_id = $_POST['parent_id'] ?? null;
+ $label = $_POST['label'] ?? null;
+ $value = $_POST['value'] ?? null;
+ if (! $label || ! $value) {
+ $_SESSION['error'] = 'Title and slug are required fields.';
+ header('Location: /admin/categories/add');
+ exit;
+ }
+ categories::add($label, $value, $parent_id);
+ $_SESSION['success'] = 'Category added successfully.';
+ header('Location: /admin/categories/add');
+ exit;
+ }
+ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
+ 'child_template' => 'admin/categories/add.twig',
+ 'page_title' => 'Add a Category',
+ 'categories' => categories::getOptions(),
+ 'breadcrumbs' => [
+ [
+ 'url' => '/admin',
+ 'title' => 'Admin',
+ ],
+ [
+ 'url' => '/admin/products',
+ 'title' => 'Products',
+ ],
+ [
+ 'url' => null,
+ 'title' => 'Add Category',
+ ],
+ ],
+ ]));
+ }
+
+ public static function categories_edit($defaults, $cat_id)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $parent_id = $_POST['parent_id'] ?? null;
+ $label = $_POST['label'] ?? null;
+ $value = $_POST['value'] ?? null;
+ if (! $label || ! $value) {
+ $_SESSION['error'] = 'Title and slug are required fields.';
+ header('Location: /admin/categories/edit/' . $cat_id);
+ exit;
+ }
+ if ($cat_id == $parent_id) {
+ $_SESSION['error'] = 'A category cannot be its own parent.';
+ header('Location: /admin/categories/edit/' . $cat_id);
+ exit;
+ }
+ categories::updateById($cat_id, $label, $value, $parent_id);
+ $_SESSION['success'] = 'Category updated successfully.';
+ header('Location: /admin/categories/edit/' . $cat_id);
+ exit;
+ }
+ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
+ 'child_template' => 'admin/categories/edit.twig',
+ 'page_title' => 'Edit Category',
+ 'category' => categories::getById($cat_id),
+ 'categories' => categories::getOptions(),
+ 'breadcrumbs' => [
+ [
+ 'url' => '/admin',
+ 'title' => 'Admin',
+ ],
+ [
+ 'url' => '/admin/categories',
+ 'title' => 'Categories',
+ ],
+ [
+ 'url' => null,
+ 'title' => 'Edit',
],
],
]));
diff --git a/src/controllers/magic_link.php b/src/controllers/magic_link.php
index 95add7c..cab07dd 100644
--- a/src/controllers/magic_link.php
+++ b/src/controllers/magic_link.php
@@ -21,7 +21,7 @@ class magic_link
$user = $link['user_id'] ? users::getById($link['user_id']) : users::getByEmail($link['email']);
if ($user) { // this email is registered or associated with a user
$user_replacing = users::getByReplaceEmailToken($link['token']);
- if ($user_replacing) { // user is replacing their email
+ if ($user_replacing && $user_replacing['id'] == $link['user_id']) { // user is replacing their email
users::updateEmailById($user_replacing['id'], $link['email']);
$_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user_replacing['id'];
@@ -66,7 +66,7 @@ class magic_link
$user = $link['user_id'] ? users::getById($link['user_id']) : users::getByEmail($link['email']);
if ($user) { // this email is registered or is associated with a user
$user_replacing = users::getByReplaceEmailToken($token);
- if ($user_replacing) { // user is replacing their email
+ if ($user_replacing && $user_replacing['id'] == $link['user_id']) { // user is replacing their email
users::updateEmailById($user_replacing['id'], $link['email']);
$_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user_replacing['id'];
diff --git a/src/models/categories.php b/src/models/categories.php
new file mode 100644
index 0000000..5d30a96
--- /dev/null
+++ b/src/models/categories.php
@@ -0,0 +1,96 @@
+exec("CREATE TABLE IF NOT EXISTS categories (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ parent_id INTEGER,
+ label TEXT UNIQUE NOT NULL,
+ value TEXT UNIQUE NOT NULL,
+ FOREIGN KEY (parent_id) REFERENCES categories(id)
+ );");
+ }
+
+ public static function add($label, $value, $parent_id = null): string
+ {
+ $stmt = app::$db->prepare("INSERT INTO categories (label, value, parent_id) VALUES (:label, :value, :parent_id)");
+ $stmt->bindParam(':label', $label);
+ $stmt->bindParam(':value', $value);
+ $stmt->bindParam(':parent_id', $parent_id);
+ $stmt->execute();
+ return app::$db->lastInsertId();
+ }
+
+ public static function updateById($id, $label, $value, $parent_id = null)
+ {
+ $stmt = app::$db->prepare("UPDATE categories SET label = :label, value = :value, parent_id = :parent_id WHERE id = :id");
+ $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
+ $stmt->bindParam(':label', $label);
+ $stmt->bindParam(':value', $value);
+ $stmt->bindParam(':parent_id', $parent_id);
+ return $stmt->execute();
+ }
+
+ public static function getById($id)
+ {
+ $stmt = app::$db->prepare("SELECT * FROM categories WHERE id = :id");
+ $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
+ }
+
+ public static function getTree(): array
+ {
+ $stmt = app::$db->query("SELECT * FROM categories");
+ $categories = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ $tree = [];
+ $map = [];
+ foreach ($categories as &$category) {
+ $category['children'] = [];
+ $map[$category['id']] = &$category;
+ }
+ foreach ($categories as &$category) {
+ if (! $category['parent_id']) {
+ $tree[] = &$category;
+ } else {
+ if (isset($map[$category['parent_id']])) {
+ $map[$category['parent_id']]['children'][] = &$category;
+ }
+ }
+ }
+ return $tree;
+ }
+
+ public static function getOptions(): array
+ {
+ $stmt = app::$db->query("SELECT * FROM categories");
+ $categories = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ $flattened = [];
+ foreach ($categories as $category) {
+ $depth = 0;
+ $current = $category;
+ while ($current['parent_id'] !== null) {
+ $depth++;
+ $current = array_filter($categories, function ($cat) use ($current) {
+ return $cat['id'] === $current['parent_id'];
+ });
+ $current = reset($current);
+ if (! $current) {
+ break;
+ }
+ }
+ $flattened[] = [
+ 'label' => str_repeat('-', $depth) . ' ' . $category['label'],
+ 'value' => $category['id'],
+ ];
+ }
+ return $flattened;
+ }
+
+}
diff --git a/src/scripts/init_db.php b/src/scripts/init_db.php
index e9bb9d2..b0996d5 100644
--- a/src/scripts/init_db.php
+++ b/src/scripts/init_db.php
@@ -16,6 +16,7 @@ app::init_db();
use app\models\addresses;
use app\models\carts;
use app\models\cart_items;
+use app\models\categories;
use app\models\emails;
use app\models\invoices;
use app\models\magic_links;
@@ -33,6 +34,7 @@ use app\models\user_settings;
addresses::init();
carts::init();
cart_items::init();
+categories::init();
emails::init();
invoices::init();
magic_links::init();
diff --git a/src/views/admin/categories/add.twig b/src/views/admin/categories/add.twig
new file mode 100644
index 0000000..c80397d
--- /dev/null
+++ b/src/views/admin/categories/add.twig
@@ -0,0 +1,9 @@
+
+ {% include 'lib/alert.twig' %}
+
+
diff --git a/src/views/admin/categories/edit.twig b/src/views/admin/categories/edit.twig
new file mode 100644
index 0000000..db01eeb
--- /dev/null
+++ b/src/views/admin/categories/edit.twig
@@ -0,0 +1,12 @@
+{% include 'lib/alert.twig' %}
+
+
diff --git a/src/views/admin/categories/index.twig b/src/views/admin/categories/index.twig
new file mode 100644
index 0000000..d695586
--- /dev/null
+++ b/src/views/admin/categories/index.twig
@@ -0,0 +1,70 @@
+
+ {% include 'lib/alert.twig' %}
+
+
+ Categories
+
+
+ {% include 'lib/buttons/primary.twig' with {
+ label: 'Create a Category'
+ } %}
+
+
+
+
+ Category ID
+ |
+
+ Title
+ |
+
+ Slug
+ |
+
+ Action
+ |
+
+
+
+ {% macro renderCategory(category, depth) %}
+
+
+ {{ category.id }}
+ |
+
+ {% if depth > 0 %}
+ {% for i in 1..depth %}
+ -
+ {% endfor %}
+ {% endif %}
+ {{ category.label }}
+ |
+
+ {{ category.value }}
+ |
+
+
+ Edit
+
+ |
+
+ {% if category.children is defined and category.children is not empty %}
+ {% for child in category.children %}
+ {{ _self.renderCategory(child, depth + 1) }}
+ {% endfor %}
+ {% endif %}
+ {% endmacro %}
+
+ {% if categories %}
+ {% for category in categories %}
+ {{ _self.renderCategory(category, 0) }}
+ {% endfor %}
+ {% else %}
+
+
+ No categories yet.
+ |
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/src/views/admin/index.twig b/src/views/admin/index.twig
index 7f6a083..a8d22e6 100644
--- a/src/views/admin/index.twig
+++ b/src/views/admin/index.twig
@@ -5,6 +5,12 @@
Users
+
+ Products
+
+
+ Categories
+
Orders
diff --git a/src/views/admin/products/index.twig b/src/views/admin/products/index.twig
new file mode 100644
index 0000000..90e213e
--- /dev/null
+++ b/src/views/admin/products/index.twig
@@ -0,0 +1,62 @@
+
+ {% include 'lib/alert.twig' %}
+
+
+ Categories
+
+
+ {% include 'lib/buttons/primary.twig' with {
+ label: 'Create a Category'
+ } %}
+
+
+
+
+ Category ID
+ |
+
+ Title
+ |
+
+ Slug
+ |
+
+
+
+ {% macro renderCategory(category, depth) %}
+
+
+ {{ category.id }}
+ |
+
+ {% if depth > 0 %}
+ {% for i in 1..depth %}
+ -
+ {% endfor %}
+ {% endif %}
+ {{ category.label }}
+ |
+
+ {{ category.value }}
+ |
+
+ {% if category.children is defined and category.children is not empty %}
+ {% for child in category.children %}
+ {{ _self.renderCategory(child, depth + 1) }}
+ {% endfor %}
+ {% endif %}
+ {% endmacro %}
+
+ {% if categories is not empty %}
+ {% for category in categories %}
+ {{ _self.renderCategory(category, 0) }}
+ {% endfor %}
+ {% else %}
+
+
+ No categories yet.
+ |
+
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/src/views/admin/transactions/add.twig b/src/views/admin/transactions/add.twig
index 309ea35..1662a9f 100644
--- a/src/views/admin/transactions/add.twig
+++ b/src/views/admin/transactions/add.twig
@@ -13,8 +13,8 @@
name: 'currency',
label: 'Currency',
options: [
- { 'value': 'sats', 'text': 'Sats' },
- { 'value': 'cents', 'text': 'Cents' }
+ { 'value': 'sats', 'label': 'Sats' },
+ { 'value': 'cents', 'label': 'Cents' }
],
required: true
} %}
diff --git a/src/views/lib/forms/category.twig b/src/views/lib/forms/category.twig
new file mode 100644
index 0000000..6a55b3e
--- /dev/null
+++ b/src/views/lib/forms/category.twig
@@ -0,0 +1,19 @@
+{% include 'lib/inputs/text.twig' with {
+ name: 'label',
+ label: 'Category Title',
+ placeholder: 'Enter category title',
+ value: label
+} %}
+{% include 'lib/inputs/text.twig' with {
+ name: 'value',
+ label: 'Category Slug',
+ placeholder: 'Enter category slug',
+ value: value
+} %}
+{% include 'lib/inputs/select.twig' with {
+ name: 'parent_id',
+ label: 'Parent Category',
+ options: categories,
+ value: parent_id,
+ no_option: { 'label': '-- None --', 'value': null}
+} %}
diff --git a/src/views/lib/inputs/select.twig b/src/views/lib/inputs/select.twig
index c1a867d..65a697b 100644
--- a/src/views/lib/inputs/select.twig
+++ b/src/views/lib/inputs/select.twig
@@ -2,9 +2,14 @@
{{ label }}