save
This commit is contained in:
parent
1ed14b5549
commit
642546040a
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
],
|
||||
]));
|
||||
|
|
|
@ -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
96
src/models/categories.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
9
src/views/admin/categories/add.twig
Normal file
9
src/views/admin/categories/add.twig
Normal 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>
|
12
src/views/admin/categories/edit.twig
Normal file
12
src/views/admin/categories/edit.twig
Normal 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>
|
70
src/views/admin/categories/index.twig
Normal file
70
src/views/admin/categories/index.twig
Normal 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>
|
|
@ -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>
|
||||
|
|
62
src/views/admin/products/index.twig
Normal file
62
src/views/admin/products/index.twig
Normal 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>
|
|
@ -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
|
||||
} %}
|
||||
|
|
19
src/views/lib/forms/category.twig
Normal file
19
src/views/lib/forms/category.twig
Normal 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}
|
||||
} %}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue