From e0b843897d76f63daac371dda3bd0bcdc6a4b8ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 12:43:41 +0000 Subject: [PATCH] Fix README, storefront mobile layout, and cart pricing consistency Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/5e161382-08ef-43a9-8cb3-d6fadad18c00 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 101 +++++++++++++++++++++++------ docs/COPILOT_TODO.md | 1 + modules/billing/add_to_cart.php | 50 +++++++++++++- modules/billing/cart.php | 104 +++++++++++++++++++++++++++--- modules/billing/checkout_free.php | 2 +- modules/billing/css/header.css | 50 ++++++++++++-- modules/billing/login.php | 89 +++++++++++++++++++++---- modules/billing/order.php | 91 ++++++++++++++++++++++---- modules/billing/timestamp.txt | 2 +- 10 files changed, 428 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c60e1971..a77fc6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 2026-05-07 +- **README + storefront mobile/cart pricing fixes:** Rewrote the root README for GSP positioning, hardened storefront mobile responsiveness (login, order, cart, shared header guardrails), fixed add-to-cart price persistence for low-decimal paid items, aligned cart/free-checkout math to `total_due` values, and refreshed the canonical storefront footer timestamp. - **Billing/cart/storefront stability pass:** Hardened `add_to_cart.php` to build schema-compatible invoice inserts dynamically (including legacy installs missing `period_start`), fixed free-checkout DB close handling so wrapper objects are never passed to `mysqli_close()`, switched cart/free-total decisions to cent-based math so low nonzero prices (e.g. $0.02) never show as FREE, improved canonical game deduplication + OS variant matching in storefront list/order pages, and aligned Steam Workshop behavior labels with the new restart/update wording. ## 2026-05-06 diff --git a/README.md b/README.md index dc34443d..c7a2511d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,91 @@ -# GSP Windows Agent +# GameServerPanel (GSP) -Cygwin-based agent that lets the GameServer Panel manage Windows Server 2019/2022 hosts. It mirrors the Linux agent feature set: signed RPC transport, GNU Screen session management, and SteamCMD-aware installers. +GSP is a modern game server hosting panel and commercial-ready hosting platform for teams that need billing, automation, and multi-node operations in one stack. -## Highlights +## What GSP is -- One-click installer (`Install/onceinstall_agent.bat`) that bootstraps Cygwin, required packages, and the `gameserver` service account. -- Task Scheduler entry that keeps the agent running after reboots. -- Helper scripts (`agent_conf.sh`, `rebase_post_ins.bat`, etc.) for maintaining the environment. -- Markdown documentation under [`documentation/agent-guide.md`](documentation/agent-guide.md). +GSP is the actively maintained GameServerPanel project. It is a modernized evolution of legacy Open Game Panel concepts, expanded for current hosting workflows and provider operations. -## Quick start +## Why GSP exists -1. Clone or download the repository to `C:\\gsp-agent`. -2. Right-click `Install\\onceinstall_agent.bat` → “Run as administrator”. -3. Open the bundled Cygwin terminal and configure the agent: - ```bash - cd /OGP - bash agent_conf.sh -p "gameserverPassword" - ``` -4. Edit `C:\\OGP\\Cfg\\Config.pm` (match the settings you entered in the GSP web panel) and start the “OGP agent start on boot” scheduled task. +Traditional game panel workflows often require manual setup, disconnected billing flows, and custom glue code between storefronts and provisioning. +GSP exists to provide a stronger foundation for automated service delivery, consistent customer experience, and safer long-term operations. -## Related repositories +## Core features -- [GSP](https://github.com/GameServerPanel/GSP) – PHP panel that issues commands to the agents. -- [GSP-Agent-Linux](https://github.com/GameServerPanel/GSP-Agent-Linux) – Linux counterpart with systemd service files. +- Unified panel + storefront architecture +- Shared customer sessions between website and panel surfaces +- Billing-aware server lifecycle management +- Multi-node service placement across locations +- XML-driven game metadata and install configuration + +## Automated provisioning + +GSP includes work toward fully automated provisioning pipelines that connect order/payment events to server creation, home assignment, and post-provision workflows. + +## Storefront, cart, billing, and PayPal support + +The billing module provides the foundation for: + +- Product catalog and order configuration +- Cart and invoice handling +- Coupon/discount workflows +- PayPal checkout and capture integration + +## Steam Workshop management + +GSP is designed to support Steam Workshop-enabled game operations, including profile-driven defaults and per-server workshop management flows. + +## XML / game configuration management + +Game definitions are XML-based so catalog, install metadata, and operational settings can stay centralized and extensible without hardcoded per-title logic. + +## Multi-location and OS-aware deployment + +GSP includes work toward OS-aware service routing and multi-location hosting so providers can target the right node/runtime combinations per game and region. + +## Database migration safety + +The project uses versioned module migrations and idempotent upgrade patterns, with guarded schema checks where needed, to reduce upgrade risk across diverse installs. + +## Security improvements + +Current code includes hardened session handling, credential verification updates, CSRF protection in key admin paths, and safer billing/provisioning validation patterns. + +## Hosting provider benefits + +- Faster order-to-server delivery +- Better control over node/location availability +- Reduced manual operations overhead +- Cleaner upgrade path for production environments + +## Customer experience improvements + +- More consistent ordering and checkout flows +- Shared login/session behavior across surfaces +- Better visibility into orders, renewals, and account actions + +## Technology stack + +- PHP 8.x (actively modernized for current compatibility needs) +- MySQL/MariaDB-backed data model +- XML-based game configuration system +- PayPal REST integration for storefront checkout + +## Roadmap and future goals + +GSP continues to focus on: + +- Stronger automation and provisioning reliability +- Expanded storefront UX and mobile usability +- Broader game/workshop tooling improvements +- Operational observability and admin quality-of-life features ## Contributing -Send pull requests through GitHub. Test installer changes on a clean Windows Server VM, keep batch files in ASCII, and update `documentation/agent-guide.md` whenever you modify the workflow. +Pull requests are welcome. +Please keep changes production-safe, follow existing GSP patterns, and avoid introducing legacy compatibility shortcuts that conflict with current architecture. + +## License + +GSP is distributed under the GNU General Public License v2. See [`LICENSE`](LICENSE) and [`COPYING`](COPYING). diff --git a/docs/COPILOT_TODO.md b/docs/COPILOT_TODO.md index af2f5d19..1b597ccc 100644 --- a/docs/COPILOT_TODO.md +++ b/docs/COPILOT_TODO.md @@ -6,3 +6,4 @@ - Add a lightweight admin UI report that flags remaining PHP files still relying on legacy PHP 7 constructs not covered by the automated compatibility pass. - Add a side-by-side before/after diff preview panel to the config_games top-level XML section editor before section saves. - Add an integration smoke test that exercises paid checkout, free checkout, and add-to-cart on installs with/without `period_start` to prevent billing schema drift regressions. +- Add a storefront visual-regression check at 375px and 430px breakpoints covering login, order, and cart pages to prevent mobile overflow regressions. diff --git a/modules/billing/add_to_cart.php b/modules/billing/add_to_cart.php index c1064515..83606b46 100644 --- a/modules/billing/add_to_cart.php +++ b/modules/billing/add_to_cart.php @@ -54,6 +54,35 @@ function billing_cents_to_money(int $cents): float return $cents / 100; } +function billing_rate_from_service(mysqli $db, string $table_prefix, int $service_id, string $rate_type): float +{ + if ($service_id <= 0) { + return 0.0; + } + + $stmt = $db->prepare("SELECT price_daily, price_monthly, price_year FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); + if (!$stmt) { + return 0.0; + } + + $stmt->bind_param('i', $service_id); + $stmt->execute(); + $stmt->bind_result($price_daily, $price_monthly, $price_year); + $rate = 0.0; + if ($stmt->fetch()) { + if ($rate_type === 'daily') { + $rate = floatval($price_daily); + } elseif ($rate_type === 'yearly') { + $rate = floatval($price_year); + } else { + $rate = floatval($price_monthly); + } + } + $stmt->close(); + + return $rate; +} + function billing_fail_add_to_cart(string $message, array $context = []): void { site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context)); @@ -97,6 +126,9 @@ $ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0; $max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0; $qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1; $invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month'; +$display_service_id = isset($_POST['display_service_id']) ? intval($_POST['display_service_id']) : 0; +$display_rate = isset($_POST['display_rate']) ? floatval($_POST['display_rate']) : 0.0; +$posted_total = isset($_POST['calculated_total']) ? floatval($_POST['calculated_total']) : 0.0; $remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : ''; $ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : ''; @@ -168,6 +200,17 @@ if ($service_id > 0) { } } +if ($base_rate <= 0 && $display_service_id > 0) { + $fallback_rate = billing_rate_from_service($db, $table_prefix, $display_service_id, $durationInfo['rate_type']); + if ($fallback_rate > 0) { + $base_rate = $fallback_rate; + } +} + +if ($base_rate <= 0 && $display_rate > 0) { + $base_rate = $display_rate; +} + if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) { $remote_control_password = billing_generate_password(); } @@ -181,7 +224,12 @@ $status = 'due'; // Invoice status: due (unpaid), paid $payment_status = 'unpaid'; $qty = max(1, $qty); $max_players = max(1, $max_players); -$subtotal_cents = billing_money_to_cents((float)$base_rate * $max_players * $qty); +$rate_per_player_cents = max(0, billing_money_to_cents($base_rate)); +$subtotal_cents = $rate_per_player_cents * $max_players * $qty; +$posted_total_cents = max(0, billing_money_to_cents($posted_total)); +if ($subtotal_cents <= 0 && $posted_total_cents > 0 && $base_rate > 0) { + $subtotal_cents = $posted_total_cents; +} $subtotal = billing_cents_to_money($subtotal_cents); $amount = $subtotal; $period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days')); diff --git a/modules/billing/cart.php b/modules/billing/cart.php index 3b7c0591..c6ccb289 100644 --- a/modules/billing/cart.php +++ b/modules/billing/cart.php @@ -317,11 +317,13 @@ $siteBase = $protocol . $host; } .cart-container { max-width: 900px; - margin: 40px auto; + margin: 24px auto; background: white; - padding: 30px; + padding: 24px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); + width: min(100%, calc(100% - 24px)); + box-sizing: border-box; } h1 { color: #333; @@ -359,6 +361,7 @@ $siteBase = $protocol . $host; width: 100%; border-collapse: collapse; margin-bottom: 30px; + table-layout: fixed; } .cart-table th { background: #f8f9fa; @@ -418,6 +421,7 @@ $siteBase = $protocol . $host; display: flex; gap: 10px; align-items: flex-end; + flex-wrap: wrap; } .coupon-form > div { flex: 1; @@ -524,6 +528,86 @@ $siteBase = $protocol . $host; } .action-buttons { margin-top: 30px; + display: flex; + flex-wrap: wrap; + gap: 10px; + } + .cart-table-wrap { + width: 100%; + max-width: 100%; + overflow-x: auto; + } + @media (max-width: 768px) { + .cart-container { + width: min(100%, calc(100% - 12px)); + padding: 14px; + margin: 12px auto; + } + h1 { + font-size: 1.5rem; + margin-bottom: 18px; + } + .cart-table thead { + display: none; + } + .cart-table, + .cart-table tbody, + .cart-table tr, + .cart-table td { + display: block; + width: 100%; + } + .cart-table tr { + border: 1px solid #dee2e6; + border-radius: 8px; + margin-bottom: 12px; + padding: 6px 8px; + background: #fff; + } + .cart-table td { + border: 0; + padding: 6px 4px; + text-align: left !important; + } + .cart-table td[data-label]::before { + content: attr(data-label) ": "; + font-weight: 600; + color: #495057; + } + .coupon-form { + flex-direction: column; + align-items: stretch; + } + .coupon-form button { + width: 100%; + } + .coupon-applied { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + .cart-total { + text-align: left; + } + .cart-total-row { + display: flex; + justify-content: space-between; + gap: 10px; + } + .cart-total-label, + .cart-total-amount, + .subtotal-amount, + .discount-amount { + font-size: 1rem; + margin-right: 0; + } + .btn { + width: 100%; + text-align: center; + } + .action-buttons { + margin-top: 16px; + } } @@ -562,6 +646,7 @@ $siteBase = $protocol . $host; Browse Servers +
@@ -576,20 +661,20 @@ $siteBase = $protocol . $host; - - - - - + + + -
+
x - $ + x + $ + @@ -598,6 +683,7 @@ $siteBase = $protocol . $host;
+
diff --git a/modules/billing/checkout_free.php b/modules/billing/checkout_free.php index d883575e..6c9b8f03 100644 --- a/modules/billing/checkout_free.php +++ b/modules/billing/checkout_free.php @@ -85,7 +85,7 @@ if ($couponCode !== '') { // Calculate total and verify it is $0 after discount $totalAmountCents = 0; foreach ($invoices as $inv) { - $lineAmount = (float)($inv['amount'] ?? 0); + $lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0); $totalAmountCents += billing_free_money_to_cents($lineAmount); } $discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0)); diff --git a/modules/billing/css/header.css b/modules/billing/css/header.css index 84c43c76..f0970baf 100644 --- a/modules/billing/css/header.css +++ b/modules/billing/css/header.css @@ -1,8 +1,21 @@ /* Global font family - legible sans-serif stack */ +html, +body { + width: 100%; + max-width: 100%; + overflow-x: hidden; +} + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } +#gsw-site { + width: 100%; + max-width: 100%; + overflow-x: hidden; +} + .gsw-top{display:flex;align-items:center;gap:12px;padding:12px 24px;background:#fff;border-bottom:1px solid rgba(0,0,0,0.05);} .gsw-top img{height:40px;width:auto;display:block} .gsw-top .gsw-site-name{font-weight:700;font-size:1.1rem;color:#333} @@ -12,7 +25,7 @@ body { .gsw-header{display:flex;flex-direction:column;align-items:stretch;padding:0;background:transparent;margin-bottom:18px;} /* Top row: contains left (logo/title) and right (login) divs as separate blocks */ -#gsw-site .gsw-header-top{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:12px 20px;background:#0b3b6f !important;backdrop-filter:blur(6px);box-shadow:0 2px 6px rgba(0,0,0,0.18);width:100%;} +#gsw-site .gsw-header-top{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:12px 20px;background:#0b3b6f !important;backdrop-filter:blur(6px);box-shadow:0 2px 6px rgba(0,0,0,0.18);width:100%;max-width:100%;} /* Left div: logo + title, takes up available space */ #gsw-site .gsw-header-left{flex:1 1 auto;display:flex;align-items:center;font-weight:700;font-size:1.4rem;color:#fff;padding-left:8px;} @@ -25,8 +38,8 @@ body { .gsw-header-left a{color:#fff;text-decoration:none;} /* Bottom row: centered navigation menu */ -#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;} -.gsw-header-nav{display:flex;gap:22px;align-items:center;} +#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;max-width:100%;} +.gsw-header-nav{display:flex;gap:22px;align-items:center;max-width:100%;} .gsw-nav-link{color:#fff;text-decoration:none;font-size:0.98rem;transition:opacity 0.2s;padding:6px 8px;border-radius:6px;} .gsw-nav-link:hover{opacity:0.9;text-decoration:underline;background:rgba(255,255,255,0.03);} /* My Account link styling - larger font in middle of menu */ @@ -79,7 +92,7 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px .cart-total-value{padding:1rem 1.5rem; text-align:left; border-top:2px solid rgba(255,255,255,0.06); font-weight:600; color:#fff; font-size:1.1rem} /* Utility classes */ -.container-wide{width:100%; max-width:1000px; margin:28px auto;} +.container-wide{width:100%; max-width:1000px; margin:28px auto; padding-inline:12px; box-sizing:border-box;} .panel{background:rgba(0,0,0,0.25); padding:16px; border-radius:8px} .muted{color:rgba(255,255,255,0.6)} .center{text-align:center} @@ -243,12 +256,12 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px /* Navigation: wrap and stack for easier tapping */ #gsw-site .gsw-header-bottom{padding:8px 12px} - .gsw-header-nav{flex-direction:column;align-items:stretch;gap:10px;width:100%} + .gsw-header-nav{flex-direction:column;align-items:stretch;gap:10px;width:100%;max-width:100%} .gsw-nav-link{display:block;padding:12px 10px;border-radius:8px} .gsw-nav-link-myaccount{font-size:1rem} /* Make main panel use full width with reduced padding */ - .site-panel{padding:0.75rem;margin:8px;border-radius:0.5rem} + .site-panel{padding:0.75rem;margin:8px;border-radius:0.5rem;max-width:100%} /* Tables and cart spacing adjustments */ .cart-table th, .cart-table td{padding:0.6rem 0.8rem} @@ -264,7 +277,7 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px .server-item{padding:12px} /* Forms: make inputs and action buttons full width */ - .form-group input, .form-group textarea, .form-group select{width:100%;box-sizing:border-box} + .form-group input, .form-group textarea, .form-group select{width:100%;box-sizing:border-box;max-width:100%} /* Invoice items: stack label and amount for readability */ .invoice-item{flex-direction:column;align-items:flex-start;gap:8px} @@ -385,3 +398,26 @@ input, textarea, select, button { color: #fff; background: #11141f; border: 1px #gsw-site .server-card img, .server-list img{max-width:100%;height:auto;display:block;object-fit:cover} +#gsw-site img, +#gsw-site video, +#gsw-site iframe, +#gsw-site canvas, +#gsw-site svg { + max-width: 100%; + height: auto; +} + +#gsw-site table { + max-width: 100%; +} + +#gsw-site input, +#gsw-site select, +#gsw-site textarea, +#gsw-site button, +#gsw-site .btn, +#gsw-site .gsw-btn, +#gsw-site .gsw-btn-secondary { + max-width: 100%; + box-sizing: border-box; +} diff --git a/modules/billing/login.php b/modules/billing/login.php index 0f387896..2092f987 100644 --- a/modules/billing/login.php +++ b/modules/billing/login.php @@ -2,10 +2,6 @@ // Start a separate session for the website (not the panel session) session_name("opengamepanel_web"); session_start(); -// Enable error display for debugging the white screen issue. Remove or gate in production. -ini_set('display_errors', 1); -ini_set('display_startup_errors', 1); -error_reporting(E_ALL); // We'll compute a site root below (up to /_website) and define a strict sanitizer after config is loaded @@ -144,21 +140,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { box-sizing: border-box; } + html, + body { + width: 100%; + max-width: 100%; + overflow-x: hidden; + } + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: block; - padding: 0; /* we'll handle padding in content wrapper */ + margin: 0; + padding: 0; } - /* content area below the top/menu; aligns the login box to the right */ .content{ display:flex; align-items:center; - justify-content:flex-end; - min-height: calc(100vh - 140px); /* leave room for header/menu */ - padding:20px; + justify-content:center; + min-height: calc(100vh - 220px); + padding: 24px 16px 32px; } .login-container { @@ -167,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28); width: 100%; max-width: 420px; - padding: 40px; + padding: 32px 28px; border: 1px solid rgba(0,0,0,0.06); } @@ -261,6 +264,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { .footer-links { margin-top: 24px; text-align: center; + word-break: break-word; } .footer-links a { @@ -279,6 +283,65 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { color: #999; font-size: 0.85rem; } + + .login-links { + margin-top: 12px; + text-align: center; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 6px 10px; + line-height: 1.5; + } + + .login-links a { + color: #667eea; + text-decoration: none; + } + + .login-links a:hover { + text-decoration: underline; + } + + @media (max-width: 600px) { + .content { + min-height: auto; + padding: 14px 10px 20px; + } + + .login-container { + max-width: 100%; + padding: 20px 16px; + border-radius: 10px; + } + + .login-header { + margin-bottom: 18px; + } + + .login-header h1 { + font-size: 1.35rem; + } + + .login-header p, + .form-group label, + .form-group input, + .btn-login { + font-size: 0.95rem; + } + + .form-group { + margin-bottom: 14px; + } + + .form-group input { + padding: 10px 12px; + } + + .btn-login { + padding: 11px 12px; + } + } @@ -319,8 +382,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { -
- Register | + @@ -335,4 +399,3 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { - diff --git a/modules/billing/order.php b/modules/billing/order.php index fdded551..e7d15b64 100644 --- a/modules/billing/order.php +++ b/modules/billing/order.php @@ -4,6 +4,57 @@ Order Server - GameServers.World + + num_rows > 0) { } ?> +
-
+
-
+
+
-<?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?> -
+
" . ""; } } else { -echo "

" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; +echo "

" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "

"; } ?>
- +
+
+ - + + +
Game Server Name @@ -318,7 +375,7 @@ continue; // Incompatible OS variant with no fallback service $available_server = true; $firstServer = false; $safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8'); -echo "
\n" +echo "
\n" . " \n" . " \n" . "
\n"; @@ -338,7 +395,7 @@ $rsResult->free();
Months
-

Player Slots:
+

Player Slots:
Price: $ USD

@@ -361,7 +418,12 @@ var slots = parseInt(slider.value, 10); var months = parseInt(invoiceslider.value, 10); output.innerHTML = slots; invoiceDuration.innerHTML = "Duration: " + months + " month" + (months !== 1 ? "s" : ""); -price.innerHTML = "Total Price: $" + (slots * months * pricePerSlot).toFixed(2); +var total = (slots * months * pricePerSlot).toFixed(2); +price.innerHTML = "Total Price: $" + total; +var totalInput = document.getElementById("calculatedTotalInput"); +if (totalInput) { +totalInput.value = total; +} } recalc(); slider.oninput = recalc; @@ -398,7 +460,9 @@ if (checked) { window.gspUpdateServiceId(checked); } $is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])); ?> +
+
@@ -409,17 +473,22 @@ $is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['websit
- + +
+
+
+
+