save
This commit is contained in:
parent
642546040a
commit
4eb1d59230
|
@ -75,7 +75,7 @@ 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|admin\/categories\/edit)\/([\w-]+)$/', $route, $matches)) {
|
||||
if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|product|subscription|cart|admin\/categories\/edit|admin\/products\/edit)\/([\w-]+)$/', $route, $matches)) {
|
||||
[$full, $type, $id] = $matches;
|
||||
$controller = [
|
||||
'address/edit' => fn($id) => address::edit($defaults, $id),
|
||||
|
@ -88,6 +88,7 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
|
|||
'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),
|
||||
'admin/products/edit' => fn($id) => $defaults['is_admin'] ? admin::products_edit($defaults, $id) : lost::index($defaults),
|
||||
];
|
||||
|
||||
if (isset($controller[$type])) {
|
||||
|
@ -114,6 +115,7 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
|
|||
'/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/products/add' => $defaults['is_admin'] ? admin::products_add($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),
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace app\controllers;
|
|||
|
||||
use app\models\categories;
|
||||
use app\models\emails;
|
||||
use app\models\products;
|
||||
use app\models\transactions;
|
||||
use app\models\users;
|
||||
|
||||
|
@ -27,6 +28,7 @@ class admin
|
|||
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||
'child_template' => 'admin/users.twig',
|
||||
'page_title' => 'Users',
|
||||
'users' => users::getUsers(20),
|
||||
'breadcrumbs' => [
|
||||
[
|
||||
'url' => '/admin',
|
||||
|
@ -46,6 +48,7 @@ class admin
|
|||
'child_template' => 'admin/products/index.twig',
|
||||
'page_title' => 'Products',
|
||||
'categories' => categories::getTree(),
|
||||
'products' => products::get(20),
|
||||
'breadcrumbs' => [
|
||||
[
|
||||
'url' => '/admin',
|
||||
|
@ -59,6 +62,84 @@ class admin
|
|||
]));
|
||||
}
|
||||
|
||||
public static function products_add($defaults)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
$valid = products::validateProductData($_POST);
|
||||
if (! empty($valid['errors'])) {
|
||||
$_SESSION['error'] = implode(' ', $valid['errors']);
|
||||
header('Location: /admin/products/add');
|
||||
exit;
|
||||
}
|
||||
products::add($valid);
|
||||
$_SESSION['success'] = 'Product added successfully.';
|
||||
unset($_SESSION['last_post']);
|
||||
header('Location: /admin/products/add');
|
||||
exit;
|
||||
}
|
||||
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||
'child_template' => 'admin/products/add.twig',
|
||||
'page_title' => 'Add a Product',
|
||||
'categories' => categories::getOptions(),
|
||||
'breadcrumbs' => [
|
||||
[
|
||||
'url' => '/admin',
|
||||
'title' => 'Admin',
|
||||
],
|
||||
[
|
||||
'url' => '/admin/products',
|
||||
'title' => 'Products',
|
||||
],
|
||||
[
|
||||
'url' => '/admin/products/add',
|
||||
'title' => 'Add Product',
|
||||
],
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function products_edit($defaults, $id)
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$_SESSION['last_post'] = $_POST;
|
||||
$valid = products::validateProductData($_POST);
|
||||
|
||||
if (! empty($valid['errors'])) {
|
||||
$_SESSION['error'] = implode(' ', $valid['errors']);
|
||||
header("Location: /admin/products/edit/$id");
|
||||
exit;
|
||||
}
|
||||
products::update($id, $valid);
|
||||
$_SESSION['success'] = 'Product updated successfully.';
|
||||
unset($_SESSION['last_post']);
|
||||
header("Location: /admin/products/edit/$id");
|
||||
exit;
|
||||
}
|
||||
$product = products::getById($id);
|
||||
|
||||
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||
'child_template' => 'admin/products/edit.twig',
|
||||
'page_title' => 'Edit Product',
|
||||
'product' => $product,
|
||||
'categories' => categories::getOptions(),
|
||||
'breadcrumbs' => [
|
||||
[
|
||||
'url' => '/admin',
|
||||
'title' => 'Admin',
|
||||
],
|
||||
[
|
||||
'url' => '/admin/products',
|
||||
'title' => 'Products',
|
||||
],
|
||||
[
|
||||
'url' => "/admin/products/edit/$id",
|
||||
'title' => 'Edit Product',
|
||||
],
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public static function categories($defaults)
|
||||
{
|
||||
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
|
||||
|
|
|
@ -9,7 +9,7 @@ class addresses
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS addresses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
company TEXT,
|
||||
addressLine1 TEXT NOT NULL,
|
||||
|
@ -17,8 +17,7 @@ class addresses
|
|||
city TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
zip TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
phone TEXT
|
||||
)");
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,10 @@ class cart_items
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS cart_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cart_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
cart_id INTEGER NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,9 @@ class carts
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS carts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
short_id TEXT UNIQUE NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,9 @@ class categories
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_id INTEGER,
|
||||
parent_id INTEGER REFERENCES categories(id),
|
||||
label TEXT UNIQUE NOT NULL,
|
||||
value TEXT UNIQUE NOT NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES categories(id)
|
||||
value TEXT UNIQUE NOT NULL
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -11,15 +11,14 @@ class emails
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS emails (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
from_email TEXT NOT NULL,
|
||||
from_name TEXT NOT NULL,
|
||||
to_email TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
html_message TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,13 @@ class invoices
|
|||
{
|
||||
$query = "CREATE TABLE IF NOT EXISTS invoices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
order_id INTEGER,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
order_id INTEGER REFERENCES orders(id),
|
||||
invoice TEXT NOT NULL,
|
||||
verify TEXT NOT NULL,
|
||||
amount_msats REAL NOT NULL,
|
||||
expiry_date DATETIME NOT NULL,
|
||||
settled BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||
FOREIGN KEY(order_id) REFERENCES orders(id)
|
||||
settled BOOLEAN DEFAULT FALSE
|
||||
)";
|
||||
app::$db->exec($query);
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@ class order_items
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS order_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,11 @@ class orders
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
value_sats INTEGER NOT NULL CHECK(value_sats >= 0),
|
||||
value_cents INTEGER NOT NULL CHECK(value_cents >= 0),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING'
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -7,64 +7,295 @@ class products
|
|||
{
|
||||
public static function init()
|
||||
{
|
||||
// max 12 images and 12 specification key/value pairs allowed
|
||||
$imageColumns = '';
|
||||
$specColumns = '';
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$imageColumns .= "image_url_$i TEXT" . ($i === 0 ? " NOT NULL" : "") . ", ";
|
||||
$specColumns .= "spec_key_$i TEXT, spec_val_$i TEXT, ";
|
||||
}
|
||||
$imageColumns = rtrim($imageColumns, ', ');
|
||||
$specColumns = rtrim($specColumns, ', ');
|
||||
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER REFERENCES categories(id),
|
||||
title TEXT NOT NULL,
|
||||
desc TEXT,
|
||||
stock_qty INTEGER NOT NULL DEFAULT 0 CHECK(stock_qty >= 0),
|
||||
specs_json TEXT,
|
||||
sats_price INTEGER NOT NULL DEFAULT 0 CHECK(sats_price >= 0),
|
||||
cents_price INTEGER NOT NULL DEFAULT 0 CHECK(cents_price >= 0),
|
||||
digital BOOLEAN NOT NULL DEFAULT 0,
|
||||
subscription BOOLEAN NOT NULL DEFAULT 0,
|
||||
image_url_0 TEXT,
|
||||
image_url_1 TEXT,
|
||||
image_url_2 TEXT,
|
||||
image_url_3 TEXT,
|
||||
image_url_4 TEXT,
|
||||
image_url_5 TEXT,
|
||||
image_url_6 TEXT,
|
||||
image_url_7 TEXT,
|
||||
image_url_8 TEXT,
|
||||
image_url_9 TEXT,
|
||||
image_url_10 TEXT,
|
||||
image_url_11 TEXT
|
||||
description TEXT,
|
||||
stock_qty INTEGER,
|
||||
is_unlimited BOOLEAN,
|
||||
is_enabled BOOLEAN,
|
||||
is_quote_only BOOLEAN,
|
||||
price_sats INTEGER,
|
||||
sats_back_percent INTEGER,
|
||||
is_accepting_sats BOOLEAN,
|
||||
is_sats_back_only BOOLEAN,
|
||||
price_cents INTEGER,
|
||||
cents_back_percent INTEGER,
|
||||
is_accepting_cents BOOLEAN,
|
||||
is_cents_back_only BOOLEAN,
|
||||
is_digital BOOLEAN,
|
||||
oz REAL,
|
||||
lbs REAL,
|
||||
length REAL,
|
||||
width REAL,
|
||||
height REAL,
|
||||
is_subscription BOOLEAN,
|
||||
is_subscription_only BOOLEAN,
|
||||
subscription_frequency TEXT,
|
||||
$imageColumns,
|
||||
$specColumns
|
||||
)");
|
||||
}
|
||||
|
||||
public static function add($title, $desc, $stock_qty, $specs_json, $sats_price, $cents_price, $digital, $subscription, $images)
|
||||
public static function validateProductData($data)
|
||||
{
|
||||
$error_messages = [];
|
||||
$title = $data['title'] ?? null;
|
||||
$description = $data['description'] ?? null;
|
||||
$price_sats = $data['price_sats'] ?? null;
|
||||
$price_cents = $data['price_cents'] ?? null;
|
||||
$stock_qty = $data['stock_qty'] ?? null;
|
||||
$is_digital = $data['is_digital'] ?? false;
|
||||
$is_subscription = $data['is_subscription'] ?? false;
|
||||
$is_subscription_only = $data['is_subscription_only'] ?? false;
|
||||
$is_quote_only = $data['is_quote_only'] ?? false;
|
||||
$oz = $data['oz'] ?? null;
|
||||
$lbs = $data['lbs'] ?? null;
|
||||
$length = $data['length'] ?? null;
|
||||
$width = $data['width'] ?? null;
|
||||
$height = $data['height'] ?? null;
|
||||
$category_id = $data['category_id'] ?? null;
|
||||
$is_unlimited = $data['is_unlimited'] ?? false;
|
||||
$is_sats_back_only = $data['is_sats_back_only'] ?? false;
|
||||
$is_cents_back_only = $data['is_cents_back_only'] ?? false;
|
||||
$is_enabled = $data['is_enabled'] ?? false;
|
||||
$is_accepting_sats = $data['is_accepting_sats'] ?? false;
|
||||
$is_accepting_cents = $data['is_accepting_cents'] ?? false;
|
||||
$subscription_frequency = $data['subscription_frequency'] ?? null;
|
||||
$images = [];
|
||||
for ($i = 0; $i <= 11; $i++) {
|
||||
$images[] = $data['image_url_' . $i] ?? null;
|
||||
}
|
||||
$specs = [];
|
||||
for ($i = 0; $i <= 11; $i++) {
|
||||
$key = $_POST['spec_key_' . $i] ?? null;
|
||||
$val = $_POST['spec_val_' . $i] ?? null;
|
||||
if ($key !== null || $val !== null) {
|
||||
if ($key === null || $val === null) {
|
||||
$error_messages[] = 'Each specification key must have a corresponding value.';
|
||||
}
|
||||
$specs[] = ['key' => $key, 'value' => $val];
|
||||
}
|
||||
}
|
||||
|
||||
if (! $title) {
|
||||
$error_messages[] = 'Title is a required field.';
|
||||
}
|
||||
if (! $description) {
|
||||
$error_messages[] = 'Description is a required field.';
|
||||
}
|
||||
if (! $category_id) {
|
||||
$error_messages[] = 'Category is a required field.';
|
||||
}
|
||||
if (! $images[0]) {
|
||||
$error_messages[] = 'The first image URL is a required field.';
|
||||
}
|
||||
if (! $is_unlimited && ! $stock_qty) {
|
||||
$error_messages[] = 'Quantity is required unless the product is marked as unlimited.';
|
||||
}
|
||||
|
||||
if (! $is_quote_only) {
|
||||
if ($price_sats === null && $price_cents === null) {
|
||||
$error_messages[] = 'Either price in sats or price in cents must be set. They can be zero.';
|
||||
}
|
||||
if ($is_cents_back_only && $is_sats_back_only) {
|
||||
$error_messages[] = 'A product cannot be both cents back only and sats back only.';
|
||||
}
|
||||
}
|
||||
|
||||
if (! $is_digital) {
|
||||
if ($oz === null || $lbs === null || $length === null || $width === null || $height === null) {
|
||||
$error_messages[] = 'For non-digital products, weight (oz and lbs), length, width, and height are required fields.';
|
||||
}
|
||||
}
|
||||
if ($is_subscription_only && ! $is_subscription) {
|
||||
$error_messages[] = 'If the product is subscription only, it must also enable subscriptions.';
|
||||
}
|
||||
|
||||
return [
|
||||
'errors' => $error_messages,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'category_id' => $category_id,
|
||||
'images' => $images,
|
||||
'specs' => $specs,
|
||||
'stock_qty' => $stock_qty,
|
||||
'price_sats' => $price_sats,
|
||||
'price_cents' => $price_cents,
|
||||
'is_digital' => $is_digital,
|
||||
'is_subscription' => $is_subscription,
|
||||
'is_subscription_only' => $is_subscription_only,
|
||||
'is_enabled' => $is_enabled,
|
||||
'is_unlimited' => $is_unlimited,
|
||||
'is_accepting_sats' => $is_accepting_sats,
|
||||
'is_accepting_cents' => $is_accepting_cents,
|
||||
'is_sats_back_only' => $is_sats_back_only,
|
||||
'is_cents_back_only' => $is_cents_back_only,
|
||||
'subscription_frequency' => $subscription_frequency,
|
||||
'is_quote_only' => $is_quote_only,
|
||||
'oz' => $oz,
|
||||
'lbs' => $lbs,
|
||||
'length' => $length,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
];
|
||||
}
|
||||
|
||||
public static function add($productData)
|
||||
{
|
||||
$imageColumns = '';
|
||||
$imageValues = '';
|
||||
$specColumns = '';
|
||||
$specValues = '';
|
||||
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$imageColumns .= "image_url_$i, ";
|
||||
$imageValues .= ":image_url_$i, ";
|
||||
$specColumns .= "spec_key_$i, spec_val_$i, ";
|
||||
$specValues .= ":spec_key_$i, :spec_val_$i, ";
|
||||
}
|
||||
|
||||
$imageColumns = rtrim($imageColumns, ', ');
|
||||
$imageValues = rtrim($imageValues, ', ');
|
||||
$specColumns = rtrim($specColumns, ', ');
|
||||
$specValues = rtrim($specValues, ', ');
|
||||
|
||||
$stmt = app::$db->prepare("INSERT INTO products (
|
||||
title, desc, stock_qty, specs_json, sats_price, cents_price, digital, subscription,
|
||||
image_url_0, image_url_1, image_url_2, image_url_3, image_url_4, image_url_5,
|
||||
image_url_6, image_url_7, image_url_8, image_url_9, image_url_10, image_url_11
|
||||
title, description, stock_qty, price_sats, price_cents, is_digital, is_subscription, is_subscription_only, is_enabled, is_unlimited, is_accepting_sats, is_accepting_cents, is_sats_back_only, is_cents_back_only, subscription_frequency, is_quote_only, category_id, oz, lbs, length, width, height,
|
||||
$imageColumns, $specColumns
|
||||
) VALUES (
|
||||
:title, :desc, :stock_qty, :specs_json, :sats_price, :cents_price, :digital, :subscription,
|
||||
:image_url_0, :image_url_1, :image_url_2, :image_url_3, :image_url_4, :image_url_5,
|
||||
:image_url_6, :image_url_7, :image_url_8, :image_url_9, :image_url_10, :image_url_11
|
||||
:title, :description, :stock_qty, :price_sats, :price_cents, :is_digital, :is_subscription, :is_subscription_only, :is_enabled, :is_unlimited, :is_accepting_sats, :is_accepting_cents, :is_sats_back_only, :is_cents_back_only, :subscription_frequency, :is_quote_only, :category_id, :oz, :lbs, :length, :width, :height,
|
||||
$imageValues, $specValues
|
||||
)");
|
||||
|
||||
$stmt->execute([
|
||||
':title' => $title,
|
||||
':desc' => $desc,
|
||||
':stock_qty' => $stock_qty,
|
||||
':specs_json' => $specs_json,
|
||||
':sats_price' => $sats_price,
|
||||
':cents_price' => $cents_price,
|
||||
':digital' => (int) $digital,
|
||||
':subscription' => (int) $subscription,
|
||||
':image_url_0' => $images[0] ?? null,
|
||||
':image_url_1' => $images[1] ?? null,
|
||||
':image_url_2' => $images[2] ?? null,
|
||||
':image_url_3' => $images[3] ?? null,
|
||||
':image_url_4' => $images[4] ?? null,
|
||||
':image_url_5' => $images[5] ?? null,
|
||||
':image_url_6' => $images[6] ?? null,
|
||||
':image_url_7' => $images[7] ?? null,
|
||||
':image_url_8' => $images[8] ?? null,
|
||||
':image_url_9' => $images[9] ?? null,
|
||||
':image_url_10' => $images[10] ?? null,
|
||||
':image_url_11' => $images[11] ?? null,
|
||||
]);
|
||||
$stmt->bindValue(':title', $productData['title']);
|
||||
$stmt->bindValue(':description', $productData['description']);
|
||||
$stmt->bindValue(':stock_qty', $productData['stock_qty']);
|
||||
$stmt->bindValue(':price_sats', $productData['price_sats']);
|
||||
$stmt->bindValue(':price_cents', $productData['price_cents']);
|
||||
$stmt->bindValue(':is_digital', (bool) $productData['is_digital'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_subscription', (bool) $productData['is_subscription'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_subscription_only', (bool) $productData['is_subscription_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_enabled', (bool) $productData['is_enabled'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_unlimited', (bool) $productData['is_unlimited'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_accepting_sats', (bool) $productData['is_accepting_sats'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_accepting_cents', (bool) $productData['is_accepting_cents'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_sats_back_only', (bool) $productData['is_sats_back_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_cents_back_only', (bool) $productData['is_cents_back_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':subscription_frequency', $productData['subscription_frequency']);
|
||||
$stmt->bindValue(':is_quote_only', (bool) $productData['is_quote_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':category_id', $productData['category_id'], \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':oz', $productData['oz']);
|
||||
$stmt->bindValue(':lbs', $productData['lbs']);
|
||||
$stmt->bindValue(':length', $productData['length']);
|
||||
$stmt->bindValue(':width', $productData['width']);
|
||||
$stmt->bindValue(':height', $productData['height']);
|
||||
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$stmt->bindValue(":image_url_$i", $productData['images'][$i] ?? null);
|
||||
$stmt->bindValue(":spec_key_$i", $productData['specs'][$i]['key'] ?? null);
|
||||
$stmt->bindValue(":spec_val_$i", $productData['specs'][$i]['value'] ?? null);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public static function update($id, $productData)
|
||||
{
|
||||
$imageColumns = '';
|
||||
$specColumns = '';
|
||||
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$imageColumns .= "image_url_$i = :image_url_$i, ";
|
||||
$specColumns .= "spec_key_$i = :spec_key_$i, spec_val_$i = :spec_val_$i, ";
|
||||
}
|
||||
|
||||
$imageColumns = rtrim($imageColumns, ', ');
|
||||
$specColumns = rtrim($specColumns, ', ');
|
||||
|
||||
$stmt = app::$db->prepare("UPDATE products SET
|
||||
title = :title,
|
||||
description = :description,
|
||||
stock_qty = :stock_qty,
|
||||
price_sats = :price_sats,
|
||||
price_cents = :price_cents,
|
||||
is_digital = :is_digital,
|
||||
is_subscription = :is_subscription,
|
||||
is_subscription_only = :is_subscription_only,
|
||||
is_enabled = :is_enabled,
|
||||
is_unlimited = :is_unlimited,
|
||||
is_accepting_sats = :is_accepting_sats,
|
||||
is_accepting_cents = :is_accepting_cents,
|
||||
is_sats_back_only = :is_sats_back_only,
|
||||
is_cents_back_only = :is_cents_back_only,
|
||||
subscription_frequency = :subscription_frequency,
|
||||
is_quote_only = :is_quote_only,
|
||||
category_id = :category_id,
|
||||
oz = :oz,
|
||||
lbs = :lbs,
|
||||
length = :length,
|
||||
width = :width,
|
||||
height = :height,
|
||||
$imageColumns,
|
||||
$specColumns
|
||||
WHERE id = :id");
|
||||
|
||||
$stmt->bindValue(':id', (int) $id, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $productData['title']);
|
||||
$stmt->bindValue(':description', $productData['description']);
|
||||
$stmt->bindValue(':stock_qty', $productData['stock_qty']);
|
||||
$stmt->bindValue(':price_sats', $productData['price_sats']);
|
||||
$stmt->bindValue(':price_cents', $productData['price_cents']);
|
||||
$stmt->bindValue(':is_digital', (bool) $productData['is_digital'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_subscription', (bool) $productData['is_subscription'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_subscription_only', (bool) $productData['is_subscription_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_enabled', (bool) $productData['is_enabled'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_unlimited', (bool) $productData['is_unlimited'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_accepting_sats', (bool) $productData['is_accepting_sats'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_accepting_cents', (bool) $productData['is_accepting_cents'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_sats_back_only', (bool) $productData['is_sats_back_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':is_cents_back_only', (bool) $productData['is_cents_back_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':subscription_frequency', $productData['subscription_frequency']);
|
||||
$stmt->bindValue(':is_quote_only', (bool) $productData['is_quote_only'], \PDO::PARAM_BOOL);
|
||||
$stmt->bindValue(':category_id', $productData['category_id'], \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':oz', $productData['oz']);
|
||||
$stmt->bindValue(':lbs', $productData['lbs']);
|
||||
$stmt->bindValue(':length', $productData['length']);
|
||||
$stmt->bindValue(':width', $productData['width']);
|
||||
$stmt->bindValue(':height', $productData['height']);
|
||||
|
||||
for ($i = 0; $i < 12; $i++) {
|
||||
$stmt->bindValue(":image_url_$i", $productData['images'][$i] ?? null);
|
||||
$stmt->bindValue(":spec_key_$i", $productData['specs'][$i]['key'] ?? null);
|
||||
$stmt->bindValue(":spec_val_$i", $productData['specs'][$i]['value'] ?? null);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public static function get($limit)
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM products LIMIT :limit");
|
||||
$stmt->bindValue(':limit', (int) $limit, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
public static function getById($id)
|
||||
{
|
||||
$stmt = app::$db->prepare("SELECT * FROM products WHERE id = :id");
|
||||
$stmt->bindValue(':id', (int) $id, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,11 @@ class quote_items
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS quote_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
quote_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
quote_id INTEGER NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
|
||||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||||
price REAL NOT NULL CHECK(price >= 0),
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (quote_id) REFERENCES quotes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,9 @@ class quotes
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS quotes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT'
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -17,15 +17,13 @@ class subscriptions
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS subscriptions (
|
||||
subscription_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
product_id INTEGER NOT NULL REFERENCES products(id),
|
||||
start_date DATETIME NOT NULL,
|
||||
renews_at DATETIME NOT NULL,
|
||||
status TEXT CHECK(status IN ('" . implode("', '", self::STATUS) . "')) NOT NULL DEFAULT 'COMPLETED',
|
||||
state TEXT CHECK(state IN ('" . implode("', '", self::STATES) . "')) NOT NULL DEFAULT 'TRIAL',
|
||||
invoice_date DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
invoice_date DATETIME NOT NULL
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -12,12 +12,11 @@ class transactions
|
|||
$typesList = "'" . implode("', '", self::TYPES) . "'";
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
type TEXT CHECK(type IN ($typesList)) NOT NULL,
|
||||
cents INTEGER DEFAULT 0,
|
||||
sats INTEGER DEFAULT 0,
|
||||
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,11 @@ class user_settings
|
|||
{
|
||||
app::$db->exec("CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
opt_in_promotional BOOLEAN NOT NULL,
|
||||
opt_in_subscription BOOLEAN DEFAULT TRUE,
|
||||
opt_in_order BOOLEAN DEFAULT TRUE,
|
||||
dark_theme BOOLEAN NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
dark_theme BOOLEAN NOT NULL
|
||||
);");
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ class users
|
|||
app::$db->exec("CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE,
|
||||
shipping_address_id INTEGER,
|
||||
billing_address_id INTEGER,
|
||||
shipping_address_id INTEGER REFERENCES addresses(id),
|
||||
billing_address_id INTEGER REFERENCES addresses(id),
|
||||
lifetime_spend INTEGER DEFAULT 0,
|
||||
lifetime_orders INTEGER DEFAULT 0,
|
||||
verified BOOLEAN NOT NULL,
|
||||
|
@ -29,6 +29,7 @@ class users
|
|||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
}
|
||||
|
||||
public static function setDefaultShipping($user_id, $shipping_address_id)
|
||||
{
|
||||
$query = "UPDATE users SET shipping_address_id = :shipping_address_id WHERE id = :user_id";
|
||||
|
@ -179,4 +180,13 @@ class users
|
|||
$stmt->execute();
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public static function getUsers($n)
|
||||
{
|
||||
$query = "SELECT * FROM users LIMIT :limit";
|
||||
$stmt = app::$db->prepare($query);
|
||||
$stmt->bindParam(':limit', $n, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
|
|
9
src/views/admin/products/add.twig
Normal file
9
src/views/admin/products/add.twig
Normal file
|
@ -0,0 +1,9 @@
|
|||
<section class="flex flex-col gap-4">
|
||||
{% include 'lib/alert.twig' %}
|
||||
<form action="/admin/products/add" method="POST" class="flex flex-col gap-4">
|
||||
{% include 'lib/forms/product.twig' %}
|
||||
{% include 'lib/buttons/submit.twig' with {
|
||||
label: 'Add Product'
|
||||
} %}
|
||||
</form>
|
||||
</section>
|
9
src/views/admin/products/edit.twig
Normal file
9
src/views/admin/products/edit.twig
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% include 'lib/alert.twig' %}
|
||||
<form action="/admin/products/edit/{{ product.id }}" method="post" class="flex flex-col gap-2">
|
||||
{% include 'lib/forms/product.twig' with {
|
||||
product: session.last_post ?? product
|
||||
} %}
|
||||
{% include 'lib/buttons/submit.twig' with {
|
||||
label: 'Save Product'
|
||||
} %}
|
||||
</form>
|
|
@ -2,61 +2,75 @@
|
|||
{% include 'lib/alert.twig' %}
|
||||
|
||||
<h3 class="text-2xl font-semibold">
|
||||
Categories
|
||||
Products
|
||||
</h3>
|
||||
<a href="/admin/categories/add">
|
||||
<a href="/admin/products/add">
|
||||
{% include 'lib/buttons/primary.twig' with {
|
||||
label: 'Create a Category'
|
||||
} %}
|
||||
label: 'Create a Product'
|
||||
} %}
|
||||
</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) %}
|
||||
<thead>
|
||||
<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>
|
||||
<th class="py-2">
|
||||
Product ID
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Title
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Description
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Stock Quantity
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Price (Sats)
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Price (Cents)
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
{% if category.children is defined and category.children is not empty %}
|
||||
{% for child in category.children %}
|
||||
{{ _self.renderCategory(child, depth + 1) }}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if products %}
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.id }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.title }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.description }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.stock_qty }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.price_sats }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ product.price_cents }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
<a href="/admin/products/edit/{{ product.id }}">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="border px-4 py-2" colspan="7">
|
||||
No products found.
|
||||
</td>
|
||||
</tr>
|
||||
{% 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>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
|
|
@ -1,3 +1,61 @@
|
|||
<section class="flex flex-col gap-4">
|
||||
USERS
|
||||
|
||||
{% include 'lib/alert.twig' %}
|
||||
|
||||
<h3 class="text-2xl font-semibold">
|
||||
Users
|
||||
</h3>
|
||||
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2">
|
||||
User ID
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Name
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Email
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Verified
|
||||
</th>
|
||||
<th class="py-2">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if users %}
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="border px-4 py-2">
|
||||
{{ user.id }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ user.name }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ user.email }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
{{ user.verified ? 'Yes' : 'No' }}
|
||||
</td>
|
||||
<td class="border px-4 py-2">
|
||||
<a href="/admin/users/edit/{{ user.id }}">
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td class="border px-4 py-2" colspan="5">
|
||||
No users found.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
|
243
src/views/lib/forms/product.twig
Normal file
243
src/views/lib/forms/product.twig
Normal file
|
@ -0,0 +1,243 @@
|
|||
<div class="flex items-end">
|
||||
<div class="w-2/3 pr-4">
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
placeholder: 'Enter product title',
|
||||
value: product.title,
|
||||
required: true
|
||||
} %}
|
||||
</div>
|
||||
<div class="w-1/3 mb-3">
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_enabled',
|
||||
label: 'Enable Product?',
|
||||
on: product.is_enabled
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
placeholder: 'Enter product description',
|
||||
value: product.description,
|
||||
required: true
|
||||
} %}
|
||||
{% include 'lib/inputs/select.twig' with {
|
||||
name: 'category_id',
|
||||
label: 'Category',
|
||||
options: categories,
|
||||
value: product.category_id,
|
||||
no_option: { 'label': '-- None --', 'value': null},
|
||||
required: true
|
||||
} %}
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'stock_qty',
|
||||
label: 'Stock Quantity',
|
||||
placeholder: 'Enter stock quantity',
|
||||
value: product.stock_qty,
|
||||
required: true
|
||||
} %}
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_umlimited',
|
||||
label: 'Unlimited Quantity?',
|
||||
on: product.is_umlimited
|
||||
} %}
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'image_url_0',
|
||||
label: 'Primary Photo URL',
|
||||
placeholder: 'Enter image URL 0 - (required)',
|
||||
value: attribute(product, 'image_url_0'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
<h3 class="text-xl font-semibold mt-4 mb-2">
|
||||
Pricing
|
||||
</h3>
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_quote_only',
|
||||
label: 'Buyer Must Ask for Quote?',
|
||||
on: product.is_quote_only
|
||||
} %}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-1">
|
||||
<h4 class="font-semibold mb-2">
|
||||
Sats Pricing
|
||||
</h4>
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_accepting_sats',
|
||||
label: 'Accept Sats?',
|
||||
on: product.is_accepting_sats
|
||||
} %}
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'price_sats',
|
||||
label: 'Price (Sats)',
|
||||
desc: 'Leave empty to auto-convert from cents price.',
|
||||
placeholder: 'Price sats',
|
||||
value: product.price_sats,
|
||||
} %}
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'sats_back_percent',
|
||||
label: 'Sats Back Percent',
|
||||
desc: 'Promotional reward when paid with sats',
|
||||
placeholder: 'Sats Back %',
|
||||
value: product.sats_back_percent,
|
||||
} %}
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_sats_back_only',
|
||||
label: 'Only give sats back?',
|
||||
on: product.is_sats_back_only
|
||||
} %}
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<h4 class="font-semibold mb-2">
|
||||
Cents Pricing
|
||||
</h4>
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_accepting_cents',
|
||||
label: 'Accept Cents?',
|
||||
on: product.is_accepting_cents
|
||||
} %}
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'price_cents',
|
||||
label: 'Price (Cents)',
|
||||
desc: 'Leave empty to auto-convert from sats price.',
|
||||
placeholder: 'Price cents',
|
||||
value: product.price_cents,
|
||||
} %}
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'cents_back_percent',
|
||||
label: 'Cents Back %',
|
||||
desc: 'Promotional reward when paid with cents',
|
||||
placeholder: 'Cents Back %',
|
||||
value: product.cents_back_percent,
|
||||
} %}
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_cents_back_only',
|
||||
label: 'Only give cents back?',
|
||||
on: product.is_cents_back_only
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mt-4 mb-2">
|
||||
Subscription
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_subscription',
|
||||
label: 'Enable Subscriptions?',
|
||||
on: product.is_subscription
|
||||
} %}
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_subscription_only',
|
||||
label: 'Subscription Only?',
|
||||
on: product.is_subscription_only
|
||||
} %}
|
||||
{% include 'lib/inputs/select.twig' with {
|
||||
name: 'subscription_frequency',
|
||||
label: 'Subscription Frequency',
|
||||
options: [
|
||||
{ 'label': 'Per Minute', 'value': 'minute' },
|
||||
{ 'label': 'Hourly', 'value': 'hour' },
|
||||
{ 'label': 'Daily', 'value': 'day' },
|
||||
{ 'label': 'Weekly', 'value': 'week' },
|
||||
{ 'label': 'Monthly', 'value': 'month' },
|
||||
{ 'label': 'Yearly', 'value': 'year' }
|
||||
],
|
||||
value: product.subscription_frequency,
|
||||
no_option: { 'label': '-- Select Frequency --', 'value': null }
|
||||
} %}
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mt-4 mb-2">
|
||||
Shipping
|
||||
</h3>
|
||||
<p>
|
||||
Non-digital products require shipping information.
|
||||
</p>
|
||||
{% include 'lib/inputs/toggle.twig' with {
|
||||
name: 'is_digital',
|
||||
label: 'Digital Product?',
|
||||
on: product.is_digital
|
||||
} %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'lbs',
|
||||
label: 'Weight (lbs)',
|
||||
placeholder: 'Enter pounds',
|
||||
value: product.lbs,
|
||||
} %}
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'oz',
|
||||
label: 'Weight (oz)',
|
||||
placeholder: 'Enter ounces',
|
||||
value: product.oz,
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'length',
|
||||
label: 'Length',
|
||||
placeholder: 'Enter length',
|
||||
value: product.length,
|
||||
} %}
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'width',
|
||||
label: 'Width',
|
||||
placeholder: 'Enter width',
|
||||
value: product.width,
|
||||
} %}
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/number.twig' with {
|
||||
name: 'height',
|
||||
label: 'Height',
|
||||
placeholder: 'Enter height',
|
||||
value: product.height,
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mt-4 mb-2">
|
||||
Additional Product Photos
|
||||
</h3>
|
||||
{% for i in 1..11 %}
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'image_url_' ~ i,
|
||||
placeholder: 'Enter image URL ' ~ i,
|
||||
value: attribute(product, 'image_url_' ~ i),
|
||||
} %}
|
||||
{% endfor %}
|
||||
<h3 class="text-xl font-semibold mt-4 mb-2">
|
||||
Product Specifications
|
||||
<span class="text-sm font-normal">
|
||||
- (optional)
|
||||
</span>
|
||||
</h3>
|
||||
<p>
|
||||
Provide any relevant specifications such as brand, material, model, year, color, and more.
|
||||
</p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
{% for i in 0..11 %}
|
||||
<div class="col-span-1">
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'spec_key_' ~ i,
|
||||
placeholder: 'Spec ' ~ i,
|
||||
value: attribute(product, 'spec_key_' ~ i),
|
||||
} %}
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
{% include 'lib/inputs/text.twig' with {
|
||||
name: 'spec_val_' ~ i,
|
||||
placeholder: 'Value ' ~ i,
|
||||
value: attribute(product, 'spec_val_' ~ i),
|
||||
} %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -1,5 +1,15 @@
|
|||
<label for="{{ id }}" class="block text-sm font-medium mt-2">
|
||||
{{ label }}
|
||||
{% if required is defined %}
|
||||
<span class="{{ colors.error.text }} ml-4">
|
||||
*
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% if desc is defined %}
|
||||
<p class="text-sm mt-1">
|
||||
{{ desc }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<input type="number" id="{{ id }}" name="{{ name }}" class="border rounded-lg p-2 w-full" {% if required %} required {% endif %} {% if min is defined %} min="{{ min }}" {% endif %} {% if max is defined %} max="{{ max }}" {% endif %} {% if step is defined %} step="{{ step }}" {% endif %} {% if value is defined %} value="{{ value }}" {% endif %} placeholder="{{ placeholder | default('Enter a number') }}">
|
||||
|
|
@ -1,6 +1,16 @@
|
|||
<label for="{{ id }}" class="block text-sm font-medium mt-2">
|
||||
{{ label }}
|
||||
{% if required is defined %}
|
||||
<span class="{{ colors.error.text }} ml-4">
|
||||
*
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% if desc is defined %}
|
||||
<p class="text-sm mt-1">
|
||||
{{ desc }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<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 }}">
|
||||
|
|
|
@ -21,6 +21,11 @@
|
|||
{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if desc is defined %}
|
||||
<p class="text-sm mt-1">
|
||||
{{ desc }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<input type="{{ type }}" name="{{ name }}" {% if placeholder %} placeholder="{{ placeholder }}" {% endif %} {% if value is not null %} value="{{ value }}" {% endif %} {% if readonly is not null %} readonly {% endif %} {% if type == 'number' %} {% if min is defined %} min="{{ min }}" {% endif %} {% if max is defined %} max="{{ max }}" {% endif %} {% endif %} class="{{ colors.input }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none"></div>
|
||||
<input type="{{ type }}" name="{{ name }}" {% if placeholder %} placeholder="{{ placeholder }}" {% endif %} {% if value is not null %} value="{{ value }}" {% endif %} {% if readonly is not null %} readonly {% endif %} {% if required is defined %} required {% endif %} {% if type == 'number' %} {% if min is defined %} min="{{ min }}" {% endif %} {% if max is defined %} max="{{ max }}" {% endif %} {% endif %} class="{{ colors.input }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none"></div>
|
||||
|
Loading…
Reference in a new issue