- Implemented the full Fat Bottom Grille storefront and staff dashboard.
Highlights include configurable menus, specials, cart/checkout, taxes, customer accounts with email 2FA, newsletters, analytics, refunds, PDF/CSV exports, Square/Mailgun adapters, and optional MySQL migration. See README.md for setup and production configuration. Verified PHP/JavaScript syntax, responsive layouts, registration, 2FA, checkout, dashboard workflows, settings persistence, and PDF generation. Real Square, Mailgun, and MySQL connections require credentials/server access. Square implementation follows the official Payments API and Refunds API.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<section class="page-hero page-hero-compact">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Good to see you</span>
|
||||
<h1>My Account</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container account-grid section">
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Your details</span>
|
||||
<h2>Pickup Profile</h2>
|
||||
</div>
|
||||
<span class="account-email"><?= e($currentUser['email']) ?></span>
|
||||
</div>
|
||||
<form class="form-stack" action="/account" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Full name
|
||||
<input type="text" name="full_name" required value="<?= e($currentUser['full_name']) ?>">
|
||||
</label>
|
||||
<label>Phone
|
||||
<input type="tel" name="phone" required value="<?= e($currentUser['phone']) ?>">
|
||||
</label>
|
||||
<label class="field-wide">Street address
|
||||
<input type="text" name="street_address" value="<?= e($currentUser['street_address']) ?>">
|
||||
</label>
|
||||
<label>City
|
||||
<input type="text" name="city" value="<?= e($currentUser['city']) ?>">
|
||||
</label>
|
||||
<label>State
|
||||
<input type="text" name="state" maxlength="2" value="<?= e($currentUser['state']) ?>">
|
||||
</label>
|
||||
<label>ZIP code
|
||||
<input type="text" name="postal_code" value="<?= e($currentUser['postal_code']) ?>">
|
||||
</label>
|
||||
</div>
|
||||
<label class="check-control">
|
||||
<input type="checkbox" name="newsletter_opt_in" value="1" <?= checked($currentUser['newsletter_opt_in']) ?>>
|
||||
<span>
|
||||
<strong>Send me restaurant news and specials</strong>
|
||||
<small>You can change this any time.</small>
|
||||
</span>
|
||||
</label>
|
||||
<details class="account-password">
|
||||
<summary>Change password</summary>
|
||||
<div class="form-stack">
|
||||
<label>Current password
|
||||
<input type="password" name="current_password" autocomplete="current-password">
|
||||
</label>
|
||||
<div class="form-grid">
|
||||
<label>New password
|
||||
<input type="password" name="new_password" minlength="10" autocomplete="new-password">
|
||||
</label>
|
||||
<label>Confirm new password
|
||||
<input type="password" name="new_password_confirmation" minlength="10" autocomplete="new-password">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<button class="button" type="submit">Save Account</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Past pickups</span>
|
||||
<h2>Order History</h2>
|
||||
</div>
|
||||
<a class="button button-small" href="/menu">Order Again</a>
|
||||
</div>
|
||||
<?php if (!$orders): ?>
|
||||
<div class="empty-state compact">
|
||||
<p>Your first online order will appear here.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="order-history">
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<article>
|
||||
<div>
|
||||
<strong><?= e($order['order_number']) ?></strong>
|
||||
<span><?= e(store_datetime((string) $order['placed_at'])) ?></span>
|
||||
</div>
|
||||
<span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucwords(str_replace('_', ' ', (string) $order['status']))) ?></span>
|
||||
<strong><?= money($order['total_cents']) ?></strong>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
<div class="stat-grid">
|
||||
<article class="stat-card stat-primary">
|
||||
<span>Today’s sales</span>
|
||||
<strong><?= money($stats['today_sales']) ?></strong>
|
||||
<small><?= (int) $stats['today_orders'] ?> paid orders</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Open orders</span>
|
||||
<strong><?= (int) $stats['open_orders'] ?></strong>
|
||||
<small>Paid through ready</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Abandoned carts</span>
|
||||
<strong><?= (int) $stats['abandoned_carts'] ?></strong>
|
||||
<small>Inactive for 30+ minutes</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Today’s visitors</span>
|
||||
<strong><?= (int) $stats['today_visitors'] ?></strong>
|
||||
<small><?= (int) $stats['newsletter_members'] ?> news subscribers</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<?php if (password_verify('ChangeMe123!', (string) $currentUser['password_hash'])): ?>
|
||||
<div class="notice admin-security-notice">
|
||||
<strong>Security reminder:</strong> the initial administrator password is still active.
|
||||
Change it from <a href="/account">My Account</a> before deployment.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-dashboard-grid">
|
||||
<section class="admin-panel admin-panel-wide">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Live queue</span>
|
||||
<h2>Recent Orders</h2>
|
||||
</div>
|
||||
<a class="button button-small" href="/admin/orders">View All Orders</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Order</th><th>Customer</th><th>Placed</th><th>Status</th><th>Total</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (!$recentOrders): ?>
|
||||
<tr><td colspan="5" class="empty-cell">No orders yet. The kitchen is standing by.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($recentOrders as $order): ?>
|
||||
<tr>
|
||||
<td><a href="/admin/order?id=<?= (int) $order['id'] ?>"><?= e($order['order_number']) ?></a></td>
|
||||
<td><?= e($order['customer_name']) ?><small><?= e($order['customer_phone']) ?></small></td>
|
||||
<td><?= e(store_datetime((string) $order['placed_at'], 'g:i A')) ?></td>
|
||||
<td><span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span></td>
|
||||
<td><?= money($order['total_cents']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Last 7 days</span>
|
||||
<h2>Store Traffic</h2>
|
||||
</div>
|
||||
</div>
|
||||
<?php $maxTraffic = max(1, ...array_map(static fn (array $row): int => (int) $row['visitors'], $traffic)); ?>
|
||||
<div class="bar-chart">
|
||||
<?php if (!$traffic): ?><p class="muted">Traffic data will appear as visitors arrive.</p><?php endif; ?>
|
||||
<?php foreach ($traffic as $day): ?>
|
||||
<div>
|
||||
<span class="bar-value"><?= (int) $day['visitors'] ?></span>
|
||||
<i style="height:<?= max(8, round(((int) $day['visitors'] / $maxTraffic) * 120)) ?>px"></i>
|
||||
<small><?= e((new DateTimeImmutable((string) $day['day']))->format('D')) ?></small>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">What’s moving</span>
|
||||
<h2>Popular Items</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-list">
|
||||
<?php if (!$popularItems): ?><p class="muted">Item sales will appear after the first order.</p><?php endif; ?>
|
||||
<?php foreach ($popularItems as $index => $item): ?>
|
||||
<div>
|
||||
<span><?= $index + 1 ?></span>
|
||||
<strong><?= e($item['item_name']) ?></strong>
|
||||
<small><?= (int) $item['quantity'] ?> sold</small>
|
||||
<b><?= money($item['sales_cents']) ?></b>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,125 @@
|
||||
<div class="admin-page-actions">
|
||||
<p>Categories and items appear on the storefront as soon as they are active.</p>
|
||||
<a class="button button-ghost" href="/admin/export/menu.pdf">Download Styled PDF</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-stack">
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Organize the storefront</span><h2>Menu Categories</h2></div>
|
||||
<span class="count-badge"><?= count($categories) ?> categories</span>
|
||||
</div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="drag-index"><?= (int) $category['display_order'] ?></span>
|
||||
<span><strong><?= e($category['name']) ?></strong><small><?= e($category['description']) ?></small></span>
|
||||
<span class="status-dot <?= $category['active'] ? 'is-on' : '' ?>"><?= $category['active'] ? 'Active' : 'Hidden' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/category/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $category['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required value="<?= e($category['name']) ?>"></label>
|
||||
<label>URL slug<input type="text" name="slug" required value="<?= e($category['slug']) ?>"></label>
|
||||
<label class="field-wide">Description<input type="text" name="description" value="<?= e($category['description']) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $category['display_order'] ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($category['active']) ?>><span><strong>Visible</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Category</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Category</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/category/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>URL slug<input type="text" name="slug" placeholder="Generated from name if blank"></label>
|
||||
<label class="field-wide">Description<input type="text" name="description"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="60"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Visible</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Category</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Price and availability</span><h2>Menu Items</h2></div>
|
||||
<span class="count-badge"><?= count($items) ?> items</span>
|
||||
</div>
|
||||
<div class="editable-list item-editable-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php $tags = json_decode((string) $item['dietary_tags'], true); ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="item-avatar"><?= e(strtoupper(substr((string) $item['name'], 0, 2))) ?></span>
|
||||
<span><strong><?= e($item['name']) ?></strong><small><?= e($item['category_name']) ?></small></span>
|
||||
<strong class="item-price"><?= money($item['price_cents']) ?></strong>
|
||||
<span class="status-dot <?= $item['active'] ? 'is-on' : '' ?>"><?= $item['active'] ? 'Active' : 'Hidden' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/item/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $item['id'] ?>">
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Name<input type="text" name="name" required value="<?= e($item['name']) ?>"></label>
|
||||
<label>Category
|
||||
<select name="category_id" required>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= (int) $category['id'] ?>" <?= selected($item['category_id'], $category['id']) ?>><?= e($category['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Price<input type="number" name="price" min="0" step="0.01" required value="<?= number_format((int) $item['price_cents'] / 100, 2, '.', '') ?>"></label>
|
||||
<label>Compare-at price<input type="number" name="compare_at_price" min="0" step="0.01" value="<?= $item['compare_at_cents'] !== null ? number_format((int) $item['compare_at_cents'] / 100, 2, '.', '') : '' ?>"></label>
|
||||
<label>URL slug<input type="text" name="slug" value="<?= e($item['slug']) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $item['display_order'] ?>"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"><?= e($item['description']) ?></textarea></label>
|
||||
<label class="field-wide">Image URL<input type="url" name="image_url" value="<?= e($item['image_url']) ?>" placeholder="Optional HTTPS image"></label>
|
||||
<label class="field-wide">Tags, comma separated<input type="text" name="dietary_tags" value="<?= e(is_array($tags) ? implode(', ', $tags) : '') ?>"></label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($item['active']) ?>><span><strong>Available</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="featured" value="1" <?= checked($item['featured']) ?>><span><strong>Featured</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Item</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Menu Item</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/item/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Category
|
||||
<select name="category_id" required>
|
||||
<?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>"><?= e($category['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Price<input type="number" name="price" min="0" step="0.01" required></label>
|
||||
<label>Compare-at price<input type="number" name="compare_at_price" min="0" step="0.01"></label>
|
||||
<label>URL slug<input type="text" name="slug" placeholder="Generated from name"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="0"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"></textarea></label>
|
||||
<label class="field-wide">Image URL<input type="url" name="image_url"></label>
|
||||
<label class="field-wide">Tags, comma separated<input type="text" name="dietary_tags"></label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Available</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="featured" value="1"><span><strong>Featured</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Item</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="admin-page-actions">
|
||||
<p><strong><?= (int) $recipientCount ?></strong> active users are currently opted in.</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-detail-grid newsletter-grid">
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Send an update</span><h2>New Newsletter</h2></div></div>
|
||||
<form class="form-stack" action="/admin/newsletters/send" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>Subject line<input type="text" name="subject" maxlength="140" required placeholder="This weekend at Fat Bottom Grille"></label>
|
||||
<label>Message<textarea name="content" rows="12" required placeholder="Share a new special, schedule update, or note from the kitchen..."></textarea></label>
|
||||
<div class="notice">Every email includes an unsubscribe link. Users can also opt out from My Account.</div>
|
||||
<button class="button button-large button-block" type="submit" data-confirm="Send this newsletter to every opted-in user?">Send to <?= (int) $recipientCount ?> Subscribers</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Delivery history</span><h2>Past Sends</h2></div></div>
|
||||
<div class="newsletter-history">
|
||||
<?php if (!$newsletters): ?><p class="muted">No newsletters have been sent yet.</p><?php endif; ?>
|
||||
<?php foreach ($newsletters as $newsletter): ?>
|
||||
<article>
|
||||
<div><strong><?= e($newsletter['subject']) ?></strong><span><?= e(store_datetime((string) ($newsletter['sent_at'] ?: $newsletter['created_at']))) ?></span></div>
|
||||
<span class="role-badge"><?= (int) $newsletter['recipient_count'] ?> sent</span>
|
||||
<small>by <?= e($newsletter['author'] ?: 'System') ?></small>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<div class="detail-heading">
|
||||
<div>
|
||||
<a class="back-link" href="/admin/orders">← Back to orders</a>
|
||||
<div class="detail-title-row">
|
||||
<h2><?= e($order['order_number']) ?></h2>
|
||||
<span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span>
|
||||
</div>
|
||||
<p>Placed <?= e(store_datetime((string) $order['placed_at'], 'F j, Y \a\t g:i A')) ?></p>
|
||||
</div>
|
||||
<strong class="detail-total"><?= money($order['total_cents']) ?></strong>
|
||||
</div>
|
||||
|
||||
<div class="admin-detail-grid">
|
||||
<div>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Order Items</h2></div>
|
||||
<div class="order-item-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<div>
|
||||
<span class="item-quantity"><?= (int) $item['quantity'] ?>x</span>
|
||||
<span><strong><?= e($item['item_name']) ?></strong><?php if ($item['notes']): ?><small><?= e($item['notes']) ?></small><?php endif; ?></span>
|
||||
<strong><?= money($item['line_total_cents']) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<dl class="admin-totals">
|
||||
<div><dt>Subtotal</dt><dd><?= money($order['subtotal_cents']) ?></dd></div>
|
||||
<div><dt>Tax</dt><dd><?= money($order['tax_cents']) ?></dd></div>
|
||||
<div><dt>Total</dt><dd><?= money($order['total_cents']) ?></dd></div>
|
||||
</dl>
|
||||
<?php if ($order['special_instructions']): ?>
|
||||
<div class="notice"><strong>Customer note:</strong> <?= e($order['special_instructions']) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Activity</h2></div>
|
||||
<div class="timeline">
|
||||
<?php foreach ($events as $event): ?>
|
||||
<div>
|
||||
<i></i>
|
||||
<span>
|
||||
<strong><?= e(ucwords(str_replace('_', ' ', (string) $event['event_type']))) ?></strong>
|
||||
<p><?= e($event['details']) ?></p>
|
||||
<small><?= e($event['staff_name'] ?: 'System') ?> · <?= e(store_datetime((string) $event['created_at'], 'M j, g:i A')) ?></small>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<aside>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Update Order</h2></div>
|
||||
<form class="form-stack" action="/admin/order" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
|
||||
<label>Status
|
||||
<select name="status">
|
||||
<?php foreach (['paid', 'preparing', 'ready', 'completed', 'cancelled'] as $status): ?>
|
||||
<option value="<?= $status ?>" <?= selected($order['status'], $status) ?>><?= e(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Staff note
|
||||
<textarea name="staff_note" rows="3" placeholder="Optional internal note"></textarea>
|
||||
</label>
|
||||
<button class="button button-block" type="submit">Update Status</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Customer</h2></div>
|
||||
<div class="customer-card">
|
||||
<strong><?= e($order['customer_name']) ?></strong>
|
||||
<a href="mailto:<?= e($order['customer_email']) ?>"><?= e($order['customer_email']) ?></a>
|
||||
<a href="tel:<?= e(preg_replace('/[^0-9+]/', '', (string) $order['customer_phone'])) ?>"><?= e($order['customer_phone']) ?></a>
|
||||
<span>Pickup<?= $order['pickup_time'] ? ' at ' . e($order['pickup_time']) : '' ?></span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Issue Refund</h2></div>
|
||||
<form class="form-stack" action="/admin/order/refund" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
|
||||
<label>Amount
|
||||
<input type="number" name="amount" min="0.01" max="<?= number_format((int) $order['total_cents'] / 100, 2, '.', '') ?>" step="0.01" required>
|
||||
</label>
|
||||
<label>Reason
|
||||
<input type="text" name="reason" required placeholder="Customer request, unavailable item...">
|
||||
</label>
|
||||
<button class="button button-danger button-block" type="submit" data-confirm="Issue this refund? This action sends money back through the payment provider.">Issue Refund</button>
|
||||
</form>
|
||||
<?php if ($refunds): ?>
|
||||
<div class="refund-list">
|
||||
<?php foreach ($refunds as $refund): ?>
|
||||
<div><strong>-<?= money($refund['amount_cents']) ?></strong><span><?= e($refund['reason']) ?></span><small><?= e($refund['staff_name'] ?? 'Staff') ?></small></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading admin-toolbar">
|
||||
<form class="admin-filters" action="/admin/orders" method="get">
|
||||
<input type="search" name="q" value="<?= e($query) ?>" placeholder="Order, customer, or email">
|
||||
<select name="status">
|
||||
<option value="">All statuses</option>
|
||||
<?php foreach (['paid', 'preparing', 'ready', 'completed', 'cancelled'] as $status): ?>
|
||||
<option value="<?= $status ?>" <?= selected($statusFilter, $status) ?>><?= e(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button button-small" type="submit">Filter</button>
|
||||
</form>
|
||||
<a class="button button-small button-ghost" href="/admin/export/orders.csv">Export CSV</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Order</th><th>Customer</th><th>Placed</th><th>Fulfillment</th><th>Payment</th><th>Total</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$orders): ?><tr><td colspan="7" class="empty-cell">No orders match those filters.</td></tr><?php endif; ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td><strong><?= e($order['order_number']) ?></strong></td>
|
||||
<td><?= e($order['customer_name']) ?><small><?= e($order['customer_email']) ?></small></td>
|
||||
<td><?= e(store_datetime((string) $order['placed_at'], 'M j, g:i A')) ?></td>
|
||||
<td><span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span></td>
|
||||
<td>
|
||||
<span class="status-pill status-<?= e($order['payment_status']) ?>"><?= e(ucwords(str_replace('_', ' ', (string) $order['payment_status']))) ?></span>
|
||||
<?php if ((int) $order['refunded_cents'] > 0): ?><small><?= money($order['refunded_cents']) ?> returned</small><?php endif; ?>
|
||||
</td>
|
||||
<td><?= money($order['total_cents']) ?></td>
|
||||
<td><a class="button button-small button-ghost" href="/admin/order?id=<?= (int) $order['id'] ?>">Open</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,116 @@
|
||||
<form class="admin-stack" action="/admin/settings" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Public identity</span><h2>Business Information</h2></div></div>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Public site URL<input type="url" name="app[url]" required value="<?= e($config->get('app.url')) ?>"></label>
|
||||
<label>Business name<input type="text" name="business[name]" required value="<?= e($config->get('business.name')) ?>"></label>
|
||||
<label>Phone<input type="tel" name="business[phone]" value="<?= e($config->get('business.phone')) ?>"></label>
|
||||
<label>Order email<input type="email" name="business[email]" value="<?= e($config->get('business.email')) ?>"></label>
|
||||
<label>Street<input type="text" name="business[street]" value="<?= e($config->get('business.street')) ?>"></label>
|
||||
<label>City<input type="text" name="business[city]" value="<?= e($config->get('business.city')) ?>"></label>
|
||||
<label>State<input type="text" name="business[state]" value="<?= e($config->get('business.state')) ?>"></label>
|
||||
<label>ZIP code<input type="text" name="business[postal_code]" value="<?= e($config->get('business.postal_code')) ?>"></label>
|
||||
<label>Published hours<input type="text" name="business[hours]" value="<?= e($config->get('business.hours')) ?>"></label>
|
||||
<label class="field-wide">Announcement bar<input type="text" name="business[announcement]" value="<?= e($config->get('business.announcement')) ?>"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Kitchen controls</span><h2>Online Ordering</h2></div></div>
|
||||
<div class="settings-row">
|
||||
<label class="check-control"><input type="checkbox" name="ordering[accepting_orders]" value="1" <?= checked($config->get('ordering.accepting_orders')) ?>><span><strong>Accept online orders</strong><small>Turn this off to pause checkout without hiding the menu.</small></span></label>
|
||||
<label>Estimated pickup time, minutes<input type="number" min="5" max="180" name="ordering[pickup_eta_minutes]" value="<?= (int) $config->get('ordering.pickup_eta_minutes') ?>"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Card processing</span><h2>Square</h2></div><span class="status-dot <?= $config->get('square.enabled') ? 'is-on' : '' ?>"><?= $config->get('square.enabled') ? 'Enabled' : 'Demo mode' ?></span></div>
|
||||
<div class="notice">When disabled, checkout runs in demo mode and never charges a card. Enable only after entering matching Square application, location, and access credentials.</div>
|
||||
<div class="form-grid">
|
||||
<label>Environment<select name="square[environment]"><option value="sandbox" <?= selected($config->get('square.environment'), 'sandbox') ?>>Sandbox</option><option value="production" <?= selected($config->get('square.environment'), 'production') ?>>Production</option></select></label>
|
||||
<label>Application ID<input type="text" name="square[application_id]" value="<?= e($config->get('square.application_id')) ?>" autocomplete="off"></label>
|
||||
<label>Location ID<input type="text" name="square[location_id]" value="<?= e($config->get('square.location_id')) ?>" autocomplete="off"></label>
|
||||
<label>Access token<input type="password" name="square[access_token]" value="" placeholder="<?= $config->get('square.access_token') ? 'Saved credential - leave blank to keep' : 'Enter access token' ?>" autocomplete="new-password"></label>
|
||||
</div>
|
||||
<label class="check-control"><input type="checkbox" name="square[enabled]" value="1" <?= checked($config->get('square.enabled')) ?>><span><strong>Enable Square payments</strong><small>Live checkout will require a valid card token.</small></span></label>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Transactional email</span><h2>Mailgun</h2></div><span class="status-dot <?= $config->get('mailgun.enabled') ? 'is-on' : '' ?>"><?= $config->get('mailgun.enabled') ? 'Enabled' : 'Local log mode' ?></span></div>
|
||||
<div class="notice">When disabled, verification and newsletter messages are written to <code>storage/logs/mail.log</code> for local testing.</div>
|
||||
<div class="form-grid">
|
||||
<label>Mailgun domain<input type="text" name="mailgun[domain]" value="<?= e($config->get('mailgun.domain')) ?>"></label>
|
||||
<label>API key<input type="password" name="mailgun[api_key]" value="" placeholder="<?= $config->get('mailgun.api_key') ? 'Saved credential - leave blank to keep' : 'Enter API key' ?>" autocomplete="new-password"></label>
|
||||
<label>From name<input type="text" name="mailgun[from_name]" value="<?= e($config->get('mailgun.from_name')) ?>"></label>
|
||||
<label>From email<input type="email" name="mailgun[from_email]" value="<?= e($config->get('mailgun.from_email')) ?>"></label>
|
||||
</div>
|
||||
<label class="check-control"><input type="checkbox" name="mailgun[enabled]" value="1" <?= checked($config->get('mailgun.enabled')) ?>><span><strong>Enable Mailgun delivery</strong></span></label>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Optional database server</span><h2>MySQL / MariaDB Connection</h2></div></div>
|
||||
<div class="notice">These values are only used by the migration tool below. The application continues using SQLite unless a migration succeeds and you explicitly switch drivers.</div>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Host<input type="text" name="database[mysql_host]" value="<?= e($config->get('database.mysql_host')) ?>"></label>
|
||||
<label>Port<input type="number" min="1" max="65535" name="database[mysql_port]" value="<?= (int) $config->get('database.mysql_port') ?>"></label>
|
||||
<label>Database<input type="text" name="database[mysql_database]" value="<?= e($config->get('database.mysql_database')) ?>"></label>
|
||||
<label>Username<input type="text" name="database[mysql_username]" value="<?= e($config->get('database.mysql_username')) ?>" autocomplete="off"></label>
|
||||
<label>Password<input type="password" name="database[mysql_password]" value="" placeholder="<?= $config->get('database.mysql_password') ? 'Saved credential - leave blank to keep' : 'Enter database password' ?>" autocomplete="new-password"></label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="sticky-save"><button class="button button-large" type="submit">Save Store Settings</button></div>
|
||||
</form>
|
||||
|
||||
<section class="admin-panel settings-followup">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">State and city</span><h2>Tax Rates</h2></div></div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($taxRates as $tax): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="item-avatar">%</span>
|
||||
<span><strong><?= e($tax['name']) ?></strong><small><?= e(ucfirst((string) $tax['jurisdiction'])) ?> jurisdiction</small></span>
|
||||
<strong><?= number_format((int) $tax['rate_basis_points'] / 100, 2) ?>%</strong>
|
||||
<span class="status-dot <?= $tax['active'] ? 'is-on' : '' ?>"><?= $tax['active'] ? 'Active' : 'Disabled' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/tax/save" method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="id" value="<?= (int) $tax['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" value="<?= e($tax['name']) ?>" required></label>
|
||||
<label>Jurisdiction<select name="jurisdiction"><option value="state" <?= selected($tax['jurisdiction'], 'state') ?>>State</option><option value="city" <?= selected($tax['jurisdiction'], 'city') ?>>City</option><option value="county" <?= selected($tax['jurisdiction'], 'county') ?>>County</option></select></label>
|
||||
<label>Rate, percent<input type="number" min="0" step="0.01" name="rate" value="<?= number_format((int) $tax['rate_basis_points'] / 100, 2, '.', '') ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($tax['active']) ?>><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Tax</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Tax Rate</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/tax/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Jurisdiction<select name="jurisdiction"><option value="state">State</option><option value="city">City</option><option value="county">County</option></select></label>
|
||||
<label>Rate, percent<input type="number" min="0" step="0.01" name="rate" required></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Tax Rate</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="admin-panel settings-followup">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Optional database mode</span><h2>MySQL / MariaDB Migration</h2></div><span class="role-badge"><?= e(strtoupper((string) $config->get('database.driver', 'sqlite'))) ?> active</span></div>
|
||||
<p class="muted">SQLite remains the default and requires no database server. This tool copies every application table and row into MySQL or MariaDB; switching drivers is optional.</p>
|
||||
<form class="form-stack" action="/admin/migrate/mysql" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Host<input type="text" value="<?= e($config->get('database.mysql_host')) ?>" disabled></label>
|
||||
<label>Port<input type="text" value="<?= e($config->get('database.mysql_port')) ?>" disabled></label>
|
||||
<label>Database<input type="text" value="<?= e($config->get('database.mysql_database')) ?>" disabled></label>
|
||||
<label>Username<input type="text" value="<?= e($config->get('database.mysql_username')) ?>" disabled></label>
|
||||
</div>
|
||||
<p class="form-footnote">Connection values are saved in the Store Settings form above before running migration.</p>
|
||||
<label class="check-control"><input type="checkbox" name="switch_driver" value="1"><span><strong>Switch the application to MySQL after a successful copy</strong><small>Do this only when the MySQL service will remain available.</small></span></label>
|
||||
<button class="button button-outline" type="submit" data-confirm="Copy the SQLite database to the configured MySQL server?">Run Database Migration</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="admin-page-actions">
|
||||
<p>All active specials are automatically arranged on the homepage based on how many are running.</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Homepage promotion</span><h2>Specials</h2></div>
|
||||
<span class="count-badge"><?= count($specials) ?> configured</span>
|
||||
</div>
|
||||
<div class="special-admin-grid">
|
||||
<?php foreach ($specials as $special): ?>
|
||||
<details class="special-admin-card">
|
||||
<summary>
|
||||
<span class="special-badge"><?= e($special['badge']) ?></span>
|
||||
<h3><?= e($special['title']) ?></h3>
|
||||
<p><?= e($special['description']) ?></p>
|
||||
<div><strong><?= $special['price_cents'] !== null ? money($special['price_cents']) : 'No override' ?></strong><span class="status-dot <?= $special['active'] ? 'is-on' : '' ?>"><?= $special['active'] ? 'Active' : 'Hidden' ?></span></div>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/special/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $special['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Title<input type="text" name="title" required value="<?= e($special['title']) ?>"></label>
|
||||
<label>Badge<input type="text" name="badge" value="<?= e($special['badge']) ?>"></label>
|
||||
<label>Linked menu item
|
||||
<select name="menu_item_id">
|
||||
<option value="">No linked item</option>
|
||||
<?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>" <?= selected($special['menu_item_id'], $item['id']) ?>><?= e($item['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Promotional price<input type="number" name="price" min="0" step="0.01" value="<?= $special['price_cents'] !== null ? number_format((int) $special['price_cents'] / 100, 2, '.', '') : '' ?>"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"><?= e($special['description']) ?></textarea></label>
|
||||
<label>Starts at<input type="datetime-local" name="starts_at" value="<?= e(store_datetime($special['starts_at'], 'Y-m-d\TH:i')) ?>"></label>
|
||||
<label>Ends at<input type="datetime-local" name="ends_at" value="<?= e(store_datetime($special['ends_at'], 'Y-m-d\TH:i')) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $special['display_order'] ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($special['active']) ?>><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Special</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Homepage Special</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/special/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Title<input type="text" name="title" required></label>
|
||||
<label>Badge<input type="text" name="badge" value="Special"></label>
|
||||
<label>Linked menu item
|
||||
<select name="menu_item_id"><option value="">No linked item</option><?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>"><?= e($item['name']) ?></option><?php endforeach; ?></select>
|
||||
</label>
|
||||
<label>Promotional price<input type="number" name="price" min="0" step="0.01"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"></textarea></label>
|
||||
<label>Starts at<input type="datetime-local" name="starts_at"></label>
|
||||
<label>Ends at<input type="datetime-local" name="ends_at"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="0"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Special</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
@@ -0,0 +1,65 @@
|
||||
<div class="admin-page-actions">
|
||||
<form class="admin-filters" action="/admin/users" method="get">
|
||||
<input type="search" name="q" value="<?= e($query) ?>" placeholder="Search name, email, or phone">
|
||||
<button class="button button-small" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Access and roster</span><h2>Users & Staff</h2></div>
|
||||
<span class="count-badge"><?= count($users) ?> shown</span>
|
||||
</div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($users as $user): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="avatar"><?= e(strtoupper(substr((string) $user['full_name'], 0, 1))) ?></span>
|
||||
<span><strong><?= e($user['full_name']) ?></strong><small><?= e($user['email']) ?> · <?= e($user['phone']) ?></small></span>
|
||||
<span class="role-badge"><?= e(ucfirst((string) $user['role'])) ?></span>
|
||||
<span class="status-dot <?= $user['active'] ? 'is-on' : '' ?>"><?= $user['active'] ? 'Active' : 'Disabled' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/user/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $user['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Full name<input type="text" name="full_name" required value="<?= e($user['full_name']) ?>"></label>
|
||||
<label>Phone<input type="tel" name="phone" value="<?= e($user['phone']) ?>"></label>
|
||||
<label>Role
|
||||
<select name="role">
|
||||
<?php foreach (['customer', 'staff', 'manager', 'admin'] as $role): ?><option value="<?= $role ?>" <?= selected($user['role'], $role) ?>><?= e(ucfirst($role)) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($user['active']) ?>><span><strong>Active account</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="newsletter_opt_in" value="1" <?= checked($user['newsletter_opt_in']) ?>><span><strong>News opted in</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save User</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Staff Member</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/user/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Full name<input type="text" name="full_name" required></label>
|
||||
<label>Email<input type="email" name="email" required></label>
|
||||
<label>Phone<input type="tel" name="phone"></label>
|
||||
<label>Temporary password<input type="password" name="password" required minlength="10"></label>
|
||||
<label>Role
|
||||
<select name="role"><option value="staff">Staff</option><option value="manager">Manager</option><option value="admin">Administrator</option><option value="customer">Customer</option></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active account</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="newsletter_opt_in" value="1"><span><strong>News opted in</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create User</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<section class="auth-section section">
|
||||
<div class="container auth-grid">
|
||||
<div class="auth-intro">
|
||||
<span class="eyebrow">Welcome back</span>
|
||||
<h1>Your usual is waiting.</h1>
|
||||
<p>Sign in to check past orders, save your pickup information, and manage restaurant news.</p>
|
||||
<div class="auth-callout">
|
||||
<strong>Staff member?</strong>
|
||||
<span>Your staff account uses the same secure sign-in.</span>
|
||||
</div>
|
||||
</div>
|
||||
<form class="auth-card form-stack" action="/login" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div>
|
||||
<span class="eyebrow">Account access</span>
|
||||
<h2>Sign In</h2>
|
||||
</div>
|
||||
<label>Email address
|
||||
<input type="email" name="email" required autocomplete="email" value="<?= e($old['email'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<div class="honeypot" aria-hidden="true">
|
||||
<label>Website <input type="text" name="website" tabindex="-1" autocomplete="off"></label>
|
||||
</div>
|
||||
<label>Quick check: what is <?= (int) $captcha[0] ?> + <?= (int) $captcha[1] ?>?
|
||||
<input type="number" name="captcha" required inputmode="numeric" autocomplete="off">
|
||||
</label>
|
||||
<button class="button button-large button-block" type="submit">Continue Securely</button>
|
||||
<p class="form-footnote">We will email a six-digit verification code after your password is accepted.</p>
|
||||
<p class="auth-switch">New here? <a href="/register">Create an account</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<section class="auth-section section">
|
||||
<div class="container auth-grid auth-grid-wide">
|
||||
<div class="auth-intro">
|
||||
<span class="eyebrow">Join the table</span>
|
||||
<h1>Faster pickup starts here.</h1>
|
||||
<p>Create an account to keep your contact details, see order history, and hear about new specials.</p>
|
||||
<ul class="check-list">
|
||||
<li>Email verification protects your account</li>
|
||||
<li>Newsletter preferences stay in your control</li>
|
||||
<li>Your payment details are handled by Square, not stored here</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form class="auth-card form-stack" action="/register" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div>
|
||||
<span class="eyebrow">Customer account</span>
|
||||
<h2>Create Account</h2>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>Full name
|
||||
<input type="text" name="full_name" required autocomplete="name" value="<?= e($old['full_name'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Phone number
|
||||
<input type="tel" name="phone" required autocomplete="tel" value="<?= e($old['phone'] ?? '') ?>">
|
||||
</label>
|
||||
<label class="field-wide">Email address
|
||||
<input type="email" name="email" required autocomplete="email" value="<?= e($old['email'] ?? '') ?>">
|
||||
</label>
|
||||
<label class="field-wide">Street address
|
||||
<input type="text" name="street_address" required autocomplete="street-address" value="<?= e($old['street_address'] ?? '') ?>">
|
||||
</label>
|
||||
<label>City
|
||||
<input type="text" name="city" required autocomplete="address-level2" value="<?= e($old['city'] ?? 'Keyser') ?>">
|
||||
</label>
|
||||
<label>State
|
||||
<input type="text" name="state" required maxlength="2" autocomplete="address-level1" value="<?= e($old['state'] ?? 'WV') ?>">
|
||||
</label>
|
||||
<label>ZIP code
|
||||
<input type="text" name="postal_code" required autocomplete="postal-code" value="<?= e($old['postal_code'] ?? '26726') ?>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>Password
|
||||
<input type="password" name="password" minlength="10" required autocomplete="new-password">
|
||||
</label>
|
||||
<label>Confirm password
|
||||
<input type="password" name="password_confirmation" minlength="10" required autocomplete="new-password">
|
||||
</label>
|
||||
</div>
|
||||
<div class="honeypot" aria-hidden="true">
|
||||
<label>Website <input type="text" name="website" tabindex="-1" autocomplete="off"></label>
|
||||
</div>
|
||||
<label>Quick check: what is <?= (int) $captcha[0] ?> + <?= (int) $captcha[1] ?>?
|
||||
<input type="number" name="captcha" required inputmode="numeric" autocomplete="off">
|
||||
</label>
|
||||
<div class="notice">Creating an account opts you into occasional Fat Bottom Grille news. You can opt out from any email or My Account.</div>
|
||||
<button class="button button-large button-block" type="submit">Create My Account</button>
|
||||
<p class="auth-switch">Already have an account? <a href="/login">Sign in</a></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,19 @@
|
||||
<section class="auth-section section">
|
||||
<div class="container narrow-container">
|
||||
<form class="auth-card form-stack verify-card" action="/verify" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="verification-icon">6</div>
|
||||
<div>
|
||||
<span class="eyebrow">One more step</span>
|
||||
<h1>Check your email</h1>
|
||||
<p>Enter the six-digit code we sent you. It is valid for 10 minutes.</p>
|
||||
</div>
|
||||
<label>Verification code
|
||||
<input class="code-input" type="text" name="code" required inputmode="numeric" maxlength="6" pattern="[0-9]{6}" autocomplete="one-time-code" autofocus>
|
||||
</label>
|
||||
<button class="button button-large button-block" type="submit">Verify & Sign In</button>
|
||||
<a class="text-link text-center" href="/login">Start over</a>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use FatBottom\Core\View;
|
||||
|
||||
$businessName = (string) $config->get('business.name', 'Fat Bottom Grille');
|
||||
$isAdmin = isset($adminPage);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#09131d">
|
||||
<meta name="description" content="Order burgers, burritos, dinner plates, sides, and drinks online from Fat Bottom Grille in Keyser, West Virginia.">
|
||||
<title><?= e($title ?? $businessName) ?> | <?= e($businessName) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<?php if (!empty($squareEnabled) && !empty($squareAppId) && !empty($squareLocationId)): ?>
|
||||
<script src="<?= $squareEnvironment === 'production'
|
||||
? 'https://web.squarecdn.com/v1/square.js'
|
||||
: 'https://sandbox.web.squarecdn.com/v1/square.js' ?>"></script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body class="<?= $isAdmin ? 'admin-body' : 'store-body' ?>">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="admin-shell">
|
||||
<?php View::partial('partials/admin-nav', compact('adminPage', 'config', 'currentUser')); ?>
|
||||
<div class="admin-content">
|
||||
<header class="admin-topbar">
|
||||
<div>
|
||||
<span class="eyebrow">Staff workspace</span>
|
||||
<h1><?= e($title ?? 'Dashboard') ?></h1>
|
||||
</div>
|
||||
<div class="admin-top-actions">
|
||||
<a class="button button-ghost button-small" href="/" target="_blank" rel="noopener">View Store</a>
|
||||
<form action="/logout" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<button class="button button-small" type="submit">Sign Out</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
<?php View::partial('partials/flash', compact('flashMessages')); ?>
|
||||
<main id="main" class="admin-main">
|
||||
<?php require $templatePath; ?>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="announcement">
|
||||
<span><?= e($config->get('business.announcement')) ?></span>
|
||||
<span class="announcement-detail"><?= e($config->get('business.hours')) ?></span>
|
||||
</div>
|
||||
<header class="site-header">
|
||||
<div class="container nav-wrap">
|
||||
<a class="brand" href="/" aria-label="<?= e($businessName) ?> home">
|
||||
<span class="brand-mark">FB</span>
|
||||
<span class="brand-copy">
|
||||
<strong><?= e($businessName) ?></strong>
|
||||
<small>Keyser, West Virginia</small>
|
||||
</span>
|
||||
</a>
|
||||
<button class="nav-toggle" type="button" aria-expanded="false" aria-controls="site-nav">
|
||||
<span></span><span></span><span></span>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
|
||||
<a class="<?= active_path('/') ?>" href="/">Home</a>
|
||||
<a class="<?= active_path('/menu') ?>" href="/menu">Order Online</a>
|
||||
<?php if ($currentUser): ?>
|
||||
<?php if (in_array($currentUser['role'], ['admin', 'manager', 'staff'], true)): ?>
|
||||
<a href="/admin">Dashboard</a>
|
||||
<?php endif; ?>
|
||||
<a class="<?= active_path('/account') ?>" href="/account">My Account</a>
|
||||
<?php else: ?>
|
||||
<a class="<?= active_path('/login') ?>" href="/login">Sign In</a>
|
||||
<?php endif; ?>
|
||||
<a class="cart-link" href="/cart">
|
||||
Your Order
|
||||
<span class="cart-count"><?= (int) ($currentCart['item_count'] ?? 0) ?></span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<?php View::partial('partials/flash', compact('flashMessages')); ?>
|
||||
<main id="main">
|
||||
<?php require $templatePath; ?>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-grid">
|
||||
<div>
|
||||
<div class="footer-brand"><?= e($businessName) ?></div>
|
||||
<p>Good food, generous portions, and a seat at the table in Keyser.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Visit</h2>
|
||||
<address>
|
||||
<?= e($config->get('business.street')) ?><br>
|
||||
<?= e($config->get('business.city')) ?>, <?= e($config->get('business.state')) ?>
|
||||
<?= e($config->get('business.postal_code')) ?>
|
||||
</address>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Get In Touch</h2>
|
||||
<?php if ($config->get('business.phone')): ?>
|
||||
<a href="tel:<?= e(preg_replace('/[^0-9+]/', '', (string) $config->get('business.phone'))) ?>">
|
||||
<?= e($config->get('business.phone')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($config->get('business.email')): ?>
|
||||
<a href="mailto:<?= e($config->get('business.email')) ?>"><?= e($config->get('business.email')) ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$config->get('business.phone') && !$config->get('business.email')): ?>
|
||||
<p>Contact details coming soon.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Hours</h2>
|
||||
<p><?= e($config->get('business.hours')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container footer-bottom">
|
||||
<span>© <?= date('Y') ?> <?= e($businessName) ?></span>
|
||||
<span>410 W Piedmont St, Keyser, WV</span>
|
||||
</div>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<script src="/assets/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<aside class="admin-sidebar">
|
||||
<a class="admin-brand" href="/admin">
|
||||
<span class="brand-mark">FB</span>
|
||||
<span>
|
||||
<strong><?= e($config->get('business.name')) ?></strong>
|
||||
<small>Operations</small>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="admin-nav" aria-label="Dashboard navigation">
|
||||
<a class="<?= $adminPage === 'dashboard' ? 'is-active' : '' ?>" href="/admin">Overview</a>
|
||||
<a class="<?= $adminPage === 'orders' ? 'is-active' : '' ?>" href="/admin/orders">Orders</a>
|
||||
<a class="<?= $adminPage === 'menu' ? 'is-active' : '' ?>" href="/admin/menu">Menu</a>
|
||||
<a class="<?= $adminPage === 'specials' ? 'is-active' : '' ?>" href="/admin/specials">Specials</a>
|
||||
<a class="<?= $adminPage === 'newsletters' ? 'is-active' : '' ?>" href="/admin/newsletters">Newsletters</a>
|
||||
<?php if (($currentUser['role'] ?? '') === 'admin'): ?>
|
||||
<a class="<?= $adminPage === 'users' ? 'is-active' : '' ?>" href="/admin/users">Users & Staff</a>
|
||||
<a class="<?= $adminPage === 'settings' ? 'is-active' : '' ?>" href="/admin/settings">Settings</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<div class="admin-user">
|
||||
<span class="avatar"><?= e(strtoupper(substr((string) ($currentUser['full_name'] ?? 'S'), 0, 1))) ?></span>
|
||||
<span>
|
||||
<strong><?= e($currentUser['full_name'] ?? '') ?></strong>
|
||||
<small><?= e(ucfirst((string) ($currentUser['role'] ?? 'staff'))) ?></small>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php if (!empty($flashMessages)): ?>
|
||||
<div class="<?= isset($adminPage) ? 'admin-flash-wrap' : 'container flash-wrap' ?>" aria-live="polite">
|
||||
<?php foreach ($flashMessages as $message): ?>
|
||||
<div class="flash flash-<?= e($message['type']) ?>">
|
||||
<span><?= e($message['message']) ?></span>
|
||||
<button type="button" class="flash-close" aria-label="Dismiss message">×</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
$tags = json_decode((string) ($item['dietary_tags'] ?? '[]'), true);
|
||||
$tags = is_array($tags) ? $tags : [];
|
||||
?>
|
||||
<article class="menu-card">
|
||||
<div class="menu-card-art <?= !empty($item['image_url']) ? 'has-image' : '' ?>"
|
||||
<?php if (!empty($item['image_url'])): ?>style="background-image:url('<?= e($item['image_url']) ?>')"<?php endif; ?>>
|
||||
<?php if (empty($item['image_url'])): ?>
|
||||
<span><?= e(strtoupper(substr((string) $item['name'], 0, 2))) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($item['featured'])): ?><span class="card-badge">House Pick</span><?php endif; ?>
|
||||
</div>
|
||||
<div class="menu-card-body">
|
||||
<div class="menu-card-heading">
|
||||
<h3><?= e($item['name']) ?></h3>
|
||||
<strong><?= money($item['price_cents']) ?></strong>
|
||||
</div>
|
||||
<p><?= e($item['description']) ?></p>
|
||||
<?php if ($tags): ?>
|
||||
<div class="tag-list">
|
||||
<?php foreach ($tags as $tag): ?><span><?= e($tag) ?></span><?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form class="quick-add" action="/cart/add" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="menu_item_id" value="<?= (int) $item['id'] ?>">
|
||||
<input type="hidden" name="return_to" value="<?= e($_SERVER['REQUEST_URI'] ?? '/menu') ?>">
|
||||
<label>
|
||||
<span class="sr-only">Quantity</span>
|
||||
<select name="quantity" aria-label="Quantity for <?= e($item['name']) ?>">
|
||||
<?php for ($quantity = 1; $quantity <= 8; $quantity++): ?>
|
||||
<option value="<?= $quantity ?>"><?= $quantity ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</label>
|
||||
<button class="button button-small" type="submit">Add to Order</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<section class="page-hero page-hero-compact">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Almost yours</span>
|
||||
<h1>Your Order</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container checkout-layout section">
|
||||
<section class="checkout-main">
|
||||
<?php if (empty($cart['items'])): ?>
|
||||
<div class="empty-state">
|
||||
<h2>Your order is wide open.</h2>
|
||||
<p>Start with a burger, a burrito, or whatever sounds good right now.</p>
|
||||
<a class="button" href="/menu">Browse the Menu</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form action="/cart/update" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="cart-list">
|
||||
<?php foreach ($cart['items'] as $item): ?>
|
||||
<article class="cart-item">
|
||||
<div class="cart-item-mark"><?= e(strtoupper(substr((string) $item['name'], 0, 2))) ?></div>
|
||||
<div class="cart-item-copy">
|
||||
<h2><?= e($item['name']) ?></h2>
|
||||
<?php if ($item['notes']): ?><p>Note: <?= e($item['notes']) ?></p><?php endif; ?>
|
||||
<span><?= money($item['unit_price_cents']) ?> each</span>
|
||||
</div>
|
||||
<label class="quantity-field">
|
||||
<span>Qty</span>
|
||||
<input type="number" min="0" max="20" name="quantity[<?= (int) $item['id'] ?>]" value="<?= (int) $item['quantity'] ?>">
|
||||
</label>
|
||||
<strong class="cart-line-total"><?= money((int) $item['quantity'] * (int) $item['unit_price_cents']) ?></strong>
|
||||
<button class="icon-button remove-cart-item" type="submit" formaction="/cart/remove" name="cart_item_id" value="<?= (int) $item['id'] ?>" aria-label="Remove <?= e($item['name']) ?>">×</button>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="cart-actions">
|
||||
<a class="text-link" href="/menu">Add More Items</a>
|
||||
<button class="button button-outline" type="submit">Update Order</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php if (!empty($cart['items'])): ?>
|
||||
<aside class="order-summary">
|
||||
<span class="eyebrow">Pickup summary</span>
|
||||
<h2><?= (int) $cart['item_count'] ?> <?= (int) $cart['item_count'] === 1 ? 'item' : 'items' ?></h2>
|
||||
<dl>
|
||||
<div><dt>Subtotal</dt><dd><?= money($totals['subtotal_cents']) ?></dd></div>
|
||||
<div><dt>Estimated tax</dt><dd><?= money($totals['tax_cents']) ?></dd></div>
|
||||
<div class="summary-total"><dt>Total</dt><dd><?= money($totals['total_cents']) ?></dd></div>
|
||||
</dl>
|
||||
<a class="button button-block button-large" href="/checkout">Continue to Checkout</a>
|
||||
<p class="summary-note">Pickup at 410 W Piedmont St, Keyser.</p>
|
||||
</aside>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<section class="page-hero page-hero-compact">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Secure checkout</span>
|
||||
<h1>Finish Your Order</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container checkout-layout section">
|
||||
<section class="checkout-main">
|
||||
<form id="checkout-form" class="panel form-stack"
|
||||
action="/checkout" method="post"
|
||||
data-square-enabled="<?= $squareEnabled ? 'true' : 'false' ?>"
|
||||
data-square-app-id="<?= e($squareAppId) ?>"
|
||||
data-square-location-id="<?= e($squareLocationId) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" id="square-token" name="square_token" value="">
|
||||
<div class="form-section">
|
||||
<div class="form-section-heading">
|
||||
<span>1</span>
|
||||
<div>
|
||||
<h2>Who is picking up?</h2>
|
||||
<p>We will use this to confirm your order.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>Full name
|
||||
<input type="text" name="customer_name" required autocomplete="name"
|
||||
value="<?= e($old['customer_name'] ?? $currentUser['full_name'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Email address
|
||||
<input type="email" name="customer_email" required autocomplete="email"
|
||||
value="<?= e($old['customer_email'] ?? $currentUser['email'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Phone number
|
||||
<input type="tel" name="customer_phone" required autocomplete="tel"
|
||||
value="<?= e($old['customer_phone'] ?? $currentUser['phone'] ?? '') ?>">
|
||||
</label>
|
||||
<label>Preferred pickup time
|
||||
<input type="time" name="pickup_time" value="<?= e($old['pickup_time'] ?? '') ?>">
|
||||
</label>
|
||||
</div>
|
||||
<label>Special instructions
|
||||
<textarea name="special_instructions" rows="3" placeholder="Sauce on the side, allergy note, pickup details..."><?= e($old['special_instructions'] ?? '') ?></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<div class="form-section-heading">
|
||||
<span>2</span>
|
||||
<div>
|
||||
<h2>Payment</h2>
|
||||
<p><?= $squareEnabled ? 'Securely processed by Square.' : 'Demo payment mode is active until Square is enabled in Settings.' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($squareEnabled): ?>
|
||||
<?php if ($squareAppId && $squareLocationId): ?>
|
||||
<div id="card-container" class="square-card"></div>
|
||||
<div id="card-errors" class="field-error" role="alert"></div>
|
||||
<?php else: ?>
|
||||
<div class="notice notice-error">Square is enabled, but the application or location ID is missing. Ask an administrator to finish the payment settings.</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="demo-payment">
|
||||
<span class="demo-payment-mark">D</span>
|
||||
<div>
|
||||
<strong>Demo payment</strong>
|
||||
<p>No card will be charged. Orders are recorded as paid for local testing.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button id="checkout-submit" class="button button-large button-block" type="submit"
|
||||
<?= $squareEnabled && (!$squareAppId || !$squareLocationId) ? 'disabled' : '' ?>>
|
||||
Pay <?= money($totals['total_cents']) ?> & Place Order
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<aside class="order-summary">
|
||||
<span class="eyebrow">Your order</span>
|
||||
<div class="mini-order-list">
|
||||
<?php foreach ($cart['items'] as $item): ?>
|
||||
<div>
|
||||
<span><b><?= (int) $item['quantity'] ?>x</b> <?= e($item['name']) ?></span>
|
||||
<strong><?= money((int) $item['quantity'] * (int) $item['unit_price_cents']) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>Subtotal</dt><dd><?= money($totals['subtotal_cents']) ?></dd></div>
|
||||
<?php foreach ($taxRates as $tax): ?>
|
||||
<div><dt><?= e($tax['name']) ?> (<?= number_format((int) $tax['rate_basis_points'] / 100, 2) ?>%)</dt><dd>Included below</dd></div>
|
||||
<?php endforeach; ?>
|
||||
<div><dt>Tax</dt><dd><?= money($totals['tax_cents']) ?></dd></div>
|
||||
<div class="summary-total"><dt>Total</dt><dd><?= money($totals['total_cents']) ?></dd></div>
|
||||
</dl>
|
||||
<p class="summary-note">Estimated ready time: <?= (int) $config->get('ordering.pickup_eta_minutes', 25) ?> minutes.</p>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<section class="hero">
|
||||
<div class="hero-texture"></div>
|
||||
<div class="container hero-grid">
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">Keyser made. Come hungry.</span>
|
||||
<h1>Big flavor.<br><em>No shortcuts.</em></h1>
|
||||
<p>Stacked burgers, loaded burritos, and dinner plates that eat like Sunday supper. Order ahead and we will have it hot.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-large" href="/menu">Start Your Order</a>
|
||||
<?php if ($config->get('business.phone')): ?>
|
||||
<a class="text-link" href="tel:<?= e(preg_replace('/[^0-9+]/', '', (string) $config->get('business.phone'))) ?>">
|
||||
Call <?= e($config->get('business.phone')) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span>Pickup in about <?= (int) $config->get('ordering.pickup_eta_minutes', 25) ?> minutes</span>
|
||||
<span><?= (bool) $config->get('ordering.accepting_orders', true) ? 'Ordering now open' : 'Ordering currently paused' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-plate" aria-hidden="true">
|
||||
<div class="plate-ring">
|
||||
<div class="burger">
|
||||
<span class="bun bun-top"></span>
|
||||
<span class="lettuce"></span>
|
||||
<span class="cheese"></span>
|
||||
<span class="patty"></span>
|
||||
<span class="onion"></span>
|
||||
<span class="patty patty-two"></span>
|
||||
<span class="bun bun-bottom"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-stamp">
|
||||
<strong>410</strong>
|
||||
<span>W Piedmont</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($specials): ?>
|
||||
<section class="specials-section section">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Happening now</span>
|
||||
<h2>Today’s Specials</h2>
|
||||
</div>
|
||||
<a class="text-link" href="/menu">See the full menu</a>
|
||||
</div>
|
||||
<div class="special-grid special-count-<?= min(count($specials), 4) ?>">
|
||||
<?php foreach ($specials as $special): ?>
|
||||
<article class="special-card">
|
||||
<span class="special-badge"><?= e($special['badge']) ?></span>
|
||||
<h3><?= e($special['title']) ?></h3>
|
||||
<p><?= e($special['description']) ?></p>
|
||||
<div class="special-footer">
|
||||
<?php if ($special['price_cents'] !== null): ?>
|
||||
<strong><?= money($special['price_cents']) ?></strong>
|
||||
<?php endif; ?>
|
||||
<?php if ($special['menu_item_id']): ?>
|
||||
<form action="/cart/add" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="menu_item_id" value="<?= (int) $special['menu_item_id'] ?>">
|
||||
<input type="hidden" name="quantity" value="1">
|
||||
<input type="hidden" name="return_to" value="/">
|
||||
<button class="button button-small button-light" type="submit">Add Item</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="section menu-preview">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Built to satisfy</span>
|
||||
<h2>Local Favorites</h2>
|
||||
</div>
|
||||
<p>These are the orders that keep showing up at the counter.</p>
|
||||
</div>
|
||||
<div class="menu-grid">
|
||||
<?php foreach ($featured as $item): ?>
|
||||
<?php \FatBottom\Core\View::partial('partials/menu-card', compact('item')); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="category-band">
|
||||
<div class="container">
|
||||
<span class="eyebrow">Pick your direction</span>
|
||||
<div class="category-links">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<a href="/menu#<?= e($category['slug']) ?>">
|
||||
<strong><?= e($category['name']) ?></strong>
|
||||
<span><?= count($category['items']) ?> picks</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="story-section section">
|
||||
<div class="container story-grid">
|
||||
<div class="story-art" aria-hidden="true">
|
||||
<span class="story-number">26726</span>
|
||||
<div class="mountain-lines"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="eyebrow">Proudly from Keyser</span>
|
||||
<h2>Food that shows up hungry.</h2>
|
||||
<p>Fat Bottom Grille is the kind of place where the portions are honest and nobody leaves wondering what is for dinner. Find us at 410 W Piedmont Street, right here in Keyser.</p>
|
||||
<a class="button button-outline" href="/menu">Order for Pickup</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,52 @@
|
||||
<section class="page-hero">
|
||||
<div class="container page-hero-inner">
|
||||
<div>
|
||||
<span class="eyebrow">Hot, fresh, yours</span>
|
||||
<h1>Order Online</h1>
|
||||
<p>Choose your favorites and we will get the kitchen moving.</p>
|
||||
</div>
|
||||
<form class="menu-search" action="/menu" method="get">
|
||||
<label for="menu-query">Search the menu</label>
|
||||
<div class="search-row">
|
||||
<input id="menu-query" type="search" name="q" value="<?= e($query) ?>" placeholder="Burger, chicken, fries...">
|
||||
<button class="button" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="category-jump">
|
||||
<div class="container">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<a href="#<?= e($category['slug']) ?>"><?= e($category['name']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container menu-page section">
|
||||
<?php if (!$categories): ?>
|
||||
<div class="empty-state">
|
||||
<span class="eyebrow">No match yet</span>
|
||||
<h2>We could not find “<?= e($query) ?>”</h2>
|
||||
<p>Try another search or browse the full menu.</p>
|
||||
<a class="button" href="/menu">Clear Search</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<section class="menu-category" id="<?= e($category['slug']) ?>">
|
||||
<div class="menu-category-heading">
|
||||
<div>
|
||||
<span class="category-index"><?= str_pad((string) $category['display_order'], 2, '0', STR_PAD_LEFT) ?></span>
|
||||
<h2><?= e($category['name']) ?></h2>
|
||||
</div>
|
||||
<p><?= e($category['description']) ?></p>
|
||||
</div>
|
||||
<div class="menu-grid">
|
||||
<?php foreach ($category['items'] as $item): ?>
|
||||
<?php \FatBottom\Core\View::partial('partials/menu-card', compact('item')); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<section class="confirmation-section section">
|
||||
<div class="container confirmation-card">
|
||||
<div class="confirmation-mark">✓</div>
|
||||
<span class="eyebrow">Paid and sent to the kitchen</span>
|
||||
<h1>We’ve got it, <?= e(explode(' ', (string) $order['customer_name'])[0]) ?>.</h1>
|
||||
<p>Your order number is <strong><?= e($order['order_number']) ?></strong>. We sent a confirmation to <?= e($order['customer_email']) ?>.</p>
|
||||
<div class="confirmation-details">
|
||||
<div><span>Pickup</span><strong><?= e($order['pickup_time'] ?: 'About ' . $config->get('ordering.pickup_eta_minutes', 25) . ' minutes') ?></strong></div>
|
||||
<div><span>Location</span><strong>410 W Piedmont St</strong></div>
|
||||
<div><span>Total paid</span><strong><?= money($order['total_cents']) ?></strong></div>
|
||||
</div>
|
||||
<div class="confirmation-items">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<div><span><?= (int) $item['quantity'] ?>x <?= e($item['item_name']) ?></span><strong><?= money($item['line_total_cents']) ?></strong></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="confirmation-actions">
|
||||
<a class="button" href="/menu">Order Something Else</a>
|
||||
<?php if ($currentUser): ?><a class="button button-outline" href="/account">View My Orders</a><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user