- 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,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