Merge pull request #138 from GameServerPanel/copilot/update-readme-and-cart-layout
This commit is contained in:
commit
5fc301e632
10 changed files with 428 additions and 63 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026-05-07
|
## 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.
|
- **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
|
## 2026-05-06
|
||||||
|
|
|
||||||
101
README.md
101
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.
|
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.
|
||||||
- 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).
|
|
||||||
|
|
||||||
## Quick start
|
## Why GSP exists
|
||||||
|
|
||||||
1. Clone or download the repository to `C:\\gsp-agent`.
|
Traditional game panel workflows often require manual setup, disconnected billing flows, and custom glue code between storefronts and provisioning.
|
||||||
2. Right-click `Install\\onceinstall_agent.bat` → “Run as administrator”.
|
GSP exists to provide a stronger foundation for automated service delivery, consistent customer experience, and safer long-term operations.
|
||||||
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.
|
|
||||||
|
|
||||||
## Related repositories
|
## Core features
|
||||||
|
|
||||||
- [GSP](https://github.com/GameServerPanel/GSP) – PHP panel that issues commands to the agents.
|
- Unified panel + storefront architecture
|
||||||
- [GSP-Agent-Linux](https://github.com/GameServerPanel/GSP-Agent-Linux) – Linux counterpart with systemd service files.
|
- 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
|
## 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).
|
||||||
|
|
|
||||||
|
|
@ -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 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 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 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.
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,35 @@ function billing_cents_to_money(int $cents): float
|
||||||
return $cents / 100;
|
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
|
function billing_fail_add_to_cart(string $message, array $context = []): void
|
||||||
{
|
{
|
||||||
site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context));
|
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;
|
$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0;
|
||||||
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1;
|
||||||
$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month';
|
$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']) : '';
|
$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']) : '';
|
$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) {
|
if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) {
|
||||||
$remote_control_password = billing_generate_password();
|
$remote_control_password = billing_generate_password();
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +224,12 @@ $status = 'due'; // Invoice status: due (unpaid), paid
|
||||||
$payment_status = 'unpaid';
|
$payment_status = 'unpaid';
|
||||||
$qty = max(1, $qty);
|
$qty = max(1, $qty);
|
||||||
$max_players = max(1, $max_players);
|
$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);
|
$subtotal = billing_cents_to_money($subtotal_cents);
|
||||||
$amount = $subtotal;
|
$amount = $subtotal;
|
||||||
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));
|
$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days'));
|
||||||
|
|
|
||||||
|
|
@ -317,11 +317,13 @@ $siteBase = $protocol . $host;
|
||||||
}
|
}
|
||||||
.cart-container {
|
.cart-container {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 40px auto;
|
margin: 24px auto;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 30px;
|
padding: 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
width: min(100%, calc(100% - 24px));
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
@ -359,6 +361,7 @@ $siteBase = $protocol . $host;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
.cart-table th {
|
.cart-table th {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
|
|
@ -418,6 +421,7 @@ $siteBase = $protocol . $host;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.coupon-form > div {
|
.coupon-form > div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -524,6 +528,86 @@ $siteBase = $protocol . $host;
|
||||||
}
|
}
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
margin-top: 30px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<?php // Font Awesome for small icon buttons ?>
|
<?php // Font Awesome for small icon buttons ?>
|
||||||
|
|
@ -562,6 +646,7 @@ $siteBase = $protocol . $host;
|
||||||
<a href="/serverlist.php" class="btn">Browse Servers</a>
|
<a href="/serverlist.php" class="btn">Browse Servers</a>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<div class="cart-table-wrap">
|
||||||
<table class="cart-table">
|
<table class="cart-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -576,20 +661,20 @@ $siteBase = $protocol . $host;
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ((array)$invoices as $inv): ?>
|
<?php foreach ((array)$invoices as $inv): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="Game Server">
|
||||||
<div class="game-name"><?php echo htmlspecialchars($inv['game_name'] ?? 'Game Server'); ?></div>
|
<div class="game-name"><?php echo htmlspecialchars($inv['game_name'] ?? 'Game Server'); ?></div>
|
||||||
<div class="server-name"><?php echo htmlspecialchars($inv['home_name']); ?></div>
|
<div class="server-name"><?php echo htmlspecialchars($inv['home_name']); ?></div>
|
||||||
<?php if (!empty($inv['description'])): ?>
|
<?php if (!empty($inv['description'])): ?>
|
||||||
<div class="description"><?php echo htmlspecialchars($inv['description']); ?></div>
|
<div class="description"><?php echo htmlspecialchars($inv['description']); ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?php echo htmlspecialchars($inv['invoice_duration']); ?></td>
|
<td data-label="Duration"><?php echo htmlspecialchars((string)($inv['invoice_duration'] ?? 'month')); ?></td>
|
||||||
<td><?php echo intval($inv['qty']); ?>x</td>
|
<td data-label="Quantity"><?php echo intval($inv['qty'] ?? 1); ?>x</td>
|
||||||
<td><span class="status-badge"><?php echo htmlspecialchars(strtoupper($inv['status'])); ?></span></td>
|
<td data-label="Status"><span class="status-badge"><?php echo htmlspecialchars(strtoupper((string)($inv['status'] ?? 'due'))); ?></span></td>
|
||||||
<td style="text-align: right;">
|
<td data-label="Price" style="text-align: right;">
|
||||||
<span class="price">$<?php echo number_format(floatval($inv['amount']), 2); ?></span>
|
<span class="price">$<?php echo number_format(floatval($inv['total_due'] ?? $inv['amount'] ?? 0), 2); ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right;">
|
<td data-label="Action" style="text-align: right;">
|
||||||
<button type="button" class="btn btn-secondary btn-small" title="Remove" onclick="removeInvoice(<?php echo intval($inv['invoice_id']); ?>)">
|
<button type="button" class="btn btn-secondary btn-small" title="Remove" onclick="removeInvoice(<?php echo intval($inv['invoice_id']); ?>)">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -598,6 +683,7 @@ $siteBase = $protocol . $host;
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Coupon Section -->
|
<!-- Coupon Section -->
|
||||||
<div class="coupon-section">
|
<div class="coupon-section">
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ if ($couponCode !== '') {
|
||||||
// Calculate total and verify it is $0 after discount
|
// Calculate total and verify it is $0 after discount
|
||||||
$totalAmountCents = 0;
|
$totalAmountCents = 0;
|
||||||
foreach ($invoices as $inv) {
|
foreach ($invoices as $inv) {
|
||||||
$lineAmount = (float)($inv['amount'] ?? 0);
|
$lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0);
|
||||||
$totalAmountCents += billing_free_money_to_cents($lineAmount);
|
$totalAmountCents += billing_free_money_to_cents($lineAmount);
|
||||||
}
|
}
|
||||||
$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0));
|
$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0));
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,21 @@
|
||||||
/* Global font family - legible sans-serif stack */
|
/* Global font family - legible sans-serif stack */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
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{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 img{height:40px;width:auto;display:block}
|
||||||
.gsw-top .gsw-site-name{font-weight:700;font-size:1.1rem;color:#333}
|
.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;}
|
.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 */
|
/* 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 */
|
/* 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;}
|
#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;}
|
.gsw-header-left a{color:#fff;text-decoration:none;}
|
||||||
|
|
||||||
/* Bottom row: centered navigation menu */
|
/* Bottom row: centered navigation menu */
|
||||||
#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;}
|
#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;}
|
.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{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);}
|
.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 */
|
/* 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}
|
.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 */
|
/* 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}
|
.panel{background:rgba(0,0,0,0.25); padding:16px; border-radius:8px}
|
||||||
.muted{color:rgba(255,255,255,0.6)}
|
.muted{color:rgba(255,255,255,0.6)}
|
||||||
.center{text-align:center}
|
.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 */
|
/* Navigation: wrap and stack for easier tapping */
|
||||||
#gsw-site .gsw-header-bottom{padding:8px 12px}
|
#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{display:block;padding:12px 10px;border-radius:8px}
|
||||||
.gsw-nav-link-myaccount{font-size:1rem}
|
.gsw-nav-link-myaccount{font-size:1rem}
|
||||||
|
|
||||||
/* Make main panel use full width with reduced padding */
|
/* 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 */
|
/* Tables and cart spacing adjustments */
|
||||||
.cart-table th, .cart-table td{padding:0.6rem 0.8rem}
|
.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}
|
.server-item{padding:12px}
|
||||||
|
|
||||||
/* Forms: make inputs and action buttons full width */
|
/* 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 items: stack label and amount for readability */
|
||||||
.invoice-item{flex-direction:column;align-items:flex-start;gap:8px}
|
.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,
|
#gsw-site .server-card img,
|
||||||
.server-list img{max-width:100%;height:auto;display:block;object-fit:cover}
|
.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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
// Start a separate session for the website (not the panel session)
|
// Start a separate session for the website (not the panel session)
|
||||||
session_name("opengamepanel_web");
|
session_name("opengamepanel_web");
|
||||||
session_start();
|
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
|
// 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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: block;
|
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{
|
.content{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:flex-end;
|
justify-content:center;
|
||||||
min-height: calc(100vh - 140px); /* leave room for header/menu */
|
min-height: calc(100vh - 220px);
|
||||||
padding:20px;
|
padding: 24px 16px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
|
|
@ -167,7 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
padding: 40px;
|
padding: 32px 28px;
|
||||||
border: 1px solid rgba(0,0,0,0.06);
|
border: 1px solid rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +264,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
.footer-links {
|
.footer-links {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links a {
|
.footer-links a {
|
||||||
|
|
@ -279,6 +283,65 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.85rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -319,8 +382,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
<button type="submit" name="login" class="btn-login">Sign In</button>
|
<button type="submit" name="login" class="btn-login">Sign In</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="center mt-12">
|
<div class="login-links">
|
||||||
<a href="register.php">Register</a> |
|
<a href="register.php">Register</a>
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
<a href="forgot_password.php">Forgot Password?</a>
|
<a href="forgot_password.php">Forgot Password?</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -335,4 +399,3 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
</body>
|
</body>
|
||||||
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
<?php include(__DIR__ . '/includes/footer.php'); ?>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,57 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Order Server - GameServers.World</title>
|
<title>Order Server - GameServers.World</title>
|
||||||
|
<link rel="stylesheet" href="css/header.css">
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
.order-shell { width: min(1100px, 100% - 24px); margin: 20px auto 28px; }
|
||||||
|
.order-catalog-item { margin: 0 0 20px; }
|
||||||
|
.order-layout { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
|
||||||
|
.order-media { flex: 0 1 310px; max-width: 100%; }
|
||||||
|
.order-media img { width: 100%; max-width: 280px; height: auto; border-radius: 8px; display: block; }
|
||||||
|
.order-media-title { margin: 10px 0 6px; text-align: center; }
|
||||||
|
.order-media-desc { color: #c6c6c6; max-width: 100%; word-break: break-word; }
|
||||||
|
.order-form-card { flex: 1 1 500px; max-width: 100%; background: rgba(0,0,0,0.25); border-radius: 10px; padding: 14px; }
|
||||||
|
.order-form-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||||
|
.order-form-table td { padding: 8px 6px; vertical-align: top; }
|
||||||
|
.order-form-table td:first-child { width: 34%; }
|
||||||
|
.order-form-table input[type="text"],
|
||||||
|
.order-form-table input[type="number"],
|
||||||
|
.order-form-table select,
|
||||||
|
.order-form-table textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
.order-form-table input[type="radio"] { margin-right: 8px; }
|
||||||
|
.location-option { margin-bottom: 8px; }
|
||||||
|
.slidecontainer { max-width: 100%; }
|
||||||
|
.slidecontainer .slider { width: 100%; }
|
||||||
|
.order-pricing { line-height: 1.5; word-break: break-word; }
|
||||||
|
.order-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.order-actions .gsw-btn,
|
||||||
|
.order-actions .gsw-btn-secondary { width: auto; }
|
||||||
|
.order-back-form { margin: 0; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.order-shell { width: min(100%, calc(100% - 16px)); margin: 12px auto 20px; }
|
||||||
|
.order-layout { gap: 12px; }
|
||||||
|
.order-media { flex: 1 1 100%; display: flex; flex-direction: column; align-items: center; }
|
||||||
|
.order-media img { max-width: min(100%, 260px); }
|
||||||
|
.order-form-card { flex: 1 1 100%; padding: 10px; }
|
||||||
|
.order-form-table,
|
||||||
|
.order-form-table tbody,
|
||||||
|
.order-form-table tr,
|
||||||
|
.order-form-table td { display: block; width: 100%; }
|
||||||
|
.order-form-table td:first-child { width: 100%; padding-bottom: 2px; }
|
||||||
|
.order-form-table td { padding: 6px 4px; }
|
||||||
|
.order-actions { flex-direction: column; }
|
||||||
|
.order-actions .gsw-btn,
|
||||||
|
.order-actions .gsw-btn-secondary { width: 100%; text-align: center; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<?php
|
<?php
|
||||||
|
|
@ -143,6 +194,7 @@ if ($osColCheck && $osColCheck->num_rows > 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
<div class="order-shell">
|
||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
<?php
|
<?php
|
||||||
foreach ($serviceRows as $row)
|
foreach ($serviceRows as $row)
|
||||||
|
|
@ -150,7 +202,7 @@ foreach ($serviceRows as $row)
|
||||||
if (!isset($_REQUEST['service_id']))
|
if (!isset($_REQUEST['service_id']))
|
||||||
{
|
{
|
||||||
?>
|
?>
|
||||||
<div class="float-left p-30-20">
|
<div class="float-left p-30-20 order-catalog-item">
|
||||||
<?php
|
<?php
|
||||||
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
|
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
|
||||||
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
|
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
|
||||||
|
|
@ -216,14 +268,15 @@ $osServiceMap[$svcGameOs] = (int)$row['service_id'];
|
||||||
$osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR);
|
$osServiceMapJson = json_encode($osServiceMap, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="float-left decorative-bottom">
|
<div class="order-layout">
|
||||||
|
<div class="order-media decorative-bottom">
|
||||||
<?php
|
<?php
|
||||||
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
|
$imgSrc = billing_image_url((string)($row['img_url'] ?? ''));
|
||||||
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
|
if ($imgSrc === '') { $imgSrc = '/images/games/default_server.png'; }
|
||||||
?>
|
?>
|
||||||
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" width="230" height="112"
|
<img src="<?php echo htmlspecialchars($imgSrc, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
|
onerror="this.src='/images/games/default_server.png'; this.onerror=null;">
|
||||||
<center><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
|
<center class="order-media-title"><b><?php echo htmlspecialchars($canonicalGameName, ENT_QUOTES, 'UTF-8'); ?></b></center>
|
||||||
<?php
|
<?php
|
||||||
$isAdmin = false;
|
$isAdmin = false;
|
||||||
if ($isAdmin) {
|
if ($isAdmin) {
|
||||||
|
|
@ -242,16 +295,20 @@ echo "<form action='' method='post'>"
|
||||||
. "</form>";
|
. "</form>";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
echo "<p style='color:gray;width:280px;'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
|
echo "<p class='order-media-desc'>" . htmlspecialchars((string)($row['description'] ?? ''), ENT_QUOTES, 'UTF-8') . "</p>";
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
<table class="float-left">
|
<div class="order-form-card">
|
||||||
|
<table class="order-form-table">
|
||||||
<form method="post" action="add_to_cart.php">
|
<form method="post" action="add_to_cart.php">
|
||||||
<!-- service_id is updated by JS when the location OS changes -->
|
<!-- service_id is updated by JS when the location OS changes -->
|
||||||
<input type="hidden" id="order_service_id" name="service_id" value="<?php echo intval($row['service_id']); ?>">
|
<input type="hidden" id="order_service_id" name="service_id" value="<?php echo intval($row['service_id']); ?>">
|
||||||
|
<input type="hidden" name="display_service_id" value="<?php echo intval($row['service_id']); ?>">
|
||||||
<input type="hidden" name="remote_control_password" value="">
|
<input type="hidden" name="remote_control_password" value="">
|
||||||
<input type="hidden" name="ftp_password" value="">
|
<input type="hidden" name="ftp_password" value="">
|
||||||
|
<input type="hidden" name="display_rate" id="displayRateInput" value="<?php echo number_format(floatval($row['price_monthly']), 2, '.', ''); ?>">
|
||||||
|
<input type="hidden" name="calculated_total" id="calculatedTotalInput" value="">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="right"><b>Game Server Name</b> </td>
|
<td align="right"><b>Game Server Name</b> </td>
|
||||||
<td align="left">
|
<td align="left">
|
||||||
|
|
@ -318,7 +375,7 @@ continue; // Incompatible OS variant with no fallback service
|
||||||
$available_server = true;
|
$available_server = true;
|
||||||
$firstServer = false;
|
$firstServer = false;
|
||||||
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
|
$safeOs = htmlspecialchars($rsOs, ENT_QUOTES, 'UTF-8');
|
||||||
echo "<div>\n"
|
echo "<div class='location-option'>\n"
|
||||||
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked} onchange='gspUpdateServiceId(this)'>\n"
|
. " <input type='radio' name='ip_id' id='rs_{$rsID}' value='{$rsID}' data-os='{$safeOs}' required{$checked} onchange='gspUpdateServiceId(this)'>\n"
|
||||||
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
|
. " <label for='rs_{$rsID}'>{$rsNAME}</label>\n"
|
||||||
. "</div>\n";
|
. "</div>\n";
|
||||||
|
|
@ -338,7 +395,7 @@ $rsResult->free();
|
||||||
<center><b>Months</b></center>
|
<center><b>Months</b></center>
|
||||||
<input type="range" name="qty" min="1" max="24" value="1" class="slider" id="invoiceRange">
|
<input type="range" name="qty" min="1" max="24" value="1" class="slider" id="invoiceRange">
|
||||||
|
|
||||||
<p>Player Slots: <span id="playerSlots"></span><br>
|
<p class="order-pricing">Player Slots: <span id="playerSlots"></span><br>
|
||||||
<span>Price: $<?php echo number_format(floatval($row['price_monthly']), 2); ?> USD</span><br>
|
<span>Price: $<?php echo number_format(floatval($row['price_monthly']), 2); ?> USD</span><br>
|
||||||
<span id="invoiceDuration"></span><br>
|
<span id="invoiceDuration"></span><br>
|
||||||
<span id="totalPrice"></span></p>
|
<span id="totalPrice"></span></p>
|
||||||
|
|
@ -361,7 +418,12 @@ var slots = parseInt(slider.value, 10);
|
||||||
var months = parseInt(invoiceslider.value, 10);
|
var months = parseInt(invoiceslider.value, 10);
|
||||||
output.innerHTML = slots;
|
output.innerHTML = slots;
|
||||||
invoiceDuration.innerHTML = "Duration: " + months + " month" + (months !== 1 ? "s" : "");
|
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();
|
recalc();
|
||||||
slider.oninput = 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']));
|
$is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) || (isset($_SESSION['website_username']) && !empty($_SESSION['website_username']));
|
||||||
?>
|
?>
|
||||||
<?php if ($available_server && $is_logged_in): ?>
|
<?php if ($available_server && $is_logged_in): ?>
|
||||||
|
<div class="order-actions">
|
||||||
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
|
<button type="submit" name="add_to_cart" class="gsw-btn">Add to Cart</button>
|
||||||
|
</div>
|
||||||
<?php elseif (!$is_logged_in): ?>
|
<?php elseif (!$is_logged_in): ?>
|
||||||
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
|
<div class="login-placeholder">Please <a href="login.php">login</a> to order</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|
@ -409,17 +473,22 @@ $is_logged_in = (isset($_SESSION['website_user_id']) && !empty($_SESSION['websit
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" colspan="2">
|
<td align="left" colspan="2">
|
||||||
<form action="serverlist.php" method="GET">
|
<form action="serverlist.php" method="GET" class="order-back-form">
|
||||||
|
<div class="order-actions">
|
||||||
<button class="gsw-btn-secondary">Back to List</button>
|
<button class="gsw-btn-secondary">Back to List</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<?php
|
<?php
|
||||||
// Close database connection
|
// Close database connection
|
||||||
billing_maybe_close_db($db);
|
billing_maybe_close_db($db);
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Last Updated at 12:31am on 2026-05-07
|
Last Updated at 12:44pm on 2026-05-07
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue