From 642546040af31c8d44a0dce73d10159a2d151750 Mon Sep 17 00:00:00 2001 From: count-null <70529195+count-null@users.noreply.github.com> Date: Thu, 6 Mar 2025 17:23:18 -0500 Subject: [PATCH] save --- public/index.php | 24 +++--- src/controllers/admin.php | 111 ++++++++++++++++++++++++-- src/controllers/magic_link.php | 4 +- src/models/categories.php | 96 ++++++++++++++++++++++ src/scripts/init_db.php | 2 + src/views/admin/categories/add.twig | 9 +++ src/views/admin/categories/edit.twig | 12 +++ src/views/admin/categories/index.twig | 70 ++++++++++++++++ src/views/admin/index.twig | 6 ++ src/views/admin/products/index.twig | 62 ++++++++++++++ src/views/admin/transactions/add.twig | 4 +- src/views/lib/forms/category.twig | 19 +++++ src/views/lib/inputs/select.twig | 7 +- 13 files changed, 406 insertions(+), 20 deletions(-) create mode 100644 src/models/categories.php create mode 100644 src/views/admin/categories/add.twig create mode 100644 src/views/admin/categories/edit.twig create mode 100644 src/views/admin/categories/index.twig create mode 100644 src/views/admin/products/index.twig create mode 100644 src/views/lib/forms/category.twig 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' %} +
+ {% include 'lib/forms/category.twig' %} + {% include 'lib/buttons/submit.twig' with { + label: 'Add Category' + } %} +
+
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' %} + +
+ {% include 'lib/forms/category.twig' with { + value: category.value, + label: category.label, + parent_id: category.parent_id + } %} + {% include 'lib/buttons/submit.twig' with { + label: 'Save Category' + } %} +
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' + } %} + + + + + + + + + + + {% macro renderCategory(category, depth) %} + + + + + + + {% 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 %} + + + + {% endif %} + +
+ Category ID + + Title + + Slug + + Action +
+ {{ category.id }} + + {% if depth > 0 %} + {% for i in 1..depth %} + - + {% endfor %} + {% endif %} + {{ category.label }} + + {{ category.value }} + + + Edit + +
+ No categories yet. +
\ 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' + } %} + + + + + + + + + + {% macro renderCategory(category, depth) %} + + + + + + {% 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 %} + + + + {% endif %} + +
+ Category ID + + Title + + Slug +
+ {{ category.id }} + + {% if depth > 0 %} + {% for i in 1..depth %} + - + {% endfor %} + {% endif %} + {{ category.label }} + + {{ category.value }} +
+ No categories yet. +
\ 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 }}