- 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:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
(() => {
|
||||
const navToggle = document.querySelector(".nav-toggle");
|
||||
const nav = document.querySelector(".site-nav");
|
||||
if (navToggle && nav) {
|
||||
navToggle.addEventListener("click", () => {
|
||||
const open = nav.classList.toggle("is-open");
|
||||
navToggle.setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".flash-close").forEach((button) => {
|
||||
button.addEventListener("click", () => button.closest(".flash")?.remove());
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.querySelectorAll(".flash").forEach((flash) => {
|
||||
flash.style.opacity = "0";
|
||||
window.setTimeout(() => flash.remove(), 220);
|
||||
});
|
||||
}, 7000);
|
||||
|
||||
document.querySelectorAll("[data-confirm]").forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
if (!window.confirm(element.dataset.confirm || "Continue?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const checkoutForm = document.querySelector("#checkout-form");
|
||||
if (!checkoutForm || checkoutForm.dataset.squareEnabled !== "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = checkoutForm.dataset.squareAppId;
|
||||
const locationId = checkoutForm.dataset.squareLocationId;
|
||||
const submit = document.querySelector("#checkout-submit");
|
||||
const errors = document.querySelector("#card-errors");
|
||||
let card;
|
||||
|
||||
const initializeSquare = async () => {
|
||||
if (!window.Square || !appId || !locationId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payments = window.Square.payments(appId, locationId);
|
||||
card = await payments.card();
|
||||
await card.attach("#card-container");
|
||||
} catch (error) {
|
||||
if (errors) {
|
||||
errors.textContent = "Payment form could not be loaded. Please refresh and try again.";
|
||||
}
|
||||
if (submit) {
|
||||
submit.disabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkoutForm.addEventListener("submit", async (event) => {
|
||||
if (!card || checkoutForm.dataset.tokenized === "true") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
submit.disabled = true;
|
||||
submit.textContent = "Securing Payment...";
|
||||
errors.textContent = "";
|
||||
|
||||
try {
|
||||
const result = await card.tokenize();
|
||||
if (result.status !== "OK") {
|
||||
throw new Error(result.errors?.[0]?.message || "Card details could not be verified.");
|
||||
}
|
||||
document.querySelector("#square-token").value = result.token;
|
||||
checkoutForm.dataset.tokenized = "true";
|
||||
checkoutForm.submit();
|
||||
} catch (error) {
|
||||
errors.textContent = error.message || "Payment could not be processed.";
|
||||
submit.disabled = false;
|
||||
submit.textContent = "Try Payment Again";
|
||||
}
|
||||
});
|
||||
|
||||
initializeSquare();
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use FatBottom\Controllers\AdminController;
|
||||
use FatBottom\Controllers\AuthController;
|
||||
use FatBottom\Controllers\StoreController;
|
||||
use FatBottom\Core\Auth;
|
||||
use FatBottom\Core\Http;
|
||||
use FatBottom\Core\Router;
|
||||
use FatBottom\Services\CartService;
|
||||
use FatBottom\Services\Mailer;
|
||||
use FatBottom\Services\OrderService;
|
||||
use FatBottom\Services\PaymentGateway;
|
||||
use FatBottom\Services\StatsService;
|
||||
use FatBottom\Services\TwoFactorService;
|
||||
|
||||
$container = require dirname(__DIR__) . '/app/bootstrap.php';
|
||||
$config = $container['config'];
|
||||
$db = $container['db'];
|
||||
|
||||
$auth = new Auth($db);
|
||||
$cart = new CartService($db);
|
||||
$mailer = new Mailer($config);
|
||||
$payments = new PaymentGateway($config);
|
||||
$orders = new OrderService($db, $payments);
|
||||
$stats = new StatsService($db);
|
||||
$twoFactor = new TwoFactorService($db, $mailer, $config);
|
||||
|
||||
$store = new StoreController($config, $auth, $cart, $db, $orders, $mailer);
|
||||
$authentication = new AuthController($config, $auth, $cart, $db, $twoFactor);
|
||||
$admin = new AdminController($config, $auth, $cart, $db, $stats, $payments, $mailer);
|
||||
|
||||
$path = Http::path();
|
||||
$user = $auth->user();
|
||||
$stats->record($path, $user ? (int) $user['id'] : null);
|
||||
|
||||
$router = new Router();
|
||||
$router->get('/', [$store, 'home']);
|
||||
$router->get('/menu', [$store, 'menu']);
|
||||
$router->post('/cart/add', [$store, 'addToCart']);
|
||||
$router->get('/cart', [$store, 'cart']);
|
||||
$router->post('/cart/update', [$store, 'updateCart']);
|
||||
$router->post('/cart/remove', [$store, 'removeFromCart']);
|
||||
$router->get('/checkout', [$store, 'checkout']);
|
||||
$router->post('/checkout', [$store, 'placeOrder']);
|
||||
$router->get('/order/complete', [$store, 'orderComplete']);
|
||||
|
||||
$router->get('/login', [$authentication, 'loginForm']);
|
||||
$router->post('/login', [$authentication, 'login']);
|
||||
$router->get('/register', [$authentication, 'registerForm']);
|
||||
$router->post('/register', [$authentication, 'register']);
|
||||
$router->get('/verify', [$authentication, 'verifyForm']);
|
||||
$router->post('/verify', [$authentication, 'verify']);
|
||||
$router->post('/logout', [$authentication, 'logout']);
|
||||
$router->get('/account', [$authentication, 'account']);
|
||||
$router->post('/account', [$authentication, 'updateAccount']);
|
||||
$router->get('/unsubscribe', [$authentication, 'unsubscribe']);
|
||||
|
||||
$router->get('/admin', [$admin, 'dashboard']);
|
||||
$router->get('/admin/orders', [$admin, 'orders']);
|
||||
$router->get('/admin/order', [$admin, 'orderDetail']);
|
||||
$router->post('/admin/order', [$admin, 'updateOrder']);
|
||||
$router->post('/admin/order/refund', [$admin, 'refundOrder']);
|
||||
$router->get('/admin/menu', [$admin, 'menu']);
|
||||
$router->post('/admin/category/save', [$admin, 'saveCategory']);
|
||||
$router->post('/admin/item/save', [$admin, 'saveItem']);
|
||||
$router->get('/admin/specials', [$admin, 'specials']);
|
||||
$router->post('/admin/special/save', [$admin, 'saveSpecial']);
|
||||
$router->get('/admin/users', [$admin, 'users']);
|
||||
$router->post('/admin/user/save', [$admin, 'saveUser']);
|
||||
$router->get('/admin/settings', [$admin, 'settings']);
|
||||
$router->post('/admin/settings', [$admin, 'saveSettings']);
|
||||
$router->post('/admin/tax/save', [$admin, 'saveTax']);
|
||||
$router->get('/admin/newsletters', [$admin, 'newsletters']);
|
||||
$router->post('/admin/newsletters/send', [$admin, 'sendNewsletter']);
|
||||
$router->get('/admin/export/menu.pdf', [$admin, 'exportMenuPdf']);
|
||||
$router->get('/admin/export/orders.csv', [$admin, 'exportOrdersCsv']);
|
||||
$router->post('/admin/migrate/mysql', [$admin, 'migrateMySql']);
|
||||
|
||||
$router->dispatch($_SERVER['REQUEST_METHOD'] ?? 'GET', $path);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||||
$file = __DIR__ . $path;
|
||||
if ($path !== '/' && is_file($file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
require __DIR__ . '/index.php';
|
||||
|
||||
Reference in New Issue
Block a user