Initial dev export (exclude uploads/runtime)
This commit is contained in:
224
plugins/store/views/admin/customers.php
Normal file
224
plugins/store/views/admin/customers.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
$pageTitle = $title ?? 'Store Customers';
|
||||
$customers = $customers ?? [];
|
||||
$currency = (string)($currency ?? 'GBP');
|
||||
$q = (string)($q ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card customers-page">
|
||||
<div class="badge">Store</div>
|
||||
<div class="customers-header">
|
||||
<div>
|
||||
<h1 class="customers-title">Customers</h1>
|
||||
<p class="customers-sub">Customer activity, value, and latest order access.</p>
|
||||
</div>
|
||||
<a href="/admin/store" class="btn outline">Back</a>
|
||||
</div>
|
||||
|
||||
<div class="customers-tabs">
|
||||
<a href="/admin/store" class="btn outline small">Overview</a>
|
||||
<a href="/admin/store/settings" class="btn outline small">Settings</a>
|
||||
<a href="/admin/store/orders" class="btn outline small">Orders</a>
|
||||
<a href="/admin/store/customers" class="btn outline small">Customers</a>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/admin/store/customers" class="customers-search">
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($q, ENT_QUOTES, 'UTF-8') ?>" placeholder="Search by email, name, or order number">
|
||||
<button type="submit" class="btn small">Search</button>
|
||||
<?php if ($q !== ''): ?>
|
||||
<a href="/admin/store/customers" class="btn outline small">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<?php if (!$customers): ?>
|
||||
<div class="customers-empty">No customers yet.</div>
|
||||
<?php else: ?>
|
||||
<div class="customers-table-wrap">
|
||||
<table class="customers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Orders</th>
|
||||
<th>Revenue</th>
|
||||
<th>Latest Order</th>
|
||||
<th>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($customers as $customer): ?>
|
||||
<?php
|
||||
$ips = is_array($customer['ips'] ?? null) ? $customer['ips'] : [];
|
||||
$email = (string)($customer['email'] ?? '');
|
||||
$lastOrderNo = (string)($customer['last_order_no'] ?? '');
|
||||
$lastOrderId = (int)($customer['last_order_id'] ?? 0);
|
||||
$lastSeen = (string)($customer['last_order_at'] ?? $customer['created_at'] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="customer-email"><?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php if ($ips): ?>
|
||||
<div class="customer-ips">
|
||||
<?php foreach ($ips as $entry): ?>
|
||||
<?php
|
||||
$ip = (string)($entry['ip'] ?? '');
|
||||
$ipLastSeen = (string)($entry['last_seen'] ?? '');
|
||||
if ($ip === '') { continue; }
|
||||
?>
|
||||
<span class="ip-chip" title="<?= htmlspecialchars($ipLastSeen, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($ip, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="num"><?= (int)($customer['order_count'] ?? 0) ?></td>
|
||||
<td class="num"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($customer['revenue'] ?? 0), 2) ?></td>
|
||||
<td>
|
||||
<?php if ($lastOrderId > 0): ?>
|
||||
<a href="/admin/store/order?id=<?= $lastOrderId ?>" class="order-link">
|
||||
<?= htmlspecialchars($lastOrderNo !== '' ? $lastOrderNo : ('#' . $lastOrderId), ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="muted">No orders</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="muted"><?= htmlspecialchars($lastSeen, ENT_QUOTES, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.customers-page { display:grid; gap:14px; }
|
||||
.customers-header { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-top:14px; }
|
||||
.customers-title { margin:0; font-size:28px; line-height:1.1; }
|
||||
.customers-sub { margin:6px 0 0; color:var(--muted); font-size:14px; }
|
||||
.customers-tabs { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.customers-search { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
.customers-search input {
|
||||
height:36px;
|
||||
min-width:280px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.15);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#fff;
|
||||
padding:0 12px;
|
||||
}
|
||||
|
||||
.customers-empty {
|
||||
padding:16px;
|
||||
border-radius:12px;
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
background:rgba(0,0,0,.15);
|
||||
color:var(--muted);
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.customers-table-wrap {
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:14px;
|
||||
overflow:hidden;
|
||||
background:rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.customers-table {
|
||||
width:100%;
|
||||
border-collapse:separate;
|
||||
border-spacing:0;
|
||||
table-layout:fixed;
|
||||
}
|
||||
|
||||
.customers-table th {
|
||||
text-align:left;
|
||||
font-size:11px;
|
||||
letter-spacing:.16em;
|
||||
text-transform:uppercase;
|
||||
color:var(--muted);
|
||||
padding:14px 16px;
|
||||
background:rgba(255,255,255,.03);
|
||||
border-bottom:1px solid rgba(255,255,255,.1);
|
||||
}
|
||||
|
||||
.customers-table td {
|
||||
padding:14px 16px;
|
||||
vertical-align:top;
|
||||
border-bottom:1px solid rgba(255,255,255,.06);
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.customers-table tbody tr:last-child td { border-bottom:none; }
|
||||
.customers-table tbody tr:hover { background:rgba(255,255,255,.03); }
|
||||
|
||||
.customer-email {
|
||||
font-size:16px;
|
||||
font-weight:600;
|
||||
line-height:1.25;
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
margin-top:4px;
|
||||
color:var(--muted);
|
||||
font-size:13px;
|
||||
}
|
||||
|
||||
.customer-ips {
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
margin-top:8px;
|
||||
}
|
||||
|
||||
.ip-chip {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
padding:3px 8px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background:rgba(255,255,255,.04);
|
||||
color:#d8def1;
|
||||
font-size:11px;
|
||||
line-height:1;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-weight:600;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.order-link {
|
||||
display:inline-flex;
|
||||
max-width:100%;
|
||||
color:#dce8ff;
|
||||
text-decoration:none;
|
||||
border-bottom:1px dashed rgba(220,232,255,.4);
|
||||
white-space:nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
.order-link:hover { color:#fff; border-bottom-color:rgba(255,255,255,.8); }
|
||||
.muted { color:var(--muted); }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.customers-table th:nth-child(3),
|
||||
.customers-table td:nth-child(3),
|
||||
.customers-table th:nth-child(5),
|
||||
.customers-table td:nth-child(5) { display:none; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.customers-header { flex-direction:column; }
|
||||
.customers-table { table-layout:auto; }
|
||||
.customers-table th:nth-child(2),
|
||||
.customers-table td:nth-child(2) { display:none; }
|
||||
.customer-email { font-size:15px; }
|
||||
.customers-search input { min-width:100%; width:100%; }
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|
||||
124
plugins/store/views/admin/index.php
Normal file
124
plugins/store/views/admin/index.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
$pageTitle = $title ?? 'Store';
|
||||
$tablesReady = (bool)($tables_ready ?? false);
|
||||
$privateRoot = (string)($private_root ?? '');
|
||||
$privateRootReady = (bool)($private_root_ready ?? false);
|
||||
$stats = is_array($stats ?? null) ? $stats : [];
|
||||
$recentOrders = is_array($recent_orders ?? null) ? $recent_orders : [];
|
||||
$newCustomers = is_array($new_customers ?? null) ? $new_customers : [];
|
||||
$currency = (string)($currency ?? 'GBP');
|
||||
$totalOrders = (int)($stats['total_orders'] ?? 0);
|
||||
$paidOrders = (int)($stats['paid_orders'] ?? 0);
|
||||
$totalRevenue = (float)($stats['total_revenue'] ?? 0);
|
||||
$totalCustomers = (int)($stats['total_customers'] ?? 0);
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
<div class="badge">Store</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
|
||||
<div>
|
||||
<h1 style="font-size:28px; margin:0;">Store</h1>
|
||||
<p style="color: var(--muted); margin-top:6px;">Commerce layer for releases/tracks.</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; align-items:center;">
|
||||
<a href="/admin/store/settings" class="btn outline">Settings</a>
|
||||
<a href="/admin/store/orders" class="btn outline">Orders</a>
|
||||
<a href="/admin/store/customers" class="btn outline">Customers</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
|
||||
<a href="/admin/store" class="btn outline small">Overview</a>
|
||||
<a href="/admin/store/settings" class="btn outline small">Settings</a>
|
||||
<a href="/admin/store/orders" class="btn outline small">Orders</a>
|
||||
<a href="/admin/store/customers" class="btn outline small">Customers</a>
|
||||
</div>
|
||||
|
||||
<?php if (!$tablesReady): ?>
|
||||
<div class="admin-card" style="margin-top:16px; padding:16px; display:flex; align-items:center; justify-content:space-between; gap:16px;">
|
||||
<div>
|
||||
<div style="font-weight:600;">Store tables not initialized</div>
|
||||
<div style="color: var(--muted); font-size:13px; margin-top:4px;">Create store tables before configuring products and checkout.</div>
|
||||
</div>
|
||||
<form method="post" action="/admin/store/install">
|
||||
<button type="submit" class="btn small">Create Tables</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card" style="margin-top:16px; padding:16px;">
|
||||
<div style="font-weight:600;">Private download root</div>
|
||||
<div style="color: var(--muted); font-size:13px; margin-top:4px; font-family:'IBM Plex Mono', monospace;">
|
||||
<?= htmlspecialchars($privateRoot, ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
<div style="margin-top:8px; font-size:12px; color: <?= $privateRootReady ? '#9be7c6' : '#f3b0b0' ?>;">
|
||||
<?= $privateRootReady ? 'Ready' : 'Missing or not writable' ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; margin-top:16px;">
|
||||
<div class="admin-card" style="padding:14px;">
|
||||
<div class="label">Total Orders</div>
|
||||
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= $totalOrders ?></div>
|
||||
<div style="font-size:12px; color:var(--muted); margin-top:4px;">Paid: <?= $paidOrders ?></div>
|
||||
</div>
|
||||
<div class="admin-card" style="padding:14px;">
|
||||
<div class="label">Revenue</div>
|
||||
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($totalRevenue, 2) ?></div>
|
||||
</div>
|
||||
<div class="admin-card" style="padding:14px;">
|
||||
<div class="label">Total Customers</div>
|
||||
<div style="font-size:26px; font-weight:700; margin-top:8px;"><?= $totalCustomers ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:16px;">
|
||||
<div class="admin-card" style="padding:14px;">
|
||||
<div style="font-weight:600;">Last 5 purchases</div>
|
||||
<?php if (!$recentOrders): ?>
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:13px;">No orders yet.</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; display:grid; gap:8px;">
|
||||
<?php foreach ($recentOrders as $order): ?>
|
||||
<div class="admin-card" style="padding:10px;">
|
||||
<div style="display:flex; justify-content:space-between; gap:10px; font-size:13px;">
|
||||
<strong><?= htmlspecialchars((string)($order['order_no'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||||
<span><?= htmlspecialchars((string)($order['status'] ?? 'pending'), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; gap:10px; color:var(--muted); font-size:12px; margin-top:4px;">
|
||||
<span><?= htmlspecialchars((string)($order['email'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span><?= htmlspecialchars((string)($order['currency'] ?? $currency), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['total'] ?? 0), 2) ?></span>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<a href="/admin/store/order?id=<?= (int)($order['id'] ?? 0) ?>" class="btn outline small">View Order</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="padding:14px;">
|
||||
<div style="font-weight:600;">Top 5 new customers</div>
|
||||
<?php if (!$newCustomers): ?>
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:13px;">No customers yet.</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; display:grid; gap:8px;">
|
||||
<?php foreach ($newCustomers as $customer): ?>
|
||||
<div class="admin-card" style="padding:10px;">
|
||||
<div style="display:flex; justify-content:space-between; gap:10px; font-size:13px;">
|
||||
<strong><?= htmlspecialchars((string)($customer['name'] ?? 'Customer'), ENT_QUOTES, 'UTF-8') ?></strong>
|
||||
<span style="color:<?= (int)($customer['is_active'] ?? 0) === 1 ? '#9be7c6' : '#f3b0b0' ?>;">
|
||||
<?= (int)($customer['is_active'] ?? 0) === 1 ? 'Active' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div style="color:var(--muted); font-size:12px; margin-top:4px;"><?= htmlspecialchars((string)($customer['email'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|
||||
123
plugins/store/views/admin/order.php
Normal file
123
plugins/store/views/admin/order.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
$pageTitle = $title ?? 'Order Detail';
|
||||
$order = is_array($order ?? null) ? $order : [];
|
||||
$items = is_array($items ?? null) ? $items : [];
|
||||
$downloadsByItem = is_array($downloads_by_item ?? null) ? $downloads_by_item : [];
|
||||
$downloadEvents = is_array($download_events ?? null) ? $download_events : [];
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
<div class="badge">Store</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
|
||||
<div>
|
||||
<h1 style="font-size:28px; margin:0;">Order Detail</h1>
|
||||
<p style="color: var(--muted); margin-top:6px;">Full order breakdown, downloads, and IP history.</p>
|
||||
</div>
|
||||
<a href="/admin/store/orders" class="btn outline">Back to Orders</a>
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
|
||||
<a href="/admin/store" class="btn outline small">Overview</a>
|
||||
<a href="/admin/store/settings" class="btn outline small">Settings</a>
|
||||
<a href="/admin/store/orders" class="btn outline small">Orders</a>
|
||||
<a href="/admin/store/customers" class="btn outline small">Customers</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="padding:14px; margin-top:16px;">
|
||||
<div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px;">
|
||||
<div>
|
||||
<div class="label">Order Number</div>
|
||||
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['order_no'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Status</div>
|
||||
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['status'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Total</div>
|
||||
<div style="font-weight:700; margin-top:6px;"><?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['total'] ?? 0), 2) ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Customer Email</div>
|
||||
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['email'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Order IP</div>
|
||||
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['customer_ip'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Created</div>
|
||||
<div style="margin-top:6px;"><?= htmlspecialchars((string)($order['created_at'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="padding:14px; margin-top:14px;">
|
||||
<div style="font-weight:600;">Items</div>
|
||||
<?php if (!$items): ?>
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:13px;">No items on this order.</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; display:grid; gap:10px;">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php
|
||||
$itemId = (int)($item['id'] ?? 0);
|
||||
$downloadMeta = $downloadsByItem[$itemId] ?? ['count' => 0, 'ips' => []];
|
||||
$ips = is_array($downloadMeta['ips'] ?? null) ? $downloadMeta['ips'] : [];
|
||||
?>
|
||||
<div class="admin-card" style="padding:12px;">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
|
||||
<div>
|
||||
<div style="font-weight:700;"><?= htmlspecialchars((string)($item['title_snapshot'] ?? 'Item'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="margin-top:4px; color:var(--muted); font-size:12px;">
|
||||
<?= htmlspecialchars((string)($item['item_type'] ?? 'track'), ENT_QUOTES, 'UTF-8') ?> #<?= (int)($item['item_id'] ?? 0) ?>
|
||||
| Qty <?= (int)($item['qty'] ?? 1) ?>
|
||||
| <?= htmlspecialchars((string)($item['currency_snapshot'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($item['line_total'] ?? 0), 2) ?>
|
||||
</div>
|
||||
<?php if (!empty($item['file_name'])): ?>
|
||||
<div style="margin-top:4px; color:var(--muted); font-size:12px;">File: <?= htmlspecialchars((string)$item['file_name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div class="pill">Downloads <?= (int)($downloadMeta['count'] ?? 0) ?>/<?= (int)($item['download_limit'] ?? 0) ?></div>
|
||||
<div style="font-size:12px; color:var(--muted); margin-top:6px;">
|
||||
Used <?= (int)($item['downloads_used'] ?? 0) ?><?= !empty($item['expires_at']) ? ' | Expires ' . htmlspecialchars((string)$item['expires_at'], ENT_QUOTES, 'UTF-8') : '' ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($ips): ?>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:6px; margin-top:10px;">
|
||||
<?php foreach ($ips as $ip): ?>
|
||||
<span class="pill"><?= htmlspecialchars((string)$ip, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="padding:14px; margin-top:14px;">
|
||||
<div style="font-weight:600;">Download Activity</div>
|
||||
<?php if (!$downloadEvents): ?>
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:13px;">No download activity yet.</div>
|
||||
<?php else: ?>
|
||||
<div style="margin-top:10px; display:grid; gap:8px;">
|
||||
<?php foreach ($downloadEvents as $event): ?>
|
||||
<div class="admin-card" style="padding:10px; display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:10px; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600;"><?= htmlspecialchars((string)($event['file_name'] ?? ('File #' . (int)($event['file_id'] ?? 0))), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="font-size:12px; color:var(--muted); margin-top:2px;">Item #<?= (int)($event['order_item_id'] ?? 0) ?> | <?= htmlspecialchars((string)($event['ip_address'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($event['downloaded_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="font-size:11px; color:var(--muted); max-width:280px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||||
<?= htmlspecialchars((string)($event['user_agent'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|
||||
390
plugins/store/views/admin/orders.php
Normal file
390
plugins/store/views/admin/orders.php
Normal file
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
$pageTitle = $title ?? 'Store Orders';
|
||||
$orders = is_array($orders ?? null) ? $orders : [];
|
||||
$q = (string)($q ?? '');
|
||||
$saved = (string)($saved ?? '');
|
||||
$error = (string)($error ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card store-orders">
|
||||
<div class="badge">Store</div>
|
||||
<div class="store-orders-head">
|
||||
<div>
|
||||
<h1>Orders</h1>
|
||||
<p>Manage order status, refunds, and clean-up.</p>
|
||||
</div>
|
||||
<a href="/admin/store" class="btn outline">Back</a>
|
||||
</div>
|
||||
|
||||
<div class="store-orders-tabs">
|
||||
<a href="/admin/store" class="btn outline small">Overview</a>
|
||||
<a href="/admin/store/settings" class="btn outline small">Settings</a>
|
||||
<a href="/admin/store/orders" class="btn small">Orders</a>
|
||||
<a href="/admin/store/customers" class="btn outline small">Customers</a>
|
||||
</div>
|
||||
|
||||
<?php if ($saved !== ''): ?>
|
||||
<div class="store-orders-msg ok">Order update saved.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="store-orders-msg error"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="store-orders-tools">
|
||||
<form method="post" action="/admin/store/orders/create" class="store-orders-create" data-confirm="Create this manual order?">
|
||||
<div class="label">Add Manual Order</div>
|
||||
<div class="store-orders-create-grid">
|
||||
<input name="email" type="email" required class="input" placeholder="customer@example.com">
|
||||
<input name="currency" class="input" value="GBP" maxlength="3" placeholder="GBP">
|
||||
<input name="total" class="input" value="0.00" placeholder="0.00">
|
||||
<select name="status" class="input">
|
||||
<option value="pending">pending</option>
|
||||
<option value="paid">paid</option>
|
||||
<option value="failed">failed</option>
|
||||
<option value="refunded">refunded</option>
|
||||
</select>
|
||||
<button type="submit" class="btn small">Add Order</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="get" action="/admin/store/orders" class="store-orders-search">
|
||||
<input type="text" name="q" value="<?= htmlspecialchars($q, ENT_QUOTES, 'UTF-8') ?>" class="input" placeholder="Search by order number, email, status, or IP">
|
||||
<button type="submit" class="btn small">Search</button>
|
||||
<?php if ($q !== ''): ?>
|
||||
<a href="/admin/store/orders" class="btn outline small">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!$orders): ?>
|
||||
<div class="store-orders-empty">No orders yet.</div>
|
||||
<?php else: ?>
|
||||
<div class="store-orders-list">
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<?php
|
||||
$status = (string)($order['status'] ?? 'pending');
|
||||
?>
|
||||
<article class="store-order-row">
|
||||
<div class="store-order-main">
|
||||
<a href="/admin/store/order?id=<?= (int)($order['id'] ?? 0) ?>" class="store-order-no">
|
||||
<?= htmlspecialchars((string)($order['order_no'] ?? ('#' . (int)($order['id'] ?? 0))), ENT_QUOTES, 'UTF-8') ?>
|
||||
</a>
|
||||
<div class="store-order-meta">
|
||||
<span><?= htmlspecialchars((string)($order['email'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span><?= htmlspecialchars((string)($order['created_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span><?= htmlspecialchars((string)($order['customer_ip'] ?? '-'), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-order-amount">
|
||||
<?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>
|
||||
<?= number_format((float)($order['total'] ?? 0), 2) ?>
|
||||
</div>
|
||||
|
||||
<div class="store-order-status pill"><?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
|
||||
<div class="store-order-actions">
|
||||
<form method="post" action="/admin/store/orders/status" class="store-order-status-form" data-confirm="Update this order status?">
|
||||
<input type="hidden" name="id" value="<?= (int)($order['id'] ?? 0) ?>">
|
||||
<select name="status" class="input">
|
||||
<option value="pending" <?= $status === 'pending' ? 'selected' : '' ?>>pending</option>
|
||||
<option value="paid" <?= $status === 'paid' ? 'selected' : '' ?>>paid</option>
|
||||
<option value="failed" <?= $status === 'failed' ? 'selected' : '' ?>>failed</option>
|
||||
<option value="refunded" <?= $status === 'refunded' ? 'selected' : '' ?>>refunded</option>
|
||||
</select>
|
||||
<button class="btn outline small" type="submit">Update</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin/store/orders/refund" data-confirm="Refund this order now? Download access will be revoked.">
|
||||
<input type="hidden" name="id" value="<?= (int)($order['id'] ?? 0) ?>">
|
||||
<button class="btn outline small store-order-refund" type="submit">Refund</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin/store/orders/delete" data-confirm="Delete this order and all related downloads? This cannot be undone.">
|
||||
<input type="hidden" name="id" value="<?= (int)($order['id'] ?? 0) ?>">
|
||||
<button class="btn outline small store-order-delete" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<div id="acConfirmModal" class="ac-confirm-modal" hidden>
|
||||
<div class="ac-confirm-backdrop"></div>
|
||||
<div class="ac-confirm-dialog">
|
||||
<div class="badge">Confirm</div>
|
||||
<p id="acConfirmText">Are you sure?</p>
|
||||
<div class="ac-confirm-actions">
|
||||
<button type="button" class="btn outline small" id="acConfirmCancel">Cancel</button>
|
||||
<button type="button" class="btn small" id="acConfirmOk">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.store-orders {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.store-orders-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.store-orders-head h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
.store-orders-head p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
.store-orders-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.store-orders-msg {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.store-orders-msg.ok {
|
||||
color: #9be7c6;
|
||||
background: rgba(34, 242, 165, .08);
|
||||
border: 1px solid rgba(34, 242, 165, .25);
|
||||
}
|
||||
.store-orders-msg.error {
|
||||
color: #f3b0b0;
|
||||
background: rgba(243, 176, 176, .08);
|
||||
border: 1px solid rgba(243, 176, 176, .25);
|
||||
}
|
||||
.store-orders-tools {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.store-orders-create,
|
||||
.store-orders-search {
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, .1);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
.store-orders-create-grid {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.8fr) 100px 110px 130px 120px;
|
||||
gap: 8px;
|
||||
}
|
||||
.store-orders-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.store-orders-search .input {
|
||||
min-width: 320px;
|
||||
}
|
||||
.store-orders-empty {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, .08);
|
||||
}
|
||||
.store-orders-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.store-order-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, .1);
|
||||
background: rgba(255, 255, 255, .02);
|
||||
}
|
||||
.store-order-no {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
letter-spacing: .01em;
|
||||
display: inline-block;
|
||||
line-height: 1.2;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.store-order-no:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.store-order-meta {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.store-order-amount {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.store-order-status {
|
||||
justify-self: start;
|
||||
}
|
||||
.store-order-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.store-order-status-form {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.store-order-status-form .input {
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
.store-order-refund {
|
||||
border-color: rgba(140, 235, 195, .45);
|
||||
color: #b8f6dc;
|
||||
}
|
||||
.store-order-delete {
|
||||
border-color: rgba(255, 120, 120, .45);
|
||||
color: #ffb9b9;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.store-orders-create-grid {
|
||||
grid-template-columns: 1fr 100px 110px 130px 120px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.store-order-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.store-order-amount {
|
||||
font-size: 18px;
|
||||
}
|
||||
.store-orders-create-grid {
|
||||
grid-template-columns: 1fr 100px 100px;
|
||||
}
|
||||
.store-orders-create-grid .btn {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: start;
|
||||
}
|
||||
.store-orders-search {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.store-orders-search .input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-confirm-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 4000;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.ac-confirm-modal[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.ac-confirm-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(5, 8, 14, .66);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.ac-confirm-dialog {
|
||||
position: relative;
|
||||
width: min(480px, calc(100vw - 32px));
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
background: linear-gradient(160deg, rgba(24, 28, 39, .98), rgba(18, 22, 32, .98));
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.ac-confirm-dialog p {
|
||||
margin: 0;
|
||||
color: #d8def0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.ac-confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const modal = document.getElementById('acConfirmModal');
|
||||
const txt = document.getElementById('acConfirmText');
|
||||
const btnCancel = document.getElementById('acConfirmCancel');
|
||||
const btnOk = document.getElementById('acConfirmOk');
|
||||
if (!modal || !txt || !btnCancel || !btnOk) return;
|
||||
|
||||
let targetForm = null;
|
||||
const forms = document.querySelectorAll('form[data-confirm]');
|
||||
|
||||
function closeModal() {
|
||||
modal.setAttribute('hidden', 'hidden');
|
||||
targetForm = null;
|
||||
}
|
||||
|
||||
function openModal(form) {
|
||||
targetForm = form;
|
||||
txt.textContent = form.getAttribute('data-confirm') || 'Are you sure?';
|
||||
modal.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
forms.forEach((form) => {
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (form.dataset.confirmed === '1') {
|
||||
form.dataset.confirmed = '0';
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
openModal(form);
|
||||
});
|
||||
});
|
||||
|
||||
btnCancel.addEventListener('click', closeModal);
|
||||
btnOk.addEventListener('click', function () {
|
||||
if (targetForm) {
|
||||
const f = targetForm;
|
||||
f.dataset.confirmed = '1';
|
||||
closeModal();
|
||||
HTMLFormElement.prototype.submit.call(f);
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
modal.addEventListener('click', function (event) {
|
||||
if (event.target.classList.contains('ac-confirm-backdrop')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && !modal.hasAttribute('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|
||||
311
plugins/store/views/admin/settings.php
Normal file
311
plugins/store/views/admin/settings.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
$pageTitle = $title ?? 'Store Settings';
|
||||
$settings = $settings ?? [];
|
||||
$gateways = is_array($gateways ?? null) ? $gateways : [];
|
||||
$error = (string)($error ?? '');
|
||||
$saved = (string)($saved ?? '');
|
||||
$tab = (string)($tab ?? 'general');
|
||||
$tab = in_array($tab, ['general', 'payments', 'emails', 'discounts', 'sales_chart'], true) ? $tab : 'general';
|
||||
$paypalTest = (string)($_GET['paypal_test'] ?? '');
|
||||
$privateRootReady = (bool)($private_root_ready ?? false);
|
||||
$discounts = is_array($discounts ?? null) ? $discounts : [];
|
||||
$chartRows = is_array($chart_rows ?? null) ? $chart_rows : [];
|
||||
$chartLastRebuildAt = (string)($chart_last_rebuild_at ?? '');
|
||||
$chartCronUrl = (string)($chart_cron_url ?? '');
|
||||
$chartCronCmd = (string)($chart_cron_cmd ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
<div class="badge">Store</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:16px; margin-top:16px;">
|
||||
<div>
|
||||
<h1 style="font-size:28px; margin:0;">Store Settings</h1>
|
||||
<p style="color: var(--muted); margin-top:6px;">Configure defaults, payments, and transactional emails.</p>
|
||||
</div>
|
||||
<a href="/admin/store" class="btn outline">Back</a>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:12px;">
|
||||
<a href="/admin/store/settings?tab=general" class="btn <?= $tab === 'general' ? '' : 'outline' ?> small">General</a>
|
||||
<a href="/admin/store/settings?tab=payments" class="btn <?= $tab === 'payments' ? '' : 'outline' ?> small">Payments</a>
|
||||
<a href="/admin/store/settings?tab=emails" class="btn <?= $tab === 'emails' ? '' : 'outline' ?> small">Emails</a>
|
||||
<a href="/admin/store/settings?tab=discounts" class="btn <?= $tab === 'discounts' ? '' : 'outline' ?> small">Discounts</a>
|
||||
<a href="/admin/store/settings?tab=sales_chart" class="btn <?= $tab === 'sales_chart' ? '' : 'outline' ?> small">Sales Chart</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div style="margin-top:12px; color:#f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($saved !== ''): ?>
|
||||
<div style="margin-top:12px; color:#9be7c6; font-size:13px;">Settings saved.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($paypalTest === 'live' || $paypalTest === 'sandbox'): ?>
|
||||
<div style="margin-top:12px; color:#9be7c6; font-size:13px;">PayPal <?= htmlspecialchars($paypalTest, ENT_QUOTES, 'UTF-8') ?> credentials are valid.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($tab === 'general'): ?>
|
||||
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
|
||||
<input type="hidden" name="tab" value="general">
|
||||
<div class="admin-card" style="padding:16px;">
|
||||
<div class="label">Currency</div>
|
||||
<input class="input" name="store_currency" value="<?= htmlspecialchars((string)($settings['store_currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?>" placeholder="GBP">
|
||||
|
||||
<div class="label" style="margin-top:12px;">Private Download Root (outside public_html)</div>
|
||||
<input class="input" name="store_private_root" value="<?= htmlspecialchars((string)($settings['store_private_root'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="/home/audiocore.site/private_downloads">
|
||||
<div style="margin-top:8px; font-size:12px; color: <?= $privateRootReady ? '#9be7c6' : '#f3b0b0' ?>;">
|
||||
<?= $privateRootReady ? 'Path is writable' : 'Path missing or not writable' ?>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:12px;">
|
||||
<div>
|
||||
<div class="label">Download Limit</div>
|
||||
<input class="input" name="store_download_limit" value="<?= htmlspecialchars((string)($settings['store_download_limit'] ?? '5'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Expiry Days</div>
|
||||
<input class="input" name="store_download_expiry_days" value="<?= htmlspecialchars((string)($settings['store_download_expiry_days'] ?? '30'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="label" style="margin-top:12px;">Order Number Prefix</div>
|
||||
<input class="input" name="store_order_prefix" value="<?= htmlspecialchars((string)($settings['store_order_prefix'] ?? 'AC-ORD'), ENT_QUOTES, 'UTF-8') ?>" placeholder="AC-ORD">
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end;">
|
||||
<button class="btn" type="submit">Save General Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php elseif ($tab === 'payments'): ?>
|
||||
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
|
||||
<input type="hidden" name="tab" value="payments">
|
||||
<div class="admin-card" style="padding:16px;">
|
||||
<div class="label" style="margin-bottom:10px;">Payment Mode</div>
|
||||
<input type="hidden" name="store_test_mode" value="0">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
|
||||
<input type="checkbox" name="store_test_mode" value="1" <?= ((string)($settings['store_test_mode'] ?? '1') === '1') ? 'checked' : '' ?>>
|
||||
Test Mode (Sandbox)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="padding:16px;">
|
||||
<div class="label" style="margin-bottom:10px;">PayPal</div>
|
||||
<input type="hidden" name="store_paypal_enabled" value="0">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
|
||||
<input type="checkbox" name="store_paypal_enabled" value="1" <?= ((string)($settings['store_paypal_enabled'] ?? '0') === '1') ? 'checked' : '' ?>>
|
||||
Enable PayPal
|
||||
</label>
|
||||
<div class="label" style="margin-top:10px;">PayPal Client ID</div>
|
||||
<input class="input" name="store_paypal_client_id" value="<?= htmlspecialchars((string)($settings['store_paypal_client_id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<div class="label" style="margin-top:10px;">PayPal Secret</div>
|
||||
<input class="input" name="store_paypal_secret" value="<?= htmlspecialchars((string)($settings['store_paypal_secret'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="live" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Live</button>
|
||||
<button class="btn outline small" type="submit" name="paypal_probe_mode" value="sandbox" formaction="/admin/store/settings/test-paypal" formmethod="post">Test PayPal Sandbox</button>
|
||||
<button class="btn" type="submit">Save Payment Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<?php elseif ($tab === 'emails'): ?>
|
||||
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
|
||||
<input type="hidden" name="tab" value="emails">
|
||||
<div class="admin-card" style="padding:16px;">
|
||||
<div class="label" style="margin-bottom:10px;">Order Email Template</div>
|
||||
<div class="label">Email Logo URL</div>
|
||||
<input class="input" name="store_email_logo_url" value="<?= htmlspecialchars((string)($settings['store_email_logo_url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" placeholder="https://example.com/logo.png">
|
||||
|
||||
<div class="label" style="margin-top:10px;">Subject</div>
|
||||
<input class="input" name="store_order_email_subject" value="<?= htmlspecialchars((string)($settings['store_order_email_subject'] ?? 'Your AudioCore order {{order_no}}'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<div class="label" style="margin-top:10px;">HTML Body</div>
|
||||
<textarea class="input" name="store_order_email_html" rows="10" style="resize:vertical; font-family:'IBM Plex Mono',monospace; font-size:12px;"><?= htmlspecialchars((string)($settings['store_order_email_html'] ?? ''), ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:12px;">
|
||||
Placeholders: <code>{{site_name}}</code>, <code>{{order_no}}</code>, <code>{{customer_email}}</code>, <code>{{currency}}</code>, <code>{{total}}</code>, <code>{{status}}</code>, <code>{{logo_url}}</code>, <code>{{logo_html}}</code>, <code>{{items_html}}</code>, <code>{{download_links_html}}</code>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px; display:grid; gap:10px; max-width:460px;">
|
||||
<div class="label">Send Test Email To</div>
|
||||
<input class="input" type="email" name="test_email_to" placeholder="you@example.com">
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button class="btn outline small" type="submit" formaction="/admin/store/settings/test-email" formmethod="post">Send Test Email</button>
|
||||
<button class="btn" type="submit">Save Email Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<?php elseif ($tab === 'discounts'): ?>
|
||||
<div class="admin-card" style="margin-top:16px; padding:16px;">
|
||||
<div class="label" style="margin-bottom:10px;">Create Discount Code</div>
|
||||
<form method="post" action="/admin/store/discounts/create" style="display:grid; gap:12px;">
|
||||
<div style="display:grid; grid-template-columns:1.2fr 1fr 0.8fr 0.9fr 1.2fr; gap:10px;">
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Code</div>
|
||||
<input class="input" name="code" placeholder="SAVE10" maxlength="32" required>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Discount Type</div>
|
||||
<select class="input" name="discount_type">
|
||||
<option value="percent">Percent</option>
|
||||
<option value="fixed">Fixed Amount</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Value</div>
|
||||
<input class="input" name="discount_value" value="10" required>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Max Uses</div>
|
||||
<input class="input" name="max_uses" value="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Expires At</div>
|
||||
<input class="input" type="datetime-local" name="expires_at">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||
<label style="display:flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.16em;">
|
||||
<input type="checkbox" name="is_active" value="1" checked> Active
|
||||
</label>
|
||||
<button class="btn small" type="submit">Save Code</button>
|
||||
</div>
|
||||
</form>
|
||||
<div style="margin-top:8px; color:var(--muted); font-size:12px;">Max uses: <strong>0</strong> means unlimited. Leave expiry blank for no expiry.</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" style="margin-top:12px; padding:14px;">
|
||||
<div class="label" style="margin-bottom:10px;">Existing Codes</div>
|
||||
<?php if (!$discounts): ?>
|
||||
<div style="color:var(--muted); font-size:13px;">No discount codes yet.</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow:auto;">
|
||||
<table style="width:100%; border-collapse:separate; border-spacing:0 8px;">
|
||||
<thead>
|
||||
<tr style="color:var(--muted); font-size:10px; letter-spacing:.14em; text-transform:uppercase; text-align:left;">
|
||||
<th style="padding:0 10px;">Code</th>
|
||||
<th style="padding:0 10px;">Type</th>
|
||||
<th style="padding:0 10px;">Value</th>
|
||||
<th style="padding:0 10px;">Usage</th>
|
||||
<th style="padding:0 10px;">Expires</th>
|
||||
<th style="padding:0 10px;">Status</th>
|
||||
<th style="padding:0 10px; text-align:right;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($discounts as $d): ?>
|
||||
<tr style="background:rgba(255,255,255,.02);">
|
||||
<td style="padding:10px; border:1px solid rgba(255,255,255,.08); border-right:none; border-radius:10px 0 0 10px; font-weight:700;"><?= htmlspecialchars((string)($d['code'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); color:var(--muted); font-size:12px;"><?= htmlspecialchars((string)($d['discount_type'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); font-size:12px;"><?= number_format((float)($d['discount_value'] ?? 0), 2) ?></td>
|
||||
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); font-size:12px; color:var(--muted);"><?= (int)($d['used_count'] ?? 0) ?>/<?= (int)($d['max_uses'] ?? 0) === 0 ? 'INF' : (int)($d['max_uses'] ?? 0) ?></td>
|
||||
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); font-size:12px; color:var(--muted);"><?= htmlspecialchars((string)($d['expires_at'] ?? 'No expiry'), ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="padding:10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08);"><span class="pill"><?= (int)($d['is_active'] ?? 0) === 1 ? 'active' : 'off' ?></span></td>
|
||||
<td style="padding:10px; border:1px solid rgba(255,255,255,.08); border-left:none; border-radius:0 10px 10px 0; text-align:right;">
|
||||
<form method="post" action="/admin/store/discounts/delete" onsubmit="return confirm('Delete this discount code?');" style="display:inline-flex;">
|
||||
<input type="hidden" name="id" value="<?= (int)($d['id'] ?? 0) ?>">
|
||||
<button class="btn outline small" type="submit" style="border-color:rgba(255,120,120,.45); color:#ffb9b9;">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="post" action="/admin/store/settings" style="margin-top:16px; display:grid; gap:16px;">
|
||||
<input type="hidden" name="tab" value="sales_chart">
|
||||
<div class="admin-card" style="padding:16px; display:grid; gap:12px;">
|
||||
<div class="label" style="margin-bottom:6px;">Sales Chart Defaults</div>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px;">
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Default Type</div>
|
||||
<select class="input" name="store_sales_chart_default_scope">
|
||||
<option value="tracks" <?= ((string)($settings['store_sales_chart_default_scope'] ?? 'tracks') === 'tracks') ? 'selected' : '' ?>>Tracks</option>
|
||||
<option value="releases" <?= ((string)($settings['store_sales_chart_default_scope'] ?? 'tracks') === 'releases') ? 'selected' : '' ?>>Releases</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Default Window</div>
|
||||
<select class="input" name="store_sales_chart_default_window">
|
||||
<option value="latest" <?= ((string)($settings['store_sales_chart_default_window'] ?? 'latest') === 'latest') ? 'selected' : '' ?>>Latest (rolling)</option>
|
||||
<option value="weekly" <?= ((string)($settings['store_sales_chart_default_window'] ?? 'latest') === 'weekly') ? 'selected' : '' ?>>Weekly</option>
|
||||
<option value="all_time" <?= ((string)($settings['store_sales_chart_default_window'] ?? 'latest') === 'all_time') ? 'selected' : '' ?>>All time</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Default Limit</div>
|
||||
<input class="input" name="store_sales_chart_limit" value="<?= htmlspecialchars((string)($settings['store_sales_chart_limit'] ?? '10'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Latest Window Hours</div>
|
||||
<input class="input" name="store_sales_chart_latest_hours" value="<?= htmlspecialchars((string)($settings['store_sales_chart_latest_hours'] ?? '24'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
<div>
|
||||
<div class="label" style="font-size:10px;">Cron Refresh Minutes</div>
|
||||
<input class="input" name="store_sales_chart_refresh_minutes" value="<?= htmlspecialchars((string)($settings['store_sales_chart_refresh_minutes'] ?? '180'), ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="label" style="font-size:10px;">Cron Key</div>
|
||||
<div style="display:grid; grid-template-columns:1fr auto; gap:8px;">
|
||||
<input class="input" value="<?= htmlspecialchars((string)($settings['store_sales_chart_cron_key'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" readonly>
|
||||
<button class="btn outline small" type="submit" name="store_sales_chart_regen_key" value="1">Regenerate</button>
|
||||
</div>
|
||||
<div class="label" style="font-size:10px;">Cron URL (dynamic)</div>
|
||||
<input class="input" value="<?= htmlspecialchars($chartCronUrl, ENT_QUOTES, 'UTF-8') ?>" readonly>
|
||||
<div class="label" style="font-size:10px;">Crontab Line</div>
|
||||
<textarea class="input" rows="2" style="resize:vertical; font-family:'IBM Plex Mono',monospace; font-size:12px;" readonly><?= htmlspecialchars($chartCronCmd, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end;">
|
||||
<button class="btn outline small" type="submit" formaction="/admin/store/settings/rebuild-sales-chart" formmethod="post">Rebuild Now</button>
|
||||
<button class="btn" type="submit">Save Sales Chart Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="admin-card" style="margin-top:12px; padding:14px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px;">
|
||||
<div class="label">Current Chart Snapshot</div>
|
||||
<div style="font-size:12px; color:var(--muted);">
|
||||
Last rebuild: <?= $chartLastRebuildAt !== '' ? htmlspecialchars($chartLastRebuildAt, ENT_QUOTES, 'UTF-8') : 'Never' ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$chartRows): ?>
|
||||
<div style="color:var(--muted); font-size:13px;">No chart rows yet. Run rebuild once.</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow:auto;">
|
||||
<table style="width:100%; border-collapse:separate; border-spacing:0 6px;">
|
||||
<thead>
|
||||
<tr style="color:var(--muted); font-size:10px; letter-spacing:.14em; text-transform:uppercase; text-align:left;">
|
||||
<th style="padding:0 10px;">#</th>
|
||||
<th style="padding:0 10px;">Item</th>
|
||||
<th style="padding:0 10px;">Units</th>
|
||||
<th style="padding:0 10px;">Revenue</th>
|
||||
<th style="padding:0 10px;">Window</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($chartRows as $row): ?>
|
||||
<tr style="background:rgba(255,255,255,.02);">
|
||||
<td style="padding:9px 10px; border:1px solid rgba(255,255,255,.08); border-right:none; border-radius:10px 0 0 10px; font-family:'IBM Plex Mono',monospace; font-size:12px;"><?= (int)($row['rank_no'] ?? 0) ?></td>
|
||||
<td style="padding:9px 10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08);"><?= htmlspecialchars((string)($row['item_label'] ?? ''), ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td style="padding:9px 10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); color:var(--muted); font-size:12px;"><?= (int)($row['units'] ?? 0) ?></td>
|
||||
<td style="padding:9px 10px; border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); color:var(--muted); font-size:12px;"><?= htmlspecialchars((string)($settings['store_currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($row['revenue'] ?? 0), 2) ?></td>
|
||||
<td style="padding:9px 10px; border:1px solid rgba(255,255,255,.08); border-left:none; border-radius:0 10px 10px 0; color:var(--muted); font-size:12px;">
|
||||
<?= htmlspecialchars((string)($row['snapshot_from'] ?? 'all'), ENT_QUOTES, 'UTF-8') ?> -> <?= htmlspecialchars((string)($row['snapshot_to'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../modules/admin/views/layout.php';
|
||||
325
plugins/store/views/site/account.php
Normal file
325
plugins/store/views/site/account.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$pageTitle = $title ?? 'Account';
|
||||
$isLoggedIn = (bool)($is_logged_in ?? false);
|
||||
$email = (string)($email ?? '');
|
||||
$orders = is_array($orders ?? null) ? $orders : [];
|
||||
$downloads = is_array($downloads ?? null) ? $downloads : [];
|
||||
$message = (string)($message ?? '');
|
||||
$error = (string)($error ?? '');
|
||||
$downloadLimit = (int)($download_limit ?? 5);
|
||||
$downloadExpiryDays = (int)($download_expiry_days ?? 30);
|
||||
$orderCount = count($orders);
|
||||
$downloadCount = count($downloads);
|
||||
$downloadsByOrder = [];
|
||||
$nowTs = time();
|
||||
foreach ($downloads as $d) {
|
||||
if (!is_array($d)) {
|
||||
continue;
|
||||
}
|
||||
$orderNo = (string)($d['order_no'] ?? '');
|
||||
if ($orderNo === '') {
|
||||
$orderNo = '__unknown__';
|
||||
}
|
||||
if (!isset($downloadsByOrder[$orderNo])) {
|
||||
$downloadsByOrder[$orderNo] = [];
|
||||
}
|
||||
$downloadsByOrder[$orderNo][] = $d;
|
||||
}
|
||||
ob_start();
|
||||
?>
|
||||
<section class="card account-wrap">
|
||||
<div class="badge">Store</div>
|
||||
<div class="account-title-row">
|
||||
<h1 style="margin:0; font-size:32px;">Account</h1>
|
||||
<span class="account-subtle">Download hub</span>
|
||||
</div>
|
||||
|
||||
<?php if ($message !== ''): ?>
|
||||
<div class="account-alert success"><?= htmlspecialchars($message, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="account-alert error"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isLoggedIn): ?>
|
||||
<div class="account-grid">
|
||||
<div class="account-panel">
|
||||
<div class="badge" style="font-size:9px;">Login</div>
|
||||
<p class="account-copy">Enter your order email and we will send a secure one-time access link.</p>
|
||||
<form method="post" action="/account/request-login" class="account-form">
|
||||
<label class="account-label">Email</label>
|
||||
<input name="email" type="email" class="account-input" placeholder="you@example.com" required>
|
||||
<button type="submit" class="account-btn">Send Login Link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="account-panel">
|
||||
<div class="badge" style="font-size:9px;">Download Policy</div>
|
||||
<ul class="account-policy-list">
|
||||
<li>Each file can be downloaded up to <?= $downloadLimit ?> times.</li>
|
||||
<li>Download links expire after <?= $downloadExpiryDays ?> days.</li>
|
||||
<li>After expiry or limit, a new order is required.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="account-panel account-panel-head">
|
||||
<div>
|
||||
<div class="badge" style="font-size:9px;">Signed In</div>
|
||||
<div class="account-email" title="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<div class="account-actions">
|
||||
<div class="account-stat">
|
||||
<span class="account-stat-label">Orders</span>
|
||||
<strong><?= $orderCount ?></strong>
|
||||
</div>
|
||||
<div class="account-stat">
|
||||
<span class="account-stat-label">Files</span>
|
||||
<strong><?= $downloadCount ?></strong>
|
||||
</div>
|
||||
<a class="account-logout" href="/account/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-panel">
|
||||
<div class="badge" style="font-size:9px;">Orders</div>
|
||||
<?php if (!$orders): ?>
|
||||
<p class="account-copy">No orders found for this account.</p>
|
||||
<?php else: ?>
|
||||
<div class="account-list">
|
||||
<?php foreach ($orders as $idx => $order): ?>
|
||||
<?php
|
||||
$orderNo = (string)($order['order_no'] ?? '');
|
||||
$orderDownloads = $downloadsByOrder[$orderNo] ?? [];
|
||||
$activeCount = 0;
|
||||
foreach ($orderDownloads as $dl) {
|
||||
$limit = max(0, (int)($dl['download_limit'] ?? 0));
|
||||
$used = max(0, (int)($dl['downloads_used'] ?? 0));
|
||||
$remaining = $limit > 0 ? max(0, $limit - $used) : 0;
|
||||
$expires = trim((string)($dl['expires_at'] ?? ''));
|
||||
$expired = false;
|
||||
if ($expires !== '') {
|
||||
$expTs = strtotime($expires);
|
||||
if ($expTs !== false && $expTs < $nowTs) {
|
||||
$expired = true;
|
||||
}
|
||||
}
|
||||
if ($remaining > 0 && !$expired) {
|
||||
$activeCount++;
|
||||
}
|
||||
}
|
||||
$hasDownloads = !empty($orderDownloads);
|
||||
$isExpired = $hasDownloads && $activeCount === 0;
|
||||
$modalId = 'orderDlModal' . $idx;
|
||||
?>
|
||||
<div class="account-order-row">
|
||||
<div>
|
||||
<div class="account-line-title"><?= htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div class="account-line-meta">
|
||||
<span class="account-status-pill"><?= htmlspecialchars((string)($order['status'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span><?= htmlspecialchars((string)($order['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($order['total'] ?? 0), 2) ?></span>
|
||||
<span><?= htmlspecialchars((string)($order['created_at'] ?? ''), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php if ($hasDownloads): ?>
|
||||
<span><?= $activeCount ?> active download<?= $activeCount === 1 ? '' : 's' ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-order-right">
|
||||
<?php if (!$hasDownloads): ?>
|
||||
<button type="button" class="account-download-btn is-disabled" disabled>No Downloads</button>
|
||||
<?php elseif ($isExpired): ?>
|
||||
<button type="button" class="account-download-btn is-expired" disabled>Expired</button>
|
||||
<?php else: ?>
|
||||
<button type="button" class="account-download-btn" data-open-modal="<?= htmlspecialchars($modalId, ENT_QUOTES, 'UTF-8') ?>">Downloads</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($hasDownloads): ?>
|
||||
<div id="<?= htmlspecialchars($modalId, ENT_QUOTES, 'UTF-8') ?>" class="account-modal" aria-hidden="true">
|
||||
<div class="account-modal-backdrop" data-close-modal="<?= htmlspecialchars($modalId, ENT_QUOTES, 'UTF-8') ?>"></div>
|
||||
<div class="account-modal-card" role="dialog" aria-modal="true" aria-label="Order downloads">
|
||||
<div class="account-modal-head">
|
||||
<div>
|
||||
<div class="badge" style="font-size:9px;">Order Downloads</div>
|
||||
<div class="account-modal-title"><?= htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
</div>
|
||||
<button type="button" class="account-modal-close" data-close-modal="<?= htmlspecialchars($modalId, ENT_QUOTES, 'UTF-8') ?>">×</button>
|
||||
</div>
|
||||
|
||||
<div class="account-modal-list">
|
||||
<?php foreach ($orderDownloads as $dl): ?>
|
||||
<?php
|
||||
$limit = max(0, (int)($dl['download_limit'] ?? 0));
|
||||
$used = max(0, (int)($dl['downloads_used'] ?? 0));
|
||||
$remaining = $limit > 0 ? max(0, $limit - $used) : 0;
|
||||
$expires = trim((string)($dl['expires_at'] ?? ''));
|
||||
$expired = false;
|
||||
if ($expires !== '') {
|
||||
$expTs = strtotime($expires);
|
||||
if ($expTs !== false && $expTs < $nowTs) {
|
||||
$expired = true;
|
||||
}
|
||||
}
|
||||
$canDownload = ($remaining > 0 && !$expired);
|
||||
$dlUrl = (string)($dl['url'] ?? '#');
|
||||
?>
|
||||
<div class="account-modal-item">
|
||||
<div>
|
||||
<div class="account-line-title"><?= htmlspecialchars((string)($dl['file_name'] ?? 'Download'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div class="account-line-meta">
|
||||
Remaining: <?= $remaining ?>
|
||||
<?php if ($expires !== ''): ?>
|
||||
· Expires: <?= htmlspecialchars($expires, ENT_QUOTES, 'UTF-8') ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($canDownload): ?>
|
||||
<a href="<?= htmlspecialchars($dlUrl, ENT_QUOTES, 'UTF-8') ?>" class="account-download-btn">Download</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="account-download-btn is-expired" disabled>Expired</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<style>
|
||||
.account-wrap { display:grid; gap:14px; }
|
||||
.account-title-row {
|
||||
display:flex;
|
||||
align-items:flex-end;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.account-subtle {
|
||||
color:var(--muted);
|
||||
font-size:12px;
|
||||
letter-spacing:.14em;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.account-grid { display:grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap:14px; }
|
||||
.account-panel { padding:18px; border-radius:14px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); }
|
||||
.account-panel-head { display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; }
|
||||
.account-alert { padding:14px; border-radius:12px; font-weight:600; }
|
||||
.account-alert.success { border:1px solid rgba(34,242,165,.4); background:rgba(34,242,165,.12); color:#d3ffef; }
|
||||
.account-alert.error { border:1px solid rgba(243,176,176,.45); background:rgba(243,176,176,.12); color:#ffd6d6; }
|
||||
.account-email {
|
||||
margin-top:8px;
|
||||
font-weight:700;
|
||||
font-size:36px;
|
||||
line-height:1.05;
|
||||
max-width:100%;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.account-actions { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
||||
.account-stat { min-width:96px; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); text-align:center; }
|
||||
.account-stat-label { display:block; font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
|
||||
.account-logout { display:inline-flex; align-items:center; justify-content:center; height:42px; padding:0 20px; border-radius:999px; border:1px solid rgba(255,255,255,.2); background:rgba(255,255,255,.06); color:#fff; text-decoration:none; font-size:12px; text-transform:uppercase; letter-spacing:.14em; font-weight:700; }
|
||||
.account-logout:hover { background:rgba(255,255,255,.14); }
|
||||
.account-list { display:grid; gap:10px; margin-top:10px; }
|
||||
.account-order-row { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:14px; align-items:center; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); }
|
||||
.account-order-right { display:grid; gap:8px; justify-items:end; }
|
||||
.account-line-title { font-weight:600; font-size:22px; line-height:1.2; }
|
||||
.account-line-meta { color:var(--muted); font-size:13px; margin-top:6px; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
|
||||
.account-status-pill {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
padding:3px 8px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,.18);
|
||||
background:rgba(255,255,255,.05);
|
||||
text-transform:uppercase;
|
||||
letter-spacing:.08em;
|
||||
font-size:10px;
|
||||
color:#cfd5e7;
|
||||
}
|
||||
.account-download-btn { display:inline-flex; align-items:center; justify-content:center; height:40px; padding:0 20px; border-radius:999px; border:1px solid rgba(34,242,165,.5); background:rgba(34,242,165,.22); color:#d7ffef; text-decoration:none; text-transform:uppercase; letter-spacing:.14em; font-size:11px; font-weight:700; white-space:nowrap; }
|
||||
.account-download-btn:hover { background:rgba(34,242,165,.32); }
|
||||
.account-download-btn.is-expired,
|
||||
.account-download-btn.is-disabled { border-color:rgba(255,255,255,.18); background:rgba(255,255,255,.08); color:rgba(255,255,255,.55); cursor:not-allowed; }
|
||||
.account-copy { margin:10px 0 0; color:var(--muted); }
|
||||
.account-form { display:grid; gap:10px; max-width:460px; }
|
||||
.account-label { font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.18em; }
|
||||
.account-input { height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.2); background:rgba(255,255,255,.05); color:#fff; padding:0 12px; }
|
||||
.account-btn{ height:42px; border-radius:999px; border:1px solid rgba(34,242,165,.45); background:rgba(34,242,165,.18); color:#cbfff1; font-weight:700; letter-spacing:.12em; text-transform:uppercase; cursor:pointer; max-width:260px; }
|
||||
.account-btn:hover { background:rgba(34,242,165,.28); }
|
||||
.account-policy-list { margin:10px 0 0; padding-left:18px; color:var(--muted); line-height:1.7; max-width:560px; }
|
||||
|
||||
.account-modal { position:fixed; inset:0; display:none; z-index:2000; }
|
||||
.account-modal.is-open { display:block; }
|
||||
.account-modal-backdrop { position:absolute; inset:0; background:rgba(0,0,0,.7); }
|
||||
.account-modal-card { position:relative; max-width:760px; margin:6vh auto; width:calc(100% - 24px); max-height:88vh; overflow:auto; background:#12141b; border:1px solid rgba(255,255,255,.12); border-radius:14px; padding:16px; box-shadow:0 24px 64px rgba(0,0,0,.55); }
|
||||
.account-modal-head { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; }
|
||||
.account-modal-title { margin-top:8px; font-weight:700; font-size:20px; }
|
||||
.account-modal-close { width:38px; height:38px; border-radius:10px; border:1px solid rgba(255,255,255,.2); background:rgba(255,255,255,.08); color:#fff; font-size:20px; line-height:1; cursor:pointer; }
|
||||
.account-modal-list { display:grid; gap:10px; }
|
||||
.account-modal-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:center; padding:12px; border-radius:10px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03); }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.account-grid { grid-template-columns: 1fr; }
|
||||
.account-email { font-size:30px; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.account-email { font-size:24px; }
|
||||
.account-actions { width:100%; }
|
||||
.account-logout { width:100%; }
|
||||
.account-order-row,
|
||||
.account-modal-item { grid-template-columns: 1fr; }
|
||||
.account-line-total { justify-self:start; }
|
||||
.account-download-btn { width:100%; }
|
||||
.account-order-right { justify-items:stretch; width:100%; }
|
||||
.account-modal-card { margin:2vh auto; max-height:94vh; }
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const openBtns = document.querySelectorAll('[data-open-modal]');
|
||||
const closeBtns = document.querySelectorAll('[data-close-modal]');
|
||||
|
||||
openBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-open-modal');
|
||||
const modal = document.getElementById(id);
|
||||
if (!modal) return;
|
||||
modal.classList.add('is-open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
closeBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-close-modal');
|
||||
const modal = document.getElementById(id);
|
||||
if (!modal) return;
|
||||
modal.classList.remove('is-open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
document.querySelectorAll('.account-modal.is-open').forEach((modal) => {
|
||||
modal.classList.remove('is-open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../views/site/layout.php';
|
||||
201
plugins/store/views/site/cart.php
Normal file
201
plugins/store/views/site/cart.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$pageTitle = $title ?? 'Cart';
|
||||
$items = is_array($items ?? null) ? $items : [];
|
||||
$totals = is_array($totals ?? null) ? $totals : ['count' => 0, 'subtotal' => 0.0, 'discount_amount' => 0.0, 'amount' => 0.0, 'currency' => 'GBP', 'discount_code' => ''];
|
||||
$discountCode = (string)($totals['discount_code'] ?? '');
|
||||
ob_start();
|
||||
?>
|
||||
<section class="card" style="display:grid; gap:16px;">
|
||||
<a href="/releases" class="badge" style="text-decoration:none; display:inline-block;">Continue shopping</a>
|
||||
<h1 style="margin:0; font-size:32px;">Your Cart</h1>
|
||||
<?php if (!$items): ?>
|
||||
<div style="padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); color:var(--muted);">
|
||||
Your basket is empty.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="display:grid; gap:10px;">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php
|
||||
$key = (string)($item['key'] ?? '');
|
||||
$title = (string)($item['title'] ?? 'Item');
|
||||
$coverUrl = (string)($item['cover_url'] ?? '');
|
||||
$qty = max(1, (int)($item['qty'] ?? 1));
|
||||
$price = (float)($item['price'] ?? 0);
|
||||
$currency = (string)($item['currency'] ?? ($totals['currency'] ?? 'GBP'));
|
||||
?>
|
||||
<div style="display:grid; grid-template-columns:58px minmax(0,1fr) auto auto; align-items:center; gap:12px; padding:12px; border-radius:12px; border:1px solid rgba(255,255,255,.08); background:rgba(0,0,0,.2);">
|
||||
<div style="width:58px; height:58px; border-radius:10px; overflow:hidden; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.06);">
|
||||
<?php if ($coverUrl !== ''): ?>
|
||||
<img src="<?= htmlspecialchars($coverUrl, ENT_QUOTES, 'UTF-8') ?>" alt="" style="width:100%; height:100%; object-fit:cover; display:block;">
|
||||
<?php else: ?>
|
||||
<div style="width:100%; height:100%; display:grid; place-items:center; font-size:10px; color:var(--muted);">AC</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="min-width:0;">
|
||||
<div style="font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div style="font-size:12px; color:var(--muted); margin-top:4px;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
|
||||
</div>
|
||||
<div style="font-weight:700;"><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>
|
||||
<form method="post" action="/cart/remove" style="margin:0;">
|
||||
<input type="hidden" name="key" value="<?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<input type="hidden" name="return_url" value="/cart">
|
||||
<button type="submit" class="cart-btn cart-btn-ghost">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div style="display:grid; gap:12px; padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.25);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||
<div style="color:var(--muted);"><?= (int)($totals['count'] ?? 0) ?> item(s)</div>
|
||||
<div style="display:grid; justify-items:end; gap:4px;">
|
||||
<div style="font-size:12px; color:var(--muted);">Subtotal: <?= htmlspecialchars((string)($totals['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($totals['subtotal'] ?? 0), 2) ?></div>
|
||||
<?php if ((float)($totals['discount_amount'] ?? 0) > 0): ?>
|
||||
<div style="font-size:12px; color:#9be7c6;">Discount (<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>): -<?= htmlspecialchars((string)($totals['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($totals['discount_amount'] ?? 0), 2) ?></div>
|
||||
<?php endif; ?>
|
||||
<div style="font-size:20px; font-weight:700;"><?= htmlspecialchars((string)($totals['currency'] ?? 'GBP'), ENT_QUOTES, 'UTF-8') ?> <?= number_format((float)($totals['amount'] ?? 0), 2) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="discountWrap" class="cart-discount-wrap<?= $discountCode !== '' ? ' is-open' : '' ?>">
|
||||
<div style="display:flex; flex-wrap:wrap; align-items:center; gap:8px;">
|
||||
<?php if ($discountCode !== ''): ?>
|
||||
<span style="font-size:12px; color:var(--muted);">Applied discount</span>
|
||||
<span class="cart-discount-chip"><?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<form method="post" action="/cart/discount/remove" style="margin:0;">
|
||||
<input type="hidden" name="return_url" value="/cart">
|
||||
<button type="submit" class="cart-btn cart-btn-danger">Remove</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div id="discountBox" class="cart-discount-box<?= $discountCode !== '' ? ' is-open' : '' ?>">
|
||||
<form method="post" action="/cart/discount/apply" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<input type="hidden" name="return_url" value="/cart">
|
||||
<input name="discount_code" value="<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>" class="input cart-discount-input" placeholder="Enter discount code">
|
||||
<button type="submit" class="cart-btn cart-btn-primary">Apply code</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; flex-wrap:wrap;">
|
||||
<button type="button" class="cart-btn cart-btn-ghost" id="toggleDiscountBox">
|
||||
<?= $discountCode !== '' ? 'Discount Active' : 'Have a discount code?' ?>
|
||||
</button>
|
||||
<a href="/checkout" class="cart-btn cart-btn-primary">Checkout</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<style>
|
||||
.cart-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255,255,255,.18);
|
||||
background: rgba(255,255,255,.08);
|
||||
color: #e9eefc;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cart-btn:hover { background: rgba(255,255,255,.14); }
|
||||
.cart-btn-primary {
|
||||
background: rgba(34,242,165,.18);
|
||||
border-color: rgba(34,242,165,.48);
|
||||
color: #cffff0;
|
||||
}
|
||||
.cart-btn-primary:hover { background: rgba(34,242,165,.28); }
|
||||
.cart-btn-ghost {
|
||||
background: rgba(255,255,255,.06);
|
||||
}
|
||||
.cart-btn-danger {
|
||||
border-color: rgba(255, 120, 120, 0.4);
|
||||
color: #ffd4d4;
|
||||
background: rgba(255, 120, 120, 0.12);
|
||||
}
|
||||
.cart-btn-danger:hover {
|
||||
background: rgba(255, 120, 120, 0.2);
|
||||
}
|
||||
.cart-discount-wrap {
|
||||
display: none;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01)),
|
||||
rgba(10, 12, 18, 0.72);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
|
||||
}
|
||||
.cart-discount-wrap.is-open {
|
||||
display: block;
|
||||
}
|
||||
.cart-discount-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(34,242,165,.38);
|
||||
background: rgba(34,242,165,.14);
|
||||
color: #cffff0;
|
||||
font-size: 11px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cart-discount-box {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.cart-discount-box.is-open {
|
||||
display: block;
|
||||
}
|
||||
.cart-discount-input {
|
||||
min-width: 220px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,.18);
|
||||
background: rgba(7,9,14,.72);
|
||||
color: #f0f4ff;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.cart-discount-input::placeholder {
|
||||
color: rgba(220,228,245,.42);
|
||||
}
|
||||
.cart-discount-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(34,242,165,.5);
|
||||
box-shadow: 0 0 0 2px rgba(34,242,165,.14);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.cart-discount-input {
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const toggleBtn = document.getElementById('toggleDiscountBox');
|
||||
const box = document.getElementById('discountBox');
|
||||
const wrap = document.getElementById('discountWrap');
|
||||
if (!toggleBtn || !box || !wrap) return;
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
box.classList.toggle('is-open');
|
||||
wrap.classList.toggle('is-open');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../views/site/layout.php';
|
||||
208
plugins/store/views/site/checkout.php
Normal file
208
plugins/store/views/site/checkout.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$pageTitle = $title ?? 'Checkout';
|
||||
$items = is_array($items ?? null) ? $items : [];
|
||||
$total = (float)($total ?? 0);
|
||||
$subtotal = (float)($subtotal ?? $total);
|
||||
$discountAmount = (float)($discount_amount ?? 0);
|
||||
$discountCode = (string)($discount_code ?? '');
|
||||
$currency = (string)($currency ?? 'GBP');
|
||||
$success = (string)($success ?? '');
|
||||
$orderNo = (string)($order_no ?? '');
|
||||
$error = (string)($error ?? '');
|
||||
$downloadLinks = is_array($download_links ?? null) ? $download_links : [];
|
||||
$downloadNotice = (string)($download_notice ?? '');
|
||||
$downloadLimit = (int)($download_limit ?? 5);
|
||||
$downloadExpiryDays = (int)($download_expiry_days ?? 30);
|
||||
ob_start();
|
||||
?>
|
||||
<section class="card checkout-wrap">
|
||||
<div class="badge">Store</div>
|
||||
<h1 style="margin:0; font-size:32px;">Checkout</h1>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div style="padding:14px; border-radius:12px; border:1px solid rgba(34,242,165,.4); background:rgba(34,242,165,.12);">
|
||||
<div style="font-weight:700;">Order complete</div>
|
||||
<?php if ($orderNo !== ''): ?>
|
||||
<div style="margin-top:4px; color:var(--muted);">Order: <?= htmlspecialchars($orderNo, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="checkout-panel">
|
||||
<div class="badge" style="font-size:9px;">Your Downloads</div>
|
||||
<?php if ($downloadLinks): ?>
|
||||
<div style="display:grid; gap:10px; margin-top:10px;">
|
||||
<?php foreach ($downloadLinks as $link): ?>
|
||||
<?php
|
||||
$label = trim((string)($link['label'] ?? 'Download'));
|
||||
$url = trim((string)($link['url'] ?? ''));
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<a href="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>" class="checkout-download-link">
|
||||
<span><?= htmlspecialchars($label !== '' ? $label : 'Download', ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="checkout-download-link-arrow">Download</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p style="margin:10px 0 0; color:var(--muted); font-size:13px;">
|
||||
<?= htmlspecialchars($downloadNotice !== '' ? $downloadNotice : 'No downloads available for this order yet.', ENT_QUOTES, 'UTF-8') ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($error !== ''): ?>
|
||||
<div style="padding:14px; border-radius:12px; border:1px solid rgba(243,176,176,.45); background:rgba(243,176,176,.12); color:#ffd6d6;">
|
||||
<?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!$items): ?>
|
||||
<div style="padding:14px; border-radius:12px; border:1px solid rgba(255,255,255,.1); background:rgba(0,0,0,.2); color:var(--muted);">
|
||||
Your cart is empty.
|
||||
</div>
|
||||
<div><a href="/releases" class="btn">Browse releases</a></div>
|
||||
<?php else: ?>
|
||||
<div class="checkout-grid">
|
||||
<div class="checkout-panel">
|
||||
<div class="badge" style="font-size:9px;">Order Summary</div>
|
||||
<div style="display:grid; gap:10px; margin-top:10px;">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php
|
||||
$title = (string)($item['title'] ?? 'Item');
|
||||
$qty = max(1, (int)($item['qty'] ?? 1));
|
||||
$price = (float)($item['price'] ?? 0);
|
||||
$lineCurrency = (string)($item['currency'] ?? $currency);
|
||||
?>
|
||||
<div class="checkout-line">
|
||||
<div class="checkout-line-title"><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<div class="checkout-line-meta"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price, 2) ?> x <?= $qty ?></div>
|
||||
<div class="checkout-line-total"><?= htmlspecialchars($lineCurrency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($price * $qty, 2) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="checkout-total">
|
||||
<span>Subtotal</span>
|
||||
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($subtotal, 2) ?></strong>
|
||||
</div>
|
||||
<?php if ($discountAmount > 0): ?>
|
||||
<div class="checkout-total" style="margin-top:8px;">
|
||||
<span>Discount (<?= htmlspecialchars($discountCode, ENT_QUOTES, 'UTF-8') ?>)</span>
|
||||
<strong style="color:#9be7c6;">-<?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($discountAmount, 2) ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="checkout-total" style="margin-top:8px;">
|
||||
<span>Order total</span>
|
||||
<strong><?= htmlspecialchars($currency, ENT_QUOTES, 'UTF-8') ?> <?= number_format($total, 2) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkout-panel">
|
||||
<div class="badge" style="font-size:9px;">Buyer Details</div>
|
||||
<form method="post" action="/checkout/place" style="display:grid; gap:12px; margin-top:10px;">
|
||||
<label style="font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:.18em;">Email</label>
|
||||
<input name="email" type="email" value="" placeholder="you@example.com" class="checkout-input" required>
|
||||
|
||||
<div class="checkout-terms">
|
||||
<div class="badge" style="font-size:9px;">Terms</div>
|
||||
<p style="margin:8px 0 0; color:var(--muted); font-size:13px; line-height:1.5;">
|
||||
Digital download products are non-refundable once purchased and delivered. Please check your order details before placing the order.
|
||||
Files are limited to <?= $downloadLimit ?> downloads and expire after <?= $downloadExpiryDays ?> days.
|
||||
</p>
|
||||
<label style="margin-top:10px; display:flex; gap:8px; align-items:flex-start; color:#d7def2; font-size:13px;">
|
||||
<input type="checkbox" name="accept_terms" value="1" required style="margin-top:2px;">
|
||||
<span>I agree to the terms and understand all sales are final.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="checkout-place-btn">Place Order</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<style>
|
||||
.checkout-wrap { display:grid; gap:14px; }
|
||||
.checkout-grid { display:grid; grid-template-columns: minmax(0,1fr) 420px; gap:14px; }
|
||||
.checkout-panel {
|
||||
padding:14px;
|
||||
border-radius:12px;
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
background:rgba(0,0,0,.2);
|
||||
}
|
||||
.checkout-line {
|
||||
display:grid;
|
||||
grid-template-columns:minmax(0,1fr) auto;
|
||||
gap:8px;
|
||||
padding:10px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
background:rgba(255,255,255,.03);
|
||||
}
|
||||
.checkout-line-title { font-weight:600; }
|
||||
.checkout-line-meta { color:var(--muted); font-size:12px; margin-top:4px; grid-column:1/2; }
|
||||
.checkout-line-total { font-weight:700; grid-column:2/3; grid-row:1/3; align-self:center; }
|
||||
.checkout-total {
|
||||
margin-top:10px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding:12px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
background:rgba(255,255,255,.04);
|
||||
}
|
||||
.checkout-total strong { font-size:22px; }
|
||||
.checkout-input {
|
||||
height:40px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.2);
|
||||
background:rgba(255,255,255,.05);
|
||||
color:#fff;
|
||||
padding:0 12px;
|
||||
}
|
||||
.checkout-terms {
|
||||
padding:12px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
background:rgba(255,255,255,.03);
|
||||
}
|
||||
.checkout-place-btn{
|
||||
height:40px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(34,242,165,.45);
|
||||
background:rgba(34,242,165,.18);
|
||||
color:#cbfff1;
|
||||
font-weight:700;
|
||||
letter-spacing:.1em;
|
||||
text-transform:uppercase;
|
||||
cursor:pointer;
|
||||
}
|
||||
.checkout-place-btn:hover { background:rgba(34,242,165,.28); }
|
||||
.checkout-download-link {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:10px;
|
||||
padding:12px;
|
||||
border-radius:10px;
|
||||
border:1px solid rgba(34,242,165,.35);
|
||||
background:rgba(34,242,165,.1);
|
||||
color:#d7ffef;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
}
|
||||
.checkout-download-link:hover { background:rgba(34,242,165,.18); }
|
||||
.checkout-download-link-arrow {
|
||||
font-size:11px;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:.14em;
|
||||
color:#8df7d1;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.checkout-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/../../../../views/site/layout.php';
|
||||
Reference in New Issue
Block a user