This commit is contained in:
count-null 2025-03-06 17:23:18 -05:00
parent 1ed14b5549
commit 642546040a
13 changed files with 406 additions and 20 deletions

View file

@ -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),

View file

@ -1,6 +1,7 @@
<?php
namespace app\controllers;
use app\models\categories;
use app\models\emails;
use app\models\transactions;
use app\models\users;
@ -39,19 +40,119 @@ class admin
]));
}
public static function orders($defaults)
public static function products($defaults)
{
echo $GLOBALS['twig']->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',
],
],
]));

View file

@ -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'];

96
src/models/categories.php Normal file
View file

@ -0,0 +1,96 @@
<?php
namespace app\models;
use app\app;
class categories
{
public static function init()
{
app::$db->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;
}
}

View file

@ -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();

View file

@ -0,0 +1,9 @@
<section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %}
<form action="/admin/categories/add" method="POST" class="flex flex-col gap-4">
{% include 'lib/forms/category.twig' %}
{% include 'lib/buttons/submit.twig' with {
label: 'Add Category'
} %}
</form>
</section>

View file

@ -0,0 +1,12 @@
{% include 'lib/alert.twig' %}
<form action="/admin/categories/edit/{{ category.id }}" method="post" class="flex flex-col gap-2">
{% 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'
} %}
</form>

View file

@ -0,0 +1,70 @@
<section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %}
<h3 class="text-2xl font-semibold">
Categories
</h3>
<a href="/admin/categories/add">
{% include 'lib/buttons/primary.twig' with {
label: 'Create a Category'
} %}
</a>
<table class="min-w-full">
<tr>
<th class="py-2">
Category ID
</th>
<th class="py-2">
Title
</th>
<th class="py-2">
Slug
</th>
<th class="py-2">
Action
</th>
</tr>
</thead>
<tbody>
{% macro renderCategory(category, depth) %}
<tr>
<td class="border px-4 py-2">
{{ category.id }}
</td>
<td class="border px-4 py-2">
{% if depth > 0 %}
{% for i in 1..depth %}
-
{% endfor %}
{% endif %}
{{ category.label }}
</td>
<td class="border px-4 py-2">
{{ category.value }}
</td>
<td class="border px-4 py-2">
<a href="/admin/categories/edit/{{ category.id }}">
Edit
</a>
</td>
</tr>
{% 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 %}
<tr>
<td class="border px-4 py-2" colspan="4">
No categories yet.
</td>
</tr>
{% endif %}
</tbody>
</table></section>

View file

@ -5,6 +5,12 @@
<a href="/admin/users">
Users
</a>
<a href="/admin/products">
Products
</a>
<a href="/admin/categories">
Categories
</a>
<a href="/admin/orders">
Orders
</a>

View file

@ -0,0 +1,62 @@
<section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %}
<h3 class="text-2xl font-semibold">
Categories
</h3>
<a href="/admin/categories/add">
{% include 'lib/buttons/primary.twig' with {
label: 'Create a Category'
} %}
</a>
<table class="min-w-full">
<tr>
<th class="py-2">
Category ID
</th>
<th class="py-2">
Title
</th>
<th class="py-2">
Slug
</th>
</tr>
</thead>
<tbody>
{% macro renderCategory(category, depth) %}
<tr>
<td class="border px-4 py-2">
{{ category.id }}
</td>
<td class="border px-4 py-2">
{% if depth > 0 %}
{% for i in 1..depth %}
-
{% endfor %}
{% endif %}
{{ category.label }}
</td>
<td class="border px-4 py-2">
{{ category.value }}
</td>
</tr>
{% 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 %}
<tr>
<td class="border px-4 py-2" colspan="3">
No categories yet.
</td>
</tr>
{% endif %}
</tbody>
</table></section>

View file

@ -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
} %}

View file

@ -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}
} %}

View file

@ -2,9 +2,14 @@
{{ label }}
</label>
<select id="{{ id }}" name="{{ name }}" class="border rounded-lg p-2 w-full" {% if required %} required {% endif %}>
{% if no_option is defined %}
<option value="{{ no_option.value }}">
{{ no_option.label }}
</option>
{% endif %}
{% for option in options %}
<option value="{{ option.value }}" {% if option.value == value %} selected {% endif %}>
{{ option.text }}
{{ option.label }}
</option>
{% endfor %}
</select>