This commit is contained in:
count-null 2025-03-04 21:56:40 -05:00
parent a2d5494f15
commit 35971fd696
30 changed files with 385 additions and 269 deletions

View file

@ -106,6 +106,7 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
'/account/billing' => $defaults['is_user'] ? account::billing($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/orders' => $defaults['is_user'] ? account::orders($defaults) : header('Location: /account/login'),
'/account/shipping' => $defaults['is_user'] ? account::shipping($defaults) : header('Location: /account/login'), '/account/shipping' => $defaults['is_user'] ? account::shipping($defaults) : header('Location: /account/login'),
'/account/notifications' => $defaults['is_user'] ? account::notifications($defaults) : header('Location: /account/login'),
'/account/address/edit' => $defaults['is_user'] ? account::address_edit($defaults) : header('Location: /account/login'), '/account/address/edit' => $defaults['is_user'] ? account::address_edit($defaults) : header('Location: /account/login'),
'/account/address/set-default-shipping' => $defaults['is_user'] ? account::set_default_shipping($defaults) : header('Location: /account/login'), '/account/address/set-default-shipping' => $defaults['is_user'] ? account::set_default_shipping($defaults) : header('Location: /account/login'),
'/account/address/set-default-billing' => $defaults['is_user'] ? account::set_default_billing($defaults) : header('Location: /account/login'), '/account/address/set-default-billing' => $defaults['is_user'] ? account::set_default_billing($defaults) : header('Location: /account/login'),

View file

@ -1,58 +1,174 @@
<?php <?php
return [ $colors = [
'header' => [ 'header' => [
'banner' => 'bg-gray-100 dark:bg-gray-600 text-gray-200 dark:text-gray-200', 'banner' => [
'light' => 'bg-gray-100 text-gray-200',
'dark' => 'bg-gray-600 text-gray-200',
],
], ],
'anchor' => [ 'anchor' => [
'primary' => 'text-blue-400 dark:text-blue-200', 'primary' => [
'light' => 'text-blue-400',
'dark' => 'text-blue-200',
],
],
'body' => [
'light' => 'bg-white text-gray-600',
'dark' => 'bg-gray-800 text-gray-300',
], ],
'body' => 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300',
'button' => [ 'button' => [
'primary' => 'border-blue-400 dark:border-blue-600 dark:hover:border-blue-800 bg-blue-400 dark:bg-blue-600 hover:bg-blue-600 hover:dark:bg-blue-800 text-white dark:text-white', 'primary' => [
'default' => 'hover:bg-gray-50 dark:hover:bg-gray-900', 'light' => 'border-blue-400 bg-blue-400 hover:bg-blue-600 text-white',
'dark' => 'border-blue-600 hover:border-blue-800 bg-blue-600 hover:bg-blue-800 text-white',
], ],
'breadcrumb' => [ 'default' => [
'parent' => 'text-gray-300 dark:text-gray-400 hover:text-gray-400 dark:hover:text-gray-500', 'light' => 'bg-gray-200 hover:bg-gray-300 text-gray-800',
'seperator' => 'text-gray-200 dark:text-gray-200', 'dark' => 'bg-gray-200 hover:bg-gray-300 text-gray-800',
'child' => 'text-gray-200 dark:text-gray-300', ],
'danger' => [
'light' => 'border-red-400 bg-red-400 hover:bg-red-600 text-white',
'dark' => 'border-red-600 hover:border-red-800 bg-red-600 hover:bg-red-800 text-white',
], ],
'dropdown' => [ 'dropdown' => [
'list' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', 'light' => 'hover:bg-gray-50',
'item' => 'hover:bg-gray-200 dark:hover:bg-gray-900', 'dark' => 'hover:bg-gray-900',
],
],
'breadcrumb' => [
'parent' => [
'light' => 'text-gray-300 hover:text-gray-400',
'dark' => 'text-gray-400 hover:text-gray-500',
],
'seperator' => [
'light' => 'text-gray-200',
'dark' => 'text-gray-200',
],
'child' => [
'light' => 'text-gray-200',
'dark' => 'text-gray-300',
],
],
'dropdown' => [
'list' => [
'light' => 'bg-white border-gray-600',
'dark' => 'bg-blue-900 border-gray-300',
],
'item' => [
'light' => 'hover:bg-gray-200',
'dark' => 'hover:bg-gray-900',
],
],
'input' => [
'light' => 'text-gray-800 bg-white border-gray-300 focus:ring-blue-500',
'dark' => 'text-gray-300 bg-gray-800 border-gray-500 focus:ring-blue-500',
], ],
'input' => 'text-gray-800 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-500 focus:ring-blue-500',
'error' => [ 'error' => [
'text' => 'text-red-600', 'text' => [
'alert' => 'bg-red-100 text-gray-800 border-red-600', 'light' => 'text-red-600',
'dark' => 'text-red-600',
],
'alert' => [
'light' => 'bg-red-100 text-gray-800 border-red-600',
'dark' => 'bg-red-100 text-gray-800 border-red-600',
],
], ],
'warning' => [ 'warning' => [
'text' => 'text-yellow-400', 'text' => [
'alert' => 'bg-yellow-100 text-gray-800 border-yellow-400', 'light' => 'text-yellow-400',
'dark' => 'text-yellow-400',
],
'alert' => [
'light' => 'bg-yellow-100 text-gray-800 border-yellow-400',
'dark' => 'bg-yellow-100 text-gray-800 border-yellow-400',
],
], ],
'success' => [ 'success' => [
'text' => 'text-green-600', 'text' => [
'alert' => 'bg-green-100 text-gray-800 border-green-600', 'light' => 'text-green-600',
'dark' => 'text-green-600',
],
'alert' => [
'light' => 'bg-green-100 text-gray-800 border-green-600',
'dark' => 'bg-green-100 text-gray-800 border-green-600',
],
], ],
'info' => [ 'info' => [
'text' => 'text-blue-400', 'text' => [
'alert' => 'bg-blue-200 text-gray-800 border-blue-400', 'light' => 'text-blue-400',
'dark' => 'text-blue-400',
],
'alert' => [
'light' => 'bg-blue-200 text-gray-800 border-blue-400',
'dark' => 'bg-blue-200 text-gray-800 border-blue-400',
],
], ],
'modal' => [ 'modal' => [
'content' => 'bg-white dark:bg-blue-900 border-gray-600 dark:border-gray-300', 'content' => [
'shadow' => 'bg-black/70', 'light' => 'bg-white border-gray-600',
'dark' => 'bg-blue-900 border-gray-300',
],
'shadow' => [
'light' => 'bg-black/70',
'dark' => 'bg-black/70',
],
], ],
'nav' => [ 'nav' => [
'bar' => 'bg-blue-400 dark:bg-blue-600 text-gray-200 dark:text-gray-200', 'bar' => [
'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', 'light' => 'bg-blue-400 text-gray-200',
'hovercontent' => 'bg-white dark:bg-slate-700 text-gray-800 dark:text-gray-300', 'dark' => 'bg-blue-600 text-gray-200',
],
'item' => [
'light' => 'hover:bg-blue-600 hover:text-gray-200 text-white border-blue-400',
'dark' => 'hover:bg-blue-800 hover:text-gray-300 text-white border-blue-600',
],
'hovercontent' => [
'light' => 'bg-white text-gray-800',
'dark' => 'bg-slate-700 text-gray-300',
],
],
'rule' => [
'light' => 'border-gray-400',
'dark' => 'border-gray-400',
], ],
'rule' => 'border-gray-400 dark:border-gray-400',
'text' => [ 'text' => [
'muted' => 'text-gray-400 dark:text-gray-300', 'muted' => [
'light' => 'text-gray-400',
'dark' => 'text-gray-300',
],
],
'toggle' => [
'light' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white",
'dark' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white",
], ],
'toggle' => "bg-gray-300 peer-checked:bg-green-400 after:bg-white",
'footer' => [ 'footer' => [
"primary" => "bg-gray-200 dark:bg-slate-600 text-gray-500 dark:text-gray-300", 'primary' => [
"policy" => "bg-slate-400 dark:bg-slate-800 text-gray-200 dark:text-gray-400", 'light' => "bg-gray-200 text-gray-500",
'dark' => "bg-slate-600 text-gray-300",
],
'policy' => [
'light' => "bg-slate-400 text-gray-200",
'dark' => "bg-slate-800 text-gray-400",
],
], ],
]; ];
function lightDarkify($colors, $level = 0)
{
$result = ['light' => [], 'dark' => []];
if ($level >= 3) {
return $result;
}
foreach ($colors as $key => $value) {
if (is_array($value)) {
$subResult = lightDarkify($value, $level + 1);
foreach ($subResult as $theme => $subValue) {
if (isset($result[$theme])) {
$result[$theme][$key] = $subValue;
}
}
} elseif ($key == 'light' || $key == 'dark') {
$result[$key] = $value;
}
}
return $result;
}
return lightDarkify($colors)[$theme];

View file

@ -3,6 +3,7 @@ namespace app\controllers;
use app\models\addresses; use app\models\addresses;
use app\models\magic_links; use app\models\magic_links;
use app\models\transactions;
use app\models\users; use app\models\users;
use app\models\user_settings; use app\models\user_settings;
@ -10,14 +11,12 @@ class account
{ {
public static function index($defaults): void public static function index($defaults): void
{ {
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/index.twig', 'child_template' => 'account/index.twig',
'page_title' => 'Manage Account - ' . $_ENV['APP_NAME'], 'page_title' => 'Manage Account - ' . $_ENV['APP_NAME'],
'user' => $user, 'user' => users::getById($_SESSION['user_id']),
'addresses' => $addresses, 'addresses' => addresses::getByUserId($_SESSION['user_id']),
'balance' => transactions::getUserBalance($_SESSION['user_id']),
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => null, 'url' => null,
@ -33,6 +32,7 @@ class account
$bill = addresses::validatePost("billing_"); $bill = addresses::validatePost("billing_");
if (isset($bill['error'])) { if (isset($bill['error'])) {
header('Location: /account/billing'); header('Location: /account/billing');
exit;
} }
$bill_id = addresses::add( $bill_id = addresses::add(
$_SESSION['user_id'], $_SESSION['user_id'],
@ -47,15 +47,13 @@ class account
); );
$_SESSION['success'] = "Billing address saved!"; $_SESSION['success'] = "Billing address saved!";
header('Location: /account/billing'); header('Location: /account/billing');
exit;
} }
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/billing.twig', 'child_template' => 'account/billing.twig',
'page_title' => 'Billing Information - ' . $_ENV['APP_NAME'], 'page_title' => 'Billing Information - ' . $_ENV['APP_NAME'],
'user' => $user, 'user' => users::getById($_SESSION['user_id']),
'addresses' => $addresses, 'addresses' => addresses::getByUserId($_SESSION['user_id']),
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
@ -74,7 +72,9 @@ class account
users::updateProfileById($_SESSION['user_id'], $_POST); users::updateProfileById($_SESSION['user_id'], $_POST);
$dark_theme = $_POST['dark_theme'] ?? false; $dark_theme = $_POST['dark_theme'] ?? false;
user_settings::update($_SESSION['user_id'], ['dark_theme' => $dark_theme]); user_settings::update($_SESSION['user_id'], ['dark_theme' => $dark_theme]);
$_SESSION['success'] = 'Profile information updated sucessfully!';
header('Location: /account'); header('Location: /account');
exit;
} }
} }
@ -122,7 +122,6 @@ class account
exit; exit;
} }
} }
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/verify.twig', 'child_template' => 'account/verify.twig',
'page_title' => $_ENV['APP_NAME'], 'page_title' => $_ENV['APP_NAME'],
@ -155,6 +154,7 @@ class account
} }
if (isset($_SESSION['user_id'])) { if (isset($_SESSION['user_id'])) {
header('Location: /account'); header('Location: /account');
exit;
} }
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/login.twig', 'child_template' => 'account/login.twig',
@ -168,6 +168,19 @@ class account
])); ]));
} }
public static function notifications($defaults)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$opt_in_promotional = isset($_POST['opt_in_promotional']) ? (bool) $_POST['opt_in_promotional'] : false;
$opt_in_subscription = isset($_POST['opt_in_subscription']) ? (bool) $_POST['opt_in_subscription'] : false;
$opt_in_order = isset($_POST['opt_in_order']) ? (bool) $_POST['opt_in_order'] : false;
users::updateOptIn($_SESSION['user_id'], $opt_in_promotional, $opt_in_subscription, $opt_in_order);
$_SESSION['success'] = "Notification preferences updated successfully.";
header('Location: /account');
exit;
}
}
public static function logout() public static function logout()
{ {
session_unset(); session_unset();
@ -216,6 +229,7 @@ class account
$ship = addresses::validatePost("shipping_"); $ship = addresses::validatePost("shipping_");
if (isset($ship['error'])) { if (isset($ship['error'])) {
header('Location: /account/shipping'); header('Location: /account/shipping');
exit;
} }
$ship_id = addresses::add( $ship_id = addresses::add(
$_SESSION['user_id'], $_SESSION['user_id'],
@ -230,15 +244,14 @@ class account
); );
$_SESSION['success'] = "Shipping address saved!"; $_SESSION['success'] = "Shipping address saved!";
header('Location: /account/shipping'); header('Location: /account/shipping');
exit;
} }
$user = users::getById($_SESSION['user_id']);
$addresses = addresses::getByUserId($_SESSION['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'account/shipping.twig', 'child_template' => 'account/shipping.twig',
'page_title' => $_ENV['APP_NAME'] . ' Shipping', 'page_title' => $_ENV['APP_NAME'] . ' Shipping',
'user' => $user, 'user' => users::getById($_SESSION['user_id']),
'addresses' => $addresses, 'addresses' => addresses::getByUserId($_SESSION['user_id']),
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => '/account', 'url' => '/account',
@ -262,6 +275,7 @@ class account
$_SESSION['error'] = "Failed to set default shipping address."; $_SESSION['error'] = "Failed to set default shipping address.";
} }
header('Location: /account/shipping'); header('Location: /account/shipping');
exit;
} }
} }
@ -276,6 +290,7 @@ class account
$_SESSION['error'] = "Failed to set default billing address."; $_SESSION['error'] = "Failed to set default billing address.";
} }
header('Location: /account/billing'); header('Location: /account/billing');
exit;
} }
} }
@ -299,6 +314,7 @@ class account
$_SESSION['error'] = "Shipping address verification failed. " . $_SESSION['error']; $_SESSION['error'] = "Shipping address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
header('Location: /account/signup'); header('Location: /account/signup');
exit;
} }
if (! $useShipping) { if (! $useShipping) {
$bill = addresses::validatePost("billing_"); $bill = addresses::validatePost("billing_");
@ -306,11 +322,13 @@ class account
$_SESSION['error'] = "Billing address verification failed. " . $_SESSION['error']; $_SESSION['error'] = "Billing address verification failed. " . $_SESSION['error'];
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
header('Location: /account/signup'); header('Location: /account/signup');
exit;
} }
} }
if (isset($_SESSION['error'])) { if (isset($_SESSION['error'])) {
$_SESSION['last_post'] = $_POST; $_SESSION['last_post'] = $_POST;
header('Location: /account/signup'); header('Location: /account/signup');
exit;
} }
$ship_id = addresses::add( $ship_id = addresses::add(
null, null,

View file

@ -13,12 +13,11 @@ class transaction
if (! $tx) { if (! $tx) {
lost::index($defaults); lost::index($defaults);
} }
$user = users::getById($tx['user_id']);
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'transaction.twig', 'child_template' => 'transaction.twig',
'page_title' => 'Transaction Reciept #' . $txid, 'page_title' => 'Transaction Reciept #' . $txid,
'tx' => $tx, 'tx' => $tx,
'user' => $user, 'user' => users::getById($tx['user_id']),
'breadcrumbs' => [ 'breadcrumbs' => [
[ [
'url' => "/transaction/" . $txid, 'url' => "/transaction/" . $txid,

View file

@ -54,7 +54,7 @@ class transactions
public static function getUserBalance($user_id) public static function getUserBalance($user_id)
{ {
$query = "SELECT COALESCE(SUM(cents), 0) AS total_cents, COALESCE(SUM(sats), 0) AS total_sats FROM transactions WHERE user_id = :user_id"; $query = "SELECT COALESCE(SUM(cents), 0) AS cents, COALESCE(SUM(sats), 0) AS sats FROM transactions WHERE user_id = :user_id";
$stmt = app::$db->prepare($query); $stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id); $stmt->bindParam(':user_id', $user_id);
$stmt->execute(); $stmt->execute();

View file

@ -20,8 +20,8 @@ class user_settings
public static function add($user_id, $opt_in_promotional, $dark_theme) public static function add($user_id, $opt_in_promotional, $dark_theme)
{ {
$query = "INSERT INTO user_settings (user_id, opt_in_promotional, opt_in_subscription, opt_in_order, dark_theme) $query = "INSERT INTO user_settings (user_id, opt_in_promotional, dark_theme)
VALUES (:user_id, :opt_in_promotional, :opt_in_subscription, :opt_in_orders, :dark_theme)"; VALUES (:user_id, :opt_in_promotional, :dark_theme)";
$stmt = app::$db->prepare($query); $stmt = app::$db->prepare($query);
$stmt->bindParam(':user_id', $user_id); $stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':opt_in_promotional', $opt_in_promotional); $stmt->bindParam(':opt_in_promotional', $opt_in_promotional);

View file

@ -21,7 +21,6 @@ class users
verified BOOLEAN NOT NULL, verified BOOLEAN NOT NULL,
nsec TEXT, nsec TEXT,
npub TEXT NOT NULL, npub TEXT NOT NULL,
attached_lightning_address TEXT,
replace_email_token TEXT, replace_email_token TEXT,
name TEXT, name TEXT,
company_name TEXT, company_name TEXT,
@ -38,6 +37,20 @@ class users
$stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT); $stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT);
$stmt->execute(); $stmt->execute();
} }
public static function updateOptIn($user_id, $opt_in_promotional, $opt_in_subscription, $opt_in_order)
{
$query = "UPDATE user_settings SET
opt_in_promotional = :opt_in_promotional,
opt_in_subscription = :opt_in_subscription,
opt_in_order = :opt_in_order
WHERE user_id = :user_id";
$stmt = app::$db->prepare($query);
$stmt->bindParam(':opt_in_promotional', $opt_in_promotional, \PDO::PARAM_BOOL);
$stmt->bindParam(':opt_in_subscription', $opt_in_subscription, \PDO::PARAM_BOOL);
$stmt->bindParam(':opt_in_order', $opt_in_order, \PDO::PARAM_BOOL);
$stmt->bindParam(':user_id', $user_id, \PDO::PARAM_INT);
$stmt->execute();
}
public static function setDefaultBilling($user_id, $billing_address_id) public static function setDefaultBilling($user_id, $billing_address_id)
{ {

View file

@ -35,9 +35,8 @@
{% include 'lib/forms/address.twig' with { {% include 'lib/forms/address.twig' with {
type: 'billing_' type: 'billing_'
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Add Address', label: 'Add Address',
onclick: 'this.parentNode.submit()',
} %} } %}
</form> </form>
{% if addresses|length > 1 %} {% if addresses|length > 1 %}

View file

@ -11,11 +11,10 @@
company_name: user.company_name, company_name: user.company_name,
company_type: user.company_type, company_type: user.company_type,
company_size: user.company_size, company_size: user.company_size,
dark_theme: defaults.user_settings.dark_theme dark_theme: user_settings.dark_theme
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/primary.twig' with {
label: 'Save Profile', label: 'Save Profile',
onclick: 'this.parentNode.submit()',
} %} } %}
</form> </form>
</div> </div>
@ -33,9 +32,8 @@
Verified: Verified:
{{ user.verified ? 'Yes' : 'No' }} {{ user.verified ? 'Yes' : 'No' }}
</h4> </h4>
{% include 'lib/button.twig' with { {% include 'lib/buttons/primary.twig' with {
label: 'Save Email', label: 'Save Email',
onclick: 'this.parentNode.submit()',
} %} } %}
</form> </form>
</div> </div>
@ -101,7 +99,7 @@
</a> </a>
</div> </div>
<span> <span>
$0.00 {{ '$' ~ (balance.cents / 100) | number_format(2, '.', ',') }}
</span> </span>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
@ -121,25 +119,36 @@
</a> </a>
</div> </div>
<span> <span>
0 {{ balance.sats | number_format(0, '', ',') }}
</span> </span>
</div> </div>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Marketing Email Notifications
</h3> </h3>
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
<form action="/account/promotionals" method="post" class="flex flex-col gap-4"> <form action="/account/notifications" method="post">
<div class="flex flex-col gap-4 mb-4">
{% include 'lib/inputs/toggle.twig' with { {% include 'lib/inputs/toggle.twig' with {
label: 'Recieve coupons & more', label: 'Recieve coupons & more',
name: 'opt_in_promotional', name: 'opt_in_promotional',
on: defailts.user_settings.opt_in_promotional on: user_settings.opt_in_promotional
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/inputs/toggle.twig' with {
label: 'Recieve subscription reminders',
name: 'opt_in_subscription',
on: user_settings.opt_in_subscription
} %}
{% include 'lib/inputs/toggle.twig' with {
label: 'Recieve order updates',
name: 'opt_in_order',
on: user_settings.opt_in_order
} %}
</div>
{% include 'lib/buttons/primary.twig' with {
label: 'Save', label: 'Save',
onclick: 'this.parentNode.submit()',
} %} } %}
</form> </form>
</div> </div>

View file

@ -15,9 +15,8 @@
subtext: 'Get a one-time link sent to your email. No passwords!', subtext: 'Get a one-time link sent to your email. No passwords!',
placeholder: 'Enter your e-mail' placeholder: 'Enter your e-mail'
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Get login link', label: 'Get login link',
onclick: 'this.parentNode.submit()',
captcha: true captcha: true
} %} } %}
</form> </form>

View file

@ -25,9 +25,8 @@
{% include 'lib/forms/address.twig' with { {% include 'lib/forms/address.twig' with {
type: 'shipping_' type: 'shipping_'
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Add Address', label: 'Add Address',
onclick: 'this.parentNode.submit()'
} %} } %}
</form> </form>
{% if addresses|length > 1 %} {% if addresses|length > 1 %}

View file

@ -85,9 +85,8 @@
} %} } %}
</div> </div>
{% include 'lib/rule.twig' with { text: 'ALL DONE!' } %} {% include 'lib/rule.twig' with { text: 'ALL DONE!' } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Register', label: 'Register',
onclick: 'this.parentNode.submit()',
captcha: true captcha: true
} %} } %}
</form> </form>

View file

@ -10,66 +10,20 @@
{% include 'lib/rule.twig' %} {% include 'lib/rule.twig' %}
</div> </div>
{% include 'lib/alert.twig' %} {% include 'lib/alert.twig' %}
<style>
.code-input {
font-size: 24px;
font-family: monospace;
text-align: left;
letter-spacing: 1.15em;
border: 2px solid #000;
padding: 10px;
width: 250px;
outline: none;
background: linear-gradient(
90deg,
white 0, white 36px, /* Box width */
black 36px, black 38px /* Border between boxes */
);
background-size: 42px 100%; /* Adjust size to fit each character in a cell */
background-clip: padding-box;
}
</style>
<form action="/account/verify" method="post" class="flex flex-col items-center gap-4"> <form action="/account/verify" method="post" class="flex flex-col items-center gap-4">
<input type="tel" name="code" placeholder="******" class="code-input" maxlength="6" inputmode="numeric" pattern="[0-9]*"> {% include 'lib/inputs/text.twig' with {
<script> type: 'tel',
document.addEventListener("DOMContentLoaded", function () { name: 'code',
const input = document.querySelector(".code-input"); placeholder: '******',
class: 'code-input',
input.addEventListener("input", function (e) { maxlength: 6,
// Remove any non-digit characters immediately inputmode: 'numeric',
this.value = this.value.replace(/\D/g, ""); pattern: '[0-9]*'
// Move the cursor back one space then blur when the 6th digit is entered } %}
if (this.value.length === 6) { {% include 'lib/buttons/submit.twig' with {
this.setSelectionRange(this.value.length - 1, this.value.length - 1);
this.blur();
}
});
input.addEventListener("paste", function (e) {
e.preventDefault();
let pastedData = e.clipboardData.getData("text").replace(/\D/g, ""); // Allow only digits
this.value = pastedData.substring(0, this.maxLength);
// Clear focus if pasted data fills the input
if (this.value.length === 6) {
this.blur();
}
});
input.addEventListener("keypress", function (e) {
if (!/[0-9]/.test(e.key)) {
e.preventDefault(); // Block non-numeric input
}
});
});
</script>
{% include 'lib/button.twig' with {
label: 'Verify Code', label: 'Verify Code',
onclick: 'this.parentNode.submit()',
captcha: true
} %} } %}
</form> </form>
</div> </div>
</section> </section>

View file

@ -12,8 +12,7 @@
zip: address.zip, zip: address.zip,
phone: address.phone, phone: address.phone,
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Save Address', label: 'Save Address',
onclick: 'this.parentNode.submit()'
} %} } %}
</form> </form>

View file

@ -2,7 +2,7 @@
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Recently Sent Emails Recently Sent Emails
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">

View file

@ -52,16 +52,16 @@
value: session.last_post.user_identifier value: session.last_post.user_identifier
} %} } %}
{% endif %} {% endif %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Submit', label: 'Submit',
onclick: 'this.parentNode.submit()'
} %} } %}
</form> </form>
{% if session.last_post %} {% if session.last_post %}
{% include 'lib/button.twig' with { <a href="/admin/transactions/reset">
label: 'Cancel', {% include 'lib/buttons/default.twig' with {
href: '/admin/transactions/reset' label: 'Cancel'
} %} } %}
</a>
{% endif %} {% endif %}
</section> </section>

View file

@ -17,16 +17,15 @@
], ],
required: true required: true
} %} } %}
{% include 'lib/button.twig' with { {% include 'lib/buttons/submit.twig' with {
label: 'Submit', label: 'Submit',
onclick: 'this.parentNode.submit()'
} %} } %}
</form> </form>
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Liabilities Liabilities
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">
@ -59,7 +58,7 @@
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Sats Transactions Sats Transactions
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">
@ -101,7 +100,7 @@
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Cents Transactions Cents Transactions
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">
@ -143,7 +142,7 @@
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Whales Sats Whales Sats
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">
@ -179,7 +178,7 @@
<h3 class="text-2xl font-semibold"> <h3 class="text-2xl font-semibold">
Whales Cents Whales Cents
</h3> </h3>
<table class="min-w-full bg-white"> <table class="min-w-full">
<thead> <thead>
<tr> <tr>
<th class="py-2"> <th class="py-2">

View file

@ -48,14 +48,20 @@
<img src="/img/logo-{{ theme }}.webp" /> <img src="/img/logo-{{ theme }}.webp" />
</a> </a>
</div> </div>
<form action="/search" method="post" class="flex-grow max-w-[900px]"> <form action="/search" method="post" class="flex w-full max-w-[900px]">
<div class='flex-grow w-full'>
{% include 'lib/inputs/text.twig' with { {% include 'lib/inputs/text.twig' with {
name: 'search', name: 'search',
type: 'search', type: 'search',
placeholder: 'What are you looking for?', placeholder: 'What are you looking for?',
submit: true,
icon: 'search'
} %} } %}
</div>
<button type="submit" class="px-4 {{ colors.button.primary }} rounded-r">
<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-search">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
</button>
</form> </form>
<div class="flex flex-1 items-center justify-end gap-4 md:basis-[100px] md:min-w-[280px] md:max-w-[280px] xl:basis-[200px] xl:min-w-[300px] xl:max-w-[300px]"> <div class="flex flex-1 items-center justify-end gap-4 md:basis-[100px] md:min-w-[280px] md:max-w-[280px] xl:basis-[200px] xl:min-w-[300px] xl:max-w-[300px]">
<div class="flex gap-1"> <div class="flex gap-1">
@ -63,7 +69,7 @@
<!-- Hidden checkbox to control the dropdown --> <!-- Hidden checkbox to control the dropdown -->
<input type="checkbox" id="dropdown-toggle" class="hidden peer"> <input type="checkbox" id="dropdown-toggle" class="hidden peer">
<!-- Dropdown button --> <!-- Dropdown button -->
<label for="dropdown-toggle" class="{{ colors.button.default }} flex gap-1 items-center cursor-pointer p-1 py-2 rounded-md"> <label for="dropdown-toggle" class="{{ colors.button.dropdown }} flex gap-1 items-center cursor-pointer p-1 py-2 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round">
<circle cx="12" cy="8" r="5" /> <circle cx="12" cy="8" r="5" />
<path d="M20 21a8 8 0 0 0-16 0" /> <path d="M20 21a8 8 0 0 0-16 0" />
@ -159,7 +165,7 @@
} }
}); });
</script> </script>
<a href="/account/orders" class="{{ colors.button.default }} flex items-center gap-1 p-1 py-2 rounded-md"> <a href="/account/orders" class="{{ colors.button.dropdown }} flex items-center gap-1 p-1 py-2 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-box"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" 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="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
<path d="m3.3 7 8.7 5 8.7-5" /> <path d="m3.3 7 8.7 5 8.7-5" />

View file

@ -35,8 +35,8 @@
{% include 'lib/modal.twig' with { {% include 'lib/modal.twig' with {
id: 'delete-modal-' ~ address.id, id: 'delete-modal-' ~ address.id,
content: 'lib/modals/confirm_delete_address.twig', content: 'lib/modals/confirm_delete_address.twig',
okText: 'Yes, delete', dangerText: 'Yes, delete',
okURL: '/address/delete/' ~ address.id ~ '?redirect=' ~ redirect dangerURL: '/address/delete/' ~ address.id ~ '?redirect=' ~ redirect
} %} } %}
<form id="delete-form-{{ address.id }}" action="{{ delete_url }}" method="post" style="display: none;" /> <form id="delete-form-{{ address.id }}" action="{{ delete_url }}" method="post" style="display: none;" />
<input type="hidden" name="address_id" value="{{ address.id }}"></form> <input type="hidden" name="address_id" value="{{ address.id }}"></form>
@ -44,9 +44,9 @@
{% if set_default %} {% if set_default %}
<form id="set-default-form-{{ address.id }}" action="/account/address/set-default-{{ set_default }}" method="post"> <form id="set-default-form-{{ address.id }}" action="/account/address/set-default-{{ set_default }}" method="post">
<input type="hidden" name="address_id" value="{{ address.id }}" /> <input type="hidden" name="address_id" value="{{ address.id }}" />
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> {% include 'lib/buttons/primary.twig' with {
Set Default label: 'Set Default'
</button> } %}
</form> </form>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,49 +0,0 @@
{% 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 %}
{% if label is defined %}
<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">
<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">
<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">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
{% endif %}
{% endif %}
{% 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>
</div>
{% endif %}

View file

@ -0,0 +1,3 @@
<button type="submit" class="{{ colors.button.danger }} font-bold py-2 px-4 rounded no-underline">
{{ label }}
</button>

View file

@ -0,0 +1,3 @@
<button class="{{ colors.button.default }} font-bold py-2 px-4 rounded">
Cancel
</button>

View file

@ -0,0 +1,3 @@
<button type="submit" class="{{ colors.button.primary }} font-bold py-2 px-4 rounded no-underline">
{{ label }}
</button>

View file

@ -0,0 +1,22 @@
<button type="submit" class="{{ colors.button.primary }} w-full rounded-lg h-[42px] flex items-center justify-center">
{% if label is defined %}
<span>
{{ label }}
</span>
{% endif %}
</button>
{% 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>
</div>
{% endif %}

View file

@ -1,11 +1,5 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2"> <label for="{{ id }}" class="block text-sm font-medium mt-2">
{{ label }} {{ label }}
</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') }}"> <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,8 +1,10 @@
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mt-2"> <label for="{{ id }}" class="block text-sm font-medium mt-2">
{{ label }} {{ label }}
</label> </label>
<select id="{{ id }}" name="{{ name }}" class="border rounded-lg p-2 w-full" {% if required %} required {% endif %}> <select id="{{ id }}" name="{{ name }}" class="border rounded-lg p-2 w-full" {% if required %} required {% endif %}>
{% for option in options %} {% for option in options %}
<option value="{{ option.value }}" {% if option.value == value %} selected {% endif %}>{{ option.text }}</option> <option value="{{ option.value }}" {% if option.value == value %} selected {% endif %}>
{{ option.text }}
</option>
{% endfor %} {% endfor %}
</select> </select>

View file

@ -21,17 +21,6 @@
{% endif %} {% endif %}
</label> </label>
{% endif %} {% endif %}
{% if submit is defined %}
<div class="flex items-center"> <input type="{{ type }}" name="{{ name }}" {% if placeholder %} placeholder="{{ placeholder }}" {% endif %} {% if value is not null %} value="{{ value }}" {% endif %} {% if readonly is not null %} readonly {% endif %} {% if type == 'number' %} {% if min is defined %} min="{{ min }}" {% endif %} {% if max is defined %} max="{{ max }}" {% endif %} {% endif %} class="{{ colors.input }} w-full p-3 h-[42px] border focus:ring-1 focus:outline-none"></div>
{% 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

@ -11,14 +11,28 @@
{% if okURL is defined %} {% if okURL is defined %}
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<a href="#hide-{{ id }}" class="mr-4 no-underline"> <a href="#hide-{{ id }}" class="mr-4 no-underline">
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded"> {% include 'lib/buttons/default.twig' with {
Cancel label: "Cancel"
</button> } %}
</a> </a>
<form action="{{ okURL }}" method="post" class="inline"> <form action="{{ okURL }}" method="post" class="inline">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded no-underline"> {% include 'lib/buttons/primary.twig' with {
{{ okText is defined ? okText : 'OK' }} label: okText is defined ? okText : 'OK',
</button> } %}
</form>
</div>
{% endif %}
{% if dangerURL is defined %}
<div class="flex justify-end mt-4">
<a href="#hide-{{ id }}" class="mr-4 no-underline">
{% include 'lib/buttons/default.twig' with {
label: "Cancel"
} %}
</a>
<form action="{{ dangerURL }}" method="post" class="inline">
{% include 'lib/buttons/danger.twig' with {
label: dangerText is defined ? dangerText : 'OK',
} %}
</form> </form>
</div> </div>
{% endif %} {% endif %}

View file

@ -1,4 +1,12 @@
<div> <div>
<div class="text-xl font-semibold mb-4 text-center">
You have
<strong>
{{ '$' ~ (balance.cents / 100) | number_format(2, '.', ',') }}
USD
</strong>
in store credit
</div>
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
Why do I have store credit? Why do I have store credit?
</div> </div>

View file

@ -1,4 +1,19 @@
<div> <div>
<div class="text-xl font-semibold mb-4 text-center">
You have
<strong>
{{ balance.sats }}
</strong>
sats!
</div>
<div class="text-lg font-semibold mb-4">
What are sats?
</div>
<ul class="list-disc pl-6 mb-4">
<li>
Sats are fractions of a Bitcoin (BTC). There are 100 million sats in one Bitcoin (BTC).
</li>
</ul>
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
Why do I have sats? Why do I have sats?
</div> </div>
@ -7,18 +22,19 @@
You may have received sats from a promotional event You may have received sats from a promotional event
</li> </li>
<li> <li>
You may have recieved sats sent to your default generated Lightning Address (LNURL): You may have recieved sats sent to your generated Lightning Address (LNURL):
{{ user.npub }} <br>
@ <strong>
{{ http_host }} {{ user.npub ~ '@' ~ http_host }}
</string>
</li> </li>
</ul> </ul>
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
What can I do with sats? What can I do with these sats?
</div> </div>
<ul class="list-disc pl-6"> <ul class="list-disc pl-6">
<li> <li>
You may spend sats at checkout You may spend these sats at checkout
</li> </li>
<li> <li>
Your subscriptions and recurring purchases can also be paid with sats Your subscriptions and recurring purchases can also be paid with sats
@ -28,3 +44,4 @@
</li> </li>
</ul> </ul>
</div> </div>