diff --git a/README.md b/README.md index ac54967..9342f8c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Order +# Lakeside Orders -_Fat Bottom Grille declined to review the website or proposal. Open sourced for all. The menu is not real. Orders do not work._ +_Demo restaurant website. The location and menu are fictional._ -A dependency-light online food ordering system for Fat Bottom Grille at -410 W Piedmont St in Keyser, West Virginia. +A dependency-light online ordering and customer-management demo for Lakeside +Orders, a fictional restaurant near the water. ## Included @@ -22,6 +22,7 @@ A dependency-light online food ordering system for Fat Bottom Grille at - Automatic newsletter unsubscribe links - SQLite by default with an optional MySQL/MariaDB copy-and-switch tool - JSON-backed business, ordering, Square, Mailgun, and database settings +- Public-domain and Creative Commons storefront photography with attribution ## Requirements @@ -72,7 +73,7 @@ and assets automatically include that subdirectory. ## Initial Administrator -- Email: `admin@fatbottomgrille.com` +- Email: `admin@lakesideorders.test` - Password: `ChangeMe123!` Email 2FA is disabled by default for every account. It can be enabled from diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index 8daeb0a..f368618 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -567,6 +567,21 @@ final class AdminController extends BaseController 'business.postal_code', 'business.hours', 'business.announcement', + 'branding.location_label', + 'branding.meta_description', + 'branding.hero_eyebrow', + 'branding.hero_heading', + 'branding.hero_emphasis', + 'branding.hero_intro', + 'branding.hero_image', + 'branding.gallery_picnic_image', + 'branding.gallery_patio_image', + 'branding.gallery_service_image', + 'branding.story_image', + 'branding.story_eyebrow', + 'branding.story_heading', + 'branding.story_text', + 'branding.footer_tagline', 'square.environment', 'square.application_id', 'square.location_id', @@ -687,7 +702,9 @@ final class AdminController extends BaseController . '/unsubscribe?user=' . $recipient['id'] . '&signature=' . $signature; $html = '

Hi ' . htmlspecialchars((string) $recipient['full_name'], ENT_QUOTES, 'UTF-8') . ',

' . '
' . nl2br(htmlspecialchars($content, ENT_QUOTES, 'UTF-8')) . '
' - . '

You received this because you joined the Fat Bottom Grille news list. ' + . '


You received this because you joined the ' + . htmlspecialchars((string) $this->config->get('business.name'), ENT_QUOTES, 'UTF-8') + . ' news list. ' . 'Unsubscribe

'; try { $this->mailer->send((string) $recipient['email'], $subject, $html); @@ -724,7 +741,7 @@ final class AdminController extends BaseController $this->auth->requireStaff(); $orders = $this->db->fetchAll('SELECT * FROM orders ORDER BY placed_at DESC'); header('Content-Type: text/csv'); - header('Content-Disposition: attachment; filename="fat-bottom-orders-' . date('Y-m-d') . '.csv"'); + header('Content-Disposition: attachment; filename="lakeside-orders-' . date('Y-m-d') . '.csv"'); $output = fopen('php://output', 'wb'); fputcsv($output, [ 'Order', 'Placed', 'Customer', 'Email', 'Phone', 'Status', 'Payment', diff --git a/app/Controllers/StoreController.php b/app/Controllers/StoreController.php index 846a776..51b1fc9 100644 --- a/app/Controllers/StoreController.php +++ b/app/Controllers/StoreController.php @@ -48,7 +48,7 @@ final class StoreController extends BaseController ); View::render('store/home', $this->shared([ - 'title' => 'Keyser comfort food, ready when you are', + 'title' => 'Lakeside food, ready when you are', 'categories' => $categories, 'specials' => $specials, 'featured' => $featured, @@ -66,6 +66,13 @@ final class StoreController extends BaseController ])); } + public function credits(): void + { + View::render('store/credits', $this->shared([ + 'title' => 'Image Credits', + ])); + } + public function addToCart(): void { Security::verifyCsrf(); diff --git a/app/Core/Database.php b/app/Core/Database.php index c75f502..2592348 100644 --- a/app/Core/Database.php +++ b/app/Core/Database.php @@ -306,6 +306,9 @@ SQL; $this->normalizeLegacyOrderStatuses(); $this->syncCustomersFromOrders(); }); + $this->runMigration('20260614_lakeside_orders_demo', function (): void { + $this->applyLakesideDemoBranding(); + }); $this->pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_tracking_token ON orders(tracking_token)'); $this->pdo->exec('CREATE INDEX IF NOT EXISTS idx_orders_customer ON orders(customer_id, placed_at)'); $this->pdo->exec('CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(active, last_order_at)'); @@ -420,6 +423,92 @@ SQL; } } + private function applyLakesideDemoBranding(): void + { + $categories = [ + [1, 'Dockside Burgers', 'dockside-burgers', 'Flame-grilled favorites stacked for hungry lake days.'], + [2, 'Picnic Baskets', 'picnic-baskets', 'Wraps, bowls, and handhelds made to travel well.'], + [3, 'Lakehouse Plates', 'lakehouse-plates', 'Relaxed comfort food for a proper table by the water.'], + [4, 'Boardwalk Sides', 'boardwalk-sides', 'Shareable extras for the dock, blanket, or patio.'], + [5, 'Cold Drinks', 'cold-drinks', 'Fresh pours for sunny afternoons.'], + ]; + $categoryStatement = $this->pdo->prepare( + 'UPDATE menu_categories + SET name = ?, slug = ?, description = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?' + ); + foreach ($categories as [$id, $name, $slug, $description]) { + $categoryStatement->execute([$name, $slug, $description, $id]); + } + + $items = [ + [1, 1, 'Lakeside Double', 'lakeside-double', 'Two grilled patties, cheddar, lettuce, tomato, pickles, and dock sauce.', 1299, '/assets/images/lakeside-burger.jpg', '["House favorite"]', 1], + [2, 1, 'Dockmaster Bacon Burger', 'dockmaster-bacon-burger', 'Two patties, smoked bacon, cheddar, crisp lettuce, tomato, and onion.', 1399, '/assets/images/lakeside-burger.jpg', '[]', 1], + [3, 1, 'Sunset Mushroom Melt', 'sunset-mushroom-melt', 'Grilled beef, Swiss, mushrooms, caramelized onions, and peppercorn mayo.', 1299, '/assets/images/lakeside-burger.jpg', '[]', 0], + [4, 2, 'Marina Chicken Wrap', 'marina-chicken-wrap', 'Chicken, spinach, tomato, cucumber, and creamy herb dressing in a soft wrap.', 1199, '/assets/images/picnic-wrap.jpg', '["Picnic pick"]', 1], + [5, 2, 'Shoreline Falafel Bowl', 'shoreline-falafel-bowl', 'Falafel, seasoned rice, bright slaw, peppers, herbs, lemon, and green tahini.', 1249, '/assets/images/shoreline-bowl.jpg', '["Vegetarian"]', 1], + [6, 3, 'Lakehouse Chicken Plate', 'lakehouse-chicken-plate', 'Seasoned chicken legs with a creamy picnic side and rotating vegetables.', 1499, '/assets/images/lakehouse-chicken.jpg', '[]', 1], + [7, 3, 'Picnic Club Wrap', 'picnic-club-wrap', 'Sliced chicken, greens, tomato, smoky bacon, and herb dressing.', 1299, '/assets/images/picnic-wrap.jpg', '[]', 0], + [8, 4, 'Boardwalk Fries', 'boardwalk-fries', 'Golden fries finished with flaky salt and your choice of dipping sauce.', 599, '/assets/images/boardwalk-fries.jpg', '["Vegetarian"]', 1], + [9, 4, 'Garden Side Bowl', 'garden-side-bowl', 'Rice, chopped vegetables, herbs, lemon, and creamy green dressing.', 649, '/assets/images/shoreline-bowl.jpg', '["Vegetarian"]', 0], + [10, 5, 'Lemon Iced Tea', 'lemon-iced-tea', 'Fresh-brewed iced tea served with lemon.', 299, '/assets/images/lemon-iced-tea.jpg', '[]', 0], + [11, 5, 'Lakeside Lemonade', 'lakeside-lemonade', 'Bright house lemonade poured over ice.', 329, '/assets/images/lemon-iced-tea.jpg', '[]', 0], + ]; + $itemStatement = $this->pdo->prepare( + 'UPDATE menu_items + SET category_id = ?, name = ?, slug = ?, description = ?, price_cents = ?, + image_url = ?, dietary_tags = ?, featured = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?' + ); + foreach ($items as [$id, $categoryId, $name, $slug, $description, $price, $image, $tags, $featured]) { + $itemStatement->execute([ + $categoryId, + $name, + $slug, + $description, + $price, + $image, + $tags, + $featured, + $id, + ]); + } + + $specialStatement = $this->pdo->prepare( + 'UPDATE specials + SET menu_item_id = ?, title = ?, description = ?, badge = ?, price_cents = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ?' + ); + $specialStatement->execute([ + 1, + 'Sunset Burger Pick', + 'The Lakeside Double at its easygoing online demo price.', + 'Dock Deal', + 1299, + 1, + ]); + $specialStatement->execute([ + 8, + 'Boardwalk Break', + 'A hot order of Boardwalk Fries for the table or picnic blanket.', + 'Picnic Pick', + 599, + 2, + ]); + + $this->pdo->exec("UPDATE tax_rates SET name = 'Demo State Sales Tax' WHERE id = 1"); + $this->pdo->exec("UPDATE tax_rates SET name = 'Lakeview Demo Tax' WHERE id = 2"); + $this->pdo->exec( + "UPDATE users SET email = 'admin@lakesideorders.test' + WHERE email = 'admin@fatbottomgrille.com' + AND NOT EXISTS ( + SELECT 1 FROM users AS existing + WHERE existing.email = 'admin@lakesideorders.test' + )" + ); + } + private function seed(): void { $categoryCount = (int) $this->pdo->query('SELECT COUNT(*) FROM menu_categories')->fetchColumn(); @@ -430,11 +519,11 @@ SQL; $this->pdo->beginTransaction(); try { $categories = [ - ['Smash Burgers', 'smash-burgers', 'Hand-pressed beef, crisp edges, and the good stuff.', 10], - ['Burritos & Bowls', 'burritos-bowls', 'Big, packed, and built your way.', 20], - ['Dinner Plates', 'dinner-plates', 'Comfort-food plates made for a proper supper.', 30], - ['Sides', 'sides', 'The supporting cast that tends to steal the show.', 40], - ['Drinks', 'drinks', 'Cold drinks for hot food.', 50], + ['Dockside Burgers', 'dockside-burgers', 'Flame-grilled favorites stacked for hungry lake days.', 10], + ['Picnic Baskets', 'picnic-baskets', 'Wraps, bowls, and handhelds made to travel well.', 20], + ['Lakehouse Plates', 'lakehouse-plates', 'Relaxed comfort food for a proper table by the water.', 30], + ['Boardwalk Sides', 'boardwalk-sides', 'Shareable extras for the dock, blanket, or patio.', 40], + ['Cold Drinks', 'cold-drinks', 'Fresh pours for sunny afternoons.', 50], ]; $categoryStatement = $this->pdo->prepare( 'INSERT INTO menu_categories (name, slug, description, display_order) VALUES (?, ?, ?, ?)' @@ -449,22 +538,22 @@ SQL; } $items = [ - ['smash-burgers', 'The Fat Bottom', 'the-fat-bottom', 'Two smashed patties, American cheese, grilled onion, pickles, and house sauce.', 1199, 1, '["House favorite"]'], - ['smash-burgers', 'Piedmont Bacon Burger', 'piedmont-bacon-burger', 'Two patties, smoked bacon, cheddar, lettuce, tomato, onion, and burger sauce.', 1349, 1, '[]'], - ['smash-burgers', 'Allegheny Melt', 'allegheny-melt', 'Smashed beef, Swiss, caramelized onions, and peppercorn mayo on toasted sourdough.', 1299, 0, '[]'], - ['burritos-bowls', 'Mountaineer Burrito', 'mountaineer-burrito', 'Seasoned beef, rice, black beans, queso, pico, lettuce, and crema.', 1249, 1, '["Big appetite"]'], - ['burritos-bowls', 'Chicken Fajita Bowl', 'chicken-fajita-bowl', 'Grilled chicken, peppers, onions, rice, corn salsa, avocado crema.', 1199, 0, '["Gluten conscious"]'], - ['dinner-plates', 'Country Fried Chicken', 'country-fried-chicken', 'Crispy chicken, peppered gravy, mashed potatoes, and green beans.', 1499, 1, '[]'], - ['dinner-plates', 'Keyser Meatloaf', 'keyser-meatloaf', 'House meatloaf with brown gravy, mashed potatoes, and seasonal vegetables.', 1449, 0, '[]'], - ['sides', 'Loaded Fries', 'loaded-fries', 'Crispy fries, queso, bacon, scallions, and ranch drizzle.', 699, 1, '[]'], - ['sides', 'Mac & Cheese', 'mac-cheese', 'Creamy, baked, and unapologetically cheesy.', 449, 0, '["Vegetarian"]'], - ['drinks', 'Sweet Tea', 'sweet-tea', 'Fresh-brewed southern sweet tea.', 299, 0, '[]'], - ['drinks', 'Fountain Drink', 'fountain-drink', 'Your choice of fountain soda.', 299, 0, '[]'], + ['dockside-burgers', 'Lakeside Double', 'lakeside-double', 'Two grilled patties, cheddar, lettuce, tomato, pickles, and dock sauce.', 1299, '/assets/images/lakeside-burger.jpg', 1, '["House favorite"]'], + ['dockside-burgers', 'Dockmaster Bacon Burger', 'dockmaster-bacon-burger', 'Two patties, smoked bacon, cheddar, crisp lettuce, tomato, and onion.', 1399, '/assets/images/lakeside-burger.jpg', 1, '[]'], + ['dockside-burgers', 'Sunset Mushroom Melt', 'sunset-mushroom-melt', 'Grilled beef, Swiss, mushrooms, caramelized onions, and peppercorn mayo.', 1299, '/assets/images/lakeside-burger.jpg', 0, '[]'], + ['picnic-baskets', 'Marina Chicken Wrap', 'marina-chicken-wrap', 'Chicken, spinach, tomato, cucumber, and creamy herb dressing in a soft wrap.', 1199, '/assets/images/picnic-wrap.jpg', 1, '["Picnic pick"]'], + ['picnic-baskets', 'Shoreline Falafel Bowl', 'shoreline-falafel-bowl', 'Falafel, seasoned rice, bright slaw, peppers, herbs, lemon, and green tahini.', 1249, '/assets/images/shoreline-bowl.jpg', 1, '["Vegetarian"]'], + ['lakehouse-plates', 'Lakehouse Chicken Plate', 'lakehouse-chicken-plate', 'Seasoned chicken legs with a creamy picnic side and rotating vegetables.', 1499, '/assets/images/lakehouse-chicken.jpg', 1, '[]'], + ['lakehouse-plates', 'Picnic Club Wrap', 'picnic-club-wrap', 'Sliced chicken, greens, tomato, smoky bacon, and herb dressing.', 1299, '/assets/images/picnic-wrap.jpg', 0, '[]'], + ['boardwalk-sides', 'Boardwalk Fries', 'boardwalk-fries', 'Golden fries finished with flaky salt and your choice of dipping sauce.', 599, '/assets/images/boardwalk-fries.jpg', 1, '["Vegetarian"]'], + ['boardwalk-sides', 'Garden Side Bowl', 'garden-side-bowl', 'Rice, chopped vegetables, herbs, lemon, and creamy green dressing.', 649, '/assets/images/shoreline-bowl.jpg', 0, '["Vegetarian"]'], + ['cold-drinks', 'Lemon Iced Tea', 'lemon-iced-tea', 'Fresh-brewed iced tea served with lemon.', 299, '/assets/images/lemon-iced-tea.jpg', 0, '[]'], + ['cold-drinks', 'Lakeside Lemonade', 'lakeside-lemonade', 'Bright house lemonade poured over ice.', 329, '/assets/images/lemon-iced-tea.jpg', 0, '[]'], ]; $itemStatement = $this->pdo->prepare( 'INSERT INTO menu_items - (category_id, name, slug, description, price_cents, featured, dietary_tags) - VALUES (?, ?, ?, ?, ?, ?, ?)' + (category_id, name, slug, description, price_cents, image_url, featured, dietary_tags) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)' ); foreach ($items as $item) { $itemStatement->execute([ @@ -475,32 +564,33 @@ SQL; $item[4], $item[5], $item[6], + $item[7], ]); } - $fatBottomId = (int) $this->pdo->query( - "SELECT id FROM menu_items WHERE slug = 'the-fat-bottom'" + $lakesideDoubleId = (int) $this->pdo->query( + "SELECT id FROM menu_items WHERE slug = 'lakeside-double'" )->fetchColumn(); - $loadedFriesId = (int) $this->pdo->query( - "SELECT id FROM menu_items WHERE slug = 'loaded-fries'" + $boardwalkFriesId = (int) $this->pdo->query( + "SELECT id FROM menu_items WHERE slug = 'boardwalk-fries'" )->fetchColumn(); $specialStatement = $this->pdo->prepare( 'INSERT INTO specials (menu_item_id, title, description, badge, price_cents, display_order) VALUES (?, ?, ?, ?, ?, ?)' ); $specialStatement->execute([ - $fatBottomId, - 'The Fat Bottom Deal', - 'Our signature burger at a special online price.', - 'Online Special', - 1099, + $lakesideDoubleId, + 'Sunset Burger Pick', + 'The Lakeside Double at its easygoing online demo price.', + 'Dock Deal', + 1299, 10, ]); $specialStatement->execute([ - $loadedFriesId, - 'Loaded-Up Lunch', - 'Loaded fries are $1 off weekdays from 11 to 2.', - 'Weekday Deal', + $boardwalkFriesId, + 'Boardwalk Break', + 'A hot order of Boardwalk Fries for the table or picnic blanket.', + 'Picnic Pick', 599, 20, ]); @@ -508,8 +598,8 @@ SQL; $taxStatement = $this->pdo->prepare( 'INSERT INTO tax_rates (name, jurisdiction, rate_basis_points) VALUES (?, ?, ?)' ); - $taxStatement->execute(['West Virginia Sales Tax', 'state', 600]); - $taxStatement->execute(['Keyser Municipal Tax', 'city', 100]); + $taxStatement->execute(['Demo State Sales Tax', 'state', 600]); + $taxStatement->execute(['Lakeview Demo Tax', 'city', 100]); $adminStatement = $this->pdo->prepare( 'INSERT INTO users @@ -518,7 +608,7 @@ SQL; VALUES (?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)' ); $adminStatement->execute([ - 'admin@fatbottomgrille.com', + 'admin@lakesideorders.test', password_hash('ChangeMe123!', PASSWORD_DEFAULT), 'Store Administrator', '', diff --git a/app/Services/Mailer.php b/app/Services/Mailer.php index 022396c..5a15c65 100644 --- a/app/Services/Mailer.php +++ b/app/Services/Mailer.php @@ -32,7 +32,7 @@ final class Mailer $from = sprintf( '%s <%s>', - $this->config->get('mailgun.from_name', 'Fat Bottom Grille'), + $this->config->get('mailgun.from_name', 'Lakeside Orders'), $this->config->get('mailgun.from_email') ); @@ -77,4 +77,3 @@ final class Mailer file_put_contents($directory . '/mail.log', $line, FILE_APPEND | LOCK_EX); } } - diff --git a/app/Services/MenuPdfExporter.php b/app/Services/MenuPdfExporter.php index 7b63cbe..9eeff2a 100644 --- a/app/Services/MenuPdfExporter.php +++ b/app/Services/MenuPdfExporter.php @@ -30,7 +30,7 @@ final class MenuPdfExporter $pdf = $this->buildPdf($pages); header('Content-Type: application/pdf'); - header('Content-Disposition: attachment; filename="fat-bottom-grille-menu.pdf"'); + header('Content-Disposition: attachment; filename="lakeside-orders-menu.pdf"'); header('Content-Length: ' . strlen($pdf)); echo $pdf; exit; @@ -52,7 +52,7 @@ final class MenuPdfExporter $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.91 0.92 0.90 rg\nBT /F1 10 Tf 46 718 Td (LAKESIDE PICKUP | 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) { diff --git a/app/Services/OrderStatusService.php b/app/Services/OrderStatusService.php index deb04f8..e4b6433 100644 --- a/app/Services/OrderStatusService.php +++ b/app/Services/OrderStatusService.php @@ -89,7 +89,7 @@ final class OrderStatusService "Hi {{customer_name}},\n\nYour order {{order_number}} is now {{status_label}}.\n\n{{note}}\n\n{{tracking_url}}" ); $values = [ - '{{business_name}}' => (string) $this->config->get('business.name', 'Fat Bottom Grille'), + '{{business_name}}' => (string) $this->config->get('business.name', 'Lakeside Orders'), '{{customer_name}}' => (string) $order['customer_name'], '{{order_number}}' => (string) $order['order_number'], '{{status_label}}' => order_status_label($status), diff --git a/app/Services/TwoFactorService.php b/app/Services/TwoFactorService.php index c1af835..2d5a927 100644 --- a/app/Services/TwoFactorService.php +++ b/app/Services/TwoFactorService.php @@ -27,9 +27,10 @@ final class TwoFactorService ); $name = htmlspecialchars((string) $user['full_name'], ENT_QUOTES, 'UTF-8'); + $businessName = (string) $this->config->get('business.name', 'Lakeside Orders'); $this->mailer->send( (string) $user['email'], - 'Your Fat Bottom Grille verification code', + "Your {$businessName} verification code", "

Hi {$name},

Your verification code is {$code}.

It expires in 10 minutes.

" ); diff --git a/config/defaults.php b/config/defaults.php index 03b9803..b90310c 100644 --- a/config/defaults.php +++ b/config/defaults.php @@ -2,22 +2,39 @@ return [ 'app' => [ - 'name' => 'Fat Bottom Grille', + 'name' => 'Lakeside Orders', 'url' => 'http://localhost:8080', 'key' => '', 'timezone' => 'America/New_York', 'environment' => 'development', ], 'business' => [ - 'name' => 'Fat Bottom Grille', + 'name' => 'Lakeside Orders', 'phone' => '', 'email' => '', - 'street' => '410 W Piedmont St', - 'city' => 'Keyser', - 'state' => 'WV', - 'postal_code' => '26726', - 'hours' => 'Hours coming soon', - 'announcement' => 'Big flavor, made right here in Keyser.', + 'street' => '12 Marina Way', + 'city' => 'Lakeview', + 'state' => 'MI', + 'postal_code' => '00000', + 'hours' => 'Demo hours: daily 11 AM-8 PM', + 'announcement' => 'Demo ordering for easy days by the water.', + ], + 'branding' => [ + 'location_label' => 'Lakeview Commons', + 'meta_description' => 'Order dockside burgers, picnic-ready wraps, bowls, sides, and cold drinks from Lakeside Orders.', + 'hero_eyebrow' => 'Lake days. Easy pickup.', + 'hero_heading' => 'Order by the water.', + 'hero_emphasis' => 'Picnic anywhere.', + 'hero_intro' => 'Burgers, picnic baskets, fresh bowls, and lakehouse favorites made for dock days, patio tables, and sunset suppers.', + 'hero_image' => 'assets/images/lakeside-burger.jpg', + 'gallery_picnic_image' => 'assets/images/lakeside-picnic.jpg', + 'gallery_patio_image' => 'assets/images/patio-dining.jpg', + 'gallery_service_image' => 'assets/images/garden-dining.jpg', + 'story_image' => 'assets/images/lakeside-sunset.jpg', + 'story_eyebrow' => 'Made for lake days', + 'story_heading' => 'Good food travels well.', + 'story_text' => 'Lakeside Orders is a demo restaurant experience built around relaxed pickup, picnic-ready meals, and welcoming tables near the water.', + 'footer_tagline' => 'Dockside burgers, picnic-ready favorites, and a table near the water.', ], 'ordering' => [ 'accepting_orders' => true, @@ -35,8 +52,8 @@ return [ 'enabled' => false, 'domain' => '', 'api_key' => '', - 'from_name' => 'Fat Bottom Grille', - 'from_email' => 'orders@fatbottomgrille.com', + 'from_name' => 'Lakeside Orders', + 'from_email' => 'orders@lakesideorders.test', ], 'order_emails' => [ 'pending' => [ @@ -68,7 +85,7 @@ return [ 'driver' => 'sqlite', 'mysql_host' => '127.0.0.1', 'mysql_port' => 3306, - 'mysql_database' => 'fatbottomgrille', + 'mysql_database' => 'lakesideorders', 'mysql_username' => '', 'mysql_password' => '', ], diff --git a/public/assets/app.css b/public/assets/app.css index 92cc7c6..08d743c 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -3,8 +3,8 @@ --ink-soft: #0e1d2b; --navy: #12283b; --navy-light: #1c3a50; - --gold: #c8973b; - --gold-light: #e1b75e; + --gold: #35d6c1; + --gold-light: #8af5e6; --cream: #f3eedf; --paper: #faf7ef; --white: #ffffff; @@ -41,9 +41,10 @@ body { body.store-body { background-image: - radial-gradient(circle at 0 0, rgba(200, 151, 59, 0.08), transparent 28%), + radial-gradient(circle at 0 0, rgba(53, 214, 193, 0.1), transparent 28%), + radial-gradient(circle at 92% 12%, rgba(114, 92, 255, 0.09), transparent 24%), linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px); - background-size: auto, 100% 22px; + background-size: auto, auto, 100% 22px; } a { @@ -409,8 +410,8 @@ h3 { overflow: hidden; color: var(--cream); background: - linear-gradient(90deg, rgba(5, 12, 18, 0.99) 0%, rgba(8, 20, 30, 0.96) 48%, rgba(8, 18, 28, 0.76) 100%), - url("images/reuben-fries.jpg") center 48% / cover no-repeat; + linear-gradient(90deg, rgba(5, 12, 18, 0.99) 0%, rgba(8, 20, 30, 0.95) 46%, rgba(8, 18, 28, 0.48) 100%), + url("images/lakeside-sunset.jpg") center 56% / cover no-repeat; } .hero::after { @@ -708,25 +709,25 @@ h3 { .special-card:nth-child(4n + 1) { background-image: linear-gradient(112deg, rgba(5, 12, 18, 0.98), rgba(8, 18, 28, 0.82)), - url("images/quesadilla.jpg"); + url("images/lakeside-burger.jpg"); } .special-card:nth-child(4n + 2) { background-image: linear-gradient(112deg, rgba(5, 12, 18, 0.98), rgba(8, 18, 28, 0.8)), - url("images/sausage-kale-soup.jpg"); + url("images/boardwalk-fries.jpg"); } .special-card:nth-child(4n + 3) { background-image: linear-gradient(112deg, rgba(5, 12, 18, 0.98), rgba(8, 18, 28, 0.78)), - url("images/grilled-chicken-salad.jpg"); + url("images/shoreline-bowl.jpg"); } .special-card:nth-child(4n + 4) { background-image: linear-gradient(112deg, rgba(5, 12, 18, 0.98), rgba(8, 18, 28, 0.8)), - url("images/curry-rice.jpg"); + url("images/picnic-wrap.jpg"); } .special-card::after { @@ -832,43 +833,43 @@ h3 { .menu-card:nth-child(7n + 1) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/reuben-fries.jpg"); + url("images/lakeside-burger.jpg"); } .menu-card:nth-child(7n + 2) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/chicken-sandwich-fries.jpg"); + url("images/picnic-wrap.jpg"); } .menu-card:nth-child(7n + 3) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/quesadilla.jpg"); + url("images/shoreline-bowl.jpg"); } .menu-card:nth-child(7n + 4) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/curry-rice.jpg"); + url("images/lakehouse-chicken.jpg"); } .menu-card:nth-child(7n + 5) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/italian-sub.jpg"); + url("images/boardwalk-fries.jpg"); } .menu-card:nth-child(7n + 6) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/sausage-kale-soup.jpg"); + url("images/lemon-iced-tea.jpg"); } .menu-card:nth-child(7n) .menu-card-art:not(.has-image) { background-image: linear-gradient(rgba(5, 12, 18, 0.62), rgba(5, 12, 18, 0.86)), - url("images/grilled-chicken-salad.jpg"); + url("images/patio-dining.jpg"); } .menu-card-art:not(.has-image) { @@ -968,6 +969,78 @@ h3 { background: #08141f; } +.lakeside-gallery { + background: + radial-gradient(circle at 92% 16%, rgba(239, 91, 161, 0.11), transparent 28%), + radial-gradient(circle at 8% 84%, rgba(31, 124, 255, 0.1), transparent 30%), + #07131d; +} + +.experience-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.experience-card { + position: relative; + min-height: 320px; + margin: 0; + overflow: hidden; + background: var(--navy); + border: 1px solid rgba(138, 245, 230, 0.2); + border-radius: var(--radius-large); +} + +.experience-card-wide { + min-height: 470px; + grid-column: 1 / -1; +} + +.experience-card img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 420ms ease; +} + +.experience-card:hover img { + transform: scale(1.025); +} + +.experience-card::after { + position: absolute; + inset: 0; + background: linear-gradient(transparent 38%, rgba(5, 12, 18, 0.9)); + content: ""; +} + +.experience-card figcaption { + position: absolute; + z-index: 2; + right: 26px; + bottom: 24px; + left: 26px; + display: flex; + flex-direction: column; +} + +.experience-card figcaption span { + color: var(--gold-light); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.experience-card figcaption strong { + color: var(--cream); + font-family: var(--font-display); + font-size: clamp(1.8rem, 4vw, 3.4rem); + line-height: 1; + text-transform: uppercase; +} + .category-band { padding-block: 55px; color: var(--cream); @@ -1034,10 +1107,23 @@ h3 { min-height: 410px; place-items: center; overflow: hidden; - color: var(--gold); - background: - linear-gradient(rgba(5, 12, 18, 0.58), rgba(5, 12, 18, 0.9)), - url("images/italian-sub.jpg") center / cover no-repeat; + color: var(--gold-light); + background: var(--navy); +} + +.story-art::after { + position: absolute; + inset: 0; + background: linear-gradient(rgba(5, 12, 18, 0.16), rgba(5, 12, 18, 0.78)); + content: ""; +} + +.story-art img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; } .story-number { @@ -1069,7 +1155,7 @@ h3 { color: var(--cream); background: linear-gradient(90deg, rgba(5, 12, 18, 0.97), rgba(8, 18, 28, 0.8)), - url("images/italian-sub.jpg") center 55% / cover no-repeat; + url("images/lakeside-sunset.jpg") center 56% / cover no-repeat; } .page-hero-compact { @@ -1086,6 +1172,41 @@ h3 { color: #b9c4ca; } +.narrow-content { + max-width: 880px; +} + +.credits-list { + border-top: 1px solid var(--line); +} + +.credits-list article { + display: flex; + align-items: center; + justify-content: space-between; + gap: 25px; + padding-block: 22px; + border-bottom: 1px solid var(--line); +} + +.credits-list h2 { + margin-bottom: 5px; + color: var(--cream); + font-size: 1.35rem; + text-transform: uppercase; +} + +.credits-list p, +.credits-note { + margin: 0; + color: var(--muted); + font-size: 0.84rem; +} + +.credits-note { + margin-top: 28px; +} + .page-hero-inner { display: grid; grid-template-columns: 1fr minmax(340px, 0.55fr); @@ -1958,8 +2079,8 @@ textarea:focus { padding-top: 68px; color: #b6c0c6; background: - linear-gradient(90deg, rgba(5, 12, 18, 0.99), rgba(5, 12, 18, 0.94)), - url("images/fat-bottom-grille-logo.jpg") right -80px center / 520px no-repeat, + linear-gradient(90deg, rgba(5, 12, 18, 0.99), rgba(5, 12, 18, 0.86)), + url("images/lakeside-sunset.jpg") center 62% / cover no-repeat, #050c12; } @@ -2047,14 +2168,14 @@ textarea:focus { .store-body .story-section { background: - linear-gradient(90deg, rgba(8, 18, 28, 0.99), rgba(8, 18, 28, 0.92)), - url("images/sausage-kale-soup.jpg") right center / 46% auto no-repeat; + radial-gradient(circle at 86% 20%, rgba(114, 92, 255, 0.12), transparent 30%), + #08121c; } .store-body .category-band { background: linear-gradient(90deg, rgba(5, 12, 18, 0.98), rgba(5, 12, 18, 0.86)), - url("images/curry-rice.jpg") center 56% / cover no-repeat; + url("images/patio-dining.jpg") center 48% / cover no-repeat; } .store-body .menu-category-heading { @@ -2116,20 +2237,20 @@ textarea:focus { .store-body .order-summary { background: linear-gradient(145deg, rgba(8, 18, 28, 0.98), rgba(8, 18, 28, 0.88)), - url("images/quesadilla.jpg") center / cover no-repeat; + url("images/lakeside-burger.jpg") center / cover no-repeat; border: 1px solid rgba(200, 151, 59, 0.22); } .store-body .auth-section { background: linear-gradient(90deg, rgba(5, 12, 18, 0.97), rgba(8, 18, 28, 0.87)), - url("images/grilled-chicken-salad.jpg") center / cover no-repeat; + url("images/garden-dining.jpg") center / cover no-repeat; } .store-body .confirmation-section { background: linear-gradient(rgba(5, 12, 18, 0.94), rgba(5, 12, 18, 0.97)), - url("images/sausage-kale-soup.jpg") center / cover no-repeat; + url("images/lakeside-sunset.jpg") center / cover no-repeat; } .store-body .status-pill { @@ -3319,6 +3440,10 @@ code { gap: 40px; } + .experience-card-wide { + min-height: 390px; + } + .story-art { min-height: 320px; } @@ -3464,6 +3589,7 @@ code { } .menu-grid, + .experience-grid, .category-links, .footer-grid, .form-grid, @@ -3474,6 +3600,18 @@ code { grid-template-columns: 1fr; } + .experience-card, + .experience-card-wide { + min-height: 280px; + grid-column: auto; + } + + .credits-list article { + align-items: flex-start; + flex-direction: column; + gap: 8px; + } + .category-links a { min-height: 85px; } diff --git a/public/assets/images/CREDITS.md b/public/assets/images/CREDITS.md new file mode 100644 index 0000000..0538075 --- /dev/null +++ b/public/assets/images/CREDITS.md @@ -0,0 +1,25 @@ +# Lakeside Orders image credits + +The active demo photography was sourced from Wikimedia Commons. Images may be +cropped or resized by the website through CSS. + +- `lakeside-sunset.jpg` - Bonnie Moreland, public domain: + https://commons.wikimedia.org/wiki/File:Sunset_Odell_Lake,_Oregon_-_Flickr_-_Bonnie_Moreland_(free_images).jpg +- `lakeside-picnic.jpg` - Gholamreza Shamsnetari, CC BY 4.0: + https://commons.wikimedia.org/wiki/File:Sizdah_Be-Dar_2018,_Valasht_Lake_(13970114000329636583522676851541_54966).jpg +- `patio-dining.jpg` - Elvert Barnes, CC BY-SA 2.0: + https://commons.wikimedia.org/wiki/File:People_eating_outside_restaurant_Baltimore.jpg +- `garden-dining.jpg` - Ibrahim Achiri, CC BY-SA 4.0: + https://commons.wikimedia.org/wiki/File:People_dining_and_socializing_at_Zen_Garden.jpg +- `lakeside-burger.jpg` - Daderot, CC0: + https://commons.wikimedia.org/wiki/File:Hamburger_with_blue_cheese,_guacamole,_and_other_toppings,_plus_French_fries_-_Massachusetts.jpg +- `picnic-wrap.jpg` - Tomwsulcer, CC0: + https://commons.wikimedia.org/wiki/File:Lunch_chicken_wrap_with_spinach_and_sauce.JPG +- `shoreline-bowl.jpg` - Andy Li, CC0: + https://commons.wikimedia.org/wiki/File:Falafel_Super_Bowl_Salad_-_Leon_2025-10-27.jpg +- `lakehouse-chicken.jpg` - Vyacheslav Argenberg, CC BY 4.0: + https://commons.wikimedia.org/wiki/File:Fried_chicken_legs,_Rostov-on-Don,_Russia.jpg +- `lemon-iced-tea.jpg` - Renee Comet, public domain: + https://commons.wikimedia.org/wiki/File:Iced_tea.jpg +- `boardwalk-fries.jpg` - Thriving Vegetarian, CC BY 2.0: + https://commons.wikimedia.org/wiki/File:Perfect_French_Fries_-_11457817933.jpg diff --git a/public/assets/images/boardwalk-fries.jpg b/public/assets/images/boardwalk-fries.jpg new file mode 100644 index 0000000..2f5b724 Binary files /dev/null and b/public/assets/images/boardwalk-fries.jpg differ diff --git a/public/assets/images/garden-dining.jpg b/public/assets/images/garden-dining.jpg new file mode 100644 index 0000000..8662d9e Binary files /dev/null and b/public/assets/images/garden-dining.jpg differ diff --git a/public/assets/images/lakehouse-chicken.jpg b/public/assets/images/lakehouse-chicken.jpg new file mode 100644 index 0000000..9b06fa9 Binary files /dev/null and b/public/assets/images/lakehouse-chicken.jpg differ diff --git a/public/assets/images/lakeside-burger.jpg b/public/assets/images/lakeside-burger.jpg new file mode 100644 index 0000000..9375265 Binary files /dev/null and b/public/assets/images/lakeside-burger.jpg differ diff --git a/public/assets/images/lakeside-orders-logo.svg b/public/assets/images/lakeside-orders-logo.svg new file mode 100644 index 0000000..da093d7 --- /dev/null +++ b/public/assets/images/lakeside-orders-logo.svg @@ -0,0 +1,27 @@ + + Lakeside Orders + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lakeside-picnic.jpg b/public/assets/images/lakeside-picnic.jpg new file mode 100644 index 0000000..63ef554 Binary files /dev/null and b/public/assets/images/lakeside-picnic.jpg differ diff --git a/public/assets/images/lakeside-sunset.jpg b/public/assets/images/lakeside-sunset.jpg new file mode 100644 index 0000000..d85a66c Binary files /dev/null and b/public/assets/images/lakeside-sunset.jpg differ diff --git a/public/assets/images/lemon-iced-tea.jpg b/public/assets/images/lemon-iced-tea.jpg new file mode 100644 index 0000000..8bce4c6 Binary files /dev/null and b/public/assets/images/lemon-iced-tea.jpg differ diff --git a/public/assets/images/patio-dining.jpg b/public/assets/images/patio-dining.jpg new file mode 100644 index 0000000..d733454 Binary files /dev/null and b/public/assets/images/patio-dining.jpg differ diff --git a/public/assets/images/picnic-wrap.jpg b/public/assets/images/picnic-wrap.jpg new file mode 100644 index 0000000..3eac352 Binary files /dev/null and b/public/assets/images/picnic-wrap.jpg differ diff --git a/public/assets/images/shoreline-bowl.jpg b/public/assets/images/shoreline-bowl.jpg new file mode 100644 index 0000000..b99af09 Binary files /dev/null and b/public/assets/images/shoreline-bowl.jpg differ diff --git a/public/index.php b/public/index.php index d2a2bbf..8c5647f 100644 --- a/public/index.php +++ b/public/index.php @@ -51,6 +51,7 @@ $stats->record($path, $user ? (int) $user['id'] : null); $router = new Router(); $router->get('/', [$store, 'home']); $router->get('/menu', [$store, 'menu']); +$router->get('/credits', [$store, 'credits']); $router->post('/cart/add', [$store, 'addToCart']); $router->get('/cart', [$store, 'cart']); $router->post('/cart/update', [$store, 'updateCart']); diff --git a/views/admin/newsletters.php b/views/admin/newsletters.php index d32561e..631e827 100644 --- a/views/admin/newsletters.php +++ b/views/admin/newsletters.php @@ -7,7 +7,7 @@
Send an update

New Newsletter

- +
Every email includes an unsubscribe link. Users can also opt out from My Account.
diff --git a/views/admin/settings.php b/views/admin/settings.php index 845911c..095176e 100644 --- a/views/admin/settings.php +++ b/views/admin/settings.php @@ -15,6 +15,27 @@ +
+
Storefront presentation

Branding & Homepage

+
Edit the demo message and active image paths here. Image files live in public/assets/images.
+
+ + + + + + + + + + + + + + + +
+
Kitchen controls

Online Ordering

diff --git a/views/auth/register.php b/views/auth/register.php index 04fdfd2..29cadbf 100644 --- a/views/auth/register.php +++ b/views/auth/register.php @@ -30,13 +30,13 @@
@@ -53,7 +53,7 @@ -
Creating an account opts you into occasional Fat Bottom Grille news. You can opt out from any email or My Account.
+
Creating an account opts you into occasional get('business.name')) ?> news. You can opt out from any email or My Account.

Already have an account? Sign in

diff --git a/views/layout.php b/views/layout.php index 6d103af..e79c374 100644 --- a/views/layout.php +++ b/views/layout.php @@ -2,7 +2,7 @@ use FatBottom\Core\View; -$businessName = (string) $config->get('business.name', 'Fat Bottom Grille'); +$businessName = (string) $config->get('business.name', 'Lakeside Orders'); $isAdmin = isset($adminPage); ?> @@ -11,7 +11,7 @@ $isAdmin = isset($adminPage); - + <?= e($title ?? $businessName) ?> | <?= e($businessName) ?> @@ -59,11 +59,11 @@ $isAdmin = isset($adminPage);
+ +
Pick your direction @@ -104,14 +130,14 @@
- diff --git a/views/store/menu.php b/views/store/menu.php index 4515b8c..dd006da 100644 --- a/views/store/menu.php +++ b/views/store/menu.php @@ -1,9 +1,9 @@
- Hot, fresh, yours + From the grill to the shoreline

Order Online

-

Choose your favorites and we will get the kitchen moving.

+

Choose a dockside favorite and we will get your lakeside pickup moving.