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!!
# NOTE: The LN_SERVICE must support LUD-21 Payment Verification
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",
"twig/twig": "^3.0",
"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",
"This file is @generated automatically"
],
"content-hash": "3f3a1571b095d2550e901def5ae562ca",
"content-hash": "d534de83ea2acfea5f56dd6efb27959e",
"packages": [
{
"name": "bitwasp/bech32",
@ -1709,6 +1709,64 @@
},
"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",
"version": "1.0.0",

View file

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

View file

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

View file

@ -4,55 +4,55 @@ return [
'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200',
],
'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',
'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',
'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900'
'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900',
],
'breadcrumb' => [
'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',
'child' => 'text-gray-200 dark:text-gray-300'
'child' => 'text-gray-200 dark:text-gray-300',
],
'dropdown' => [
'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',
'error' => [
'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' => [
'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' => [
'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' => [
'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' => [
'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300',
'shadow' => 'bg-black/70'
'shadow' => 'bg-black/70',
],
'nav' => [
'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',
'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',
'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",
'footer' => [
"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;
use app\models\addresses;
use app\models\users;
use app\models\emails;
use app\models\user_addresses;
use app\models\magic_links;
use app\models\users;
class account
{
public static function index($defaults): void
{
if (!isset($_SESSION['user_id'])) {
header('Location: /account/login');
}
$user_id = $_SESSION['user_id'];
$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, [
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/index.twig',
'page_title' => 'Manage Account - ' . $_ENV['APP_NAME'],
'user' => $user,
'shipping' => $ship_addrs,
'billing' => $bill_addrs,
'default_shipping' => $default_shipping,
'default_billing' => $default_billing,
'addresses' => $addresses,
'breadcrumbs' => [
[
'url' => null,
'title' => 'My Account',
]
]
],
],
]));
}
public static function billing($defaults)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) {
http_response_code(403);
$bill = addresses::validatePost("billing_");
if (isset($bill['error'])) {
header('Location: /account/billing');
}
$bill = addresses::validatePost("billing");
$bill_id = addresses::add(
$_SESSION['user_id'],
$bill['name'],
$bill['company'],
$bill['addressLine1'],
@ -68,51 +44,34 @@ class account
$bill['state'],
$bill['zip'],
$bill['phone'],
1,
0
);
user_addresses::add(
$_SESSION['user_id'],
$bill_id
$bill['hash']
);
$_SESSION['success'] = "Billing address saved!";
header('Location: /account/billing');
}
$user_id = $_SESSION['user_id'];
$user = users::getById($user_id);
$default_billing = null;
$bill_addrs = [];
$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, [
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/billing.twig',
'page_title' => 'Billing Information - ' . $_ENV['APP_NAME'],
'billing' => $bill_addrs,
'default_billing' => $default_billing,
'user' => $user,
'addresses' => $addresses,
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account'
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Billing'
]
]
'title' => 'Billing',
],
],
]));
}
public static function profile()
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) {
http_response_code(403);
}
users::updateProfileById($_SESSION['user_id'], $_POST);
header('Location: /account');
}
@ -120,10 +79,6 @@ class account
public static function email()
{
$user_id = $_SESSION['user_id'] ?? null;
if (empty($user_id)){
header('Location: /account/login');
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'] ?? null;
if (empty($email)) {
@ -131,6 +86,7 @@ class account
header('Location: /account');
exit;
} else {
$user_id = $_SESSION['user_id'];
$token = magic_links::add($email, $user_id);
users::updateReplaceEmailTokenById($user_id, $token);
header('Location: /account');
@ -149,7 +105,7 @@ class account
if ($user) {
$_SESSION['user_email'] = $link['email'];
$_SESSION['user_id'] = $user['id'];
if (!$user['verified']) {
if (! $user['verified']) {
users::verify($link['email']);
}
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',
'page_title' => $_ENV['APP_NAME'],
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account'
'title' => 'My Account',
],
[
'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'])) {
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',
'page_title' => 'Sign In or Create an Account!',
'breadcrumbs' => [
[
'url' => null,
'title' => 'My Account'
'title' => 'My Account',
],
],
]
]));
}
@ -220,50 +212,48 @@ class account
public static function orders($defaults)
{
if (!isset($_SESSION['user_id'])) {
header('Location: /account/login');
}
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/orders.twig',
'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Orders',
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account'
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Orders'
]
]
'title' => 'Orders',
],
],
]));
}
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',
'page_title' => 'View ' . $_ENV['APP_NAME'] . ' Returns',
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account'
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Returns'
]
]
'title' => 'Returns',
],
],
]));
}
public static function shipping($defaults)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!$_SESSION['user_id']) {
http_response_code(403);
$ship = addresses::validatePost("shipping_");
if (isset($ship['error'])) {
header('Location: /account/shipping');
}
$ship = addresses::validatePost("shipping");
$ship_id = addresses::add(
$_SESSION['user_id'],
$ship['name'],
$ship['company'],
$ship['addressLine1'],
@ -272,43 +262,28 @@ class account
$ship['state'],
$ship['zip'],
$ship['phone'],
0,
1
);
user_addresses::add(
$_SESSION['user_id'],
$ship_id
$ship['hash']
);
$_SESSION['success'] = "Shipping address saved!";
header('Location: /account/shipping');
}
$user_id = $_SESSION['user_id'];
$user = users::getById($user_id);
$addresses = user_addresses::getShippingByUserId($user['id']);
$default_shipping = null;
$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, [
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/shipping.twig',
'page_title' => $_ENV['APP_NAME'] . ' Shipping',
'shipping' => $ship_addrs,
'default_shipping' => $default_shipping,
'addresses' => $addresses,
'breadcrumbs' => [
[
'url' => '/account',
'title' => 'My Account'
'title' => 'My Account',
],
[
'url' => null,
'title' => 'Shipping'
]
]
'title' => 'Shipping',
],
],
]));
}
@ -327,16 +302,16 @@ class account
exit;
}
$useShipping = $_POST['use_shipping'] ?? false;
$ship = addresses::validatePost("shipping");
if (!isset($ship['name'])){
$_SESSION['error'] = "Shipping address verification failed. Check your entry for errors.";
$ship = addresses::validatePost("shipping_");
if (isset($ship['error'])) {
$_SESSION['error'] = "Shipping address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST;
header('Location: /account/signup');
}
if (!$useShipping) {
$bill = addresses::validatePost("billing");
if (!isset($bill['name'])){
$_SESSION['error'] = "Billing address verification failed. Check your entry for errors.";
if (! $useShipping) {
$bill = addresses::validatePost("billing_");
if (isset($bill['error'])) {
$_SESSION['error'] = "Billing address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST;
header('Location: /account/signup');
}
@ -346,6 +321,7 @@ class account
header('Location: /account/signup');
}
$ship_id = addresses::add(
null,
$ship['name'],
$ship['company'],
$ship['addressLine1'],
@ -354,12 +330,12 @@ class account
$ship['state'],
$ship['zip'],
$ship['phone'],
$useShipping == 'on',
1
$ship['hash']
);
$bill_id = $ship_id;
if (!$useShipping) {
if (! $useShipping) {
$bill_id = addresses::add(
null,
$bill['name'],
$bill['company'],
$bill['addressLine1'],
@ -368,8 +344,7 @@ class account
$bill['state'],
$bill['zip'],
$bill['phone'],
1,
0
$bill['hash']
);
}
$opt_in_promotional = $_POST['opt_in_promotional'] ?? false;
@ -384,18 +359,8 @@ class account
$dark_theme
);
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;
if (!$verified) {
if (! $verified) {
header("Location: /magic-link?email=$email&signup=1");
exit;
}
@ -408,9 +373,9 @@ class account
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',
'page_title' => 'Create an Account - ' . $_ENV['APP_NAME']
'page_title' => 'Create an Account - ' . $_ENV['APP_NAME'],
]));
}
}

View file

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

View file

@ -1,17 +1,18 @@
<?php
namespace app\controllers;
class cart
{
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',
'page_title' => $_ENV['APP_NAME'] . ' Cart',
'breadcrumbs' => [
[
'url' => null,
'title' => 'Cart'
]
'title' => 'Cart',
],
],
]));
}

View file

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

View file

@ -1,26 +1,27 @@
<?php
namespace app\controllers;
class checkout
{
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',
'page_title' => 'Checkout with ' . $_ENV['APP_NAME'],
]));
}
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',
'page_title' => 'Review & Payment | ' . $_ENV['APP_NAME']
'page_title' => 'Review & Payment | ' . $_ENV['APP_NAME'],
]));
}
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',
'page_title' => 'Order Recieved! - Thank You'
'page_title' => 'Order Recieved! - Thank You',
]));
}
}

View file

@ -1,12 +1,13 @@
<?php
namespace app\controllers;
class home
{
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',
'page_title' => $_ENV['APP_NAME'] . ": Specialty Hardware"
'page_title' => $_ENV['APP_NAME'] . ": Specialty Hardware",
]));
}
}

View file

@ -1,11 +1,10 @@
<?php
namespace app\controllers;
use app\app;
use app\models\invoices;
$invoice = 'lnbc234340n1pnm5uh2pp5pwekmjzj3hgadahfeprtc6f2ppmhnqjzh556q4fvve8z953v3t0sdqqcqzysxqrrsssp5uq25yjnmvdpnglmv42nf64wk0pugrynq549f3wgghgtkfapwdfhq9qxpqysgqyjq2ewqkm6s2dlhuruuc4k9md22wraz829tlhfeuxrsnwmephfkjz8e9g7j6373989mfccajy3cxexac8xu6yen4qfs4947fkrg9ynsq7x72ze';
use Jorijn\Bitcoin\Bolt11\Encoder\PaymentRequestDecoder;
use Jorijn\Bitcoin\Bolt11\Model\Tag;
use Jorijn\Bitcoin\Bolt11\Normalizer\PaymentRequestDenormalizer;
class lnurlp
@ -39,10 +38,10 @@ class lnurlp
}
// for when the user is not registered
$user = users::getByNpub($username);
if (!$user){
if (! $user) {
returnJson([
'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)
@ -72,7 +71,7 @@ class lnurlp
}
$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
$invoice = $res['pr'];
$decoder = new PaymentRequestDecoder();
@ -92,7 +91,7 @@ class lnurlp
'status' => 'OK',
'pr' => $invoice,
'routes' => $res['routes'],
'verify' => "https://$host/lnurlp?verify=$proxy_verify"
'verify' => "https://$host/lnurlp?verify=$proxy_verify",
]);
} else {
returnJson($res);

View file

@ -1,10 +1,11 @@
<?php
namespace app\controllers;
class lost
{
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',
'page_title' => 'Not Found - ' . $_ENV['APP_NAME'],
]));

View file

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

View file

@ -1,34 +1,35 @@
<?php
namespace app\controllers;
class support
{
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',
'page_title' => $_ENV['APP_NAME'] . ': Frequently Asked Questions',
'breadcrumbs' => [
[
'url' => null,
'title' => 'Support'
]
]
'title' => 'Support',
],
],
]));
}
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',
'page_title' => $_ENV['APP_NAME'] . ' Bitcoin Accepted',
'breadcrumbs' => [
[
'url' => '/support/ask',
'title' => 'Support'
'title' => 'Support',
],
[
'url' => null,
'title' => 'Bitcoin'
]
'title' => 'Bitcoin',
],
],
]));
}

View file

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

View file

@ -2,12 +2,14 @@
namespace app\models;
use app\app;
class addresses
{
public static function init()
{
app::$db->exec("CREATE TABLE IF NOT EXISTS addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
name TEXT NOT NULL,
company TEXT,
addressLine1 TEXT NOT NULL,
@ -16,57 +18,131 @@ class addresses
state TEXT NOT NULL,
zip TEXT NOT NULL,
phone TEXT,
billing BOOLEAN NOT NULL,
shipping BOOLEAN NOT NULL
hash TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)");
}
public static function validatePost($type)
public static function validatePost($type = "")
{
$name = $_POST["{$type}_name"];
$company = $_POST["{$type}_company"] ?? null;
$addressLine2 = $_POST["{$type}_addressLine2"] ?? null;
$addressLine1 = $_POST["{$type}_addressLine1"];
$city = $_POST["{$type}_city"];
$state = $_POST["{$type}_state"];
$zip = $_POST["{$type}_zip"];
$phone = $_POST["{$type}_phone"];
$name = $_POST["{$type}name"];
$company = $_POST["{$type}company"] ?? null;
$addressLine2 = $_POST["{$type}addressLine2"] ?? null;
$addressLine1 = $_POST["{$type}addressLine1"];
$city = $_POST["{$type}city"];
$state = $_POST["{$type}state"];
$zip = $_POST["{$type}zip"];
$phone = $_POST["{$type}phone"];
$email = $_POST["{$type}email"] ?? null; // Assuming email is part of the form
// check all required fields are set
if (empty($name) || empty($addressLine1) || empty($city) || empty($state) || empty($zip) || empty($phone)) {
$_SESSION['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
];
return ["error" => "Missing required {$type} information."];
}
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 (
user_id,
name,
company,
addressLine1,
@ -75,9 +151,9 @@ class addresses
state,
zip,
phone,
billing,
shipping
hash
) VALUES (
:user_id,
:name,
:company,
:addressLine1,
@ -86,10 +162,10 @@ class addresses
:state,
:zip,
:phone,
:billing,
:shipping
:hash
)";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':company', $company);
$stmt->bindParam(':addressLine1', $addressLine1);
@ -98,8 +174,7 @@ class addresses
$stmt->bindParam(':state', $state);
$stmt->bindParam(':zip', $zip);
$stmt->bindParam(':phone', $phone);
$stmt->bindParam(':billing', $billing);
$stmt->bindParam(':shipping', $shipping);
$stmt->bindParam(':hash', $hash);
$stmt->execute();
return app::$db->lastInsertId();
}

View file

@ -25,7 +25,7 @@ class cart_items
$stmt->execute([
'cart_id' => $cartId,
'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->execute([
'cart_item_id' => $cartItemId,
'quantity' => $quantity
'quantity' => $quantity,
]);
}

View file

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

View file

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

View file

@ -29,7 +29,7 @@ class order_items
$stmt->execute([
'order_id' => $orderId,
'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->execute([
'order_item_id' => $orderItemId,
'quantity' => $quantity
'quantity' => $quantity,
]);
}

View file

@ -7,7 +7,7 @@ class orders
{
const STATUSES = [
'SHIPPED', 'PENDING', 'HOLD', 'PARTIAL',
'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING'
'BACKORDER', 'FAILED', 'CANCELED', 'PROCESSING',
];
public static function init()
@ -33,7 +33,7 @@ class orders
'user_id' => $userId,
'value_sats' => $valueSats,
'value_cents' => $valueCents,
'status' => $status
'status' => $status,
]);
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->execute([
'order_id' => $orderId,
'status' => $status
'status' => $status,
]);
}
@ -66,7 +66,7 @@ class orders
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");
}
}

View file

@ -64,7 +64,7 @@ class products
':image_url_8' => $images[8] ?? null,
':image_url_9' => $images[9] ?? 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,
'product_id' => $productId,
'quantity' => $quantity,
'price' => $price
'price' => $price,
]);
}
@ -51,7 +51,7 @@ class quote_items
$stmt->execute([
'quote_item_id' => $quoteItemId,
'quantity' => $quantity,
'price' => $price
'price' => $price,
]);
}

View file

@ -7,7 +7,7 @@ class quotes
{
private const STATUSES = [
'DRAFT', 'PUBLISHED', 'SENT', 'PURCHASED',
'EXPIRED', 'CANCELED'
'EXPIRED', 'CANCELED',
];
public static function init()
@ -29,7 +29,7 @@ class quotes
VALUES (:user_id, :status)");
$stmt->execute([
'user_id' => $userId,
'status' => $status
'status' => $status,
]);
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->execute([
'quote_id' => $quoteId,
'status' => $status
'status' => $status,
]);
}
@ -62,7 +62,7 @@ class quotes
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");
}
}

View file

@ -6,11 +6,11 @@ use app\app;
class subscriptions
{
const STATES = [
'TRIAL', 'START', 'RENEWAL'
'TRIAL', 'START', 'RENEWAL',
];
const STATUS = [
'COMPLETED', 'CANCELED'
'COMPLETED', 'CANCELED',
];
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::validateStatus($status);
@ -43,7 +43,7 @@ class subscriptions
'status' => $status,
'start_date' => $startDate,
'renews_at' => $renewAt,
'invoice_date' => $invoiceDate
'invoice_date' => $invoiceDate,
]);
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->execute([
'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->execute([
'subscription_id' => $subscriptionId,
'status' => $status
'status' => $status,
]);
}
@ -87,14 +87,14 @@ class subscriptions
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");
}
}
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");
}
}

View file

@ -32,7 +32,7 @@ class transactions
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.");
}
if ($cents < 0 || $sats < 0) {
@ -72,7 +72,7 @@ class transactions
public static function getWhales($n, $currency)
{
if (!in_array($currency, ['cents', 'sats'])) {
if (! in_array($currency, ['cents', 'sats'])) {
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";
@ -84,7 +84,7 @@ class transactions
public static function getLiabilities($currency)
{
if (!in_array($currency, ['cents', 'sats'])) {
if (! in_array($currency, ['cents', 'sats'])) {
throw new \Exception("Invalid currency type.");
}
$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;
use app\app;
use app\models\addresses;
use swentel\nostr\Key\Key;
class users
@ -113,7 +114,12 @@ class users
$stmt->bindParam(':nsec', $nsec);
$stmt->bindParam(':npub', $npub);
$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)

View file

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

View file

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

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

View file

@ -1,10 +1,12 @@
<section class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
{% include 'lib/alert.twig' %}
<h3 class="text-2xl font-semibold">Profile</h3>
<h3 class="text-2xl font-semibold">
Profile
</h3>
{% include 'lib/rule.twig' %}
<form action="/account/profile" method="post">
{% include 'lib/form/profile.twig' with {
{% include 'lib/forms/profile.twig' with {
name: user.name,
company_name: user.company_name,
company_type: user.company_type,
@ -17,14 +19,19 @@
</form>
</div>
<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">
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
type: 'text',
name: '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 {
label: 'Save Email',
onclick: 'this.parentNode.submit()',
@ -33,66 +40,102 @@
</div>
<div class="flex flex-col gap-1">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Shipping</h3>
<a href="/account/shipping">Edit</a>
<h3 class="text-2xl font-semibold">
Shipping
</h3>
<a href="/account/shipping">
Edit
</a>
</div>
{% include 'lib/rule.twig' %}
<div class="flex flex-col gap-1">
<h4 class="font-semibold">{{ default_shipping.name }}</h4>
<h4 class="font-semibold">{{ default_shipping.company }}</h4>
<h4 class="font-semibold">{{ default_shipping.addressLine1 }}</h4>
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4>
<h4 class="font-semibold">{{ default_shipping.city }}, {{ default_shipping.state }}, {{ default_shipping.zip }}</h4>
<h4 class="font-semibold">{{ default_shipping.phone }}</h4>
</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 class="flex flex-col gap-1">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Billing</h3>
<a href="/account/billing">Edit</a>
<h3 class="text-2xl font-semibold">
Billing
</h3>
<a href="/account/billing">
Edit
</a>
</div>
{% include 'lib/rule.twig' %}
<div class="flex flex-col gap-1">
<h4 class="font-semibold">{{ default_billing.name }}</h4>
<h4 class="font-semibold">{{ default_billing.company }}</h4>
<h4 class="font-semibold">{{ default_billing.addressLine1 }}</h4>
<h4 class="font-semibold">{{ default_billing.addressLine2 }}</h4>
<h4 class="font-semibold">{{ default_billing.city }}, {{ default_billing.state }}, {{ default_billing.zip }}</h4>
<h4 class="font-semibold">{{ default_billing.phone }}</h4>
</div>
{% for address in addresses %}
{% if address.id == user.billing_address_id %}
{% include 'lib/address.twig' with {
address: address,
edit_url: '/account/billing/edit/' ~ address.id,
delete_url: '/account/billing/delete/' ~ address.id
} %}
{% endif %}
{% endfor %}
</div>
<div class="flex flex-col gap-1">
<div class="flex justify-between items-center">
<h3 class="text-2xl font-semibold">Credit Card</h3>
<a href="/account/billing">Edit</a>
<h3 class="text-2xl font-semibold">
Credit Card
</h3>
<a href="/account/billing">
Edit
</a>
</div>
{% include 'lib/rule.twig' %}
</div>
<div class="flex flex-col gap-1">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<h3 class="text-2xl font-semibold">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>
<h3 class="text-2xl font-semibold">
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>
<span>$0.00</span>
<span>
$0.00
</span>
</div>
{% include 'lib/rule.twig' %}
</div>
<div class="flex flex-col gap-1">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<h3 class="text-2xl font-semibold">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>
<h3 class="text-2xl font-semibold">
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>
<span>0</span>
<span>
0
</span>
</div>
{% include 'lib/rule.twig' %}
</div>
<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' %}
<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',
name: 'opt_in_promotional',
on: user.opt_in_promotional
@ -106,9 +149,9 @@
</section>
{% include 'lib/modal.twig' with {
id: 'store-credit',
content: 'lib/policy/credit.twig'
content: 'lib/modals/credit.twig'
} %}
{% include 'lib/modal.twig' with {
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">
<div class="flex flex-col gap-2">
<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' %}
</div>
{% include 'lib/alert.twig' %}
<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',
name: 'email',
label: 'Email link',

View file

@ -1,6 +1,8 @@
<section>
<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' %}
</div>
{% include 'lib/empty.twig' with {

View file

@ -1,6 +1,8 @@
<section>
<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' %}
</div>
{% include 'lib/empty.twig' with {

View file

@ -1,21 +1,26 @@
<section class="flex flex-col gap-4">
<div>
<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' %}
<p class="text-xs">This is your default shipping address for orders at checkout.</p>
</div>
<div class='flex flex-col'>
<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>
<p class="text-xs">
This is your default shipping address for orders at checkout.
</p>
</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>
<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'
} %}
{% include 'lib/button.twig' with {
@ -23,9 +28,17 @@
onclick: 'this.parentNode.submit()'
} %}
</form>
{% if addresses|length > 1 %}
{% include 'lib/rule.twig' with {
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>

View file

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

View file

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

View file

@ -1,25 +1,45 @@
<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">
<thead>
<tr>
<th class="py-2">To</th>
<th class="py-2">From</th>
<th class="py-2">Subject</th>
<th class="py-2">Created At</th>
<th class="py-2">
To
</th>
<th class="py-2">
From
</th>
<th class="py-2">
Subject
</th>
<th class="py-2">
Created At
</th>
</tr>
</thead>
<tbody>
{% for email in recent_emails %}
<tr>
<td class="border px-4 py-2">{{ email.to_email }}</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>
<td class="border px-4 py-2">
{{ email.to_email }}
</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>
{% else %}
<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>
{% endfor %}
</tbody>

View file

@ -1,10 +1,22 @@
<section class="flex flex-col gap-4">
<a href="/admin">Dashboard</a>
<a href="/admin/users">Users</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>
<a href="/admin">
Dashboard
</a>
<a href="/admin/users">
Users
</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
</section>

View file

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

View file

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

View file

@ -1,6 +1,8 @@
<section>
<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' %}
</div>
{% include 'lib/empty.twig' with {

View file

@ -47,13 +47,31 @@
</div>
<div class="{{ colors.footer.policy }} flex justify-center w-full py-4">
<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'>
<a href="/policy#terms-of-sale" class="hover:underline">Terms of Sale</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>
<a href="/policy#terms-of-sale" class="hover:underline">
Terms of Sale
</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>

View file

@ -1,11 +1,13 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="robots" content="noindex">
<link rel="stylesheet" href="/style.css">
<title>{{ page_title }}</title>
<link rel="icon" href="/img/icon.png">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="robots" content="noindex" />
<link rel="stylesheet" href="/style.css" />
<title>
{{ page_title }}
</title>
<link rel="icon" href="/img/icon.png" />
<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>
<script>

View file

@ -29,8 +29,15 @@
<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="w-[97%] lg:w-[90%] xl:w-4/5 flex justify-between">
<a href="/support/ask">Help Center</a>
<span>Save 5% when you pay with Bitcoin</span><a href="/support/bitcoin">Learn More</a>
<a href="/support/ask">
Help Center
</a>
<span>
Save 5% when you pay with Bitcoin
</span>
<a href="/support/bitcoin">
Learn More
</a>
</div>
</div>
@ -42,7 +49,7 @@
</a>
</div>
<form action="/search" method="post" class="flex-grow max-w-[900px]">
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
name: 'search',
type: 'search',
placeholder: 'What are you looking for?',
@ -56,11 +63,8 @@
<!-- Hidden checkbox to control the dropdown -->
<input type="checkbox" id="dropdown-toggle" class="hidden peer">
<!-- Dropdown button -->
<label for="dropdown-toggle"
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">
<label for="dropdown-toggle" 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">
<circle cx="12" cy="8" r="5" />
<path d="M20 21a8 8 0 0 0-16 0" />
</svg>
@ -73,32 +77,55 @@
{% endif %}
</div>
<div class="flex items-center gap-[1px]">
<div class="text-sm font-semibold leading-none">Account</div> <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">
<div class="text-sm font-semibold leading-none">
Account
</div>
<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" />
</svg>
</div>
</div>
</label>
<div
class="absolute mt-2 {{ colors.dropdown.list }} border rounded-md shadow-md w-48 hidden peer-checked:block z-50">
<div class="absolute mt-2 {{ colors.dropdown.list }} border rounded-md shadow-md w-48 hidden peer-checked:block z-50">
<ul class="py-2">
{% 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><a href="/account/orders" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Orders</a></li>
<li><a href="/account/returns" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">Returns</a>
<li>
<a href="/account" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
Account
</a>
</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><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>
{% else %}
<li><a href="/account/login" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">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>
<a href="/account/login" class="block px-4 py-2 m-1 rounded-lg {{ colors.dropdown.item }}">
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>
{% endif %}
@ -106,13 +133,21 @@
{% if is_admin %}
{% include 'lib/rule.twig' %}
<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>
{% endif %}
{% if session.user_id is defined %}
{% include 'lib/rule.twig' %}
<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>
{% endif %}
</div>
@ -124,39 +159,34 @@
}
});
</script>
<a href="/account/orders"
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">
<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" />
<a href="/account/orders" 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">
<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="M12 22V12" />
</svg>
<div>
<div class="text-xs whitespace-nowrap leading-none">Returns &</div>
<div class="text-sm font-semibold leading-none">Orders</div>
<div class="text-xs whitespace-nowrap leading-none">
Returns &
</div>
<div class="text-sm font-semibold leading-none">
Orders
</div>
</div>
</a>
</div>
<a href="/cart">
<div class="flex items-center">
<div
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">
<div 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">
<circle cx="8" cy="21" r="1"></circle>
<circle cx="19" cy="21" r="1"></circle>
<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>
<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>
</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">
7</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">
7
</div>
</div>
</a>
</div>
@ -165,18 +195,22 @@
<nav class="w-full relative rounded-lg {{ colors.nav.bar }}">
<div class="flex">
<ul class="flex">
<li
class="hoverable rounded-xl border-[5px] {{ colors.nav.item }}">
<a href="/power-meters" class="relative p-1 rounded-xl block text-sm font-bold">220V
Power Meters</a>
<li class="hoverable rounded-xl border-[5px] {{ colors.nav.item }}">
<a href="/power-meters" class="relative p-1 rounded-xl block text-sm font-bold">
220V
Power Meters
</a>
<div class="mega-menu">
<div class="bg-transparent h-[3px]">
<!-- invisible content to keep menu shown when cursor is b/t the item and the content -->
<div class="bg-transparent h-[3px]"><!-- invisible content to keep menu shown when cursor is b/t the item and the content -->
</div>
<div class="p-6 mb-16 rounded-b shadow-lg {{ colors.nav.hovercontent }}">
<div class='flex gap-3 items-baseline'>
<h4 class="text-xl font-semibold">220V Power Meters</h4>
<a href="/power-meters" class="hover:underline font-semibold text-xs {{ colors.anchor.primary }}">Shop All</a>
<h4 class="text-xl font-semibold">
220V Power Meters
</h4>
<a href="/power-meters" class="hover:underline font-semibold text-xs {{ colors.anchor.primary }}">
Shop All
</a>
</div>
</div>
</div>
@ -191,17 +225,23 @@
<a href="/" class="hover:underline {{ colors.breadcrumb.parent }}">
{{ env.APP_NAME }}
</a>
<span class="{{ colors.breadcrumb.seperator }}">></span>
<span class="{{ colors.breadcrumb.seperator }}">
>
</span>
{% for crumb in breadcrumbs %}
{% 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 %}
<a href="{{ crumb.url }}" class="hover:underline {{ colors.breadcrumb.parent }}">
{{ crumb.title }}
</a>
{% endif %}
{% if loop.index < breadcrumbs|length %}
<span class="{{ colors.breadcrumb.seperator }}">></span>
<span class="{{ colors.breadcrumb.seperator }}">
>
</span>
{% endif %}
{% endfor %}
</div>
@ -209,4 +249,5 @@
</div>
{% endif %}
</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 %}
<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"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x {{ colors.error.text }}">
<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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x {{ colors.error.text }}">
<circle cx="12" cy="12" r="10" />
<path d="m15 9-6 6" />
<path d="m9 9 6 6" />
</svg>
<p>{{ session.error }}</p>
</div>
<p>
{{ session.error }}
</p>
</div>
{% endif %}
{% if session.warning is defined %}
<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"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-circle-alert {{ colors.warning.text }}">
<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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert {{ colors.warning.text }}">
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<p>{{ session.warning }}</p>
</div>
<p>
{{ session.warning }}
</p>
</div>
{% endif %}
{% if session.success is defined %}
<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"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-circle-check {{ colors.success.text }}">
<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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check {{ colors.success.text }}">
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
<p>{{ session.success }}</p>
</div>
<p>
{{ session.success }}
</p>
</div>
{% endif %}
{% if session.info is defined %}
<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"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info {{ colors.info.text }}">
<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" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info {{ colors.info.text }}">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
<p>{{ session.info }}</p>
</div>
<p>
{{ session.info }}
</p>
</div>
{% endif %}

View file

@ -1,46 +1,49 @@
{% if href is defined %}
<a 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 %}
<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">
{% endif %}
<a
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 %}
<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">
{% endif %}
{% if label is defined %}
<span>{{ label }}</span>
<span>
{{ label }}
</span>
{% endif %}
{% if icon is defined %}
{% if icon == 'search' %}
<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">
<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">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</svg>
{% elseif icon == 'add' %}
<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">
<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">
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
{% elseif icon == 'enter' %}
<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">
<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">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
{% endif %}
{% endif %}
{% if href is defined %}
{% if href is defined %}
</a>
{% else %}
</div>
{% endif %}
{% if captcha is defined %}
<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
<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 class="w-[250px] text-[10px] text-center">
This form is protected by reCAPTCHA and the Google
<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>
</div>
{% 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>
<tr>
<td>
<div style="margin: 0px auto; max-width: 648px">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="width: 100%">
<tbody>
@ -15,41 +14,51 @@
<tr>
<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%">
<tbody>
<tr>
<td align="left" class="x_225906249text x_225906249text-surtitle" 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>
<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>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text x_225906249text-header-title" 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>
<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>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text" 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>
<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>
</td>
</tr>
<tr>
<td align="left" class="x_225906249text x_225906249text-code" 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>
<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>
</td>
</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%">
<tbody>
<tr>
<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>
</tr>
</tbody>
@ -58,8 +67,10 @@
</tr>
<tr>
<td align="left" class="x_225906249text" 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>
<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 associates will never ask you for this code. Do not share this passcode with anyone for any reason.
</div>
</td>
</tr>
<tr>
@ -82,7 +93,6 @@
</tbody>
</table>
</div>
</td>
</tr>
</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" />
<h3 class="text-2xl font-semibold">{{ title }}</h3>
<p class="w-[300px] text-sm">{{ subtitle }}</p>
<h3 class="text-2xl font-semibold">
{{ title }}
</h3>
<p class="w-[300px] text-sm">
{{ subtitle }}
</p>
</div>

View file

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

View file

@ -1,31 +1,31 @@
<div class="flex flex-col gap-4 my-4">
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
type: 'text',
name: 'name',
label: 'Name',
value: user.name
} %}
<div class="flex gap-4">
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
type: 'text',
name: 'company_name',
label: 'Company Name',
value: user.company_name
} %}
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
type: 'text',
name: 'company_type',
label: 'Company Type',
value: user.company_type
} %}
{% include 'lib/input.twig' with {
{% include 'lib/inputs/text.twig' with {
type: 'text',
name: 'company_size',
label: 'Company Size',
value: user.company_size
} %}
</div>
{% include 'lib/toggle.twig' with {
{% include 'lib/inputs/toggle.twig' with {
name: 'dark_theme',
label: 'Use 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 class="{{ colors.modal.content }} p-8 border rounded relative">
{% 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>

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 %}
<div class="relative">
<div class="absolute inset-0 flex items-center"><span class="w-full border-t {{ colors.rule }}"></span></div>
<div class="relative flex justify-center text-xs"><span
class="px-2 {{ colors.body }}">{{ text }}</span>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<span class="w-full border-t {{ colors.rule }}"></span>
</div>
<div class="relative flex justify-center text-xs">
<span class="px-2 {{ colors.body }}">
{{ text }}
</span>
</div>
</div>
</div>
{% else %}
<hr class="{{ colors.rule }}" />
<hr class="{{ colors.rule }}" />
{% endif %}

View file

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