Sekut Bakery
This commit is contained in:
parent
6f0ca0a546
commit
d282dac2e9
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,63 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const chatForm = document.getElementById('chat-form');
|
if (window.bootstrap && window.bootstrap.Toast) {
|
||||||
const chatInput = document.getElementById('chat-input');
|
document.querySelectorAll('.toast').forEach((element) => {
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
const toast = new window.bootstrap.Toast(element);
|
||||||
|
toast.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const appendMessage = (text, sender) => {
|
document.querySelectorAll('[data-qty-target][data-qty-step]').forEach((button) => {
|
||||||
const msgDiv = document.createElement('div');
|
button.addEventListener('click', () => {
|
||||||
msgDiv.classList.add('message', sender);
|
const targetId = button.getAttribute('data-qty-target');
|
||||||
msgDiv.textContent = text;
|
const input = targetId ? document.getElementById(targetId) : null;
|
||||||
chatMessages.appendChild(msgDiv);
|
if (!input) {
|
||||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
chatForm.addEventListener('submit', async (e) => {
|
const min = parseInt(input.getAttribute('min') || '1', 10);
|
||||||
e.preventDefault();
|
const max = parseInt(input.getAttribute('max') || '20', 10);
|
||||||
const message = chatInput.value.trim();
|
const step = parseInt(button.getAttribute('data-qty-step') || '0', 10);
|
||||||
if (!message) return;
|
const current = parseInt(input.value || String(min), 10) || min;
|
||||||
|
const next = Math.max(min, Math.min(max, current + step));
|
||||||
|
input.value = String(next);
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
appendMessage(message, 'visitor');
|
document.querySelectorAll('[data-copy-text]').forEach((button) => {
|
||||||
chatInput.value = '';
|
button.addEventListener('click', async () => {
|
||||||
|
const value = button.getAttribute('data-copy-text');
|
||||||
|
if (!value || !navigator.clipboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const originalText = button.textContent;
|
||||||
const response = await fetch('api/chat.php', {
|
try {
|
||||||
method: 'POST',
|
await navigator.clipboard.writeText(value);
|
||||||
headers: { 'Content-Type': 'application/json' },
|
button.textContent = 'Tersalin';
|
||||||
body: JSON.stringify({ message })
|
button.classList.remove('btn-outline-success');
|
||||||
});
|
button.classList.add('btn-success');
|
||||||
const data = await response.json();
|
window.setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
// Artificial delay for realism
|
button.classList.remove('btn-success');
|
||||||
setTimeout(() => {
|
button.classList.add('btn-outline-success');
|
||||||
appendMessage(data.reply, 'bot');
|
}, 1500);
|
||||||
}, 500);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('Clipboard error', error);
|
||||||
console.error('Error:', error);
|
}
|
||||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-auto-disable]').forEach((form) => {
|
||||||
|
form.addEventListener('submit', () => {
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
if (!submitButton || submitButton.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton.dataset.originalText = submitButton.innerHTML;
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = 'Memproses…';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
110
cart.php
Normal file
110
cart.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/store.php';
|
||||||
|
|
||||||
|
$summary = store_cart_summary();
|
||||||
|
$lines = $summary['lines'];
|
||||||
|
|
||||||
|
store_page_start('Keranjang', 'Tinjau item, ubah quantity, dan lanjutkan checkout.', ['noindex' => true]);
|
||||||
|
?>
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="section-heading mb-4">
|
||||||
|
<span class="eyebrow">Keranjang</span>
|
||||||
|
<h1 class="section-title">Tinjau pesanan sebelum checkout.</h1>
|
||||||
|
<p class="section-copy mb-0">Update quantity, cek ongkir, lalu lanjut ke checkout untuk menyimpan pesanan ke sistem.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$lines): ?>
|
||||||
|
<div class="empty-state-card text-center mx-auto">
|
||||||
|
<span class="eyebrow">Keranjang kosong</span>
|
||||||
|
<h2 class="section-title">Belum ada produk di keranjang.</h2>
|
||||||
|
<p class="section-copy">Mulai dari katalog untuk mencoba alur add-to-cart lalu kembali ke halaman ini.</p>
|
||||||
|
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||||
|
<a class="btn btn-dark" href="index.php#catalog">Buka katalog</a>
|
||||||
|
<a class="btn btn-outline-secondary" href="order_status.php">Lacak pesanan</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-4 align-items-start">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<form id="cart-update-form" action="cart_action.php" method="post" class="d-grid gap-3" data-auto-disable>
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="hidden" name="redirect_to" value="cart.php">
|
||||||
|
<?php foreach ($lines as $line): ?>
|
||||||
|
<article class="cart-item">
|
||||||
|
<div class="product-visual product-visual--<?= h($line['product']['tone']) ?> product-visual--mini">
|
||||||
|
<span class="product-visual__meta"><?= h($line['product']['category_label']) ?></span>
|
||||||
|
<strong class="product-visual__code"><?= h($line['product']['visual_code']) ?></strong>
|
||||||
|
<span class="product-visual__name"><?= h($line['product']['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="cart-item__body">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="cart-item__title"><?= h($line['product']['name']) ?></h2>
|
||||||
|
<p class="text-muted mb-1"><?= h($line['product']['short_description']) ?></p>
|
||||||
|
<div class="detail-chip-group mt-2">
|
||||||
|
<span class="detail-chip"><?= h($line['product']['lead_time']) ?></span>
|
||||||
|
<span class="detail-chip"><?= h($line['product']['serves']) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-md-end">
|
||||||
|
<div class="price-tag price-tag--inline"><?= h(store_money((float)$line['product']['price'])) ?></div>
|
||||||
|
<div class="small text-muted mt-1">per <?= h($line['product']['unit']) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column flex-sm-row align-items-sm-center justify-content-between gap-3 mt-3">
|
||||||
|
<div class="quantity-field quantity-field--compact">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-qty-target="qty-<?= h($line['slug']) ?>" data-qty-step="-1" aria-label="Kurangi jumlah">−</button>
|
||||||
|
<input id="qty-<?= h($line['slug']) ?>" class="form-control text-center" type="number" name="quantities[<?= h($line['slug']) ?>]" min="1" max="20" value="<?= h((string)$line['quantity']) ?>">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-qty-target="qty-<?= h($line['slug']) ?>" data-qty-step="1" aria-label="Tambah jumlah">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2 ms-sm-auto">
|
||||||
|
<div class="cart-item__total"><?= h(store_money((float)$line['line_total'])) ?></div>
|
||||||
|
<button type="submit" class="btn btn-outline-secondary btn-sm" form="remove-<?= h($line['slug']) ?>">Hapus</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-2">
|
||||||
|
<a class="btn btn-outline-secondary" href="index.php#catalog">Tambah produk lain</a>
|
||||||
|
<button class="btn btn-dark" type="submit">Perbarui keranjang</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php foreach ($lines as $line): ?>
|
||||||
|
<form id="remove-<?= h($line['slug']) ?>" action="cart_action.php" method="post" class="d-none">
|
||||||
|
<input type="hidden" name="action" value="remove">
|
||||||
|
<input type="hidden" name="slug" value="<?= h($line['slug']) ?>">
|
||||||
|
<input type="hidden" name="redirect_to" value="cart.php">
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<aside class="summary-card sticky-summary">
|
||||||
|
<div class="card-kicker">Ringkasan pembayaran</div>
|
||||||
|
<h2 class="summary-title">Siap dilanjutkan ke checkout.</h2>
|
||||||
|
<div class="receipt-line">
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['subtotal'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-line">
|
||||||
|
<span>Ongkir</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['shipping_fee'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-line receipt-line--total">
|
||||||
|
<span>Total</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<p class="note-copy mt-3 mb-4">Gratis ongkir otomatis untuk subtotal mulai Rp 250.000.</p>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a class="btn btn-dark btn-lg" href="checkout.php">Lanjut ke checkout</a>
|
||||||
|
<a class="btn btn-outline-secondary" href="order_status.php">Sudah punya kode pesanan?</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
<?php store_page_end(); ?>
|
||||||
47
cart_action.php
Normal file
47
cart_action.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/store.php';
|
||||||
|
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||||
|
header('Location: cart.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = (string)($_POST['action'] ?? '');
|
||||||
|
$redirectTo = store_safe_redirect((string)($_POST['redirect_to'] ?? 'cart.php'), 'cart.php');
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'add':
|
||||||
|
$slug = (string)($_POST['slug'] ?? '');
|
||||||
|
$quantity = (int)($_POST['quantity'] ?? 1);
|
||||||
|
if (store_add_to_cart($slug, $quantity)) {
|
||||||
|
$product = store_product($slug);
|
||||||
|
store_flash('success', ($product['name'] ?? 'Produk') . ' ditambahkan ke keranjang.');
|
||||||
|
} else {
|
||||||
|
store_flash('danger', 'Produk tidak ditemukan.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
$quantities = $_POST['quantities'] ?? [];
|
||||||
|
if (!is_array($quantities)) {
|
||||||
|
$quantities = [];
|
||||||
|
}
|
||||||
|
store_update_cart($quantities);
|
||||||
|
store_flash('success', 'Keranjang berhasil diperbarui.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'remove':
|
||||||
|
$slug = (string)($_POST['slug'] ?? '');
|
||||||
|
store_remove_from_cart($slug);
|
||||||
|
store_flash('warning', 'Produk dihapus dari keranjang.');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
store_flash('danger', 'Aksi keranjang tidak dikenali.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: ' . $redirectTo);
|
||||||
|
exit;
|
||||||
142
checkout.php
Normal file
142
checkout.php
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/store.php';
|
||||||
|
|
||||||
|
$summary = store_cart_summary();
|
||||||
|
if (!$summary['lines']) {
|
||||||
|
store_flash('warning', 'Keranjang kosong. Tambahkan produk terlebih dahulu.');
|
||||||
|
header('Location: cart.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formData = store_checkout_defaults();
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||||
|
$result = store_create_order($_POST);
|
||||||
|
if (!empty($result['success'])) {
|
||||||
|
store_flash('success', 'Pesanan berhasil dibuat. Gunakan kode pesanan untuk melacak status.');
|
||||||
|
header('Location: order_status.php?order=' . urlencode((string)$result['order_number']) . '&created=1');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formData = $result['data'] ?? $formData;
|
||||||
|
$errors = $result['errors'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
store_page_start('Checkout', 'Lengkapi data pelanggan, alamat, dan metode pembayaran untuk membuat pesanan.', ['noindex' => true]);
|
||||||
|
?>
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="section-heading mb-4">
|
||||||
|
<span class="eyebrow">Checkout</span>
|
||||||
|
<h1 class="section-title">Simpan pesanan ke sistem.</h1>
|
||||||
|
<p class="section-copy mb-0">Status awal pesanan adalah <strong>Menunggu Pembayaran</strong>. Instruksi pembayaran akan tampil setelah order berhasil dibuat.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($errors['cart'])): ?>
|
||||||
|
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<?= h($errors['cart']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-4 align-items-start">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<form action="checkout.php" method="post" class="surface-panel p-4 p-lg-5" data-auto-disable>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="customer_name">Nama penerima</label>
|
||||||
|
<input id="customer_name" name="customer_name" type="text" class="form-control<?= store_input_class($errors, 'customer_name') ?>" value="<?= h($formData['customer_name']) ?>" required>
|
||||||
|
<?php if (isset($errors['customer_name'])): ?><div class="invalid-feedback"><?= h($errors['customer_name']) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="phone">Nomor telepon</label>
|
||||||
|
<input id="phone" name="phone" type="tel" class="form-control<?= store_input_class($errors, 'phone') ?>" value="<?= h($formData['phone']) ?>" required>
|
||||||
|
<?php if (isset($errors['phone'])): ?><div class="invalid-feedback"><?= h($errors['phone']) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label" for="email">Email</label>
|
||||||
|
<input id="email" name="email" type="email" class="form-control<?= store_input_class($errors, 'email') ?>" value="<?= h($formData['email']) ?>" required>
|
||||||
|
<?php if (isset($errors['email'])): ?><div class="invalid-feedback"><?= h($errors['email']) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label" for="address">Alamat lengkap</label>
|
||||||
|
<textarea id="address" name="address" rows="4" class="form-control<?= store_input_class($errors, 'address') ?>" required><?= h($formData['address']) ?></textarea>
|
||||||
|
<?php if (isset($errors['address'])): ?><div class="invalid-feedback"><?= h($errors['address']) ?></div><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label" for="note">Catatan pesanan <span class="text-muted">(opsional)</span></label>
|
||||||
|
<textarea id="note" name="note" rows="3" class="form-control"><?= h($formData['note']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="h5 mb-1">Metode pembayaran</h2>
|
||||||
|
<p class="text-muted mb-0">Pilih salah satu. Instruksi lengkap muncul di halaman status pesanan.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php foreach (store_payment_methods() as $value => $method): ?>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="payment-option">
|
||||||
|
<input class="btn-check" type="radio" name="payment_method" id="payment-<?= h($value) ?>" value="<?= h($value) ?>" <?= $formData['payment_method'] === $value ? 'checked' : '' ?>>
|
||||||
|
<label class="choice-card" for="payment-<?= h($value) ?>">
|
||||||
|
<span class="choice-title"><?= h($method['label']) ?></span>
|
||||||
|
<span class="choice-copy"><?= h($method['description']) ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($errors['payment_method'])): ?>
|
||||||
|
<div class="text-danger small mt-2"><?= h($errors['payment_method']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column flex-sm-row justify-content-between gap-2 mt-4">
|
||||||
|
<a class="btn btn-outline-secondary" href="cart.php">Kembali ke keranjang</a>
|
||||||
|
<button class="btn btn-dark btn-lg" type="submit">Buat pesanan</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<aside class="summary-card sticky-summary">
|
||||||
|
<div class="card-kicker">Order summary</div>
|
||||||
|
<h2 class="summary-title">Ringkasan item yang akan disimpan.</h2>
|
||||||
|
<div class="receipt-card receipt-card--flat">
|
||||||
|
<?php foreach ($summary['lines'] as $line): ?>
|
||||||
|
<div class="receipt-line">
|
||||||
|
<span><?= h($line['product']['name']) ?> × <?= h((string)$line['quantity']) ?></span>
|
||||||
|
<strong><?= h(store_money((float)$line['line_total'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<div class="receipt-line">
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['subtotal'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-line">
|
||||||
|
<span>Ongkir</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['shipping_fee'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-line receipt-line--total">
|
||||||
|
<span>Total pembayaran</span>
|
||||||
|
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-box mt-4 mb-0">
|
||||||
|
<div class="card-kicker">Setelah checkout</div>
|
||||||
|
<ul class="list-clean compact-list mb-0">
|
||||||
|
<li><span class="list-index">A</span><span>Sistem membuat order number unik.</span></li>
|
||||||
|
<li><span class="list-index">B</span><span>Data alamat dan item pesanan tersimpan di MySQL.</span></li>
|
||||||
|
<li><span class="list-index">C</span><span>Pelanggan bisa membuka status pesanan kapan saja.</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php store_page_end(); ?>
|
||||||
342
index.php
342
index.php
@ -1,150 +1,202 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
require_once __DIR__ . '/store.php';
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
|
$categories = store_categories();
|
||||||
|
$selectedCategory = (string)($_GET['category'] ?? 'all');
|
||||||
|
if (!isset($categories[$selectedCategory])) {
|
||||||
|
$selectedCategory = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$products = store_filtered_products($selectedCategory);
|
||||||
|
$summary = store_cart_summary();
|
||||||
|
$cartLines = array_slice($summary['lines'], 0, 3);
|
||||||
|
|
||||||
|
store_page_start(
|
||||||
|
'Toko Online Bakery',
|
||||||
|
'Pilih cake, roti, dan pastry favorit, simpan ke keranjang, checkout, lalu pantau status pesanan dari satu halaman.'
|
||||||
|
);
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<section class="hero-panel mb-4 mb-lg-5">
|
||||||
<html lang="en">
|
<div class="row g-4 align-items-center">
|
||||||
<head>
|
<div class="col-lg-7">
|
||||||
<meta charset="utf-8" />
|
<span class="eyebrow">Initial MVP slice</span>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<h1 class="display-title">Belanja bakery tanpa alur yang berantakan.</h1>
|
||||||
<title>New Style</title>
|
<p class="lead-copy">
|
||||||
<?php
|
Katalog, keranjang, checkout, dan halaman status pesanan sekarang terhubung dalam satu alur yang rapi.
|
||||||
// Read project preview data from environment
|
Cocok untuk validasi toko online berbasis PHP + MySQL sebelum lanjut ke admin panel penuh.
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
</p>
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||||
?>
|
<a class="btn btn-dark btn-lg" href="#catalog">Belanja sekarang</a>
|
||||||
<?php if ($projectDescription): ?>
|
<a class="btn btn-outline-secondary btn-lg" href="order_status.php">Cek status pesanan</a>
|
||||||
<!-- Meta description -->
|
</div>
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
<div class="row g-3 mt-4">
|
||||||
<!-- Open Graph meta tags -->
|
<div class="col-sm-4">
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<div class="metric-card">
|
||||||
<!-- Twitter meta tags -->
|
<div class="metric-value"><?= h((string)count(store_products())) ?></div>
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<div class="metric-label">produk demo siap dijual</div>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
<?php if ($projectImageUrl): ?>
|
</div>
|
||||||
<!-- Open Graph image -->
|
<div class="col-sm-4">
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
<div class="metric-card">
|
||||||
<!-- Twitter image -->
|
<div class="metric-value">1 tabel</div>
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
<div class="metric-label">pesanan tersimpan di MySQL</div>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
</div>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<div class="col-sm-4">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
<div class="metric-card">
|
||||||
<style>
|
<div class="metric-value">4 status</div>
|
||||||
:root {
|
<div class="metric-label">progress order yang mudah dipahami</div>
|
||||||
--bg-color-start: #6a11cb;
|
</div>
|
||||||
--bg-color-end: #2575fc;
|
</div>
|
||||||
--text-color: #ffffff;
|
</div>
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
</div>
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
<div class="col-lg-5">
|
||||||
}
|
<aside class="summary-card h-100">
|
||||||
body {
|
<div class="card-kicker">Workflow pelanggan</div>
|
||||||
margin: 0;
|
<h2 class="summary-title">Dari pilih produk sampai tracking status.</h2>
|
||||||
font-family: 'Inter', sans-serif;
|
<ul class="list-clean compact-list mb-4">
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
<li><span class="list-index">1</span><span>Pelanggan pilih produk dari katalog atau halaman detail.</span></li>
|
||||||
color: var(--text-color);
|
<li><span class="list-index">2</span><span>Keranjang menyimpan item, quantity, subtotal, dan ongkir.</span></li>
|
||||||
display: flex;
|
<li><span class="list-index">3</span><span>Checkout membuat order number dan menyimpan data di sistem.</span></li>
|
||||||
justify-content: center;
|
<li><span class="list-index">4</span><span>Halaman status menampilkan detail pesanan dan instruksi pembayaran.</span></li>
|
||||||
align-items: center;
|
</ul>
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
<div class="receipt-card">
|
||||||
overflow: hidden;
|
<div class="receipt-card__head">
|
||||||
position: relative;
|
<span>Ringkasan keranjang</span>
|
||||||
}
|
<strong><?= h((string)store_cart_count()) ?> item</strong>
|
||||||
body::before {
|
</div>
|
||||||
content: '';
|
<?php if ($cartLines): ?>
|
||||||
position: absolute;
|
<?php foreach ($cartLines as $line): ?>
|
||||||
top: 0;
|
<div class="receipt-line">
|
||||||
left: 0;
|
<span><?= h($line['product']['name']) ?> × <?= h((string)$line['quantity']) ?></span>
|
||||||
width: 100%;
|
<strong><?= h(store_money((float)$line['line_total'])) ?></strong>
|
||||||
height: 100%;
|
</div>
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
<?php endforeach; ?>
|
||||||
animation: bg-pan 20s linear infinite;
|
<div class="receipt-line receipt-line--total">
|
||||||
z-index: -1;
|
<span>Total sementara</span>
|
||||||
}
|
<strong><?= h(store_money((float)$summary['grand_total'])) ?></strong>
|
||||||
@keyframes bg-pan {
|
</div>
|
||||||
0% { background-position: 0% 0%; }
|
<a class="btn btn-dark w-100 mt-3" href="cart.php">Lanjutkan checkout</a>
|
||||||
100% { background-position: 100% 100%; }
|
<?php else: ?>
|
||||||
}
|
<p class="text-muted mb-3">Keranjang masih kosong. Tambahkan produk di bawah untuk mencoba alur checkout.</p>
|
||||||
main {
|
<a class="btn btn-outline-secondary w-100" href="#catalog">Mulai dari katalog</a>
|
||||||
padding: 2rem;
|
<?php endif; ?>
|
||||||
}
|
</div>
|
||||||
.card {
|
</aside>
|
||||||
background: var(--card-bg-color);
|
</div>
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
margin: 1.25rem auto 1.25rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
padding: 0; margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
|
||||||
<span class="sr-only">Loading…</span>
|
|
||||||
</div>
|
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<section class="mb-4">
|
||||||
</footer>
|
<div class="row g-3">
|
||||||
</body>
|
<div class="col-lg-4">
|
||||||
</html>
|
<div class="feature-card h-100">
|
||||||
|
<div class="feature-card__title">Katalog yang langsung bisa dijual</div>
|
||||||
|
<p class="feature-card__copy">Ada kategori, harga, deskripsi singkat, dan halaman detail produk untuk tiap item.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="feature-card h-100">
|
||||||
|
<div class="feature-card__title">Checkout tersimpan di sistem</div>
|
||||||
|
<p class="feature-card__copy">Order number dibuat otomatis, item pesanan disimpan di MySQL, dan status awal langsung tercatat.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="feature-card h-100">
|
||||||
|
<div class="feature-card__title">Status pesanan mandiri</div>
|
||||||
|
<p class="feature-card__copy">Pelanggan cukup masukkan kode pesanan dan email untuk melihat progress dan instruksi pembayaran.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="catalog" class="section-block">
|
||||||
|
<div class="section-heading d-flex flex-column flex-lg-row align-items-lg-end justify-content-between gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Katalog Produk</span>
|
||||||
|
<h2 class="section-title">Pilihan cake, bread, dan pastry untuk toko demo.</h2>
|
||||||
|
<p class="section-copy mb-0">Gunakan katalog ini sebagai starting point sebelum menambahkan admin, stok, kupon, dan pembayaran real.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2 filter-pills" data-filter-bar>
|
||||||
|
<?php foreach ($categories as $key => $category): ?>
|
||||||
|
<?php $isActive = $key === $selectedCategory; ?>
|
||||||
|
<a class="filter-pill<?= $isActive ? ' is-active' : '' ?>" href="index.php?category=<?= h($key) ?>#catalog">
|
||||||
|
<?= h($category['label']) ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<?php foreach ($products as $product): ?>
|
||||||
|
<div class="col-sm-6 col-xl-4">
|
||||||
|
<article class="product-card h-100">
|
||||||
|
<div class="product-visual product-visual--<?= h($product['tone']) ?>">
|
||||||
|
<span class="product-visual__meta"><?= h($product['category_label']) ?></span>
|
||||||
|
<strong class="product-visual__code"><?= h($product['visual_code']) ?></strong>
|
||||||
|
<span class="product-visual__name"><?= h($product['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="product-card__body">
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 class="product-card__title"><?= h($product['name']) ?></h3>
|
||||||
|
<p class="text-muted small mb-0"><?= h($product['lead_time']) ?></p>
|
||||||
|
</div>
|
||||||
|
<span class="price-tag"><?= h(store_money((float)$product['price'])) ?></span>
|
||||||
|
</div>
|
||||||
|
<p class="product-card__copy"><?= h($product['short_description']) ?></p>
|
||||||
|
<div class="detail-chip-group mb-3">
|
||||||
|
<?php foreach ($product['highlights'] as $highlight): ?>
|
||||||
|
<span class="detail-chip"><?= h($highlight) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column gap-2 mt-auto">
|
||||||
|
<a class="btn btn-outline-secondary w-100" href="product.php?slug=<?= h($product['slug']) ?>">Lihat detail</a>
|
||||||
|
<form action="cart_action.php" method="post" class="d-grid" data-auto-disable>
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<input type="hidden" name="slug" value="<?= h($product['slug']) ?>">
|
||||||
|
<input type="hidden" name="quantity" value="1">
|
||||||
|
<input type="hidden" name="redirect_to" value="<?= h($_SERVER['REQUEST_URI'] ?? 'index.php') ?>">
|
||||||
|
<button class="btn btn-dark" type="submit">Tambah ke keranjang</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="section-heading mb-4">
|
||||||
|
<span class="eyebrow">Cara Order</span>
|
||||||
|
<h2 class="section-title">Alur sederhana yang siap dipakai sebagai base toko online.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="step-card h-100">
|
||||||
|
<div class="step-card__number">01</div>
|
||||||
|
<h3>Pilih item</h3>
|
||||||
|
<p>Pelanggan lihat katalog, buka detail produk, lalu tambahkan item ke keranjang.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="step-card h-100">
|
||||||
|
<div class="step-card__number">02</div>
|
||||||
|
<h3>Checkout</h3>
|
||||||
|
<p>Isi nama, email, telepon, alamat, dan metode pembayaran. Order number dibuat otomatis.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="step-card h-100">
|
||||||
|
<div class="step-card__number">03</div>
|
||||||
|
<h3>Lacak status</h3>
|
||||||
|
<p>Gunakan order number dan email untuk melihat status pesanan dan instruksi pembayaran kapan saja.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php store_page_end(); ?>
|
||||||
|
|||||||
217
order_status.php
Normal file
217
order_status.php
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/store.php';
|
||||||
|
|
||||||
|
$lookupOrder = store_sanitize_line((string)($_GET['order'] ?? ''), 30);
|
||||||
|
$lookupEmail = trim(store_lower((string)($_GET['email'] ?? '')));
|
||||||
|
$created = (string)($_GET['created'] ?? '') === '1';
|
||||||
|
$lastLookup = store_last_order_lookup();
|
||||||
|
|
||||||
|
if ($lookupOrder !== '' && $lookupEmail === '' && ($lastLookup['order_number'] ?? '') === $lookupOrder) {
|
||||||
|
$lookupEmail = (string)($lastLookup['email'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = null;
|
||||||
|
$searchError = '';
|
||||||
|
if ($lookupOrder !== '') {
|
||||||
|
if ($lookupEmail === '') {
|
||||||
|
$searchError = 'Masukkan email yang dipakai saat checkout untuk membuka detail pesanan.';
|
||||||
|
} else {
|
||||||
|
$order = store_find_order($lookupOrder, $lookupEmail);
|
||||||
|
if (!$order) {
|
||||||
|
$searchError = 'Pesanan tidak ditemukan. Pastikan kode pesanan dan email sudah benar.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store_page_start('Status Pesanan', 'Lacak status pesanan menggunakan order number dan email.', ['noindex' => true]);
|
||||||
|
?>
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="section-heading mb-4">
|
||||||
|
<span class="eyebrow">Status Pesanan</span>
|
||||||
|
<h1 class="section-title">Lacak progress pesanan pelanggan.</h1>
|
||||||
|
<p class="section-copy mb-0">Masukkan kode pesanan dan email untuk melihat item, total, metode pembayaran, dan status terbaru.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($created && ($lastLookup['order_number'] ?? '') !== ''): ?>
|
||||||
|
<div class="alert alert-success border-0 shadow-sm mb-4 d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3" role="alert">
|
||||||
|
<div>
|
||||||
|
<strong>Pesanan berhasil dibuat.</strong>
|
||||||
|
<div class="small mt-1">Simpan kode <strong><?= h((string)$lastLookup['order_number']) ?></strong> untuk pelacakan status.</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm" data-copy-text="<?= h((string)$lastLookup['order_number']) ?>">Salin kode pesanan</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-4 align-items-start">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<form action="order_status.php" method="get" class="surface-panel p-4 sticky-summary">
|
||||||
|
<div class="card-kicker">Cek pesanan</div>
|
||||||
|
<h2 class="h5 mb-3">Cari berdasarkan kode & email</h2>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="order">Kode pesanan</label>
|
||||||
|
<input id="order" name="order" type="text" class="form-control" value="<?= h($lookupOrder) ?>" placeholder="contoh: SB260526-1234" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="email">Email checkout</label>
|
||||||
|
<input id="email" name="email" type="email" class="form-control" value="<?= h($lookupEmail) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button class="btn btn-dark" type="submit">Tampilkan status</button>
|
||||||
|
<a class="btn btn-outline-secondary" href="index.php#catalog">Belanja lagi</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box mt-4 mb-0">
|
||||||
|
<div class="card-kicker">Status default</div>
|
||||||
|
<div class="status-legend">
|
||||||
|
<?php foreach (store_status_steps() as $step): ?>
|
||||||
|
<div class="status-legend__item">
|
||||||
|
<span class="<?= h(store_status_class($step['value'])) ?>"><?= h($step['label']) ?></span>
|
||||||
|
<p class="mb-0"><?= h($step['description']) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<?php if ($searchError !== ''): ?>
|
||||||
|
<div class="alert alert-warning border-0 shadow-sm mb-4" role="alert">
|
||||||
|
<?= h($searchError) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($order): ?>
|
||||||
|
<section class="surface-panel p-4 p-lg-5 mb-4">
|
||||||
|
<div class="d-flex flex-column flex-lg-row align-items-lg-start justify-content-between gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<div class="card-kicker">Order detail</div>
|
||||||
|
<h2 class="section-title mb-2"><?= h((string)$order['order_number']) ?></h2>
|
||||||
|
<p class="section-copy mb-0">Dibuat pada <?= h(store_format_datetime((string)$order['created_at'])) ?></p>
|
||||||
|
</div>
|
||||||
|
<span class="<?= h(store_status_class((string)$order['status'])) ?>"><?= h((string)$order['status']) ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="metric-card h-100">
|
||||||
|
<div class="metric-value metric-value--small"><?= h(store_money((float)$order['grand_total'])) ?></div>
|
||||||
|
<div class="metric-label">total pembayaran</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="metric-card h-100">
|
||||||
|
<div class="metric-value metric-value--small"><?= h((string)$order['payment_method_label']) ?></div>
|
||||||
|
<div class="metric-label">metode bayar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="metric-card h-100">
|
||||||
|
<div class="metric-value metric-value--small"><?= h((string)count($order['items'])) ?> item</div>
|
||||||
|
<div class="metric-label">jumlah item di order</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<h3 class="h6 text-uppercase text-muted mb-3">Item pesanan</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle table-borderless order-table mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Produk</th>
|
||||||
|
<th class="text-center">Qty</th>
|
||||||
|
<th class="text-end">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($order['items'] as $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?= h((string)$item['name']) ?></strong>
|
||||||
|
<div class="small text-muted"><?= h(store_money((float)$item['price'])) ?> / item</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center"><?= h((string)$item['quantity']) ?></td>
|
||||||
|
<td class="text-end"><?= h(store_money((float)$item['line_total'])) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">Subtotal</td>
|
||||||
|
<td class="text-end"><?= h(store_money((float)$order['subtotal'])) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">Ongkir</td>
|
||||||
|
<td class="text-end"><?= h(store_money((float)$order['shipping_fee'])) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="fw-semibold">
|
||||||
|
<td colspan="2">Grand total</td>
|
||||||
|
<td class="text-end"><?= h(store_money((float)$order['grand_total'])) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="info-box mb-3">
|
||||||
|
<div class="card-kicker">Data penerima</div>
|
||||||
|
<p class="mb-1"><strong><?= h((string)$order['customer_name']) ?></strong></p>
|
||||||
|
<p class="mb-1"><?= h((string)$order['email']) ?></p>
|
||||||
|
<p class="mb-1"><?= h((string)$order['phone']) ?></p>
|
||||||
|
<p class="mb-0"><?= nl2br(h((string)$order['address'])) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="info-box mb-0">
|
||||||
|
<div class="card-kicker">Instruksi pembayaran</div>
|
||||||
|
<p class="mb-2"><?= h((string)$order['payment_instruction']) ?></p>
|
||||||
|
<?php if (!empty($order['note'])): ?>
|
||||||
|
<div class="note-copy"><strong>Catatan pelanggan:</strong> <?= h((string)$order['note']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php if ((string)$order['status'] !== 'Batal'): ?>
|
||||||
|
<?php $currentIndex = store_status_index((string)$order['status']); ?>
|
||||||
|
<section class="surface-panel p-4 p-lg-5">
|
||||||
|
<div class="card-kicker">Progress pesanan</div>
|
||||||
|
<div class="timeline-list mt-3">
|
||||||
|
<?php foreach (store_status_steps() as $index => $step): ?>
|
||||||
|
<?php
|
||||||
|
$stateClass = 'timeline-step';
|
||||||
|
if ($index < $currentIndex) {
|
||||||
|
$stateClass .= ' is-complete';
|
||||||
|
} elseif ($index === $currentIndex) {
|
||||||
|
$stateClass .= ' is-current';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="<?= h($stateClass) ?>">
|
||||||
|
<div class="timeline-step__dot"></div>
|
||||||
|
<div>
|
||||||
|
<div class="timeline-step__title"><?= h($step['label']) ?></div>
|
||||||
|
<p class="timeline-step__copy mb-0"><?= h($step['description']) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<section class="empty-state-card">
|
||||||
|
<span class="eyebrow">Belum ada detail</span>
|
||||||
|
<h2 class="section-title">Masukkan kode pesanan untuk melihat status.</h2>
|
||||||
|
<p class="section-copy">Setelah checkout berhasil, halaman ini akan menampilkan ringkasan item, total pembayaran, dan progress status order.</p>
|
||||||
|
<ul class="list-clean compact-list mt-4 mb-0">
|
||||||
|
<li><span class="list-index">1</span><span>Masuk dari checkout otomatis akan mengisi kode pesanan terbaru.</span></li>
|
||||||
|
<li><span class="list-index">2</span><span>Untuk kunjungan berikutnya, pelanggan cukup ingat order number dan email.</span></li>
|
||||||
|
<li><span class="list-index">3</span><span>Status berikutnya bisa diubah admin di iterasi lanjutan.</span></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php store_page_end(); ?>
|
||||||
136
product.php
Normal file
136
product.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/store.php';
|
||||||
|
|
||||||
|
$slug = (string)($_GET['slug'] ?? '');
|
||||||
|
$product = store_product($slug);
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
http_response_code(404);
|
||||||
|
store_page_start('Produk tidak ditemukan', 'Produk yang diminta tidak tersedia.', ['noindex' => true]);
|
||||||
|
?>
|
||||||
|
<section class="empty-state-card text-center mx-auto">
|
||||||
|
<span class="eyebrow">404</span>
|
||||||
|
<h1 class="section-title">Produk tidak ditemukan.</h1>
|
||||||
|
<p class="section-copy">Slug produk tidak valid atau item sudah dihapus dari katalog demo.</p>
|
||||||
|
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||||
|
<a class="btn btn-dark" href="index.php#catalog">Kembali ke katalog</a>
|
||||||
|
<a class="btn btn-outline-secondary" href="cart.php">Lihat keranjang</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
store_page_end();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relatedProducts = store_related_products($product['slug'], $product['category']);
|
||||||
|
|
||||||
|
store_page_start($product['name'], $product['short_description']);
|
||||||
|
?>
|
||||||
|
<nav aria-label="Breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small mb-0">
|
||||||
|
<li class="breadcrumb-item"><a href="index.php">Katalog</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page"><?= h($product['name']) ?></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="row g-4 align-items-start">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="surface-panel p-4 p-lg-5 h-100">
|
||||||
|
<div class="product-visual product-visual--<?= h($product['tone']) ?> product-visual--large mb-4">
|
||||||
|
<span class="product-visual__meta"><?= h($product['category_label']) ?></span>
|
||||||
|
<strong class="product-visual__code"><?= h($product['visual_code']) ?></strong>
|
||||||
|
<span class="product-visual__name"><?= h($product['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php foreach ($product['highlights'] as $highlight): ?>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<div class="metric-card h-100">
|
||||||
|
<div class="metric-value metric-value--small"><?= h($highlight) ?></div>
|
||||||
|
<div class="metric-label">informasi cepat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="surface-panel p-4 p-lg-5 h-100">
|
||||||
|
<span class="eyebrow"><?= h($product['category_label']) ?></span>
|
||||||
|
<h1 class="section-title mb-2"><?= h($product['name']) ?></h1>
|
||||||
|
<p class="lead-copy mb-3"><?= h($product['description']) ?></p>
|
||||||
|
<div class="price-block mb-4">
|
||||||
|
<div class="price-block__amount"><?= h(store_money((float)$product['price'])) ?></div>
|
||||||
|
<div class="price-block__caption">per <?= h($product['unit']) ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-grid mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="detail-grid__label">Lead time</span>
|
||||||
|
<strong><?= h($product['lead_time']) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="detail-grid__label">Porsi</span>
|
||||||
|
<strong><?= h($product['serves']) ?></strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="detail-grid__label">Kategori</span>
|
||||||
|
<strong><?= h($product['category_label']) ?></strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="cart_action.php" method="post" class="purchase-box" data-auto-disable>
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<input type="hidden" name="slug" value="<?= h($product['slug']) ?>">
|
||||||
|
<input type="hidden" name="redirect_to" value="product.php?slug=<?= h($product['slug']) ?>">
|
||||||
|
<label class="form-label" for="quantity">Jumlah</label>
|
||||||
|
<div class="quantity-field mb-3">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-qty-target="product-qty" data-qty-step="-1" aria-label="Kurangi jumlah">−</button>
|
||||||
|
<input id="product-qty" class="form-control text-center" type="number" name="quantity" min="1" max="20" value="1">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-qty-target="product-qty" data-qty-step="1" aria-label="Tambah jumlah">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 d-sm-flex">
|
||||||
|
<button class="btn btn-dark btn-lg flex-sm-fill" type="submit">Tambah ke keranjang</button>
|
||||||
|
<a class="btn btn-outline-secondary btn-lg flex-sm-fill" href="cart.php">Lihat keranjang</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="info-box mt-4">
|
||||||
|
<div class="card-kicker">Catatan checkout</div>
|
||||||
|
<p class="mb-0">Saat checkout, pelanggan akan memilih metode pembayaran dan sistem langsung membuat order number untuk pelacakan status.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section-block pt-0">
|
||||||
|
<div class="section-heading mb-4">
|
||||||
|
<span class="eyebrow">Produk terkait</span>
|
||||||
|
<h2 class="section-title">Tambahkan item lain untuk melengkapi pesanan.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<?php foreach ($relatedProducts as $related): ?>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<article class="product-card h-100">
|
||||||
|
<div class="product-visual product-visual--<?= h($related['tone']) ?>">
|
||||||
|
<span class="product-visual__meta"><?= h($related['category_label']) ?></span>
|
||||||
|
<strong class="product-visual__code"><?= h($related['visual_code']) ?></strong>
|
||||||
|
<span class="product-visual__name"><?= h($related['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="product-card__body">
|
||||||
|
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||||
|
<h3 class="product-card__title"><?= h($related['name']) ?></h3>
|
||||||
|
<span class="price-tag"><?= h(store_money((float)$related['price'])) ?></span>
|
||||||
|
</div>
|
||||||
|
<p class="product-card__copy"><?= h($related['short_description']) ?></p>
|
||||||
|
<a class="btn btn-outline-secondary w-100 mt-auto" href="product.php?slug=<?= h($related['slug']) ?>">Buka detail</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php store_page_end(); ?>
|
||||||
830
store.php
Normal file
830
store.php
Normal file
@ -0,0 +1,830 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORE_CART_KEY = 'sekut_cart';
|
||||||
|
const STORE_FLASH_KEY = 'sekut_flash';
|
||||||
|
const STORE_LAST_ORDER_KEY = 'sekut_last_order';
|
||||||
|
|
||||||
|
function app_env(string $key, string $fallback = ''): string
|
||||||
|
{
|
||||||
|
$serverValue = $_SERVER[$key] ?? '';
|
||||||
|
if (is_string($serverValue) && $serverValue !== '') {
|
||||||
|
return $serverValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$envValue = getenv($key);
|
||||||
|
if (is_string($envValue) && $envValue !== '') {
|
||||||
|
return $envValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function h($value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_brand(): string
|
||||||
|
{
|
||||||
|
return 'SEKUT BAKERY';
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_categories(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'all' => [
|
||||||
|
'label' => 'Semua',
|
||||||
|
'description' => 'Seluruh katalog bakery untuk daily order dan pre-order.',
|
||||||
|
],
|
||||||
|
'signature-cakes' => [
|
||||||
|
'label' => 'Signature Cakes',
|
||||||
|
'description' => 'Whole cake dan sliced cake untuk momen spesial.',
|
||||||
|
],
|
||||||
|
'artisan-bread' => [
|
||||||
|
'label' => 'Artisan Bread',
|
||||||
|
'description' => 'Roti harian dengan tekstur bersih dan fermentasi lebih lama.',
|
||||||
|
],
|
||||||
|
'cookies-pastry' => [
|
||||||
|
'label' => 'Cookies & Pastry',
|
||||||
|
'description' => 'Pilihan pastry dan cookies untuk coffee break dan hampers.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_products(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'burnt-cheesecake' => [
|
||||||
|
'slug' => 'burnt-cheesecake',
|
||||||
|
'name' => 'Burnt Cheesecake',
|
||||||
|
'category' => 'signature-cakes',
|
||||||
|
'category_label' => 'Signature Cakes',
|
||||||
|
'price' => 185000.0,
|
||||||
|
'unit' => 'whole cake',
|
||||||
|
'short_description' => 'Cheesecake creamy dengan permukaan karamel gelap dan finish yang clean.',
|
||||||
|
'description' => 'Whole cake 18 cm dengan rasa vanilla-cream cheese yang lembut dan lapisan atas yang sedikit smoky. Cocok untuk 8–10 porsi dan ideal untuk dessert keluarga.',
|
||||||
|
'lead_time' => 'Pre-order 1 hari',
|
||||||
|
'serves' => '8–10 porsi',
|
||||||
|
'tone' => 'ink',
|
||||||
|
'visual_code' => 'BC',
|
||||||
|
'highlights' => ['18 cm', 'Best seller', 'Pre-order 1 hari'],
|
||||||
|
],
|
||||||
|
'matcha-roll-cake' => [
|
||||||
|
'slug' => 'matcha-roll-cake',
|
||||||
|
'name' => 'Matcha Roll Cake',
|
||||||
|
'category' => 'signature-cakes',
|
||||||
|
'category_label' => 'Signature Cakes',
|
||||||
|
'price' => 158000.0,
|
||||||
|
'unit' => 'roll cake',
|
||||||
|
'short_description' => 'Sponge cake tipis dengan filling krim matcha yang ringan.',
|
||||||
|
'description' => 'Roll cake dengan sponge lembut, rasa matcha yang clean, dan krim yang tidak terlalu manis. Cocok untuk sharing di meeting kecil atau hadiah praktis.',
|
||||||
|
'lead_time' => 'Ready setiap pagi',
|
||||||
|
'serves' => '6–8 potong',
|
||||||
|
'tone' => 'stone',
|
||||||
|
'visual_code' => 'MR',
|
||||||
|
'highlights' => ['6–8 potong', 'Fresh daily', 'Light sweetness'],
|
||||||
|
],
|
||||||
|
'sesame-sourdough' => [
|
||||||
|
'slug' => 'sesame-sourdough',
|
||||||
|
'name' => 'Sesame Sourdough Loaf',
|
||||||
|
'category' => 'artisan-bread',
|
||||||
|
'category_label' => 'Artisan Bread',
|
||||||
|
'price' => 52000.0,
|
||||||
|
'unit' => 'loaf',
|
||||||
|
'short_description' => 'Roti sourdough dengan crust renyah, crumb lembut, dan taburan wijen.',
|
||||||
|
'description' => 'Loaf fermentasi alami dengan aroma gandum yang lebih dalam. Enak untuk sandwich, toast sarapan, atau stok roti rumah selama 2–3 hari.',
|
||||||
|
'lead_time' => 'Ready setiap siang',
|
||||||
|
'serves' => '8 slice tebal',
|
||||||
|
'tone' => 'sand',
|
||||||
|
'visual_code' => 'SD',
|
||||||
|
'highlights' => ['Fermentasi alami', '8 slice', 'Crust renyah'],
|
||||||
|
],
|
||||||
|
'focaccia-olive' => [
|
||||||
|
'slug' => 'focaccia-olive',
|
||||||
|
'name' => 'Focaccia Tomato Olive',
|
||||||
|
'category' => 'artisan-bread',
|
||||||
|
'category_label' => 'Artisan Bread',
|
||||||
|
'price' => 48000.0,
|
||||||
|
'unit' => 'tray',
|
||||||
|
'short_description' => 'Focaccia savory dengan olive oil, tomat, dan olive hitam.',
|
||||||
|
'description' => 'Roti focaccia bertekstur empuk dengan permukaan yang sedikit garing. Cocok untuk snack meeting, teman sup, atau dibuat sandwich terbuka.',
|
||||||
|
'lead_time' => 'Ready terbatas',
|
||||||
|
'serves' => '4–5 porsi',
|
||||||
|
'tone' => 'taupe',
|
||||||
|
'visual_code' => 'FO',
|
||||||
|
'highlights' => ['Savory', '4–5 porsi', 'Ready terbatas'],
|
||||||
|
],
|
||||||
|
'butter-croissant' => [
|
||||||
|
'slug' => 'butter-croissant',
|
||||||
|
'name' => 'Butter Croissant',
|
||||||
|
'category' => 'cookies-pastry',
|
||||||
|
'category_label' => 'Cookies & Pastry',
|
||||||
|
'price' => 28000.0,
|
||||||
|
'unit' => 'pcs',
|
||||||
|
'short_description' => 'Croissant berlapis dengan aroma butter dan tekstur flaky.',
|
||||||
|
'description' => 'Pastry klasik dengan lapisan tipis yang renyah di luar dan ringan di dalam. Cocok untuk sarapan cepat atau pairing dengan kopi hitam.',
|
||||||
|
'lead_time' => 'Ready dari pagi',
|
||||||
|
'serves' => '1 pcs besar',
|
||||||
|
'tone' => 'stone',
|
||||||
|
'visual_code' => 'CR',
|
||||||
|
'highlights' => ['Flaky', 'Fresh baked', 'Coffee pairing'],
|
||||||
|
],
|
||||||
|
'almond-cookies-tin' => [
|
||||||
|
'slug' => 'almond-cookies-tin',
|
||||||
|
'name' => 'Almond Cookies Tin',
|
||||||
|
'category' => 'cookies-pastry',
|
||||||
|
'category_label' => 'Cookies & Pastry',
|
||||||
|
'price' => 68000.0,
|
||||||
|
'unit' => 'tin',
|
||||||
|
'short_description' => 'Cookies renyah dengan potongan almond panggang dalam kemasan kaleng.',
|
||||||
|
'description' => 'Cookies butter dengan tekstur crisp dan rasa toasted almond yang hangat. Praktis untuk hampers kecil, pantry kantor, atau stok camilan rumah.',
|
||||||
|
'lead_time' => 'Ready stock',
|
||||||
|
'serves' => '20–24 pcs',
|
||||||
|
'tone' => 'ink',
|
||||||
|
'visual_code' => 'AC',
|
||||||
|
'highlights' => ['20–24 pcs', 'Giftable', 'Ready stock'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_product(string $slug): ?array
|
||||||
|
{
|
||||||
|
$products = store_products();
|
||||||
|
return $products[$slug] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_filtered_products(string $category = 'all'): array
|
||||||
|
{
|
||||||
|
$products = array_values(store_products());
|
||||||
|
if ($category === 'all' || !isset(store_categories()[$category])) {
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$products,
|
||||||
|
static fn(array $product): bool => $product['category'] === $category
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_related_products(string $slug, string $category, int $limit = 3): array
|
||||||
|
{
|
||||||
|
$products = array_values(array_filter(
|
||||||
|
store_products(),
|
||||||
|
static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] === $category
|
||||||
|
));
|
||||||
|
|
||||||
|
if (count($products) < $limit) {
|
||||||
|
$fallback = array_values(array_filter(
|
||||||
|
store_products(),
|
||||||
|
static fn(array $product): bool => $product['slug'] !== $slug && $product['category'] !== $category
|
||||||
|
));
|
||||||
|
$products = array_merge($products, $fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice($products, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_cart(): array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION[STORE_CART_KEY]) || !is_array($_SESSION[STORE_CART_KEY])) {
|
||||||
|
$_SESSION[STORE_CART_KEY] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SESSION[STORE_CART_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_set_cart(array $cart): void
|
||||||
|
{
|
||||||
|
$_SESSION[STORE_CART_KEY] = $cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_cart_count(): int
|
||||||
|
{
|
||||||
|
return array_sum(store_cart());
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_add_to_cart(string $slug, int $quantity = 1): bool
|
||||||
|
{
|
||||||
|
$product = store_product($slug);
|
||||||
|
if (!$product) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = max(1, min(20, $quantity));
|
||||||
|
$cart = store_cart();
|
||||||
|
$cart[$slug] = min(20, ((int)($cart[$slug] ?? 0)) + $quantity);
|
||||||
|
store_set_cart($cart);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_update_cart(array $quantities): void
|
||||||
|
{
|
||||||
|
$products = store_products();
|
||||||
|
$updated = [];
|
||||||
|
|
||||||
|
foreach ($quantities as $slug => $quantity) {
|
||||||
|
if (!isset($products[$slug])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$qty = (int)$quantity;
|
||||||
|
if ($qty > 0) {
|
||||||
|
$updated[$slug] = min(20, $qty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store_set_cart($updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_remove_from_cart(string $slug): void
|
||||||
|
{
|
||||||
|
$cart = store_cart();
|
||||||
|
unset($cart[$slug]);
|
||||||
|
store_set_cart($cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_money(float $amount): string
|
||||||
|
{
|
||||||
|
return 'Rp ' . number_format($amount, 0, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_substr(string $value, int $start, int $length): string
|
||||||
|
{
|
||||||
|
if (function_exists('mb_substr')) {
|
||||||
|
return (string)mb_substr($value, $start, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)substr($value, $start, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_strlen(string $value): int
|
||||||
|
{
|
||||||
|
if (function_exists('mb_strlen')) {
|
||||||
|
return (int)mb_strlen($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_lower(string $value): string
|
||||||
|
{
|
||||||
|
if (function_exists('mb_strtolower')) {
|
||||||
|
return (string)mb_strtolower($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_cart_summary(): array
|
||||||
|
{
|
||||||
|
$products = store_products();
|
||||||
|
$lines = [];
|
||||||
|
|
||||||
|
foreach (store_cart() as $slug => $quantity) {
|
||||||
|
if (!isset($products[$slug])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = $products[$slug];
|
||||||
|
$lineTotal = ((float)$product['price']) * (int)$quantity;
|
||||||
|
$lines[] = [
|
||||||
|
'slug' => $slug,
|
||||||
|
'quantity' => (int)$quantity,
|
||||||
|
'product' => $product,
|
||||||
|
'line_total' => $lineTotal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtotal = array_reduce(
|
||||||
|
$lines,
|
||||||
|
static fn(float $carry, array $line): float => $carry + (float)$line['line_total'],
|
||||||
|
0.0
|
||||||
|
);
|
||||||
|
|
||||||
|
$shippingFee = $subtotal <= 0 ? 0.0 : ($subtotal >= 250000 ? 0.0 : 18000.0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'lines' => $lines,
|
||||||
|
'subtotal' => $subtotal,
|
||||||
|
'shipping_fee' => $shippingFee,
|
||||||
|
'grand_total' => $subtotal + $shippingFee,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_payment_methods(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'bank_transfer' => [
|
||||||
|
'label' => 'Transfer Bank',
|
||||||
|
'description' => 'Instruksi pembayaran tampil setelah checkout selesai.',
|
||||||
|
'instruction' => 'Transfer ke rekening demo bakery lalu simpan bukti bayar untuk verifikasi admin.',
|
||||||
|
],
|
||||||
|
'qris' => [
|
||||||
|
'label' => 'QRIS',
|
||||||
|
'description' => 'Praktis untuk mobile banking dan e-wallet.',
|
||||||
|
'instruction' => 'Gunakan QRIS demo yang diberikan admin setelah pesanan masuk ke sistem.',
|
||||||
|
],
|
||||||
|
'cod' => [
|
||||||
|
'label' => 'Bayar di Tempat',
|
||||||
|
'description' => 'Tersedia untuk pengantaran area terdekat.',
|
||||||
|
'instruction' => 'Siapkan nominal pas saat kurir mengantarkan pesanan ke alamat Anda.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_status_steps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'value' => 'Menunggu Pembayaran',
|
||||||
|
'label' => 'Menunggu Pembayaran',
|
||||||
|
'description' => 'Pesanan tersimpan dan menunggu pembayaran pelanggan.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => 'Diproses',
|
||||||
|
'label' => 'Diproses',
|
||||||
|
'description' => 'Kitchen sedang menyiapkan item sesuai pesanan.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => 'Dikirim',
|
||||||
|
'label' => 'Dikirim',
|
||||||
|
'description' => 'Pesanan sedang diantar ke alamat tujuan.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => 'Selesai',
|
||||||
|
'label' => 'Selesai',
|
||||||
|
'description' => 'Pesanan telah diterima pelanggan.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_status_index(string $status): int
|
||||||
|
{
|
||||||
|
foreach (store_status_steps() as $index => $step) {
|
||||||
|
if ($step['value'] === $status) {
|
||||||
|
return $index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_status_class(string $status): string
|
||||||
|
{
|
||||||
|
return match ($status) {
|
||||||
|
'Menunggu Pembayaran' => 'status-pill status-pill--pending',
|
||||||
|
'Diproses' => 'status-pill status-pill--processing',
|
||||||
|
'Dikirim' => 'status-pill status-pill--shipping',
|
||||||
|
'Selesai' => 'status-pill status-pill--done',
|
||||||
|
'Batal' => 'status-pill status-pill--cancelled',
|
||||||
|
default => 'status-pill',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_flash(string $type, string $message): void
|
||||||
|
{
|
||||||
|
$_SESSION[STORE_FLASH_KEY][] = [
|
||||||
|
'type' => $type,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_consume_flashes(): array
|
||||||
|
{
|
||||||
|
$flashes = $_SESSION[STORE_FLASH_KEY] ?? [];
|
||||||
|
unset($_SESSION[STORE_FLASH_KEY]);
|
||||||
|
|
||||||
|
return is_array($flashes) ? $flashes : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_input_class(array $errors, string $field): string
|
||||||
|
{
|
||||||
|
return isset($errors[$field]) ? ' is-invalid' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_checkout_defaults(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'customer_name' => '',
|
||||||
|
'email' => '',
|
||||||
|
'phone' => '',
|
||||||
|
'address' => '',
|
||||||
|
'note' => '',
|
||||||
|
'payment_method' => 'bank_transfer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_sanitize_line(string $value, int $max = 255): string
|
||||||
|
{
|
||||||
|
$value = trim(preg_replace('/\s+/', ' ', $value) ?? '');
|
||||||
|
return store_substr($value, 0, $max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_normalize_checkout_input(array $source): array
|
||||||
|
{
|
||||||
|
$defaults = store_checkout_defaults();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'customer_name' => store_sanitize_line((string)($source['customer_name'] ?? $defaults['customer_name']), 120),
|
||||||
|
'email' => store_lower(trim((string)($source['email'] ?? $defaults['email']))),
|
||||||
|
'phone' => store_sanitize_line((string)($source['phone'] ?? $defaults['phone']), 40),
|
||||||
|
'address' => trim(store_substr((string)($source['address'] ?? $defaults['address']), 0, 500)),
|
||||||
|
'note' => trim(store_substr((string)($source['note'] ?? $defaults['note']), 0, 500)),
|
||||||
|
'payment_method' => store_sanitize_line((string)($source['payment_method'] ?? $defaults['payment_method']), 40),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_validate_checkout_input(array $data): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
$methods = store_payment_methods();
|
||||||
|
|
||||||
|
if (store_strlen($data['customer_name']) < 3) {
|
||||||
|
$errors['customer_name'] = 'Nama minimal 3 karakter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors['email'] = 'Masukkan email yang valid.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedPhone = preg_replace('/[^0-9+]/', '', $data['phone']) ?? '';
|
||||||
|
if (strlen($normalizedPhone) < 8) {
|
||||||
|
$errors['phone'] = 'Nomor telepon minimal 8 digit.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store_strlen($data['address']) < 10) {
|
||||||
|
$errors['address'] = 'Alamat minimal 10 karakter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($methods[$data['payment_method']])) {
|
||||||
|
$errors['payment_method'] = 'Pilih metode pembayaran yang tersedia.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_ensure_schema(): void
|
||||||
|
{
|
||||||
|
static $ready = false;
|
||||||
|
if ($ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db()->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
order_number VARCHAR(30) NOT NULL UNIQUE,
|
||||||
|
customer_name VARCHAR(120) NOT NULL,
|
||||||
|
email VARCHAR(160) NOT NULL,
|
||||||
|
phone VARCHAR(40) NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
note TEXT NULL,
|
||||||
|
payment_method VARCHAR(40) NOT NULL,
|
||||||
|
status VARCHAR(40) NOT NULL DEFAULT 'Menunggu Pembayaran',
|
||||||
|
subtotal DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||||
|
shipping_fee DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||||
|
grand_total DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||||
|
items_json LONGTEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_order_number (order_number),
|
||||||
|
INDEX idx_email_created_at (email, created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
|
);
|
||||||
|
|
||||||
|
$ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_generate_order_number(): string
|
||||||
|
{
|
||||||
|
store_ensure_schema();
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
for ($attempt = 0; $attempt < 6; $attempt++) {
|
||||||
|
$candidate = 'SB' . date('ymd') . '-' . (string)random_int(1000, 9999);
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = :order_number LIMIT 1');
|
||||||
|
$stmt->execute(['order_number' => $candidate]);
|
||||||
|
if (!$stmt->fetch()) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'SB' . date('ymdHis');
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_create_order(array $input): array
|
||||||
|
{
|
||||||
|
$data = store_normalize_checkout_input($input);
|
||||||
|
$errors = store_validate_checkout_input($data);
|
||||||
|
$summary = store_cart_summary();
|
||||||
|
|
||||||
|
if (empty($summary['lines'])) {
|
||||||
|
$errors['cart'] = 'Keranjang masih kosong. Tambahkan produk terlebih dahulu.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errors' => $errors,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
store_ensure_schema();
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function (array $line): array {
|
||||||
|
return [
|
||||||
|
'slug' => $line['slug'],
|
||||||
|
'name' => $line['product']['name'],
|
||||||
|
'price' => (float)$line['product']['price'],
|
||||||
|
'quantity' => (int)$line['quantity'],
|
||||||
|
'line_total' => (float)$line['line_total'],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$summary['lines']
|
||||||
|
);
|
||||||
|
|
||||||
|
$orderNumber = store_generate_order_number();
|
||||||
|
$stmt = db()->prepare(
|
||||||
|
'INSERT INTO orders (
|
||||||
|
order_number,
|
||||||
|
customer_name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
address,
|
||||||
|
note,
|
||||||
|
payment_method,
|
||||||
|
status,
|
||||||
|
subtotal,
|
||||||
|
shipping_fee,
|
||||||
|
grand_total,
|
||||||
|
items_json
|
||||||
|
) VALUES (
|
||||||
|
:order_number,
|
||||||
|
:customer_name,
|
||||||
|
:email,
|
||||||
|
:phone,
|
||||||
|
:address,
|
||||||
|
:note,
|
||||||
|
:payment_method,
|
||||||
|
:status,
|
||||||
|
:subtotal,
|
||||||
|
:shipping_fee,
|
||||||
|
:grand_total,
|
||||||
|
:items_json
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->bindValue(':order_number', $orderNumber);
|
||||||
|
$stmt->bindValue(':customer_name', $data['customer_name']);
|
||||||
|
$stmt->bindValue(':email', $data['email']);
|
||||||
|
$stmt->bindValue(':phone', $data['phone']);
|
||||||
|
$stmt->bindValue(':address', $data['address']);
|
||||||
|
$stmt->bindValue(':note', $data['note'] === '' ? null : $data['note']);
|
||||||
|
$stmt->bindValue(':payment_method', $data['payment_method']);
|
||||||
|
$stmt->bindValue(':status', 'Menunggu Pembayaran');
|
||||||
|
$stmt->bindValue(':subtotal', $summary['subtotal']);
|
||||||
|
$stmt->bindValue(':shipping_fee', $summary['shipping_fee']);
|
||||||
|
$stmt->bindValue(':grand_total', $summary['grand_total']);
|
||||||
|
$stmt->bindValue(':items_json', json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$_SESSION[STORE_CART_KEY] = [];
|
||||||
|
$_SESSION[STORE_LAST_ORDER_KEY] = [
|
||||||
|
'order_number' => $orderNumber,
|
||||||
|
'email' => $data['email'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'order_number' => $orderNumber,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_find_order(string $orderNumber, string $email = ''): ?array
|
||||||
|
{
|
||||||
|
$orderNumber = store_sanitize_line($orderNumber, 30);
|
||||||
|
$email = trim(store_lower($email));
|
||||||
|
|
||||||
|
if ($orderNumber === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
store_ensure_schema();
|
||||||
|
|
||||||
|
$sql = 'SELECT * FROM orders WHERE order_number = :order_number';
|
||||||
|
$params = ['order_number' => $orderNumber];
|
||||||
|
|
||||||
|
if ($email !== '') {
|
||||||
|
$sql .= ' AND email = :email';
|
||||||
|
$params['email'] = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' LIMIT 1';
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
foreach ($params as $key => $value) {
|
||||||
|
$stmt->bindValue(':' . $key, $value);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$order = $stmt->fetch();
|
||||||
|
if (!$order) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order['items'] = json_decode((string)$order['items_json'], true) ?: [];
|
||||||
|
$paymentMethods = store_payment_methods();
|
||||||
|
$order['payment_method_label'] = $paymentMethods[$order['payment_method']]['label'] ?? $order['payment_method'];
|
||||||
|
$order['payment_instruction'] = $paymentMethods[$order['payment_method']]['instruction'] ?? 'Tim admin akan menghubungi Anda untuk instruksi berikutnya.';
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_last_order_lookup(): array
|
||||||
|
{
|
||||||
|
$lookup = $_SESSION[STORE_LAST_ORDER_KEY] ?? [];
|
||||||
|
return is_array($lookup) ? $lookup : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_safe_redirect(string $target, string $fallback = 'cart.php'): string
|
||||||
|
{
|
||||||
|
$target = trim($target);
|
||||||
|
if ($target === '') {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = parse_url($target);
|
||||||
|
if ($parts === false || isset($parts['scheme']) || isset($parts['host'])) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $parts['path'] ?? '';
|
||||||
|
if ($path === '' || str_contains($path, '..')) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = ltrim($path, '/');
|
||||||
|
if ($path === '') {
|
||||||
|
$path = $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||||
|
|
||||||
|
return $path . $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_format_datetime(string $value): string
|
||||||
|
{
|
||||||
|
$timestamp = strtotime($value);
|
||||||
|
if ($timestamp === false) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$months = [
|
||||||
|
1 => 'Jan',
|
||||||
|
2 => 'Feb',
|
||||||
|
3 => 'Mar',
|
||||||
|
4 => 'Apr',
|
||||||
|
5 => 'Mei',
|
||||||
|
6 => 'Jun',
|
||||||
|
7 => 'Jul',
|
||||||
|
8 => 'Agu',
|
||||||
|
9 => 'Sep',
|
||||||
|
10 => 'Okt',
|
||||||
|
11 => 'Nov',
|
||||||
|
12 => 'Des',
|
||||||
|
];
|
||||||
|
|
||||||
|
$month = $months[(int)date('n', $timestamp)] ?? date('M', $timestamp);
|
||||||
|
return date('d', $timestamp) . ' ' . $month . ' ' . date('Y, H:i', $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_nav_link(string $href, string $label, string $currentPath): string
|
||||||
|
{
|
||||||
|
$active = $currentPath === $href ? ' nav-link active' : ' nav-link';
|
||||||
|
return '<li class="nav-item"><a class="' . $active . '" href="' . h($href) . '">' . h($label) . '</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_page_start(string $title, string $description = '', array $options = []): void
|
||||||
|
{
|
||||||
|
$projectName = app_env('PROJECT_NAME', store_brand());
|
||||||
|
$projectDescription = app_env('PROJECT_DESCRIPTION', 'Toko online bakery dengan katalog, keranjang, checkout, dan halaman status pesanan.');
|
||||||
|
$projectImageUrl = app_env('PROJECT_IMAGE_URL', '');
|
||||||
|
$metaDescription = $description !== '' ? $description : $projectDescription;
|
||||||
|
$fullTitle = trim($title) !== '' ? $title . ' • ' . $projectName : $projectName;
|
||||||
|
$cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string)filemtime(__DIR__ . '/assets/css/custom.css') : (string)time();
|
||||||
|
$currentPath = basename(parse_url($_SERVER['REQUEST_URI'] ?? '/index.php', PHP_URL_PATH) ?: 'index.php');
|
||||||
|
if ($currentPath === '' || $currentPath === '/') {
|
||||||
|
$currentPath = 'index.php';
|
||||||
|
}
|
||||||
|
$robots = !empty($options['noindex']) ? '<meta name="robots" content="noindex, nofollow" />' : '';
|
||||||
|
$cartCount = store_cart_count();
|
||||||
|
|
||||||
|
echo '<!doctype html>';
|
||||||
|
echo '<html lang="id">';
|
||||||
|
echo '<head>';
|
||||||
|
echo '<meta charset="utf-8" />';
|
||||||
|
echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
|
||||||
|
echo '<title>' . h($fullTitle) . '</title>';
|
||||||
|
echo $robots;
|
||||||
|
|
||||||
|
if ($metaDescription !== '') {
|
||||||
|
echo '<meta name="description" content="' . h($metaDescription) . '" />';
|
||||||
|
echo '<meta property="og:title" content="' . h($fullTitle) . '" />';
|
||||||
|
echo '<meta property="og:description" content="' . h($metaDescription) . '" />';
|
||||||
|
echo '<meta name="twitter:title" content="' . h($fullTitle) . '" />';
|
||||||
|
echo '<meta property="twitter:description" content="' . h($metaDescription) . '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($projectImageUrl !== '') {
|
||||||
|
echo '<meta property="og:image" content="' . h($projectImageUrl) . '" />';
|
||||||
|
echo '<meta property="twitter:image" content="' . h($projectImageUrl) . '" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<link rel="preconnect" href="https://fonts.googleapis.com">';
|
||||||
|
echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
||||||
|
echo '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">';
|
||||||
|
echo '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">';
|
||||||
|
echo '<link rel="stylesheet" href="assets/css/custom.css?v=' . h($cssVersion) . '">';
|
||||||
|
echo '</head>';
|
||||||
|
echo '<body>';
|
||||||
|
echo '<header class="site-header sticky-top">';
|
||||||
|
echo ' <nav class="navbar navbar-expand-lg navbar-light">';
|
||||||
|
echo ' <div class="container-xxl">';
|
||||||
|
echo ' <a class="navbar-brand brand-mark" href="index.php">';
|
||||||
|
echo ' <span class="brand-mark__title">' . h(store_brand()) . '</span>';
|
||||||
|
echo ' <span class="brand-mark__subtitle">Online bakery store</span>';
|
||||||
|
echo ' </a>';
|
||||||
|
echo ' <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#siteNav" aria-controls="siteNav" aria-expanded="false" aria-label="Toggle navigation">';
|
||||||
|
echo ' <span class="navbar-toggler-icon"></span>';
|
||||||
|
echo ' </button>';
|
||||||
|
echo ' <div class="collapse navbar-collapse" id="siteNav">';
|
||||||
|
echo ' <ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">';
|
||||||
|
echo store_nav_link('index.php', 'Katalog', $currentPath);
|
||||||
|
echo store_nav_link('cart.php', 'Keranjang', $currentPath);
|
||||||
|
echo store_nav_link('order_status.php', 'Status Pesanan', $currentPath);
|
||||||
|
echo ' <li class="nav-item ms-lg-2">';
|
||||||
|
echo ' <a class="btn btn-dark btn-sm btn-cart" href="cart.php">Keranjang <span class="badge rounded-pill text-bg-light ms-2">' . h((string)$cartCount) . '</span></a>';
|
||||||
|
echo ' </li>';
|
||||||
|
echo ' </ul>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' </nav>';
|
||||||
|
echo '</header>';
|
||||||
|
|
||||||
|
$flashes = store_consume_flashes();
|
||||||
|
if ($flashes) {
|
||||||
|
echo '<div class="toast-stack position-fixed top-0 end-0 p-3">';
|
||||||
|
foreach ($flashes as $index => $flash) {
|
||||||
|
$type = $flash['type'] ?? 'info';
|
||||||
|
$class = match ($type) {
|
||||||
|
'success' => 'toast-theme toast-theme--success',
|
||||||
|
'warning' => 'toast-theme toast-theme--warning',
|
||||||
|
'danger' => 'toast-theme toast-theme--danger',
|
||||||
|
default => 'toast-theme',
|
||||||
|
};
|
||||||
|
echo '<div class="toast ' . h($class) . '" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="3800">';
|
||||||
|
echo ' <div class="toast-body">' . h((string)($flash['message'] ?? '')) . '</div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<main class="site-main">';
|
||||||
|
echo ' <div class="container-xxl py-4 py-lg-5">';
|
||||||
|
}
|
||||||
|
|
||||||
|
function store_page_end(): void
|
||||||
|
{
|
||||||
|
$jsVersion = file_exists(__DIR__ . '/assets/js/main.js') ? (string)filemtime(__DIR__ . '/assets/js/main.js') : (string)time();
|
||||||
|
|
||||||
|
echo ' </div>';
|
||||||
|
echo '</main>';
|
||||||
|
echo '<footer class="site-footer">';
|
||||||
|
echo ' <div class="container-xxl d-flex flex-column flex-lg-row justify-content-between gap-3 py-4">';
|
||||||
|
echo ' <div>';
|
||||||
|
echo ' <div class="footer-title">' . h(store_brand()) . '</div>';
|
||||||
|
echo ' <p class="footer-copy">MVP toko online dengan katalog, keranjang, checkout tersimpan, dan pelacakan status pesanan.</p>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <div class="footer-links d-flex flex-wrap gap-3">';
|
||||||
|
echo ' <a href="index.php#catalog">Lihat katalog</a>';
|
||||||
|
echo ' <a href="cart.php">Buka keranjang</a>';
|
||||||
|
echo ' <a href="order_status.php">Lacak pesanan</a>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo '</footer>';
|
||||||
|
echo '<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>';
|
||||||
|
echo '<script src="assets/js/main.js?v=' . h($jsVersion) . '"></script>';
|
||||||
|
echo '</body>';
|
||||||
|
echo '</html>';
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user