- 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:
Ty Clifford
2026-06-10 13:27:42 -04:00
parent d46b0be488
commit 16235369cb
49 changed files with 7685 additions and 2 deletions
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace FatBottom\Services;
final class MenuPdfExporter
{
public function output(string $businessName, array $categories): never
{
$pages = [];
$lines = [];
foreach ($categories as $category) {
$lines[] = ['type' => 'category', 'text' => strtoupper((string) $category['name'])];
foreach ($category['items'] as $item) {
$lines[] = [
'type' => 'item',
'text' => sprintf('%s $%.2f', $item['name'], $item['price_cents'] / 100),
];
$lines[] = ['type' => 'description', 'text' => (string) $item['description']];
}
}
foreach (array_chunk($lines, 24) as $chunk) {
$pages[] = $this->pageStream($businessName, $chunk);
}
if ($pages === []) {
$pages[] = $this->pageStream($businessName, []);
}
$pdf = $this->buildPdf($pages);
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="fat-bottom-grille-menu.pdf"');
header('Content-Length: ' . strlen($pdf));
echo $pdf;
exit;
}
private function pageStream(string $businessName, array $lines): string
{
$escape = static function (string $value): string {
$ascii = function_exists('iconv')
? (iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value) ?: $value)
: preg_replace('/[^\x20-\x7E]/', '', $value);
return str_replace(
['\\', '(', ')', "\r", "\n"],
['\\\\', '\\(', '\\)', ' ', ' '],
(string) $ascii
);
};
$stream = "0.035 0.071 0.102 rg\n0 0 612 792 re f\n";
$stream .= "0.78 0.58 0.20 rg\nBT /F1 25 Tf 46 738 Td (" . $escape($businessName) . ") Tj ET\n";
$stream .= "0.91 0.92 0.90 rg\nBT /F1 10 Tf 46 718 Td (KEYSER, WEST VIRGINIA | ONLINE MENU) Tj ET\n";
$stream .= "0.78 0.58 0.20 RG\n46 704 m 566 704 l S\n";
$y = 678;
foreach ($lines as $line) {
$text = $escape(substr((string) $line['text'], 0, 92));
if ($line['type'] === 'category') {
$stream .= "0.78 0.58 0.20 rg\nBT /F1 15 Tf 46 {$y} Td ({$text}) Tj ET\n";
$y -= 24;
} elseif ($line['type'] === 'item') {
$stream .= "0.96 0.95 0.89 rg\nBT /F1 12 Tf 54 {$y} Td ({$text}) Tj ET\n";
$y -= 17;
} else {
$stream .= "0.68 0.72 0.72 rg\nBT /F1 8 Tf 54 {$y} Td ({$text}) Tj ET\n";
$y -= 23;
}
}
$stream .= "0.55 0.60 0.62 rg\nBT /F1 8 Tf 46 35 Td (Prices and availability are subject to change.) Tj ET\n";
return $stream;
}
private function buildPdf(array $streams): string
{
$objects = [];
$objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';
$pageIds = [];
$nextId = 4;
foreach ($streams as $index => $stream) {
$pageId = $nextId++;
$contentId = $nextId++;
$pageIds[] = $pageId . ' 0 R';
$objects[$pageId] = sprintf(
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 3 0 R >> >> /Contents %d 0 R >>',
$contentId
);
$objects[$contentId] = "<< /Length " . strlen($stream) . " >>\nstream\n{$stream}endstream";
}
$objects[2] = '<< /Type /Pages /Kids [' . implode(' ', $pageIds) . '] /Count ' . count($pageIds) . ' >>';
$objects[3] = '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>';
ksort($objects);
$pdf = "%PDF-1.4\n";
$offsets = [0];
foreach ($objects as $id => $object) {
$offsets[$id] = strlen($pdf);
$pdf .= "{$id} 0 obj\n{$object}\nendobj\n";
}
$xref = strlen($pdf);
$count = max(array_keys($objects)) + 1;
$pdf .= "xref\n0 {$count}\n0000000000 65535 f \n";
for ($id = 1; $id < $count; $id++) {
$pdf .= sprintf("%010d 00000 n \n", $offsets[$id] ?? 0);
}
$pdf .= "trailer\n<< /Size {$count} /Root 1 0 R >>\nstartxref\n{$xref}\n%%EOF";
return $pdf;
}
}