This commit is contained in:
count-null 2025-02-27 16:38:19 -05:00
parent a0cb5fb6b0
commit e435d32588
88 changed files with 1781 additions and 1383 deletions

View file

@ -11,3 +11,6 @@ SMTP_FROM="noreply@example.com"
# !! Choose your LN_SERVICE carefully!! # !! Choose your LN_SERVICE carefully!!
# NOTE: The LN_SERVICE must support LUD-21 Payment Verification # NOTE: The LN_SERVICE must support LUD-21 Payment Verification
LN_ADDRESS="your@node.win" LN_ADDRESS="your@node.win"
# Shippo API for address validation and getting live shipping rates
# buying and printing labels
SHIPPO_API_KEY="your-shippo.com-API-key"

View file

@ -12,6 +12,7 @@
"web-auth/webauthn-lib": "^5.0", "web-auth/webauthn-lib": "^5.0",
"twig/twig": "^3.0", "twig/twig": "^3.0",
"swentel/nostr-php": "^1.5", "swentel/nostr-php": "^1.5",
"jorijn/bitcoin-bolt11": "^1.0" "jorijn/bitcoin-bolt11": "^1.0",
"shippo/shippo-php": "^2.0"
} }
} }

60
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "3f3a1571b095d2550e901def5ae562ca", "content-hash": "d534de83ea2acfea5f56dd6efb27959e",
"packages": [ "packages": [
{ {
"name": "bitwasp/bech32", "name": "bitwasp/bech32",
@ -1709,6 +1709,64 @@
}, },
"time": "2023-03-08T19:51:13+00:00" "time": "2023-03-08T19:51:13+00:00"
}, },
{
"name": "shippo/shippo-php",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/goshippo/shippo-php-client.git",
"reference": "7422af5b97c8b36718593dbaf5cfb757771fb907"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/goshippo/shippo-php-client/zipball/7422af5b97c8b36718593dbaf5cfb757771fb907",
"reference": "7422af5b97c8b36718593dbaf5cfb757771fb907",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.3"
},
"require-dev": {
"phpunit/phpunit": "9.5.*"
},
"type": "library",
"autoload": {
"classmap": [
"lib/Shippo/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Shippo & Contributors",
"homepage": "https://goshippo.com/"
}
],
"description": "A PHP library for connecting with multiple carriers (FedEx, UPS, USPS) using Shippo.",
"homepage": "https://goshippo.com/",
"keywords": [
"FedEx",
"Uber",
"address",
"dhl",
"shipping",
"shyp",
"tracking",
"ups",
"usps"
],
"support": {
"issues": "https://github.com/goshippo/shippo-php-client/issues",
"source": "https://github.com/goshippo/shippo-php-client/tree/v2.0.0"
},
"time": "2022-01-19T22:36:48+00:00"
},
{ {
"name": "simplito/bigint-wrapper-php", "name": "simplito/bigint-wrapper-php",
"version": "1.0.0", "version": "1.0.0",

View file

@ -5,8 +5,8 @@
use app\app; use app\app;
use app\controllers\account; use app\controllers\account;
use app\controllers\admin; use app\controllers\admin;
use app\controllers\category;
use app\controllers\cart; use app\controllers\cart;
use app\controllers\category;
use app\controllers\checkout; use app\controllers\checkout;
use app\controllers\home; use app\controllers\home;
use app\controllers\lnurlp; use app\controllers\lnurlp;
@ -26,7 +26,7 @@ session_start();
session_regenerate_id(true); // prevent session fixation attacks session_regenerate_id(true); // prevent session fixation attacks
// prevent session hijack // prevent session hijack
if (!isset($_SESSION['fingerprint'])) { if (! isset($_SESSION['fingerprint'])) {
$_SESSION['fingerprint'] = hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); $_SESSION['fingerprint'] = hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
} else { } else {
if ($_SESSION['fingerprint'] !== hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'])) { if ($_SESSION['fingerprint'] !== hash('sha256', $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'])) {
@ -41,6 +41,7 @@ $defaults = [
'session' => $_SESSION, 'session' => $_SESSION,
'http_host' => $_SERVER['HTTP_HOST'], 'http_host' => $_SERVER['HTTP_HOST'],
'env' => $_ENV, 'env' => $_ENV,
'is_user' => isset($_SESSION['user_id']),
'is_admin' => isset($_SESSION['user_id']) && $_SESSION['user_id'] == 1, 'is_admin' => isset($_SESSION['user_id']) && $_SESSION['user_id'] == 1,
// uses cookie-js to get the client's preferred theme // uses cookie-js to get the client's preferred theme
// used to conditionally deliver image assets // used to conditionally deliver image assets
@ -82,17 +83,19 @@ if (preg_match('/^\/(transaction|user|order|product)\/([\w-]+)$/', $route, $matc
} else { } else {
$controller = match ($route) { $controller = match ($route) {
'/' => home::index($defaults), '/' => home::index($defaults),
'/account' => account::index($defaults),
'/account/profile' => account::profile(),
'/account/login' => account::login($defaults), '/account/login' => account::login($defaults),
'/account/email' => account::email(),
'/account/logout' => account::logout(),
'/account/returns' => account::returns($defaults),
'/account/signup' => account::signup($defaults), '/account/signup' => account::signup($defaults),
'/account/billing' => account::billing($defaults),
'/account/orders' => account::orders($defaults),
'/account/shipping' => account::shipping($defaults),
'/account/verify' => account::verify($defaults), '/account/verify' => account::verify($defaults),
'/account' => $defaults['is_user'] ? account::index($defaults) : header('Location: /account/login'),
'/account/profile' => $defaults['is_user'] ? account::profile() : header('Location: /account/login'),
'/account/email' => $defaults['is_user'] ? account::email() : header('Location: /account/login'),
'/account/logout' => $defaults['is_user'] ? account::logout() : header('Location: /account/login'),
'/account/returns' => $defaults['is_user'] ? account::returns($defaults) : header('Location: /account/login'),
'/account/billing' => $defaults['is_user'] ? account::billing($defaults) : header('Location: /account/login'),
'/account/orders' => $defaults['is_user'] ? account::orders($defaults) : header('Location: /account/login'),
'/account/shipping' => $defaults['is_user'] ? account::shipping($defaults) : header('Location: /account/login'),
'/account/address/edit' => $defaults['is_user'] ? account::address_edit($defaults) : header('Location: /account/login'),
'/account/address/confirm' => $defaults['is_user'] ? account::address_confirm($defaults) : header('Location: /account/login'),
'/admin' => $defaults['is_admin'] ? admin::index($defaults) : lost::index($defaults), '/admin' => $defaults['is_admin'] ? admin::index($defaults) : lost::index($defaults),
'/admin/users' => $defaults['is_admin'] ? admin::users($defaults) : lost::index($defaults), '/admin/users' => $defaults['is_admin'] ? admin::users($defaults) : lost::index($defaults),
'/admin/orders' => $defaults['is_admin'] ? admin::orders($defaults) : lost::index($defaults), '/admin/orders' => $defaults['is_admin'] ? admin::orders($defaults) : lost::index($defaults),
@ -113,7 +116,8 @@ if (preg_match('/^\/(transaction|user|order|product)\/([\w-]+)$/', $route, $matc
'/power-meters' => category::power_meters($defaults), '/power-meters' => category::power_meters($defaults),
default => lost::index($defaults) default => lost::index($defaults)
}; };
}; }
;
// Clear alerts after rendering // Clear alerts after rendering
foreach (['error', 'warning', 'info', 'success'] as $alert) { foreach (['error', 'warning', 'info', 'success'] as $alert) {

View file

@ -1,7 +1,7 @@
<?php <?php
namespace app; namespace app;
// for email
// for email
class app class app
{ {

View file

@ -4,55 +4,55 @@ return [
'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200', 'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200',
], ],
'anchor' => [ 'anchor' => [
'primary' => 'text-blue-400 dark:text-blue-200' 'primary' => 'text-blue-400 dark:text-blue-200',
], ],
'body' => 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300', 'body' => 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300',
'button' => [ 'button' => [
'primary' => 'border-blue-400 dark:border-blue-600 dark:hover:border-blue-800 bg-blue-400 dark:bg-blue-600 hover:bg-blue-600 hover:dark:bg-blue-800 text-white dark:text-white', 'primary' => 'border-blue-400 dark:border-blue-600 dark:hover:border-blue-800 bg-blue-400 dark:bg-blue-600 hover:bg-blue-600 hover:dark:bg-blue-800 text-white dark:text-white',
'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900' 'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900',
], ],
'breadcrumb' => [ 'breadcrumb' => [
'parent' => 'text-gray-300 dark:text-gray-400 hover:text-gray-400 dark:hover:text-gray-500', 'parent' => 'text-gray-300 dark:text-gray-400 hover:text-gray-400 dark:hover:text-gray-500',
'seperator' => 'text-gray-200 dark:text-gray-200', 'seperator' => 'text-gray-200 dark:text-gray-200',
'child' => 'text-gray-200 dark:text-gray-300' 'child' => 'text-gray-200 dark:text-gray-300',
], ],
'dropdown' => [ 'dropdown' => [
'list' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', 'list' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300',
'item' => 'hover:bg-gray-200 dark:hover:bg-gray-900' 'item' => 'hover:bg-gray-200 dark:hover:bg-gray-900',
], ],
'input' => 'text-gray-800 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-500 focus:ring-blue-500', 'input' => 'text-gray-800 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-500 focus:ring-blue-500',
'error' => [ 'error' => [
'text' => 'text-red-600', 'text' => 'text-red-600',
'alert' => 'bg-red-100 text-gray-800 border-red-600' 'alert' => 'bg-red-100 text-gray-800 border-red-600',
], ],
'warning' => [ 'warning' => [
'text' => 'text-yellow-400', 'text' => 'text-yellow-400',
'alert' => 'bg-yellow-100 text-gray-800 border-yellow-400' 'alert' => 'bg-yellow-100 text-gray-800 border-yellow-400',
], ],
'success' => [ 'success' => [
'text' => 'text-green-600', 'text' => 'text-green-600',
'alert' => 'bg-green-100 text-gray-800 border-green-600' 'alert' => 'bg-green-100 text-gray-800 border-green-600',
], ],
'info' => [ 'info' => [
'text' => 'text-blue-400', 'text' => 'text-blue-400',
'alert' => 'bg-blue-200 text-gray-800 border-blue-400' 'alert' => 'bg-blue-200 text-gray-800 border-blue-400',
], ],
'modal' => [ 'modal' => [
'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', 'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300',
'shadow' => 'bg-black/70' 'shadow' => 'bg-black/70',
], ],
'nav' => [ 'nav' => [
'bar' => 'bg-blue-400 dark:bg-blue-600 text-gray-200 dark:text-gray-200', 'bar' => 'bg-blue-400 dark:bg-blue-600 text-gray-200 dark:text-gray-200',
'item' => 'hover:bg-blue-600 dark:hover:bg-blue-800 hover:text-gray-200 dark:hover:text-gray-300 text-white border-blue-400 dark:border-blue-600', 'item' => 'hover:bg-blue-600 dark:hover:bg-blue-800 hover:text-gray-200 dark:hover:text-gray-300 text-white border-blue-400 dark:border-blue-600',
'hovercontent' => 'bg-white dark:bg-slate-700 text-gray-800 dark:text-gray-300' 'hovercontent' => 'bg-white dark:bg-slate-700 text-gray-800 dark:text-gray-300',
], ],
'rule' => 'border-gray-400 dark:border-gray-400', 'rule' => 'border-gray-400 dark:border-gray-400',
'text' => [ 'text' => [
'muted' => 'text-gray-400 dark:text-gray-300' 'muted' => 'text-gray-400 dark:text-gray-300',
], ],
'toggle' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white", 'toggle' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white",
'footer' => [ 'footer' => [
"primary" => "bg-gray-200 dark:bg-slate-600 text-gray-500 dark:text-gray-300", "primary" => "bg-gray-200 dark:bg-slate-600 text-gray-500 dark:text-gray-300",
"policy" => "bg-slate-400 dark:bg-slate-800 text-gray-200 dark:text-gray-400" "policy" => "bg-slate-400 dark:bg-slate-800 text-gray-200 dark:text-gray-400",
], ],
]; ];

View file

@ -2,64 +2,40 @@
namespace app\controllers; namespace app\controllers;
use app\models\addresses; use app\models\addresses;
use app\models\users;
use app\models\emails; use app\models\emails;
use app\models\user_addresses;
use app\models\magic_links; use app\models\magic_links;
use app\models\users;
class account class account
{ {
public static function index($defaults): void public static function index($defaults): void
{ {
if (!isset($_SESSION['user_id'])) { $user = users::getById($_SESSION['user_id']);
header('Location: /account/login'); $addresses = addresses::getByUserId($_SESSION['user_id']);
}
$user_id = $_SESSION['user_id']; echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
$user = users::getById($user_id);
$default_shipping = null;
$default_billing = null;
$ship_addrs = [];
$bill_addrs = [];
$addresses = user_addresses::getShippingByUserId($user['id']);
foreach ($addresses as $address) {
if ($address['id'] == $user['shipping_address_id']){
$default_shipping = $address;
} else {
$ship_addrs[] = $address;
}
}
$bill_addresses = user_addresses::getBillingByUserId($_SESSION['user_id']);
foreach ($bill_addresses as $addr) {
if ($addr['id'] == $user['billing_address_id']){
$default_billing = $addr;
} else {
$bill_addrs[] = $addr;
}
}
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [
'child_template' => 'account/index.twig', 'child_template' => 'account/index.twig',
'page_title' => 'Manage Account - ' . $_ENV['APP_NAME'], 'page_title' => 'Manage Account - ' . $_ENV['APP_NAME'],
'user' => $user, 'user' => $user,
'shipping' => $ship_addrs, 'addresses' => $addresses,
'billing' => $bill_addrs,
'default_shipping' => $default_shipping,
'default_billing' => $default_billing,
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => null, 'url' => null,
'title' => 'My Account', 'title' => 'My Account',
] ],
] ],
])); ]));
} }
public static function billing($defaults) public static function billing($defaults)
{ {
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) { $bill = addresses::validatePost("billing_");
http_response_code(403); if (isset($bill['error'])) {
header('Location: /account/billing');
} }
$bill = addresses::validatePost("billing");
$bill_id = addresses::add( $bill_id = addresses::add(
$_SESSION['user_id'],
$bill['name'], $bill['name'],
$bill['company'], $bill['company'],
$bill['addressLine1'], $bill['addressLine1'],
@ -68,51 +44,34 @@ class account
$bill['state'], $bill['state'],
$bill['zip'], $bill['zip'],
$bill['phone'], $bill['phone'],
1, $bill['hash']
0
);
user_addresses::add(
$_SESSION['user_id'],
$bill_id
); );
$_SESSION['success'] = "Billing address saved!"; $_SESSION['success'] = "Billing address saved!";
header('Location: /account/billing'); header('Location: /account/billing');
} }
$user_id = $_SESSION['user_id']; $user = users::getById($_SESSION['user_id']);
$user = users::getById($user_id); $addresses = addresses::getByUserId($_SESSION['user_id']);
$default_billing = null;
$bill_addrs = []; echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
$bill_addresses = user_addresses::getBillingByUserId($_SESSION['user_id']);
foreach ($bill_addresses as $addr) {
if ($addr['id'] == $user['billing_address_id']){
$default_billing = $addr;
} else {
$bill_addrs[] = $addr;
}
}
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [
'child_template' => 'account/billing.twig', 'child_template' => 'account/billing.twig',
'page_title' => 'Billing Information - ' . $_ENV['APP_NAME'], 'page_title' => 'Billing Information - ' . $_ENV['APP_NAME'],
'billing' => $bill_addrs, 'user' => $user,
'default_billing' => $default_billing, 'addresses' => $addresses,
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
'title' => 'My Account' 'title' => 'My Account',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Billing' 'title' => 'Billing',
] ],
] ],
])); ]));
} }
public static function profile() public static function profile()
{ {
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) {
http_response_code(403);
}
users::updateProfileById($_SESSION['user_id'], $_POST); users::updateProfileById($_SESSION['user_id'], $_POST);
header('Location: /account'); header('Location: /account');
} }
@ -120,10 +79,6 @@ class account
public static function email() public static function email()
{ {
$user_id = $_SESSION['user_id'] ?? null;
if (empty($user_id)){
header('Location: /account/login');
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'] ?? null; $email = $_POST['email'] ?? null;
if (empty($email)) { if (empty($email)) {
@ -131,6 +86,7 @@ class account
header('Location: /account'); header('Location: /account');
exit; exit;
} else { } else {
$user_id = $_SESSION['user_id'];
$token = magic_links::add($email, $user_id); $token = magic_links::add($email, $user_id);
users::updateReplaceEmailTokenById($user_id, $token); users::updateReplaceEmailTokenById($user_id, $token);
header('Location: /account'); header('Location: /account');
@ -149,7 +105,7 @@ class account
if ($user) { if ($user) {
$_SESSION['user_email'] = $link['email']; $_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
if (!$user['verified']) { if (! $user['verified']) {
users::verify($link['email']); users::verify($link['email']);
} }
header('Location: /account'); header('Location: /account');
@ -166,19 +122,55 @@ class account
} }
} }
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/verify.twig', 'child_template' => 'account/verify.twig',
'page_title' => $_ENV['APP_NAME'], 'page_title' => $_ENV['APP_NAME'],
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
'title' => 'My Account' 'title' => 'My Account',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Verify' 'title' => 'Verify',
] ],
] ],
]));
}
public static function address_edit($defaults)
{
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/address/edit.twig',
'page_title' => 'Edit Address - ' . $_ENV['APP_NAME'],
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Edit Address',
],
],
]));
}
public static function address_confirm($defaults)
{
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/address/confirm.twig',
'page_title' => 'Confirm Address - ' . $_ENV['APP_NAME'],
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Confirm Address',
],
],
])); ]));
} }
@ -199,15 +191,15 @@ class account
if (isset($_SESSION['user_id'])) { if (isset($_SESSION['user_id'])) {
header('Location: /account'); header('Location: /account');
} }
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/login.twig', 'child_template' => 'account/login.twig',
'page_title' => 'Sign In or Create an Account!', 'page_title' => 'Sign In or Create an Account!',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => null, 'url' => null,
'title' => 'My Account' 'title' => 'My Account',
],
], ],
]
])); ]));
} }
@ -220,50 +212,48 @@ class account
public static function orders($defaults) public static function orders($defaults)
{ {
if (!isset($_SESSION['user_id'])) { echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
header('Location: /account/login');
}
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [
'child_template' => 'account/orders.twig', 'child_template' => 'account/orders.twig',
'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Orders', 'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Orders',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
'title' => 'My Account' 'title' => 'My Account',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Orders' 'title' => 'Orders',
] ],
] ],
])); ]));
} }
public static function returns($defaults) public static function returns($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/returns.twig', 'child_template' => 'account/returns.twig',
'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Returns', 'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Returns',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
'title' => 'My Account' 'title' => 'My Account',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Returns' 'title' => 'Returns',
] ],
] ],
])); ]));
} }
public static function shipping($defaults) public static function shipping($defaults)
{ {
if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) { $ship = addresses::validatePost("shipping_");
http_response_code(403); if (isset($ship['error'])) {
header('Location: /account/shipping');
} }
$ship = addresses::validatePost("shipping");
$ship_id = addresses::add( $ship_id = addresses::add(
$_SESSION['user_id'],
$ship['name'], $ship['name'],
$ship['company'], $ship['company'],
$ship['addressLine1'], $ship['addressLine1'],
@ -272,43 +262,28 @@ class account
$ship['state'], $ship['state'],
$ship['zip'], $ship['zip'],
$ship['phone'], $ship['phone'],
0, $ship['hash']
1
);
user_addresses::add(
$_SESSION['user_id'],
$ship_id
); );
$_SESSION['success'] = "Shipping address saved!"; $_SESSION['success'] = "Shipping address saved!";
header('Location: /account/shipping'); header('Location: /account/shipping');
} }
$user_id = $_SESSION['user_id']; $user = users::getById($_SESSION['user_id']);
$user = users::getById($user_id); $addresses = addresses::getByUserId($_SESSION['user_id']);
$addresses = user_addresses::getShippingByUserId($user['id']);
$default_shipping = null; echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
$ship_addrs = [];
foreach ($addresses as $addr) {
if ($addr['id'] == $user['shipping_address_id']){
$default_shipping = $addr;
} else {
$ship_addrs[] = $addr;
}
}
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [
'child_template' => 'account/shipping.twig', 'child_template' => 'account/shipping.twig',
'page_title' => $_ENV['APP_NAME'] . ' Shipping', 'page_title' => $_ENV['APP_NAME'] . ' Shipping',
'shipping' => $ship_addrs, 'addresses' => $addresses,
'default_shipping' => $default_shipping,
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
'title' => 'My Account' 'title' => 'My Account',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Shipping' 'title' => 'Shipping',
] ],
] ],
])); ]));
} }
@ -327,16 +302,16 @@ class account
exit; exit;
} }
$useShipping = $_POST['use_shipping'] ?? false; $useShipping = $_POST['use_shipping'] ?? false;
$ship = addresses::validatePost("shipping"); $ship = addresses::validatePost("shipping_");
if (!isset($ship['name'])){ if (isset($ship['error'])) {
$_SESSION['error'] = "Shipping address verification failed. Check your entry for errors."; $_SESSION['error'] = "Shipping address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
header('Location: /account/signup'); header('Location: /account/signup');
} }
if (!$useShipping) { if (! $useShipping) {
$bill = addresses::validatePost("billing"); $bill = addresses::validatePost("billing_");
if (!isset($bill['name'])){ if (isset($bill['error'])) {
$_SESSION['error'] = "Billing address verification failed. Check your entry for errors."; $_SESSION['error'] = "Billing address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
header('Location: /account/signup'); header('Location: /account/signup');
} }
@ -346,6 +321,7 @@ class account
header('Location: /account/signup'); header('Location: /account/signup');
} }
$ship_id = addresses::add( $ship_id = addresses::add(
null,
$ship['name'], $ship['name'],
$ship['company'], $ship['company'],
$ship['addressLine1'], $ship['addressLine1'],
@ -354,12 +330,12 @@ class account
$ship['state'], $ship['state'],
$ship['zip'], $ship['zip'],
$ship['phone'], $ship['phone'],
$useShipping == 'on', $ship['hash']
1
); );
$bill_id = $ship_id; $bill_id = $ship_id;
if (!$useShipping) { if (! $useShipping) {
$bill_id = addresses::add( $bill_id = addresses::add(
null,
$bill['name'], $bill['name'],
$bill['company'], $bill['company'],
$bill['addressLine1'], $bill['addressLine1'],
@ -368,8 +344,7 @@ class account
$bill['state'], $bill['state'],
$bill['zip'], $bill['zip'],
$bill['phone'], $bill['phone'],
1, $bill['hash']
0
); );
} }
$opt_in_promotional = $_POST['opt_in_promotional'] ?? false; $opt_in_promotional = $_POST['opt_in_promotional'] ?? false;
@ -384,18 +359,8 @@ class account
$dark_theme $dark_theme
); );
emails::updateUserIdByEmail($email, $user_id); emails::updateUserIdByEmail($email, $user_id);
user_addresses::add(
user_id: $user_id,
address_id: $ship_id
);
if (!$useShipping) {
user_addresses::add(
user_id: $user_id,
address_id: $bill_id
);
}
$_SESSION['user_id'] = $user_id; $_SESSION['user_id'] = $user_id;
if (!$verified) { if (! $verified) {
header("Location: /magic-link?email=$email&signup=1"); header("Location: /magic-link?email=$email&signup=1");
exit; exit;
} }
@ -408,9 +373,9 @@ class account
exit; exit;
} }
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/signup.twig', 'child_template' => 'account/signup.twig',
'page_title' => 'Create an Account - ' . $_ENV['APP_NAME'] 'page_title' => 'Create an Account - ' . $_ENV['APP_NAME'],
])); ]));
} }
} }

View file

@ -1,83 +1,83 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
use app\models\transactions;
use app\models\emails; use app\models\emails;
use app\models\transactions;
use app\models\users; use app\models\users;
class admin class admin
{ {
public static function index($defaults) public static function index($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/index.twig', 'child_template' => 'admin/index.twig',
'page_title' => 'Dashboard', 'page_title' => 'Dashboard',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
] ],
], ],
])); ]));
} }
public static function users($defaults) public static function users($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/users.twig', 'child_template' => 'admin/users.twig',
'page_title' => 'Users', 'page_title' => 'Users',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/users', 'url' => '/admin/users',
'title' => 'Users' 'title' => 'Users',
] ],
], ],
])); ]));
} }
public static function orders($defaults) public static function orders($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/orders.twig', 'child_template' => 'admin/orders.twig',
'page_title' => 'Orders', 'page_title' => 'Orders',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/orders', 'url' => '/admin/orders',
'title' => 'Orders' 'title' => 'Orders',
] ],
], ],
])); ]));
} }
public static function returns($defaults) public static function returns($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/returns.twig', 'child_template' => 'admin/returns.twig',
'page_title' => 'Returns', 'page_title' => 'Returns',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/returns', 'url' => '/admin/returns',
'title' => 'Returns' 'title' => 'Returns',
] ],
], ],
])); ]));
} }
public static function transactions($defaults) public static function transactions($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/transactions/index.twig', 'child_template' => 'admin/transactions/index.twig',
'page_title' => 'Transactions', 'page_title' => 'Transactions',
'recent_sats' => transactions::getRecent(10, 'sats'), 'recent_sats' => transactions::getRecent(10, 'sats'),
@ -89,12 +89,12 @@ class admin
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/transactions', 'url' => '/admin/transactions',
'title' => 'Transactions' 'title' => 'Transactions',
] ],
], ],
])); ]));
} }
@ -105,23 +105,23 @@ class admin
$amount = $_POST['amount'] ?? null; $amount = $_POST['amount'] ?? null;
$currency = $_POST['currency']; $currency = $_POST['currency'];
$user_identifier = $_POST['user_identifier'] ?? null; $user_identifier = $_POST['user_identifier'] ?? null;
if (!$amount || !$user_identifier) { if (! $amount || ! $user_identifier) {
$_SESSION['error'] = !$amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction."; $_SESSION['error'] = ! $amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} else { } else {
if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) { if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) {
$user = users::getByEmail($user_identifier); $user = users::getByEmail($user_identifier);
} elseif (is_numeric($user_identifier)) { } elseif (is_numeric($user_identifier)) {
$user = users::getById((int)$user_identifier); $user = users::getById((int) $user_identifier);
} else { } else {
$_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID."; $_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} }
if (!$user) { if (! $user) {
$_SESSION['error'] = "User not found. Please enter a valid email or user ID."; $_SESSION['error'] = "User not found. Please enter a valid email or user ID.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} else { } else {
if($_POST['confirm']){ if ($_POST['confirm']) {
// create the transaction // create the transaction
$txid = transactions::add($user['id'], $amount > 0 ? 'CREDIT' : 'REVOKE', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0); $txid = transactions::add($user['id'], $amount > 0 ? 'CREDIT' : 'REVOKE', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0);
header('Location: /transaction/' . $txid); header('Location: /transaction/' . $txid);
@ -135,22 +135,22 @@ class admin
} }
} }
} }
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/transactions/add.twig', 'child_template' => 'admin/transactions/add.twig',
'page_title' => 'Add a Transaction', 'page_title' => 'Add a Transaction',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/transactions', 'url' => '/admin/transactions',
'title' => 'Transactions' 'title' => 'Transactions',
], ],
[ [
'url' => '/admin/transactions/add', 'url' => '/admin/transactions/add',
'title' => 'Add' 'title' => 'Add',
] ],
], ],
])); ]));
} }
@ -158,19 +158,19 @@ class admin
public static function emails($defaults) public static function emails($defaults)
{ {
$recent_emails = emails::getRecent(20); $recent_emails = emails::getRecent(20);
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/emails.twig', 'child_template' => 'admin/emails.twig',
'page_title' => 'Emails', 'page_title' => 'Emails',
'recent_emails' => $recent_emails, 'recent_emails' => $recent_emails,
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/emails', 'url' => '/admin/emails',
'title' => 'Emails' 'title' => 'Emails',
] ],
], ],
])); ]));
} }

View file

@ -1,17 +1,18 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class cart class cart
{ {
public static function index($defaults) public static function index($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'cart.twig', 'child_template' => 'cart.twig',
'page_title' => $_ENV['APP_NAME'] . ' Cart', 'page_title' => $_ENV['APP_NAME'] . ' Cart',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => null, 'url' => null,
'title' => 'Cart' 'title' => 'Cart',
] ],
], ],
])); ]));
} }

View file

@ -1,14 +1,14 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class category class category
{ {
public static function power_meters($defaults) public static function power_meters($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', context: array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', context: array_merge($defaults, [
'child_template' => 'lib/page/category.twig', 'child_template' => 'lib/pages/category.twig',
'page_title' => 'Power Meters - ' . $_ENV['APP_NAME'], 'page_title' => 'Power Meters - ' . $_ENV['APP_NAME'],
'product_category' => 'power_meters', 'product_category' => 'power_meters',
])); ]));
} }
} }

View file

@ -1,26 +1,27 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class checkout class checkout
{ {
public static function shipping_billing($defaults) public static function shipping_billing($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/flow.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/flow.twig', array_merge($defaults, [
'child_template' => 'checkout/shipping_billing.twig', 'child_template' => 'checkout/shipping_billing.twig',
'page_title' => 'Checkout with ' . $_ENV['APP_NAME'], 'page_title' => 'Checkout with ' . $_ENV['APP_NAME'],
])); ]));
} }
public static function review_pay($defaults) public static function review_pay($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/flow.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/flow.twig', array_merge($defaults, [
'child_template' => 'checkout/review_pay.twig', 'child_template' => 'checkout/review_pay.twig',
'page_title' => 'Review & Payment | ' . $_ENV['APP_NAME'] 'page_title' => 'Review & Payment | ' . $_ENV['APP_NAME'],
])); ]));
} }
public static function confirmed($defaults) public static function confirmed($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/flow.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/flow.twig', array_merge($defaults, [
'child_template' => 'checkout/confirmed.twig', 'child_template' => 'checkout/confirmed.twig',
'page_title' => 'Order Recieved! - Thank You' 'page_title' => 'Order Recieved! - Thank You',
])); ]));
} }
} }

View file

@ -1,12 +1,13 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class home class home
{ {
public static function index($defaults) public static function index($defaults)
{ {
echo $GLOBALS['twig']->render(name: 'lib/page/index.twig', context: array_merge($defaults, [ echo $GLOBALS['twig']->render(name: 'lib/pages/index.twig', context: array_merge($defaults, [
'child_template' => 'home.twig', 'child_template' => 'home.twig',
'page_title' => $_ENV['APP_NAME'] . ": Specialty Hardware" 'page_title' => $_ENV['APP_NAME'] . ": Specialty Hardware",
])); ]));
} }
} }

View file

@ -1,11 +1,10 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
use app\app; use app\app;
use app\models\invoices; use app\models\invoices;
$invoice = 'lnbc234340n1pnm5uh2pp5pwekmjzj3hgadahfeprtc6f2ppmhnqjzh556q4fvve8z953v3t0sdqqcqzysxqrrsssp5uq25yjnmvdpnglmv42nf64wk0pugrynq549f3wgghgtkfapwdfhq9qxpqysgqyjq2ewqkm6s2dlhuruuc4k9md22wraz829tlhfeuxrsnwmephfkjz8e9g7j6373989mfccajy3cxexac8xu6yen4qfs4947fkrg9ynsq7x72ze'; $invoice = 'lnbc234340n1pnm5uh2pp5pwekmjzj3hgadahfeprtc6f2ppmhnqjzh556q4fvve8z953v3t0sdqqcqzysxqrrsssp5uq25yjnmvdpnglmv42nf64wk0pugrynq549f3wgghgtkfapwdfhq9qxpqysgqyjq2ewqkm6s2dlhuruuc4k9md22wraz829tlhfeuxrsnwmephfkjz8e9g7j6373989mfccajy3cxexac8xu6yen4qfs4947fkrg9ynsq7x72ze';
use Jorijn\Bitcoin\Bolt11\Encoder\PaymentRequestDecoder; use Jorijn\Bitcoin\Bolt11\Encoder\PaymentRequestDecoder;
use Jorijn\Bitcoin\Bolt11\Model\Tag;
use Jorijn\Bitcoin\Bolt11\Normalizer\PaymentRequestDenormalizer; use Jorijn\Bitcoin\Bolt11\Normalizer\PaymentRequestDenormalizer;
class lnurlp class lnurlp
@ -39,10 +38,10 @@ class lnurlp
} }
// for when the user is not registered // for when the user is not registered
$user = users::getByNpub($username); $user = users::getByNpub($username);
if (!$user){ if (! $user) {
returnJson([ returnJson([
'status' => 'ERROR', 'status' => 'ERROR',
'reason' => "@$username is not registered" 'reason' => "@$username is not registered",
]); ]);
} }
// for when the client makes it's first call (querying the lightning address) // for when the client makes it's first call (querying the lightning address)
@ -72,7 +71,7 @@ class lnurlp
} }
$res = json_decode(file_get_contents($proxy_url), true); $res = json_decode(file_get_contents($proxy_url), true);
if ($res['status'] === 'OK'){ if ($res['status'] === 'OK') {
// subscribe to this invoice by adding to our db // subscribe to this invoice by adding to our db
$invoice = $res['pr']; $invoice = $res['pr'];
$decoder = new PaymentRequestDecoder(); $decoder = new PaymentRequestDecoder();
@ -92,7 +91,7 @@ class lnurlp
'status' => 'OK', 'status' => 'OK',
'pr' => $invoice, 'pr' => $invoice,
'routes' => $res['routes'], 'routes' => $res['routes'],
'verify' => "https://$host/lnurlp?verify=$proxy_verify" 'verify' => "https://$host/lnurlp?verify=$proxy_verify",
]); ]);
} else { } else {
returnJson($res); returnJson($res);

View file

@ -1,10 +1,11 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class lost class lost
{ {
public static function index($defaults) public static function index($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => '404.twig', 'child_template' => '404.twig',
'page_title' => 'Not Found - ' . $_ENV['APP_NAME'], 'page_title' => 'Not Found - ' . $_ENV['APP_NAME'],
])); ]));

View file

@ -1,21 +1,22 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
use app\app; use app\app;
use app\models\users;
use app\models\magic_links; use app\models\magic_links;
use app\models\users;
class magic_link class magic_link
{ {
public static function index() public static function index()
{ {
$token = $_GET['token'] ?? null; $token = $_GET['token'] ?? null;
if (!$token) { if (! $token) {
$_SESSION['error'] = "Invalid or expired link."; $_SESSION['error'] = "Invalid or expired link.";
header('Location: /account/login'); header('Location: /account/login');
exit; exit;
} else { } else {
$link = magic_links::validateToken(token: $token); $link = magic_links::validateToken(token: $token);
if (!$link) { if (! $link) {
$_SESSION['error'] = "Invalid or expired link."; $_SESSION['error'] = "Invalid or expired link.";
header('Location: /account/login'); header('Location: /account/login');
exit; exit;
@ -24,7 +25,7 @@ class magic_link
if ($user) { // user with this email exists, log them in if ($user) { // user with this email exists, log them in
$_SESSION['user_email'] = $link['email']; $_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
if (!$user['verified']) { if (! $user['verified']) {
users::verify($link['email']); users::verify($link['email']);
} }
header('Location: /account'); header('Location: /account');
@ -36,7 +37,7 @@ class magic_link
users::updateEmailById($user_id, $link['email']); users::updateEmailById($user_id, $link['email']);
$_SESSION['user_email'] = $link['email']; $_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user_id; $_SESSION['user_id'] = $user_id;
if (!$user['verified']) { if (! $user['verified']) {
users::verify($link['email']); users::verify($link['email']);
} }
header('Location: /account'); header('Location: /account');

View file

@ -1,34 +1,35 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
class support class support
{ {
public static function index($defaults) public static function index($defaults)
{ {
$GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'support/ask.twig', 'child_template' => 'support/ask.twig',
'page_title' => $_ENV['APP_NAME'] . ': Frequently Asked Questions', 'page_title' => $_ENV['APP_NAME'] . ': Frequently Asked Questions',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => null, 'url' => null,
'title' => 'Support' 'title' => 'Support',
] ],
] ],
])); ]));
} }
public static function bitcoin($defaults) public static function bitcoin($defaults)
{ {
$GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'support/bitcoin.twig', 'child_template' => 'support/bitcoin.twig',
'page_title' => $_ENV['APP_NAME'] . ' Bitcoin Accepted', 'page_title' => $_ENV['APP_NAME'] . ' Bitcoin Accepted',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/support/ask', 'url' => '/support/ask',
'title' => 'Support' 'title' => 'Support',
], ],
[ [
'url' => null, 'url' => null,
'title' => 'Bitcoin' 'title' => 'Bitcoin',
] ],
], ],
])); ]));
} }

View file

@ -1,21 +1,20 @@
<?php <?php
namespace app\controllers; namespace app\controllers;
use app\controllers\lost;
use app\models\transactions; use app\models\transactions;
use app\models\users; use app\models\users;
use app\controllers\lost;
class transaction class transaction
{ {
public static function view($defaults, $txid) public static function view($defaults, $txid)
{ {
$tx = transactions::getById($txid); $tx = transactions::getById($txid);
if (!$tx) { if (! $tx) {
lost::index($defaults); lost::index($defaults);
} }
$user = users::getById($tx['user_id']); $user = users::getById($tx['user_id']);
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'transaction.twig', 'child_template' => 'transaction.twig',
'page_title' => 'Transaction Reciept #' . $txid, 'page_title' => 'Transaction Reciept #' . $txid,
'tx' => $tx, 'tx' => $tx,
@ -23,69 +22,69 @@ class transaction
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => "/transaction/" . $txid, 'url' => "/transaction/" . $txid,
'title' => 'Transaction Reciept' 'title' => 'Transaction Reciept',
] ],
], ],
])); ]));
} }
public static function users($defaults) public static function users($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/users.twig', 'child_template' => 'admin/users.twig',
'page_title' => $_ENV['APP_NAME'] . ' Users', 'page_title' => $_ENV['APP_NAME'] . ' Users',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/users', 'url' => '/admin/users',
'title' => 'Users' 'title' => 'Users',
] ],
], ],
])); ]));
} }
public static function orders($defaults) public static function orders($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/orders.twig', 'child_template' => 'admin/orders.twig',
'page_title' => $_ENV['APP_NAME'] . ' Orders', 'page_title' => $_ENV['APP_NAME'] . ' Orders',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/orders', 'url' => '/admin/orders',
'title' => 'Orders' 'title' => 'Orders',
] ],
], ],
])); ]));
} }
public static function returns($defaults) public static function returns($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/returns.twig', 'child_template' => 'admin/returns.twig',
'page_title' => $_ENV['APP_NAME'] . ' Returns', 'page_title' => $_ENV['APP_NAME'] . ' Returns',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/returns', 'url' => '/admin/returns',
'title' => 'Returns' 'title' => 'Returns',
] ],
], ],
])); ]));
} }
public static function transactions($defaults) public static function transactions($defaults)
{ {
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/transactions/index.twig', 'child_template' => 'admin/transactions/index.twig',
'page_title' => $_ENV['APP_NAME'] . ' Transactions', 'page_title' => $_ENV['APP_NAME'] . ' Transactions',
'recent_sats' => transactions::getRecent(10, 'sats'), 'recent_sats' => transactions::getRecent(10, 'sats'),
@ -97,12 +96,12 @@ class transaction
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/transactions', 'url' => '/admin/transactions',
'title' => 'Transactions' 'title' => 'Transactions',
] ],
], ],
])); ]));
} }
@ -113,23 +112,23 @@ class transaction
$amount = $_POST['amount'] ?? null; $amount = $_POST['amount'] ?? null;
$currency = $_POST['currency']; $currency = $_POST['currency'];
$user_identifier = $_POST['user_identifier'] ?? null; $user_identifier = $_POST['user_identifier'] ?? null;
if (!$amount || !$user_identifier) { if (! $amount || ! $user_identifier) {
$_SESSION['error'] = !$amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction."; $_SESSION['error'] = ! $amount ? "Please enter an amount for the transaction." : "Please enter a user email or id for the transaction.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} else { } else {
if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) { if (strpos($user_identifier, '@') !== false && strpos($user_identifier, '.') !== false) {
$user = users::getByEmail($user_identifier); $user = users::getByEmail($user_identifier);
} elseif (is_numeric($user_identifier)) { } elseif (is_numeric($user_identifier)) {
$user = users::getById((int)$user_identifier); $user = users::getById((int) $user_identifier);
} else { } else {
$_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID."; $_SESSION['error'] = "Invalid user identifier. Please enter a valid email or user ID.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} }
if (!$user) { if (! $user) {
$_SESSION['error'] = "User not found. Please enter a valid email or user ID."; $_SESSION['error'] = "User not found. Please enter a valid email or user ID.";
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
} else { } else {
if($_POST['confirm']){ if ($_POST['confirm']) {
// create the transaction // create the transaction
$txid = transactions::add($user['id'], 'CREDIT', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0); $txid = transactions::add($user['id'], 'CREDIT', $currency == 'cents' ? $amount : 0, $currency == 'sats' ? $amount : 0);
header('Location: /transaction/' . $txid); header('Location: /transaction/' . $txid);
@ -143,22 +142,22 @@ class transaction
} }
} }
} }
echo $GLOBALS['twig']->render('lib/page/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/transactions/add.twig', 'child_template' => 'admin/transactions/add.twig',
'page_title' => 'Add a Transaction', 'page_title' => 'Add a Transaction',
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/admin', 'url' => '/admin',
'title' => 'Admin' 'title' => 'Admin',
], ],
[ [
'url' => '/admin/transactions', 'url' => '/admin/transactions',
'title' => 'Transactions' 'title' => 'Transactions',
], ],
[ [
'url' => '/admin/transactions/add', 'url' => '/admin/transactions/add',
'title' => 'Add' 'title' => 'Add',
] ],
], ],
])); ]));
} }

View file

@ -2,12 +2,14 @@
namespace app\models; namespace app\models;
use app\app; use app\app;
class addresses class addresses
{ {
public static function init() public static function init()
{ {
app::$db->exec("CREATE TABLE IF NOT EXISTS addresses ( app::$db->exec("CREATE TABLE IF NOT EXISTS addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL, name TEXT NOT NULL,
company TEXT, company TEXT,
addressLine1 TEXT NOT NULL, addressLine1 TEXT NOT NULL,
@ -16,57 +18,131 @@ class addresses
state TEXT NOT NULL, state TEXT NOT NULL,
zip TEXT NOT NULL, zip TEXT NOT NULL,
phone TEXT, phone TEXT,
billing BOOLEAN NOT NULL, hash TEXT NOT NULL,
shipping BOOLEAN NOT NULL FOREIGN KEY (user_id) REFERENCES users(id)
)"); )");
} }
public static function validatePost($type) public static function validatePost($type = "")
{ {
$name = $_POST["{$type}_name"]; $name = $_POST["{$type}name"];
$company = $_POST["{$type}_company"] ?? null; $company = $_POST["{$type}company"] ?? null;
$addressLine2 = $_POST["{$type}_addressLine2"] ?? null; $addressLine2 = $_POST["{$type}addressLine2"] ?? null;
$addressLine1 = $_POST["{$type}_addressLine1"]; $addressLine1 = $_POST["{$type}addressLine1"];
$city = $_POST["{$type}_city"]; $city = $_POST["{$type}city"];
$state = $_POST["{$type}_state"]; $state = $_POST["{$type}state"];
$zip = $_POST["{$type}_zip"]; $zip = $_POST["{$type}zip"];
$phone = $_POST["{$type}_phone"]; $phone = $_POST["{$type}phone"];
$email = $_POST["{$type}email"] ?? null; // Assuming email is part of the form
// check all required fields are set // check all required fields are set
if (empty($name) || empty($addressLine1) || empty($city) || empty($state) || empty($zip) || empty($phone)) { if (empty($name) || empty($addressLine1) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
$_SESSION['error'] = "Missing required {$type} information."; $_SESSION['error'] = "Missing required {$type} information.";
} return ["error" => "Missing required {$type} information."];
$url = "https://nominatim.openstreetmap.org/search?" . http_build_query([
"q" => implode(" ", array_filter([$addressLine1, $addressLine2, $city, $state, $zip])),
"format" => "json",
"addressdetails" => 1,
"limit" => 1
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_USERAGENT, "AddressValidator/1.0");
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (empty($data)) {
return ["error" => "Address not found"];
}
$addressDetails = $data[0]['address'];
return [
'name' => $name,
'company' => $company,
"addressLine1" => ($addressDetails['house_number'] ?? '') . ' ' . ($addressDetails['building'] ?? '') . ' ' . ($addressDetails['road'] ?? ''),
"addressLine2" => $addressLine2,
"city" => $addressDetails['city'] ?? $addressDetails['town'] ?? $addressDetails['village'] ?? '',
"state" => $addressDetails['state_code'] ?? ($addressDetails['state'] ?? ''),
"zip" => $addressDetails['postcode'] ?? '',
'phone' => $phone
];
} }
public static function add($name, $company, $addressLine1, $addressLine2, $city, $state, $zip, $phone, $billing, $shipping) // Set up the cURL request
$ch = curl_init();
// Set the Shippo API endpoint for address validation
$url = sprintf(
'https://api.goshippo.com/v2/addresses/validate?address_line_1=%s&city_locality=%s&state_province=%s&postal_code=%s&country_code=US&organization=%s',
urlencode($addressLine1),
urlencode($city),
urlencode($state),
urlencode($zip),
urlencode($company)
);
// Set the cURL options
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: ShippoToken ' . $_ENV['SHIPPO_API_KEY'],
]);
// Execute the cURL request
$response = curl_exec($ch);
curl_close($ch);
$validated_address = json_decode($response, true);
// print_r($response);
// Check the status of the validation
if (isset($validated_address['analysis']['validation_result']['value']) && $validated_address['analysis']['validation_result']['value'] === 'valid') {
$result = [
'name' => $name,
'company' => $company,
"addressLine1" => $addressLine1,
"addressLine2" => $addressLine2,
"city" => $city,
"state" => $state,
"zip" => $zip,
'phone' => $phone,
];
} elseif (isset($validated_address['recommended_address']['confidence_result']['score']) && $validated_address['recommended_address']['confidence_result']['score'] === 'high') {
$recommended = $validated_address['recommended_address'];
$result = [
'name' => $name,
'company' => $company,
"addressLine1" => $recommended['address_line_1'],
"addressLine2" => $addressLine2,
"city" => $recommended['city_locality'],
"state" => $recommended['state_province'],
"zip" => $recommended['postal_code'],
'phone' => $phone,
];
} else {
$error_message = "Address is invalid.";
if (isset($validated_address['analysis']['validation_result']['reasons'])) {
foreach ($validated_address['analysis']['validation_result']['reasons'] as $reason) {
if ($reason['type'] === 'error') {
$error_message = $reason['description'];
break;
}
}
}
$_SESSION['error'] = $error_message;
return ["error" => $error_message];
}
$result['hash'] = hash("sha256", json_encode($result));
$existing = self::getByHash($result['hash']);
if ($existing) {
$_SESSION['error'] = "The address already exists.";
return ["error" => $_SESSION['error']];
}
return $result;
}
public static function updateUserIdById($bill_id, $user_id)
{
$query = "UPDATE addresses SET user_id = :user_id WHERE id = :bill_id";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':bill_id', $bill_id);
$stmt->execute();
}
public static function getByHash($hash)
{
$query = "SELECT * FROM addresses WHERE hash = :hash";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':hash', $hash);
$stmt->execute();
return $stmt->fetch(\PDO::FETCH_ASSOC);
}
public static function getByUserId($user_id)
{
$query = "SELECT * FROM addresses WHERE user_id = :user_id";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public static function add($user_id, $name, $company, $addressLine1, $addressLine2, $city, $state, $zip, $phone, $hash)
{ {
$query = "INSERT INTO addresses ( $query = "INSERT INTO addresses (
user_id,
name, name,
company, company,
addressLine1, addressLine1,
@ -75,9 +151,9 @@ class addresses
state, state,
zip, zip,
phone, phone,
billing, hash
shipping
) VALUES ( ) VALUES (
:user_id,
:name, :name,
:company, :company,
:addressLine1, :addressLine1,
@ -86,10 +162,10 @@ class addresses
:state, :state,
:zip, :zip,
:phone, :phone,
:billing, :hash
:shipping
)"; )";
$stmt = app::$db->prepare($query); $stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':name', $name); $stmt->bindParam(':name', $name);
$stmt->bindParam(':company', $company); $stmt->bindParam(':company', $company);
$stmt->bindParam(':addressLine1', $addressLine1); $stmt->bindParam(':addressLine1', $addressLine1);
@ -98,8 +174,7 @@ class addresses
$stmt->bindParam(':state', $state); $stmt->bindParam(':state', $state);
$stmt->bindParam(':zip', $zip); $stmt->bindParam(':zip', $zip);
$stmt->bindParam(':phone', $phone); $stmt->bindParam(':phone', $phone);
$stmt->bindParam(':billing', $billing); $stmt->bindParam(':hash', $hash);
$stmt->bindParam(':shipping', $shipping);
$stmt->execute(); $stmt->execute();
return app::$db->lastInsertId(); return app::$db->lastInsertId();
} }

View file

@ -25,7 +25,7 @@ class cart_items
$stmt->execute([ $stmt->execute([
'cart_id' => $cartId, 'cart_id' => $cartId,
'product_id' => $productId, 'product_id' => $productId,
'quantity' => $quantity 'quantity' => $quantity,
]); ]);
} }
@ -34,7 +34,7 @@ class cart_items
$stmt = app::$db->prepare("UPDATE cart_items SET quantity = :quantity WHERE cart_item_id = :cart_item_id"); $stmt = app::$db->prepare("UPDATE cart_items SET quantity = :quantity WHERE cart_item_id = :cart_item_id");
$stmt->execute([ $stmt->execute([
'cart_item_id' => $cartItemId, 'cart_item_id' => $cartItemId,
'quantity' => $quantity 'quantity' => $quantity,
]); ]);
} }

View file

@ -2,10 +2,8 @@
namespace app\models; namespace app\models;
use app\app; use app\app;
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP; use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
class emails class emails
{ {

View file

@ -1,7 +1,8 @@
<?php <?php
namespace app\models; namespace app\models;
use app\models\transactions;
use app\app; use app\app;
use app\models\transactions;
class invoices class invoices
{ {
@ -93,4 +94,3 @@ class invoices
} }
} }
} }

View file

@ -29,7 +29,7 @@ class order_items
$stmt->execute([ $stmt->execute([
'order_id' => $orderId, 'order_id' => $orderId,
'product_id' => $productId, 'product_id' => $productId,
'quantity' => $quantity 'quantity' => $quantity,
]); ]);
} }
@ -42,7 +42,7 @@ class order_items
$stmt = app::$db->prepare("UPDATE order_items SET quantity = :quantity WHERE order_item_id = :order_item_id"); $stmt = app::$db->prepare("UPDATE order_items SET quantity = :quantity WHERE order_item_id = :order_item_id");
$stmt->execute([ $stmt->execute([
'order_item_id' => $orderItemId, 'order_item_id' => $orderItemId,
'quantity' => $quantity 'quantity' => $quantity,
]); ]);
} }

View file

@ -7,7 +7,7 @@ class orders
{ {
const STATUSES = [ const STATUSES = [
'SHIPPED', 'PENDING', 'HOLD', 'PARTIAL', 'SHIPPED', 'PENDING', 'HOLD', 'PARTIAL',
'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING' 'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING',
]; ];
public static function init() public static function init()
@ -33,7 +33,7 @@ class orders
'user_id' => $userId, 'user_id' => $userId,
'value_sats' => $valueSats, 'value_sats' => $valueSats,
'value_cents' => $valueCents, 'value_cents' => $valueCents,
'status' => $status 'status' => $status,
]); ]);
return app::$db->lastInsertId(); return app::$db->lastInsertId();
@ -46,7 +46,7 @@ class orders
$stmt = app::$db->prepare("UPDATE orders SET status = :status WHERE order_id = :order_id"); $stmt = app::$db->prepare("UPDATE orders SET status = :status WHERE order_id = :order_id");
$stmt->execute([ $stmt->execute([
'order_id' => $orderId, 'order_id' => $orderId,
'status' => $status 'status' => $status,
]); ]);
} }
@ -66,7 +66,7 @@ class orders
private static function validateStatus(string $status) private static function validateStatus(string $status)
{ {
if (!in_array($status, self::STATUSES, true)) { if (! in_array($status, self::STATUSES, true)) {
throw new \InvalidArgumentException("Invalid order status: $status"); throw new \InvalidArgumentException("Invalid order status: $status");
} }
} }

View file

@ -64,7 +64,7 @@ class products
':image_url_8' => $images[8] ?? null, ':image_url_8' => $images[8] ?? null,
':image_url_9' => $images[9] ?? null, ':image_url_9' => $images[9] ?? null,
':image_url_10' => $images[10] ?? null, ':image_url_10' => $images[10] ?? null,
':image_url_11' => $images[11] ?? null ':image_url_11' => $images[11] ?? null,
]); ]);
} }
} }

View file

@ -34,7 +34,7 @@ class quote_items
'quote_id' => $quoteId, 'quote_id' => $quoteId,
'product_id' => $productId, 'product_id' => $productId,
'quantity' => $quantity, 'quantity' => $quantity,
'price' => $price 'price' => $price,
]); ]);
} }
@ -51,7 +51,7 @@ class quote_items
$stmt->execute([ $stmt->execute([
'quote_item_id' => $quoteItemId, 'quote_item_id' => $quoteItemId,
'quantity' => $quantity, 'quantity' => $quantity,
'price' => $price 'price' => $price,
]); ]);
} }

View file

@ -7,7 +7,7 @@ class quotes
{ {
private const STATUSES = [ private const STATUSES = [
'DRAFT', 'PUBLISHED', 'SENT', 'PURCHASED', 'DRAFT', 'PUBLISHED', 'SENT', 'PURCHASED',
'EXPIRED', 'CANCELED' 'EXPIRED', 'CANCELED',
]; ];
public static function init() public static function init()
@ -29,7 +29,7 @@ class quotes
VALUES (:user_id, :status)"); VALUES (:user_id, :status)");
$stmt->execute([ $stmt->execute([
'user_id' => $userId, 'user_id' => $userId,
'status' => $status 'status' => $status,
]); ]);
return app::$db->lastInsertId(); return app::$db->lastInsertId();
@ -42,7 +42,7 @@ class quotes
$stmt = app::$db->prepare("UPDATE quotes SET status = :status WHERE quote_id = :quote_id"); $stmt = app::$db->prepare("UPDATE quotes SET status = :status WHERE quote_id = :quote_id");
$stmt->execute([ $stmt->execute([
'quote_id' => $quoteId, 'quote_id' => $quoteId,
'status' => $status 'status' => $status,
]); ]);
} }
@ -62,7 +62,7 @@ class quotes
private static function validateStatus(string $status) private static function validateStatus(string $status)
{ {
if (!in_array($status, self::STATUSES, true)) { if (! in_array($status, self::STATUSES, true)) {
throw new \InvalidArgumentException("Invalid quote status: $status"); throw new \InvalidArgumentException("Invalid quote status: $status");
} }
} }

View file

@ -6,11 +6,11 @@ use app\app;
class subscriptions class subscriptions
{ {
const STATES = [ const STATES = [
'TRIAL', 'START', 'RENEWAL' 'TRIAL', 'START', 'RENEWAL',
]; ];
const STATUS = [ const STATUS = [
'COMPLETED', 'CANCELED' 'COMPLETED', 'CANCELED',
]; ];
public static function init() public static function init()
@ -29,7 +29,7 @@ class subscriptions
);"); );");
} }
public static function createSubscription( $userId, $productId, $state, $startDate, $renewAt, $invoiceDate) public static function createSubscription($userId, $productId, $state, $startDate, $renewAt, $invoiceDate)
{ {
self::validateState($state); self::validateState($state);
self::validateStatus($status); self::validateStatus($status);
@ -43,7 +43,7 @@ class subscriptions
'status' => $status, 'status' => $status,
'start_date' => $startDate, 'start_date' => $startDate,
'renews_at' => $renewAt, 'renews_at' => $renewAt,
'invoice_date' => $invoiceDate 'invoice_date' => $invoiceDate,
]); ]);
return app::$db->lastInsertId(); return app::$db->lastInsertId();
@ -56,7 +56,7 @@ class subscriptions
$stmt = app::$db->prepare("UPDATE subscriptions SET state = :state WHERE subscription_id = :subscription_id"); $stmt = app::$db->prepare("UPDATE subscriptions SET state = :state WHERE subscription_id = :subscription_id");
$stmt->execute([ $stmt->execute([
'subscription_id' => $subscriptionId, 'subscription_id' => $subscriptionId,
'state' => $state 'state' => $state,
]); ]);
} }
@ -67,7 +67,7 @@ class subscriptions
$stmt = app::$db->prepare("UPDATE subscriptions SET status = :status WHERE subscription_id = :subscription_id"); $stmt = app::$db->prepare("UPDATE subscriptions SET status = :status WHERE subscription_id = :subscription_id");
$stmt->execute([ $stmt->execute([
'subscription_id' => $subscriptionId, 'subscription_id' => $subscriptionId,
'status' => $status 'status' => $status,
]); ]);
} }
@ -87,14 +87,14 @@ class subscriptions
private static function validateState(string $state) private static function validateState(string $state)
{ {
if (!in_array($state, self::STATES, true)) { if (! in_array($state, self::STATES, true)) {
throw new \InvalidArgumentException("Invalid subscription state: $state"); throw new \InvalidArgumentException("Invalid subscription state: $state");
} }
} }
private static function validateStatus(string $status) private static function validateStatus(string $status)
{ {
if (!in_array($status, self::STATUS, true)) { if (! in_array($status, self::STATUS, true)) {
throw new \InvalidArgumentException("Invalid subscription status: $status"); throw new \InvalidArgumentException("Invalid subscription status: $status");
} }
} }

View file

@ -32,7 +32,7 @@ class transactions
public static function add($user_id, $transaction_type, $cents, $sats) public static function add($user_id, $transaction_type, $cents, $sats)
{ {
if (!in_array($transaction_type, self::TYPES)) { if (! in_array($transaction_type, self::TYPES)) {
throw new \Exception("Invalid transaction type."); throw new \Exception("Invalid transaction type.");
} }
if ($cents < 0 || $sats < 0) { if ($cents < 0 || $sats < 0) {
@ -72,7 +72,7 @@ class transactions
public static function getWhales($n, $currency) public static function getWhales($n, $currency)
{ {
if (!in_array($currency, ['cents', 'sats'])) { if (! in_array($currency, ['cents', 'sats'])) {
throw new \Exception("Invalid currency type."); throw new \Exception("Invalid currency type.");
} }
$query = "SELECT user_id, COALESCE(SUM($currency), 0) AS total FROM transactions GROUP BY user_id ORDER BY total DESC LIMIT :n"; $query = "SELECT user_id, COALESCE(SUM($currency), 0) AS total FROM transactions GROUP BY user_id ORDER BY total DESC LIMIT :n";
@ -84,7 +84,7 @@ class transactions
public static function getLiabilities($currency) public static function getLiabilities($currency)
{ {
if (!in_array($currency, ['cents', 'sats'])) { if (! in_array($currency, ['cents', 'sats'])) {
throw new \Exception("Invalid currency type."); throw new \Exception("Invalid currency type.");
} }
$query = "SELECT COALESCE(SUM($currency), 0) AS total FROM transactions"; $query = "SELECT COALESCE(SUM($currency), 0) AS total FROM transactions";

View file

@ -1,61 +0,0 @@
<?php
namespace app\models;
use app\app;
use PDO; // Added PDO class import
class user_addresses
{
public static function init()
{
app::$db->exec("CREATE TABLE IF NOT EXISTS user_addresses (
user_id INTEGER NOT NULL,
address_id INTEGER NOT NULL,
PRIMARY KEY (user_id, address_id),
FOREIGN KEY (address_id) REFERENCES addresses(id)
)");
}
public static function getShippingByUserId($id)
{
$query = "SELECT a.* FROM users u
JOIN user_addresses ua ON u.id = ua.user_id
JOIN addresses a ON ua.address_id = a.id
WHERE u.id = :id AND a.shipping = 1";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
return [$addrs];
}
public static function getBillingByUserId($id)
{
$query = "SELECT a.* FROM users u
JOIN user_addresses ua ON u.id = ua.user_id
JOIN addresses a ON ua.address_id = a.id
WHERE u.id = :id AND a.billing = 1";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$addrs = $stmt->fetch(\PDO::FETCH_ASSOC);
return [$addrs];
}
public static function add($user_id, $address_id)
{
$query = "INSERT INTO user_addresses (
user_id,
address_id
) VALUES (
:user_id,
:address_id
)";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':address_id', $address_id);
$stmt->execute();
return app::$db->lastInsertId();
}
}

View file

@ -2,6 +2,7 @@
namespace app\models; namespace app\models;
use app\app; use app\app;
use app\models\addresses;
use swentel\nostr\Key\Key; use swentel\nostr\Key\Key;
class users class users
@ -113,7 +114,12 @@ class users
$stmt->bindParam(':nsec', $nsec); $stmt->bindParam(':nsec', $nsec);
$stmt->bindParam(':npub', $npub); $stmt->bindParam(':npub', $npub);
$stmt->execute(); $stmt->execute();
return app::$db->lastInsertId(); $user_id = app::$db->lastInsertId();
addresses::updateUserIdById($ship_id, $user_id);
if ($ship_id != $bill_id) {
addresses::updateUserIdById($bill_id, $user_id);
}
return $user_id;
} }
public static function verify($email) public static function verify($email)

View file

@ -22,4 +22,3 @@ use app\models\invoices;
app::init_db(); app::init_db();
invoices::checkAll(); invoices::checkAll();

View file

@ -13,48 +13,33 @@ Dotenv\Dotenv::createImmutable(__DIR__ . '/../../')->load();
use app\app; use app\app;
app::init_db(); app::init_db();
// db models go brrr...
use app\models\addresses; use app\models\addresses;
addresses::init();
use app\models\cart_items;
cart_items::init();
use app\models\carts; use app\models\carts;
carts::init(); use app\models\cart_items;
use app\models\emails; use app\models\emails;
emails::init();
use app\models\invoices; use app\models\invoices;
invoices::init();
use app\models\magic_links; use app\models\magic_links;
magic_links::init();
use app\models\order_items;
order_items::init();
use app\models\orders; use app\models\orders;
orders::init(); use app\models\order_items;
use app\models\products; use app\models\products;
products::init();
use app\models\quote_items;
quote_items::init();
use app\models\quotes; use app\models\quotes;
quotes::init(); use app\models\quote_items;
use app\models\subscriptions; use app\models\subscriptions;
subscriptions::init();
use app\models\transactions; use app\models\transactions;
transactions::init();
use app\models\user_addresses;
user_addresses::init();
use app\models\users; use app\models\users;
// db models go brrr...
addresses::init();
carts::init();
cart_items::init();
emails::init();
invoices::init();
magic_links::init();
order_items::init();
orders::init();
products::init();
quote_items::init();
quotes::init();
subscriptions::init();
transactions::init();
users::init(); users::init();

View file

@ -0,0 +1 @@
confirm the address

View file

@ -0,0 +1 @@
edit the address

View file

@ -2,7 +2,9 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Payment Methods</h3> <h3 class="text-2xl font-semibold">
Payment Methods
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/empty.twig' with { {% include 'lib/empty.twig' with {
@ -11,20 +13,25 @@
subtitle: 'Add a payment method' subtitle: 'Add a payment method'
} %} } %}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Billing Address</h3> <h3 class="text-2xl font-semibold">
Billing Address
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<p class="text-xs">Your billing information must match the information associatied with the credit card making the purchase.</p> <p class="text-xs">
<div class='flex flex-col'> Your billing information must match the information associatied with the credit card making the purchase.
<span>{{ default_billing.name }}</span> </p>
<span>{{ default_billing.company }}</span> {% for address in addresses %}
<span>{{ default_billing.addressLine1 }}</span> {% if address.id == user.billing_address_id %}
<span>{{ default_billing.addressLine2 }}</span> {% include 'lib/address.twig' with {
<span>{{ default_billing.city }}, {{ default_billing.state }} {{ default_billing.zip }}</span> address: address,
<span>{{ default_billing.phone }}</span> edit_url: '/account/billing/edit/' ~ address.id,
</div> delete_url: '/account/billing/delete/' ~ address.id
} %}
{% endif %}
{% endfor %}
</div> </div>
<form action="/account/billing" method="post" class="flex flex-col gap-2"> <form action="/account/billing" method="post" class="flex flex-col gap-2">
{% include 'lib/form/address.twig' with { {% include 'lib/forms/address.twig' with {
action: 'billing' action: 'billing'
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/button.twig' with {
@ -32,9 +39,17 @@
onclick: 'this.parentNode.submit()', onclick: 'this.parentNode.submit()',
} %} } %}
</form> </form>
{% if addresses|length > 1 %}
{% include 'lib/rule.twig' with { {% include 'lib/rule.twig' with {
text: 'OR' text: 'OR'
} %} } %}
<span>Use saved address</span> {% for address in addresses %}
{% include 'lib/address.twig' with {
address: address,
edit_url: '/account/billing/edit/' ~ address.id,
delete_url: '/account/billing/delete/' ~ address.id
} %}
{% endfor %}
{% endif %}
</div> </div>
</section> </section>

View file

@ -1,10 +1,12 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
<h3 class="text-2xl font-semibold">Profile</h3> <h3 class="text-2xl font-semibold">
Profile
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<form action="/account/profile" method="post"> <form action="/account/profile" method="post">
{% include 'lib/form/profile.twig' with { {% include 'lib/forms/profile.twig' with {
name: user.name, name: user.name,
company_name: user.company_name, company_name: user.company_name,
company_type: user.company_type, company_type: user.company_type,
@ -17,14 +19,19 @@
</form> </form>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Email</h3> <h3 class="text-2xl font-semibold">
Email
</h3>
<form action="/account/email" method="post"> <form action="/account/email" method="post">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'email', name: 'email',
value: user.email value: user.email
} %} } %}
<h4 class="font-semibold">Verified: {{ user.verified ? 'Yes' : 'No' }}</h4> <h4 class="font-semibold">
Verified:
{{ user.verified ? 'Yes' : 'No' }}
</h4>
{% include 'lib/button.twig' with { {% include 'lib/button.twig' with {
label: 'Save Email', label: 'Save Email',
onclick: 'this.parentNode.submit()', onclick: 'this.parentNode.submit()',
@ -33,66 +40,102 @@
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Shipping</h3> <h3 class="text-2xl font-semibold">
<a href="/account/shipping">Edit</a> Shipping
</h3>
<a href="/account/shipping">
Edit
</a>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<div class="flex flex-col gap-1"> {% for address in addresses %}
<h4 class="font-semibold">{{ default_shipping.name }}</h4> {% if address.id == user.shipping_address_id %}
<h4 class="font-semibold">{{ default_shipping.company }}</h4> {% include 'lib/address.twig' with {
<h4 class="font-semibold">{{ default_shipping.addressLine1 }}</h4> address: address,
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4> edit_url: '/account/shipping/edit/' ~ address.id,
<h4 class="font-semibold">{{ default_shipping.city }}, {{ default_shipping.state }}, {{ default_shipping.zip }}</h4> delete_url: '/account/shipping/delete/' ~ address.id
<h4 class="font-semibold">{{ default_shipping.phone }}</h4> } %}
</div> {% endif %}
{% endfor %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Billing</h3> <h3 class="text-2xl font-semibold">
<a href="/account/billing">Edit</a> Billing
</h3>
<a href="/account/billing">
Edit
</a>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<div class="flex flex-col gap-1"> {% for address in addresses %}
<h4 class="font-semibold">{{ default_billing.name }}</h4> {% if address.id == user.billing_address_id %}
<h4 class="font-semibold">{{ default_billing.company }}</h4> {% include 'lib/address.twig' with {
<h4 class="font-semibold">{{ default_billing.addressLine1 }}</h4> address: address,
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4> edit_url: '/account/billing/edit/' ~ address.id,
<h4 class="font-semibold">{{ default_billing.city }}, {{ default_billing.state }}, {{ default_billing.zip }}</h4> delete_url: '/account/billing/delete/' ~ address.id
<h4 class="font-semibold">{{ default_billing.phone }}</h4> } %}
</div> {% endif %}
{% endfor %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Credit Card</h3> <h3 class="text-2xl font-semibold">
<a href="/account/billing">Edit</a> Credit Card
</h3>
<a href="/account/billing">
Edit
</a>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="text-2xl font-semibold">Store Credit</h3> <h3 class="text-2xl font-semibold">
<a href='#store-credit'><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></a> Store Credit
</h3>
<a href='#store-credit'>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</a>
</div> </div>
<span>$0.00</span> <span>
$0.00
</span>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="text-2xl font-semibold">Sats</h3> <h3 class="text-2xl font-semibold">
<a href='#sats'><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></a> Sats
</h3>
<a href='#sats'>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</a>
</div> </div>
<span>0</span> <span>
0
</span>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Marketing</h3> <h3 class="text-2xl font-semibold">
Marketing
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<form action="/account/promotionals" method="post" class="flex flex-col gap-4"> <form action="/account/promotionals" method="post" class="flex flex-col gap-4">
{% include 'lib/toggle.twig' with { {% include 'lib/inputs/toggle.twig' with {
label: 'Recieve coupons & more', label: 'Recieve coupons & more',
name: 'opt_in_promotional', name: 'opt_in_promotional',
on: user.opt_in_promotional on: user.opt_in_promotional
@ -106,9 +149,9 @@
</section> </section>
{% include 'lib/modal.twig' with { {% include 'lib/modal.twig' with {
id: 'store-credit', id: 'store-credit',
content: 'lib/policy/credit.twig' content: 'lib/modals/credit.twig'
} %} } %}
{% include 'lib/modal.twig' with { {% include 'lib/modal.twig' with {
id: 'sats', id: 'sats',
content: 'lib/policy/sats.twig' content: 'lib/modals/sats.twig'
} %} } %}

View file

@ -1,12 +1,14 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col gap-1 mb-4"> <div class="flex flex-col gap-1 mb-4">
<h3 class="text-2xl font-semibold">Login</h3> <h3 class="text-2xl font-semibold">
Login
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
<form action="/account/login" method="post" class="flex flex-col gap-4"> <form action="/account/login" method="post" class="flex flex-col gap-4">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'email', type: 'email',
name: 'email', name: 'email',
label: 'Email link', label: 'Email link',

View file

@ -1,6 +1,8 @@
<section> <section>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Order History</h3> <h3 class="text-2xl font-semibold">
Order History
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/empty.twig' with { {% include 'lib/empty.twig' with {

View file

@ -1,6 +1,8 @@
<section> <section>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Returns</h3> <h3 class="text-2xl font-semibold">
Returns
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/empty.twig' with { {% include 'lib/empty.twig' with {

View file

@ -1,21 +1,26 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div> <div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Saved Shipping Address</h3> <h3 class="text-2xl font-semibold">
Saved Shipping Address
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<p class="text-xs">This is your default shipping address for orders at checkout.</p> <p class="text-xs">
</div> This is your default shipping address for orders at checkout.
<div class='flex flex-col'> </p>
<span>{{ default_shipping.name }}</span>
<span>{{ default_shipping.company }}</span>
<span>{{ default_shipping.addressLine1 }}</span>
<span>{{ default_shipping.addressLine2 }}</span>
<span>{{ default_shipping.city }}, {{ default_shipping.state }} {{ default_shipping.zip }}</span>
<span>{{ default_shipping.phone }}</span>
</div> </div>
{% for address in addresses %}
{% if address.id == user.shipping_address_id %}
{% include 'lib/address.twig' with {
address: address,
edit_url: '/account/shipping/edit/' ~ address.id,
delete_url: '/account/shipping/delete/' ~ address.id
} %}
{% endif %}
{% endfor %}
</div> </div>
<form action="/account/shipping" method="post" class="flex flex-col gap-2"> <form action="/account/shipping" method="post" class="flex flex-col gap-2">
{% include 'lib/form/address.twig' with { {% include 'lib/forms/address.twig' with {
action: 'shipping' action: 'shipping'
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/button.twig' with {
@ -23,9 +28,17 @@
onclick: 'this.parentNode.submit()' onclick: 'this.parentNode.submit()'
} %} } %}
</form> </form>
{% if addresses|length > 1 %}
{% include 'lib/rule.twig' with { {% include 'lib/rule.twig' with {
text: 'OR' text: 'OR'
} %} } %}
<span>Use a saved address</span> {% for address in addresses %}
{% include 'lib/address.twig' with {
address: address,
edit_url: '/account/billing/edit/' ~ address.id,
delete_url: '/account/billing/delete/' ~ address.id
} %}
{% endfor %}
{% endif %}
</section> </section>

View file

@ -1,26 +1,34 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col"> <div class="flex flex-col">
<h3 class="font-semibold text-4xl">Create an Account</h3> <h3 class="font-semibold text-4xl">
<span class="text-sm">You can manage all your orders, addresses, and payment cards in one place!</span> Create an Account
</h3>
<span class="text-sm">
You can manage all your orders, addresses, and payment cards in one place!
</span>
</div> </div>
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
<form action="/account/signup" method="post" class="flex flex-col gap-4"> <form action="/account/signup" method="post" class="flex flex-col gap-4">
{% include 'lib/rule.twig' with { text: 'STEP 1' } %} {% include 'lib/rule.twig' with { text: 'STEP 1' } %}
<div class="flex flex-col"> <div class="flex flex-col">
<h4 class="font-semibold text-2xl">Email Address</h4> <h4 class="font-semibold text-2xl">
<span class="text-sm">Recieve login link to verify. Order and account updates will be sent here.</span> Email Address
</h4>
<span class="text-sm">
Recieve login link to verify. Order and account updates will be sent here.
</span>
</div> </div>
<div class="w-full flex items-center justify-between gap-4"> <div class="w-full flex items-center justify-between gap-4">
<div class="w-3/5"> <div class="w-3/5">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'email', name: 'email',
value: session.user_email is defined ? session.user_email : null, value: session.user_email is defined ? session.user_email : null,
readonly: session.user_email is defined ? true : null readonly: session.user_email is defined ? true : null
} %} } %}
</div> </div>
{% include 'lib/toggle.twig' with { {% include 'lib/inputs/toggle.twig' with {
label: 'Recieve coupons & more', label: 'Recieve coupons & more',
name: 'opt_in_promotional', name: 'opt_in_promotional',
on: true on: true
@ -28,10 +36,14 @@
</div> </div>
{% include 'lib/rule.twig' with { text: 'STEP 2' } %} {% include 'lib/rule.twig' with { text: 'STEP 2' } %}
<div class="flex flex-col"> <div class="flex flex-col">
<h4 class="font-semibold text-2xl">Shipping Address</h4> <h4 class="font-semibold text-2xl">
<span class="text-sm">Your orders will ship to this address (USA only).</span> Shipping Address
</h4>
<span class="text-sm">
Your orders will ship to this address (USA only).
</span>
</div> </div>
{% include 'lib/form/address.twig' with { {% include 'lib/forms/address.twig' with {
action: 'shipping', action: 'shipping',
name: session.last_post.shipping_name, name: session.last_post.shipping_name,
addressLine1: session.last_post.shipping_addressLine1, addressLine1: session.last_post.shipping_addressLine1,
@ -45,18 +57,22 @@
{% include 'lib/rule.twig' with { text: 'STEP 3' } %} {% include 'lib/rule.twig' with { text: 'STEP 3' } %}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
<h4 class="font-semibold text-2xl">Billing Address</h4> <h4 class="font-semibold text-2xl">
<span class="text-sm">Info must match the credit card making the purchase. </span> Billing Address
</h4>
<span class="text-sm">
Info must match the credit card making the purchase.
</span>
</div> </div>
</h4> </h4>
{% include 'lib/toggle.twig' with { {% include 'lib/inputs/toggle.twig' with {
label: 'Same as shipping', label: 'Same as shipping',
name: 'use_shipping', name: 'use_shipping',
on: true on: true
} %} } %}
</div> </div>
<div id="billing-address" style="display: none;"> <div id="billing-address" style="display: none;">
{% include 'lib/form/address.twig' with { {% include 'lib/forms/address.twig' with {
action: 'billing', action: 'billing',
name: session.last_post.billing_name, name: session.last_post.billing_name,
addressLine1: session.last_post.billing_addressLine1, addressLine1: session.last_post.billing_addressLine1,
@ -75,8 +91,8 @@
captcha: true captcha: true
} %} } %}
</form> </form>
</div> </div>
<script> <script>
// this bit-of-script handles show/hide the billing address form fields // this bit-of-script handles show/hide the billing address form fields
const useShippingCheckbox = document.getElementById('use_shipping'); const useShippingCheckbox = document.getElementById('use_shipping');
const billingAddress = document.getElementById('billing-address'); const billingAddress = document.getElementById('billing-address');
@ -88,5 +104,4 @@
billingAddress.style.display = 'flex'; billingAddress.style.display = 'flex';
} }
}); });
</script> </script></section>
</section>

View file

@ -1,8 +1,12 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col gap-1 mb-4"> <div class="flex flex-col gap-1 mb-4">
<h3 class="text-2xl font-semibold">Check Your Email</h3> <h3 class="text-2xl font-semibold">
<p>We have sent a verification code to your email.</p> Check Your Email
</h3>
<p>
We have sent a verification code to your email.
</p>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
@ -67,4 +71,5 @@
} %} } %}
</form> </form>
</div> </div>
</section> </section>

View file

@ -1,25 +1,45 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<h3 class="text-2xl font-semibold">Recently Sent Emails</h3> <h3 class="text-2xl font-semibold">
Recently Sent Emails
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">To</th> <th class="py-2">
<th class="py-2">From</th> To
<th class="py-2">Subject</th> </th>
<th class="py-2">Created At</th> <th class="py-2">
From
</th>
<th class="py-2">
Subject
</th>
<th class="py-2">
Created At
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for email in recent_emails %} {% for email in recent_emails %}
<tr> <tr>
<td class="border px-4 py-2">{{ email.to_email }}</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ email.from_email }}</td> {{ email.to_email }}
<td class="border px-4 py-2">{{ email.subject }}</td> </td>
<td class="border px-4 py-2">{{ email.created_at }}</td> <td class="border px-4 py-2">
{{ email.from_email }}
</td>
<td class="border px-4 py-2">
{{ email.subject }}
</td>
<td class="border px-4 py-2">
{{ email.created_at }}
</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td class="border px-4 py-2" colspan="4">No recent emails found.</td> <td class="border px-4 py-2" colspan="4">
No recent emails found.
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -1,10 +1,22 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<a href="/admin">Dashboard</a> <a href="/admin">
<a href="/admin/users">Users</a> Dashboard
<a href="/admin/orders">Orders</a> </a>
<a href="/admin/returns">Returns</a> <a href="/admin/users">
<a href="/admin/emails">Emails</a> Users
<a href="/admin/transactions">Transactions</a> </a>
<a href="/admin/orders">
Orders
</a>
<a href="/admin/returns">
Returns
</a>
<a href="/admin/emails">
Emails
</a>
<a href="/admin/transactions">
Transactions
</a>
INDEX INDEX
</section> </section>

View file

@ -6,10 +6,12 @@
<input type="hidden" name="confirm" id="confirm" value="{{ session.last_post.confirm }}"> <input type="hidden" name="confirm" id="confirm" value="{{ session.last_post.confirm }}">
{% endif %} {% endif %}
{% if session.last_post.amount is defined %} {% if session.last_post.amount is defined %}
{{ session.last_post.amount }} {{ session.last_post.currency }} {{ session.last_post.amount }}
<input type="hidden" name="amount" id="amount" value="{{ session.last_post.amount }}"> {{ session.last_post.currency }}
<input
type="hidden" name="amount" id="amount" value="{{ session.last_post.amount }}" />
{% else %} {% else %}
{% include 'lib/number_input.twig' with { {% include 'lib/inputs/number.twig' with {
id: 'amount', id: 'amount',
name: 'amount', name: 'amount',
label: 'Amount', label: 'Amount',
@ -18,10 +20,11 @@
required: true required: true
} %} } %}
{% endif %} {% endif %}
{% if session.last_post.currency is defined %} {% if session.last_post.currency %}
<input type="hidden" name="currency" id="currency" value="{{ session.last_post.currency }}"> <input
type="hidden" name="currency" id="currency" value="{{ session.last_post.currency }}" />
{% else %} {% else %}
{% include 'lib/select.twig' with { {% include 'lib/inputs/select.twig' with {
id: 'currency', id: 'currency',
name: 'currency', name: 'currency',
label: 'Currency', label: 'Currency',
@ -35,11 +38,13 @@
{% endif %} {% endif %}
{% if session.last_post.user_identifier is defined %} {% if session.last_post.user_identifier is defined %}
{% if session.last_post.email is defined %} {% if session.last_post.email is defined %}
{{ session.last_post.id }} {{ session.last_post.email }} {{ session.last_post.id }}
{{ session.last_post.email }}
{% endif %} {% endif %}
<input type="hidden" name="user_identifier" id="user_identifier" value="{{ session.last_post.user_identifier }}"> <input
type="hidden" name="user_identifier" id="user_identifier" value="{{ session.last_post.user_identifier }}" />
{% else %} {% else %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'user_identifier', name: 'user_identifier',
label: 'User Identifier', label: 'User Identifier',
@ -58,4 +63,5 @@
href: '/admin/transactions/reset' href: '/admin/transactions/reset'
} %} } %}
{% endif %} {% endif %}
</section> </section>

View file

@ -1,13 +1,13 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
<form action="/admin/transactions/add" method="post" class="flex flex-col gap-4"> <form action="/admin/transactions/add" method="post" class="flex flex-col gap-4">
{% include 'lib/number_input.twig' with { {% include 'lib/inputs/number.twig' with {
id: 'amount', id: 'amount',
name: 'amount', name: 'amount',
label: 'Amount', label: 'Amount',
required: true required: true
} %} } %}
{% include 'lib/select.twig' with { {% include 'lib/inputs/select.twig' with {
id: 'currency', id: 'currency',
name: 'currency', name: 'currency',
label: 'Currency', label: 'Currency',
@ -23,120 +23,190 @@
} %} } %}
</form> </form>
<h3 class="text-2xl font-semibold">Liabilities</h3> <h3 class="text-2xl font-semibold">
Liabilities
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">Currency</th> <th class="py-2">
<th class="py-2">Total Liability</th> Currency
</th>
<th class="py-2">
Total Liability
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td class="border px-4 py-2">Sats</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ sats_liability }}</td> Sats
</td>
<td class="border px-4 py-2">
{{ sats_liability }}
</td>
</tr> </tr>
<tr> <tr>
<td class="border px-4 py-2">Cents</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ cents_liability }}</td> Cents
</td>
<td class="border px-4 py-2">
{{ cents_liability }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h3 class="text-2xl font-semibold">Sats Transactions</h3> <h3 class="text-2xl font-semibold">
Sats Transactions
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">Type</th> <th class="py-2">
<th class="py-2">Amount (Sats)</th> Type
<th class="py-2">Date</th> </th>
<th class="py-2">
Amount (Sats)
</th>
<th class="py-2">
Date
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if recent_sats is not empty %} {% if recent_sats is not empty %}
{% for transaction in recent_sats %} {% for transaction in recent_sats %}
<tr> <tr>
<td class="border px-4 py-2">{{ transaction.type }}</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ transaction.sats }}</td> {{ transaction.type }}
<td class="border px-4 py-2">{{ transaction.date }}</td> </td>
<td class="border px-4 py-2">
{{ transaction.sats }}
</td>
<td class="border px-4 py-2">
{{ transaction.date }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td class="border px-4 py-2" colspan="3">No Sats transactions available.</td> <td class="border px-4 py-2" colspan="3">
No Sats transactions available.
</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
<h3 class="text-2xl font-semibold">Cents Transactions</h3> <h3 class="text-2xl font-semibold">
Cents Transactions
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">Type</th> <th class="py-2">
<th class="py-2">Amount (Cents)</th> Type
<th class="py-2">Date</th> </th>
<th class="py-2">
Amount (Cents)
</th>
<th class="py-2">
Date
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if recent_cents is not empty %} {% if recent_cents is not empty %}
{% for transaction in recent_cents %} {% for transaction in recent_cents %}
<tr> <tr>
<td class="border px-4 py-2">{{ transaction.type }}</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ transaction.cents }}</td> {{ transaction.type }}
<td class="border px-4 py-2">{{ transaction.date }}</td> </td>
<td class="border px-4 py-2">
{{ transaction.cents }}
</td>
<td class="border px-4 py-2">
{{ transaction.date }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td class="border px-4 py-2" colspan="3">No Cents transactions available.</td> <td class="border px-4 py-2" colspan="3">
No Cents transactions available.
</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
<h3 class="text-2xl font-semibold">Whales Sats</h3> <h3 class="text-2xl font-semibold">
Whales Sats
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">User ID</th> <th class="py-2">
<th class="py-2">Total Sats</th> User ID
</th>
<th class="py-2">
Total Sats
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if whales_sats is not empty %} {% if whales_sats is not empty %}
{% for whale in whales_sats %} {% for whale in whales_sats %}
<tr> <tr>
<td class="border px-4 py-2">{{ whale.user_id }}</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ whale.total }}</td> {{ whale.user_id }}
</td>
<td class="border px-4 py-2">
{{ whale.total }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td class="border px-4 py-2" colspan="2">No Sats whales available.</td> <td class="border px-4 py-2" colspan="2">
No Sats whales available.
</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
<h3 class="text-2xl font-semibold">Whales Cents</h3> <h3 class="text-2xl font-semibold">
Whales Cents
</h3>
<table class="min-w-full bg-white"> <table class="min-w-full bg-white">
<thead> <thead>
<tr> <tr>
<th class="py-2">User ID</th> <th class="py-2">
<th class="py-2">Total Cents</th> User ID
</th>
<th class="py-2">
Total Cents
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if whales_cents is not empty %} {% if whales_cents is not empty %}
{% for whale in whales_cents %} {% for whale in whales_cents %}
<tr> <tr>
<td class="border px-4 py-2">{{ whale.user_id }}</td> <td class="border px-4 py-2">
<td class="border px-4 py-2">{{ whale.total }}</td> {{ whale.user_id }}
</td>
<td class="border px-4 py-2">
{{ whale.total }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr> <tr>
<td class="border px-4 py-2" colspan="2">No Cents whales available.</td> <td class="border px-4 py-2" colspan="2">
No Cents whales available.
</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>

View file

@ -1,6 +1,8 @@
<section> <section>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold">Cart</h3> <h3 class="text-2xl font-semibold">
Cart
</h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/empty.twig' with { {% include 'lib/empty.twig' with {

View file

@ -47,13 +47,31 @@
</div> </div>
<div class="{{ colors.footer.policy }} flex justify-center w-full py-4"> <div class="{{ colors.footer.policy }} flex justify-center w-full py-4">
<div class="flex justify-between text-xs w-4/5"> <div class="flex justify-between text-xs w-4/5">
<div>© {{ copyright_year }} BuysForLife - All Rights Reserved.</div> <div>
©
{{ copyright_year }}
BuysForLife - All Rights Reserved.
</div>
<div class='text-right'> <div class='text-right'>
<a href="/policy#terms-of-sale" class="hover:underline">Terms of Sale</a> | <a href="/policy#terms-of-sale" class="hover:underline">
<a href="/policy#privacy-policy" class="hover:underline">Privacy Policy</a> | Terms of Sale
<a href="/policy#terms-of-use" class="hover:underline">Terms of Use</a> | </a>
<a href="/policy#accessibility-policy" class="hover:underline">Accessibility Policy</a> | |
<a href="/policy#do-not-sell-my-personal-information" class="hover:underline">Do Not Sell My Personal Information</a> <a href="/policy#privacy-policy" class="hover:underline">
Privacy Policy
</a>
|
<a href="/policy#terms-of-use" class="hover:underline">
Terms of Use
</a>
|
<a href="/policy#accessibility-policy" class="hover:underline">
Accessibility Policy
</a>
|
<a href="/policy#do-not-sell-my-personal-information" class="hover:underline">
Do Not Sell My Personal Information
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,11 +1,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="robots" content="noindex"> <meta name="robots" content="noindex" />
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css" />
<title>{{ page_title }}</title> <title>
<link rel="icon" href="/img/icon.png"> {{ page_title }}
</title>
<link rel="icon" href="/img/icon.png" />
<script>/*! js-cookie v3.0.5 | MIT */ <script>/*! js-cookie v3.0.5 | MIT */
!function (e, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self, function () { var n = e.Cookies, o = e.Cookies = t(); o.noConflict = function () { return e.Cookies = n, o } }()) }(this, (function () { "use strict"; function e(e) { for (var t = 1; t < arguments.length; t++) { var n = arguments[t]; for (var o in n) e[o] = n[o] } return e } var t = function t(n, o) { function r(t, r, i) { if ("undefined" != typeof document) { "number" == typeof (i = e({}, o, i)).expires && (i.expires = new Date(Date.now() + 864e5 * i.expires)), i.expires && (i.expires = i.expires.toUTCString()), t = encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent).replace(/[()]/g, escape); var c = ""; for (var u in i) i[u] && (c += "; " + u, !0 !== i[u] && (c += "=" + i[u].split(";")[0])); return document.cookie = t + "=" + n.write(r, t) + c } } return Object.create({ set: r, get: function (e) { if ("undefined" != typeof document && (!arguments.length || e)) { for (var t = document.cookie ? document.cookie.split("; ") : [], o = {}, r = 0; r < t.length; r++) { var i = t[r].split("="), c = i.slice(1).join("="); try { var u = decodeURIComponent(i[0]); if (o[u] = n.read(c, u), e === u) break } catch (e) { } } return e ? o[e] : o } }, remove: function (t, n) { r(t, "", e({}, n, { expires: -1 })) }, withAttributes: function (n) { return t(this.converter, e({}, this.attributes, n)) }, withConverter: function (n) { return t(e({}, this.converter, n), this.attributes) } }, { attributes: { value: Object.freeze(o) }, converter: { value: Object.freeze(n) } }) }({ read: function (e) { return '"' === e[0] && (e = e.slice(1, -1)), e.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) }, write: function (e) { return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent) } }, { path: "/" }); return t }));</script> !function (e, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self, function () { var n = e.Cookies, o = e.Cookies = t(); o.noConflict = function () { return e.Cookies = n, o } }()) }(this, (function () { "use strict"; function e(e) { for (var t = 1; t < arguments.length; t++) { var n = arguments[t]; for (var o in n) e[o] = n[o] } return e } var t = function t(n, o) { function r(t, r, i) { if ("undefined" != typeof document) { "number" == typeof (i = e({}, o, i)).expires && (i.expires = new Date(Date.now() + 864e5 * i.expires)), i.expires && (i.expires = i.expires.toUTCString()), t = encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent).replace(/[()]/g, escape); var c = ""; for (var u in i) i[u] && (c += "; " + u, !0 !== i[u] && (c += "=" + i[u].split(";")[0])); return document.cookie = t + "=" + n.write(r, t) + c } } return Object.create({ set: r, get: function (e) { if ("undefined" != typeof document && (!arguments.length || e)) { for (var t = document.cookie ? document.cookie.split("; ") : [], o = {}, r = 0; r < t.length; r++) { var i = t[r].split("="), c = i.slice(1).join("="); try { var u = decodeURIComponent(i[0]); if (o[u] = n.read(c, u), e === u) break } catch (e) { } } return e ? o[e] : o } }, remove: function (t, n) { r(t, "", e({}, n, { expires: -1 })) }, withAttributes: function (n) { return t(this.converter, e({}, this.attributes, n)) }, withConverter: function (n) { return t(e({}, this.converter, n), this.attributes) } }, { attributes: { value: Object.freeze(o) }, converter: { value: Object.freeze(n) } }) }({ read: function (e) { return '"' === e[0] && (e = e.slice(1, -1)), e.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) }, write: function (e) { return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent) } }, { path: "/" }); return t }));</script>
<script> <script>

View file

@ -29,8 +29,15 @@
<header class="flex flex-col items-center w-full gap-3 mb-8"> <header class="flex flex-col items-center w-full gap-3 mb-8">
<div class="{{ colors.header.banner }} py-1 text-sm flex w-full justify-center"> <div class="{{ colors.header.banner }} py-1 text-sm flex w-full justify-center">
<div class="w-[97%] lg:w-[90%] xl:w-4/5 flex justify-between"> <div class="w-[97%] lg:w-[90%] xl:w-4/5 flex justify-between">
<a href="/support/ask">Help Center</a> <a href="/support/ask">
<span>Save 5% when you pay with Bitcoin</span><a href="/support/bitcoin">Learn More</a> Help Center
</a>
<span>
Save 5% when you pay with Bitcoin
</span>
<a href="/support/bitcoin">
Learn More
</a>
</div> </div>
</div> </div>
@ -42,7 +49,7 @@
</a> </a>
</div> </div>
<form action="/search" method="post" class="flex-grow max-w-[900px]"> <form action="/search" method="post" class="flex-grow max-w-[900px]">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
name: 'search', name: 'search',
type: 'search', type: 'search',
placeholder: 'What are you looking for?', placeholder: 'What are you looking for?',
@ -56,11 +63,8 @@
<!-- Hidden checkbox to control the dropdown --> <!-- Hidden checkbox to control the dropdown -->
<input type="checkbox" id="dropdown-toggle" class="hidden peer"> <input type="checkbox" id="dropdown-toggle" class="hidden peer">
<!-- Dropdown button --> <!-- Dropdown button -->
<label for="dropdown-toggle" <label for="dropdown-toggle" class="{{ colors.button.default }} flex gap-1 items-center cursor-pointer p-1 py-2 rounded-md">
class="{{ colors.button.default }} flex gap-1 items-center cursor-pointer p-1 py-2 rounded-md"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="lucide lucide-user-round">
<circle cx="12" cy="8" r="5" /> <circle cx="12" cy="8" r="5" />
<path d="M20 21a8 8 0 0 0-16 0" /> <path d="M20 21a8 8 0 0 0-16 0" />
</svg> </svg>
@ -73,32 +77,55 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex items-center gap-[1px]"> <div class="flex items-center gap-[1px]">
<div class="text-sm font-semibold leading-none">Account</div> <svg <div class="text-sm font-semibold leading-none">
xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" Account
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" </div>
stroke-linejoin="round" class="lucide lucide-chevron-down"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down">
<path d="m6 9 6 6 6-6" /> <path d="m6 9 6 6 6-6" />
</svg> </svg>
</div> </div>
</div> </div>
</label> </label>
<div <div class="absolute mt-2 {{ colors.dropdown.list }} border rounded-md shadow-md w-48 hidden peer-checked:block z-50">
class="absolute mt-2 {{ colors.dropdown.list }} border rounded-md shadow-md w-48 hidden peer-checked:block z-50">
<ul class="py-2"> <ul class="py-2">
{% if session.user_id is defined %} {% if session.user_id is defined %}
<li><a href="/account" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Account</a></li> <li>
<li><a href="/account/orders" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Orders</a></li> <a href="/account" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
<li><a href="/account/returns" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Returns</a> Account
</a>
</li> </li>
<li><a href="/account/shipping" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Shipping</a> <li>
<a href="/account/orders" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Orders
</a>
</li> </li>
<li><a href="/account/billing" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Billing</a> <li>
<a href="/account/returns" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Returns
</a>
</li>
<li>
<a href="/account/shipping" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Shipping
</a>
</li>
<li>
<a href="/account/billing" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Billing
</a>
</li> </li>
{% else %} {% else %}
<li><a href="/account/login" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Sign In</a></li> <li>
<li><a href="/account/signup" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Create an <a href="/account/login" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Account</a> Sign In
</a>
</li>
<li>
<a href="/account/signup" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Create an
Account
</a>
</li> </li>
{% endif %} {% endif %}
@ -106,13 +133,21 @@
{% if is_admin %} {% if is_admin %}
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<ul class="py-2"> <ul class="py-2">
<li><a href="/admin" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Admin</a></li> <li>
<a href="/admin" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Admin
</a>
</li>
</ul> </ul>
{% endif %} {% endif %}
{% if session.user_id is defined %} {% if session.user_id is defined %}
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<ul class="py-2"> <ul class="py-2">
<li><a href="/account/logout" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Logout</a></li> <li>
<a href="/account/logout" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Logout
</a>
</li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
@ -124,39 +159,34 @@
} }
}); });
</script> </script>
<a href="/account/orders" <a href="/account/orders" class="{{ colors.button.default }} flex items-center gap-1 p-1 py-2 rounded-md">
class="{{ colors.button.default }} flex items-center gap-1 p-1 py-2 rounded-md"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-box">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-box">
<path
d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
<path d="m3.3 7 8.7 5 8.7-5" /> <path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" /> <path d="M12 22V12" />
</svg> </svg>
<div> <div>
<div class="text-xs whitespace-nowrap leading-none">Returns &</div> <div class="text-xs whitespace-nowrap leading-none">
<div class="text-sm font-semibold leading-none">Orders</div> Returns &
</div>
<div class="text-sm font-semibold leading-none">
Orders
</div>
</div> </div>
</a> </a>
</div> </div>
<a href="/cart"> <a href="/cart">
<div class="flex items-center"> <div class="flex items-center">
<div <div class="{{ colors.button.primary }} h-[42px] flex items-center border p-2 rounded-tl-lg rounded-bl-lg">
class="{{ colors.button.primary }} h-[42px] flex items-center border p-2 rounded-tl-lg rounded-bl-lg"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart lucide-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="lucide lucide-shopping-cart lucide-icon">
<circle cx="8" cy="21" r="1"></circle> <circle cx="8" cy="21" r="1"></circle>
<circle cx="19" cy="21" r="1"></circle> <circle cx="19" cy="21" r="1"></circle>
<path <path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"></path>
d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12">
</path>
</svg> </svg>
</div> </div>
<div <div class="{{ colors.input }} flex h-[42px] font-semibold justify-center items-center rounded-tr-lg rounded-br-lg border border-l-0 px-3 py-2">
class="{{ colors.input }} flex h-[42px] font-semibold justify-center items-center rounded-tr-lg rounded-br-lg border border-l-0 px-3 py-2"> 7
7</div> </div>
</div> </div>
</a> </a>
</div> </div>
@ -165,18 +195,22 @@
<nav class="w-full relative rounded-lg {{ colors.nav.bar }}"> <nav class="w-full relative rounded-lg {{ colors.nav.bar }}">
<div class="flex"> <div class="flex">
<ul class="flex"> <ul class="flex">
<li <li class="hoverable rounded-xl border-[5px] {{ colors.nav.item }}">
class="hoverable rounded-xl border-[5px] {{ colors.nav.item }}"> <a href="/power-meters" class="relative p-1 rounded-xl block text-sm font-bold">
<a href="/power-meters" class="relative p-1 rounded-xl block text-sm font-bold">220V 220V
Power Meters</a> Power Meters
</a>
<div class="mega-menu"> <div class="mega-menu">
<div class="bg-transparent h-[3px]"> <div class="bg-transparent h-[3px]"><!-- invisible content to keep menu shown when cursor is b/t the item and the content -->
<!-- invisible content to keep menu shown when cursor is b/t the item and the content -->
</div> </div>
<div class="p-6 mb-16 rounded-b shadow-lg {{ colors.nav.hovercontent }}"> <div class="p-6 mb-16 rounded-b shadow-lg {{ colors.nav.hovercontent }}">
<div class='flex gap-3 items-baseline'> <div class='flex gap-3 items-baseline'>
<h4 class="text-xl font-semibold">220V Power Meters</h4> <h4 class="text-xl font-semibold">
<a href="/power-meters" class="hover:underline font-semibold text-xs {{ colors.anchor.primary }}">Shop All</a> 220V Power Meters
</h4>
<a href="/power-meters" class="hover:underline font-semibold text-xs {{ colors.anchor.primary }}">
Shop All
</a>
</div> </div>
</div> </div>
</div> </div>
@ -191,17 +225,23 @@
<a href="/" class="hover:underline {{ colors.breadcrumb.parent }}"> <a href="/" class="hover:underline {{ colors.breadcrumb.parent }}">
{{ env.APP_NAME }} {{ env.APP_NAME }}
</a> </a>
<span class="{{ colors.breadcrumb.seperator }}">></span> <span class="{{ colors.breadcrumb.seperator }}">
>
</span>
{% for crumb in breadcrumbs %} {% for crumb in breadcrumbs %}
{% if crumb.url is null %} {% if crumb.url is null %}
<span class="font-bold {{ colors.breadcrumb.child }}">{{ crumb.title }}</span> <span class="font-bold {{ colors.breadcrumb.child }}">
{{ crumb.title }}
</span>
{% else %} {% else %}
<a href="{{ crumb.url }}" class="hover:underline {{ colors.breadcrumb.parent }}"> <a href="{{ crumb.url }}" class="hover:underline {{ colors.breadcrumb.parent }}">
{{ crumb.title }} {{ crumb.title }}
</a> </a>
{% endif %} {% endif %}
{% if loop.index < breadcrumbs|length %} {% if loop.index < breadcrumbs|length %}
<span class="{{ colors.breadcrumb.seperator }}">></span> <span class="{{ colors.breadcrumb.seperator }}">
>
</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@ -209,4 +249,5 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</header> </header>

View file

@ -0,0 +1,42 @@
<div class="flex flex-col gap-1">
<span>
{{ address.name }}
</span>
<span>
{{ address.company }}
</span>
<span>
{{ address.addressLine1 }}
</span>
<span>
{{ address.addressLine2 }}
</span>
<span>
{{ address.city }}
,
{{ address.state }}
{{ address.zip }}
</span>
<span>
{{ address.phone }}
</span>
{% if edit_url is not null %}
<a href="{{ edit_url }}" onclick="event.preventDefault(); document.getElementById('edit-form-{{ address.id }}').submit();">
Edit
</a>
<form id="edit-form-{{ address.id }}" action="{{ edit_url }}" method="post" style="display: none;">
<input type="hidden" name="address_id" value="{{ address.id }}" />
</form>
{% endif %}
{% if delete_url is not null %}
<a href="#delete-modal-{{ address.id }}">
Delete
</a>
{% include 'lib/modal.twig' with {
id: 'delete-modal-' ~ address.id,
content: 'lib/modals/confirm_delete_address.twig',
} %}
<form id="delete-form-{{ address.id }}" action="{{ delete_url }}" method="post" style="display: none;" />
<input type="hidden" name="address_id" value="{{ address.id }}"></form>
{% endif %}
</div>

View file

@ -1,45 +1,47 @@
{% if session.error is defined %} {% if session.error is defined %}
<div class="flex gap-3 items-center p-3 border rounded-md {{ colors.error.alert }}"> <div class="flex gap-3 items-center p-3 border rounded-md {{ colors.error.alert }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x {{ colors.error.text }}">
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x {{ colors.error.text }}">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6" /> <path d="m15 9-6 6" />
<path d="m9 9 6 6" /> <path d="m9 9 6 6" />
</svg> </svg>
<p>{{ session.error }}</p> <p>
</div> {{ session.error }}
</p>
</div>
{% endif %} {% endif %}
{% if session.warning is defined %} {% if session.warning is defined %}
<div class="flex gap-3 items-center p-3 border rounded-md {{ colors.warning.alert }}"> <div class="flex gap-3 items-center p-3 border rounded-md {{ colors.warning.alert }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert {{ colors.warning.text }}">
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-circle-alert {{ colors.warning.text }}">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" /> <line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" /> <line x1="12" x2="12.01" y1="16" y2="16" />
</svg> </svg>
<p>{{ session.warning }}</p> <p>
</div> {{ session.warning }}
</p>
</div>
{% endif %} {% endif %}
{% if session.success is defined %} {% if session.success is defined %}
<div class="flex gap-3 items-center p-3 border rounded-md {{ colors.success.alert }}"> <div class="flex gap-3 items-center p-3 border rounded-md {{ colors.success.alert }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check {{ colors.success.text }}">
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-circle-check {{ colors.success.text }}">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" /> <path d="m9 12 2 2 4-4" />
</svg> </svg>
<p>{{ session.success }}</p> <p>
</div> {{ session.success }}
</p>
</div>
{% endif %} {% endif %}
{% if session.info is defined %} {% if session.info is defined %}
<div class="flex gap-3 items-center p-3 border rounded-md {{ colors.info.alert }}"> <div class="flex gap-3 items-center p-3 border rounded-md {{ colors.info.alert }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info {{ colors.info.text }}">
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info {{ colors.info.text }}">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" /> <path d="M12 16v-4" />
<path d="M12 8h.01" /> <path d="M12 8h.01" />
</svg> </svg>
<p>{{ session.info }}</p> <p>
</div> {{ session.info }}
</p>
</div>
{% endif %} {% endif %}

View file

@ -1,46 +1,49 @@
{% if href is defined %} {% if href is defined %}
<a href="{{ href }}" <a
class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center"> href="{{ href }}" class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center">
{% else %} {% else %}
<div onclick="{{ onclick }}" <div onclick="{{ onclick }}" class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center">
class="cursor-pointer {{ submit is defined ? 'px-4 rounded-l-none' : 'w-full' }} {{ colors.button.primary }} rounded-lg h-[42px] flex items-center justify-center"> {% endif %}
{% endif %}
{% if label is defined %} {% if label is defined %}
<span>{{ label }}</span> <span>
{{ label }}
</span>
{% endif %} {% endif %}
{% if icon is defined %} {% if icon is defined %}
{% if icon == 'search' %} {% if icon == 'search' %}
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path> <path d="m21 21-4.3-4.3"></path>
</svg> </svg>
{% elseif icon == 'add' %} {% elseif icon == 'add' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus">
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-plus">
<path d="M5 12h14" /> <path d="M5 12h14" />
<path d="M12 5v14" /> <path d="M12 5v14" />
</svg> </svg>
{% elseif icon == 'enter' %} {% elseif icon == 'enter' %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right">
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-arrow-right">
<path d="M5 12h14" /> <path d="M5 12h14" />
<path d="m12 5 7 7-7 7" /> <path d="m12 5 7 7-7 7" />
</svg> </svg>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if href is defined %} {% if href is defined %}
</a> </a>
{% else %} {% else %}
</div> </div>
{% endif %} {% endif %}
{% if captcha is defined %} {% if captcha is defined %}
<div class="flex justify-center {{ colors.text.muted }}"> <div class="flex justify-center {{ colors.text.muted }}">
<p class="w-[250px] text-[10px] text-center">This form is protected by reCAPTCHA and the Google <p class="w-[250px] text-[10px] text-center">
<a class="underline" href="https://policies.google.com/privacy">Privacy Policy</a> and <a class="underline" This form is protected by reCAPTCHA and the Google
href="https://policies.google.com/terms">Terms of Service</a> apply. <a class="underline" href="https://policies.google.com/privacy">
Privacy Policy
</a>
and
<a class="underline" href="https://policies.google.com/terms">
Terms of Service
</a>
apply.
</p> </p>
</div> </div>
{% endif %} {% endif %}

View file

@ -1,8 +1,7 @@
<table align="center" class="x_225906249wrapper x_225906249wrapper-callout x_225906249wrapper-without-padding-top" border="0" cellpadding="0" cellspacing="0" style="background: rgb(251, 251, 251); background-color: rgb(251, 251, 251); width: 100%"> <table align="center" border="0" cellpadding="0" cellspacing="0" style="background: rgb(251, 251, 251); background-color: rgb(251, 251, 251); width: 100%">
<tbody> <tbody>
<tr> <tr>
<td> <td>
<div style="margin: 0px auto; max-width: 648px"> <div style="margin: 0px auto; max-width: 648px">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="width: 100%"> <table align="center" border="0" cellpadding="0" cellspacing="0" style="width: 100%">
<tbody> <tbody>
@ -15,41 +14,51 @@
<tr> <tr>
<td style="direction: ltr; font-size: 0px; padding: 0; text-align: center"> <td style="direction: ltr; font-size: 0px; padding: 0; text-align: center">
<div class="x_225906249mj-column-per-100 x_225906249mj-outlook-group-fix" style="font-size: 0px; text-align: left; direction: ltr; display: inline-block; vertical-align: top; width: 100%"> <div style="font-size: 0px; text-align: left; direction: ltr; display: inline-block; vertical-align: top; width: 100%">
<table border="0" cellpadding="0" cellspacing="0" style="vertical-align: top" width="100%"> <table border="0" cellpadding="0" cellspacing="0" style="vertical-align: top" width="100%">
<tbody> <tbody>
<tr> <tr>
<td align="left" class="x_225906249text x_225906249text-surtitle" style="font-size: 0px; padding: 0 24px; padding-bottom: 12px"> <td align="left" style="font-size: 0px; padding: 0 24px; padding-bottom: 12px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; letter-spacing: 0.12em; line-height: 24px; text-align: left; text-transform: uppercase; color: rgb(49, 89, 128)">Your Account</div> <div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; letter-spacing: 0.12em; line-height: 24px; text-align: left; text-transform: uppercase; color: rgb(49, 89, 128)">
Your Account
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" class="x_225906249text x_225906249text-header-title" style="font-size: 0px; padding: 0 24px; padding-bottom: 24px"> <td align="left" style="font-size: 0px; padding: 0 24px; padding-bottom: 24px">
<div style="font-family: &quot;Source Serif Pro&quot;, Georgia, Cambria, &quot;Times New Roman&quot;, Times, serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 41px; font-weight: 600; line-height: 48px; text-align: left; color: rgb(0, 9, 19)">One-Time Passcode</div> <div style="font-family: &quot;Source Serif Pro&quot;, Georgia, Cambria, &quot;Times New Roman&quot;, Times, serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 41px; font-weight: 600; line-height: 48px; text-align: left; color: rgb(0, 9, 19)">
One-Time Passcode
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" class="x_225906249text" style="font-size: 0px; padding: 0 24px"> <td align="left" style="font-size: 0px; padding: 0 24px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; line-height: 24px; text-align: left; color: rgb(0, 9, 19)">Click the button below to access your secure login form, then enter your one-time passcode.</div> <div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; line-height: 24px; text-align: left; color: rgb(0, 9, 19)">
Click the button below to access your secure login form, then enter your one-time passcode.
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" class="x_225906249text x_225906249text-code" style="font-size: 0px; padding: 0 24px; padding-top: 24px"> <td align="left" style="font-size: 0px; padding: 0 24px; padding-top: 24px">
<div style="font-family: &quot;Source Code Pro&quot;, &quot;ui-monospace&quot;, Menlo, Consolas, &quot;Roboto Mono&quot;, &quot;Ubuntu Monospace&quot;, &quot;Noto Mono&quot;, &quot;Oxygen Mono&quot;, &quot;Liberation Mono&quot;, monospace, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot; !important; font-size: 41px; font-weight: bold; letter-spacing: 0.12em; line-height: 48px; text-align: left; color: rgb(0, 48, 94)">{{ code }}</div> <div style="font-family: &quot;Source Code Pro&quot;, &quot;ui-monospace&quot;, Menlo, Consolas, &quot;Roboto Mono&quot;, &quot;Ubuntu Monospace&quot;, &quot;Noto Mono&quot;, &quot;Oxygen Mono&quot;, &quot;Liberation Mono&quot;, monospace, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot; !important; font-size: 41px; font-weight: bold; letter-spacing: 0.12em; line-height: 48px; text-align: left; color: rgb(0, 48, 94)">
{{ code }}
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td align="left" class="x_225906249button" style="font-size: 0px; padding: 24px 24px"> <td align="left" style="font-size: 0px; padding: 24px 24px">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; line-height: 100%"> <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; line-height: 100%">
<tbody> <tbody>
<tr> <tr>
<td align="center" bgcolor="#FCC800" style="border: none; border-radius: 6px; cursor: auto; background: rgb(252, 200, 0)" valign="middle"> <td align="center" bgcolor="#FCC800" style="border: none; border-radius: 6px; cursor: auto; background: rgb(252, 200, 0)" valign="middle">
<a href="{{ link }}" style="display: inline-block; background: rgb(252, 200, 0); color: rgb(0, 48, 94); font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; margin: 0; text-decoration: none; text-transform: none; padding: 18px 24px; border-radius: 6px" target="_blank"> Log in&nbsp;→ </a> <a href="{{ link }}" style="display: inline-block; background: rgb(252, 200, 0); color: rgb(0, 48, 94); font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; margin: 0; text-decoration: none; text-transform: none; padding: 18px 24px; border-radius: 6px" target="_blank">
Log in
</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -58,8 +67,10 @@
</tr> </tr>
<tr> <tr>
<td align="left" class="x_225906249text" style="font-size: 0px; padding: 0 24px"> <td align="left" style="font-size: 0px; padding: 0 24px">
<div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; text-align: left">BuysForLife agents will never ask you for this code. Do not share this passcode with anyone for any reason.</div> <div style="font-family: Greycliff, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Ubuntu, Cantarell, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;; font-size: 16px; font-weight: bold; line-height: 24px; text-align: left">
BuysForLife associates will never ask you for this code. Do not share this passcode with anyone for any reason.
</div>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -82,7 +93,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -1,5 +1,9 @@
<div class="text-center flex flex-col items-center mb-24 mt-20"> <div class="text-center flex flex-col items-center mb-24 mt-20">
<img src="/img/empty/{{ type }}-{{ theme }}.svg" width="200px" /> <img src="/img/empty/{{ type }}-{{ theme }}.svg" width="200px" />
<h3 class="text-2xl font-semibold">{{ title }}</h3> <h3 class="text-2xl font-semibold">
<p class="w-[300px] text-sm">{{ subtitle }}</p> {{ title }}
</h3>
<p class="w-[300px] text-sm">
{{ subtitle }}
</p>
</div> </div>

View file

@ -1,24 +1,24 @@
<div class="flex flex-col gap-4 mb-4"> <div class="flex flex-col gap-4 mb-4">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_name', name: action ~ '_name',
label: 'Name', label: 'Name',
value: name value: name
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_company', name: action ~ '_company',
label: 'Company', label: 'Company',
optional: true, optional: true,
value: company value: company
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_addressLine1', name: action ~ '_addressLine1',
label: 'Address Line 1', label: 'Address Line 1',
value: addressLine1 value: addressLine1
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_addressLine2', name: action ~ '_addressLine2',
label: 'Address Line 2', label: 'Address Line 2',
@ -26,26 +26,26 @@
value: addressLine2 value: addressLine2
} %} } %}
<div class="flex gap-4"> <div class="flex gap-4">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_city', name: action ~ '_city',
label: 'City', label: 'City',
value: city value: city
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_state', name: action ~ '_state',
label: 'State', label: 'State',
value: state value: state
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_zip', name: action ~ '_zip',
label: 'Zip', label: 'Zip',
value: zip value: zip
} %} } %}
</div> </div>
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: action ~ '_phone', name: action ~ '_phone',
label: 'Phone', label: 'Phone',

View file

@ -1,31 +1,31 @@
<div class="flex flex-col gap-4 my-4"> <div class="flex flex-col gap-4 my-4">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'name', name: 'name',
label: 'Name', label: 'Name',
value: user.name value: user.name
} %} } %}
<div class="flex gap-4"> <div class="flex gap-4">
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'company_name', name: 'company_name',
label: 'Company Name', label: 'Company Name',
value: user.company_name value: user.company_name
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'company_type', name: 'company_type',
label: 'Company Type', label: 'Company Type',
value: user.company_type value: user.company_type
} %} } %}
{% include 'lib/input.twig' with { {% include 'lib/inputs/text.twig' with {
type: 'text', type: 'text',
name: 'company_size', name: 'company_size',
label: 'Company Size', label: 'Company Size',
value: user.company_size value: user.company_size
} %} } %}
</div> </div>
{% include 'lib/toggle.twig' with { {% include 'lib/inputs/toggle.twig' with {
name: 'dark_theme', name: 'dark_theme',
label: 'Use dark theme', label: 'Use dark theme',
on: user.dark_theme on: user.dark_theme

View file

@ -1,48 +0,0 @@
<div class="flex flex-col gap-4">
{% if label is defined %}
<label for="{{ name }}" class="flex flex-col gap-2">
<span class="font-semibold">
{{ label }}
{% if required is defined %}
<span class="{{ colors.error.text }} ml-4">*</span>
{% endif %}
{% if optional is defined %}
<span class="text-sm font-normal {{ colors.text.muted }}"> - (optional)</span>
{% endif %}
</span>
{% if subtext is defined %}
<span class="text-sm {{ colors.text.muted }}">{{ subtext }}</span>
{% endif %}
</label>
{% endif %}
{% if submit is defined %}
<div class="flex items-center">
{% endif %}
<input type="{{ type }}" name="{{ name }}"
{% if placeholder is defined %}
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 }} {{ submit is defined ? 'rounded-l-lg border-r-0' : 'rounded-lg' }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none">
{% if submit is defined %}
{% include 'lib/button.twig' with {
icon: 'search'
} %}
{% endif %}
{% if submit is defined %}
</div>
{% endif %}
</div>

View file

@ -0,0 +1,11 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2">
{{ label }}
</label>
<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') }}">
{% if subtext is defined %}
<p class="text-xs text-gray-500">
{{ subtext }}
</p>
{% endif %}

View file

@ -0,0 +1,37 @@
<div class="flex flex-col gap-4">
{% if label is defined %}
<label for="{{ name }}" class="flex flex-col gap-2">
<span class="font-semibold">
{{ label }}
{% if required is defined %}
<span class="{{ colors.error.text }} ml-4">
*
</span>
{% endif %}
{% if optional is defined %}
<span class="text-sm font-normal {{ colors.text.muted }}">
- (optional)
</span>
{% endif %}
</span>
{% if subtext is defined %}
<span class="text-sm {{ colors.text.muted }}">
{{ subtext }}
</span>
{% endif %}
</label>
{% endif %}
{% if submit is defined %}
<div class="flex items-center">
{% 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 }} {{ submit is defined ? 'rounded-l-lg border-r-0' : 'rounded-lg' }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none">
{% if submit is defined %}
{% include 'lib/button.twig' with {
icon: 'search'
} %}
{% endif %}
{% if submit is defined %}
</div>
{% endif %}
</div>

View file

@ -8,7 +8,12 @@
<div id="hide-{{ id }}"> <div id="hide-{{ id }}">
<div class="{{ colors.modal.content }} p-8 border rounded relative"> <div class="{{ colors.modal.content }} p-8 border rounded relative">
{% include content %} {% include content %}
<a href="#hide-{{ id }}" class="absolute top-2 right-2 no-underline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></a> <a href="#hide-{{ id }}" class="absolute top-2 right-2 no-underline">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,3 @@
<div>
Are you sure you want to delete this address?
</div>

View file

@ -0,0 +1,24 @@
<div>
<div class="text-lg font-semibold mb-4">
Why do I have store credit?
</div>
<ul class="list-disc pl-6 mb-4">
<li>
You may have received credit as a refund, dispute resolution, or promotional event.
</li>
<li>
You can also reload your store credit by ordering gift cards.
</li>
</ul>
<div class="text-lg font-semibold mb-4">
What can I do with store credit?
</div>
<ul class="list-disc pl-6">
<li>
You may spend store credit at checkout.
</li>
<li>
Your subscriptions and recurring purchases can also be paid with store credit.
</li>
</ul>
</div>

View file

@ -0,0 +1,30 @@
<div>
<div class="text-lg font-semibold mb-4">
Why do I have sats?
</div>
<ul class="list-disc pl-6 mb-4">
<li>
You may have received sats from a promotional event
</li>
<li>
You may have recieved sats sent to your default generated Lightning Address (LNURL):
{{ user.npub }}
@
{{ http_host }}
</li>
</ul>
<div class="text-lg font-semibold mb-4">
What can I do with sats?
</div>
<ul class="list-disc pl-6">
<li>
You may spend sats at checkout
</li>
<li>
Your subscriptions and recurring purchases can also be paid with sats
</li>
<li>
You can configure these sats to autowithdraw by attaching a Lightning Address (LNURL)
</li>
</ul>
</div>

View file

@ -1,17 +0,0 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2">
{{ label }}
</label>
<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') }}">
{% if subtext is defined %}
<p class="text-xs text-gray-500">{{ subtext }}</p>
{% endif %}

View file

@ -1,12 +0,0 @@
<div>
<div class="text-lg font-semibold mb-4">Why do I have store credit?</div>
<ul class="list-disc pl-6 mb-4">
<li>You may have received credit as a refund, dispute resolution, or promotional event.</li>
<li>You can also reload your store credit by ordering gift cards.</li>
</ul>
<div class="text-lg font-semibold mb-4">What can I do with store credit?</div>
<ul class="list-disc pl-6">
<li>You may spend store credit at checkout.</li>
<li>Your subscriptions and recurring purchases can also be paid with store credit.</li>
</ul>
</div>

View file

@ -1,13 +0,0 @@
<div>
<div class="text-lg font-semibold mb-4">Why do I have sats?</div>
<ul class="list-disc pl-6 mb-4">
<li>You may have received sats from a promotional event</li>
<li>You may have recieved sats sent to your default generated Lightning Address (LNURL): {{ user.npub }}@{{ http_host }}</li>
</ul>
<div class="text-lg font-semibold mb-4">What can I do with sats?</div>
<ul class="list-disc pl-6">
<li>You may spend sats at checkout</li>
<li>Your subscriptions and recurring purchases can also be paid with sats</li>
<li>You can configure these sats to autowithdraw by attaching a Lightning Address (LNURL)</li>
</ul>
</div>

View file

@ -1,10 +1,14 @@
{% if text is defined %} {% if text is defined %}
<div class="relative"> <div class="relative">
<div class="absolute inset-0 flex items-center"><span class="w-full border-t {{ colors.rule }}"></span></div> <div class="absolute inset-0 flex items-center">
<div class="relative flex justify-center text-xs"><span <span class="w-full border-t {{ colors.rule }}"></span>
class="px-2 {{ colors.body }}">{{ text }}</span> </div>
<div class="relative flex justify-center text-xs">
<span class="px-2 {{ colors.body }}">
{{ text }}
</span>
</div>
</div> </div>
</div>
{% else %} {% else %}
<hr class="{{ colors.rule }}" /> <hr class="{{ colors.rule }}" />
{% endif %} {% endif %}

View file

@ -1,8 +1,14 @@
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
Transaction Id: {{ tx.id }} Transaction Id:
Date: {{ tx.date }} {{ tx.id }}
User: {{ user.email }} Date:
Transaction Type: {{ tx.type }} {{ tx.date }}
Sats: {{ tx.sats }} User:
Cents: {{ tx.cents }} {{ user.email }}
Transaction Type:
{{ tx.type }}
Sats:
{{ tx.sats }}
Cents:
{{ tx.cents }}
</section> </section>