diff --git a/README.md b/README.md index cd494d2..4c354a0 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,26 @@ Content collections: `brews`, `menu_categories`, `menu_items`, `experiences`, `t The seed only creates records that are missing (matched by `uid`), so edits made in the admin are never overwritten by redeploys. Schema changes belong in `pb/migrations/`. +## Updating the food menu + +The food menu is managed in code: `app/src/data/menu-data.mjs` (11 categories, ~137 items, +single source for the PocketBase sync **and** the site's offline fallback). + +```bash +# 1. Edit app/src/data/menu-data.mjs, commit & push, deploy (if the image changed) +# 2. Sync the menu into PocketBase — from anywhere with admin credentials: +PB_URL=https://admin.greatbear.in \ +PB_SUPERUSER_EMAIL=admin@greatbear.in \ +PB_SUPERUSER_PASSWORD=... \ +node scripts/update-menu.mjs +``` + +The script **replaces** all `menu_categories` / `menu_items` records (deletes old, inserts +new) and touches nothing else. Item prices are text, so ranges like `449/549` work; +`group` (`veg` / `nonveg`) drives the dietary badge; `image` is an optional URL. Admin edits +to menu records are overwritten by the next sync — treat this file as the menu's source of +truth. + ## Container environment | Variable | Default | Purpose | diff --git a/app/src/components/menu-browser.tsx b/app/src/components/menu-browser.tsx index 9042638..58e052f 100644 --- a/app/src/components/menu-browser.tsx +++ b/app/src/components/menu-browser.tsx @@ -1,6 +1,20 @@ import * as React from 'react' import type { MenuCategory, MenuItem } from '~/lib/types' +const CATEGORY_ICONS: Record = { + 'brew-bites': 'tapas', + 'sharing-plates': 'restaurant_menu', + salads: 'eco', + platters: 'restaurant', + 'international-appetizers': 'public', + 'indian-appetizers': 'local_dining', + 'main-course': 'dinner_dining', + curries: 'soup_kitchen', + 'biryanis-rice-breads': 'rice_bowl', + desserts: 'icecream', + pizzas: 'local_pizza', +} + export function MenuBrowser({ categories, items, @@ -38,43 +52,7 @@ export function MenuBrowser({
{visible.map((item) => ( -
-
-
-
-
-

{item.name}

- ₹{item.price} -
-

- {item.description} -

-
-
-
- - sports_bar - - - Perfect Pairing:{' '} - {item.pairing} - -
- - {item.tag} - -
-
-
+ ))}
@@ -82,6 +60,91 @@ export function MenuBrowser({ ) } +function MenuCard({ item }: { item: MenuItem }) { + const icon = CATEGORY_ICONS[item.category] ?? 'restaurant' + const showFooter = Boolean(item.pairing || item.tag) + + return ( +
+ {item.image ? ( +
+ {item.group ? ( +
+ +
+ ) : null} +
+ ) : ( +
+ {icon} + {item.group ? ( +
+ +
+ ) : null} +
+ )} + +
+
+
+

{item.name}

+ {item.price ? ( + ₹{item.price} + ) : null} +
+ {item.description ? ( +

{item.description}

+ ) : null} +
+ + {showFooter ? ( +
+ {item.pairing ? ( +
+ + sports_bar + + + Perfect Pairing: {item.pairing} + +
+ ) : ( + + )} + {item.tag ? ( + + {item.tag} + + ) : null} +
+ ) : null} +
+
+ ) +} + +/** FSSAI-style dietary mark: green square for veg, red for non-veg. */ +function DietBadge({ group }: { group: string }) { + const isVeg = group === 'veg' + const border = isVeg ? 'border-green-500' : 'border-red-500' + const dot = isVeg ? 'bg-green-500' : 'bg-red-500' + return ( + + + + ) +} + function FilterTab({ label, active, diff --git a/app/src/data/content.json b/app/src/data/content.json index 7f39b84..984e273 100644 --- a/app/src/data/content.json +++ b/app/src/data/content.json @@ -14,10 +14,30 @@ "maps_query": "The Great Bear Microbrewery, SCO 32, Sector 26, Madhya Marg, Chandigarh" }, "stats": [ - { "uid": "stat-taps", "value": "12+", "label": "On-Tap Varieties", "sort": 1 }, - { "uid": "stat-malts", "value": "100%", "label": "Artisanal Malts", "sort": 2 }, - { "uid": "stat-rooftop", "value": "3,000", "label": "Sq. Ft. Rooftop", "sort": 3 }, - { "uid": "stat-music", "value": "7", "label": "Days Live Music", "sort": 4 } + { + "uid": "stat-taps", + "value": "12+", + "label": "On-Tap Varieties", + "sort": 1 + }, + { + "uid": "stat-malts", + "value": "100%", + "label": "Artisanal Malts", + "sort": 2 + }, + { + "uid": "stat-rooftop", + "value": "3,000", + "label": "Sq. Ft. Rooftop", + "sort": 3 + }, + { + "uid": "stat-music", + "value": "7", + "label": "Days Live Music", + "sort": 4 + } ], "brews": [ { @@ -263,102 +283,6 @@ "sort": 3 } ], - "menu_categories": [ - { "uid": "cat-pizza", "slug": "pizza", "name": "Wood-Fired Pizzas", "sort": 1 }, - { "uid": "cat-tapas", "slug": "tapas", "name": "Tandoori & Tapas", "sort": 2 }, - { "uid": "cat-burgers", "slug": "burgers", "name": "Burgers & Sliders", "sort": 3 }, - { "uid": "cat-desserts", "slug": "desserts", "name": "Artisanal Desserts", "sort": 4 } - ], - "menu_items": [ - { - "uid": "item-truffle-burrata", - "category": "pizza", - "name": "The Truffle Burrata Wood-Fired Pizza", - "description": "Hand-stretched sourdough crust, black truffle cream, fresh Fior di Latte, creamy burrata center, wild arugula, and aged parmesan shavings.", - "price": 595, - "pairing": "Bear's Witbier", - "tag": "Wood-Fired", - "image": "/images/menu-pizza-burrata.jpg", - "sort": 1 - }, - { - "uid": "item-calabrian-salami", - "category": "pizza", - "name": "Fiery Calabrian Salami Pizza", - "description": "San Marzano tomato reduction, spicy artisan Calabrian salami, hot honey infusion, fresh basil, and molten mozzarella.", - "price": 545, - "pairing": "Imperial IPA", - "tag": "Wood-Fired", - "image": "/images/menu-pizza-salami.jpg", - "sort": 2 - }, - { - "uid": "item-tandoori-lamb", - "category": "tapas", - "name": "Smoked Tandoori Lamb Chops", - "description": "New Zealand lamb chops marinated in Kashmiri chili, hung curd, and roasted garlic, charred over hot coals in our clay tandoor oven.", - "price": 725, - "pairing": "Robust Oatmeal Stout", - "tag": "Tandoori", - "image": "/images/menu-tandoori-lamb.jpg", - "sort": 3 - }, - { - "uid": "item-paneer-skewers", - "category": "tapas", - "name": "Crispy Tandoori Paneer Skewers", - "description": "Farmhouse cottage cheese chunks stuffed with mint chutney, skewered with bell peppers and onions, roasted over live embers.", - "price": 475, - "pairing": "Classic Czech Pilsner", - "tag": "Tapas", - "image": "/images/menu-paneer-skewers.jpg", - "sort": 4 - }, - { - "uid": "item-bear-burger", - "category": "burgers", - "name": "The Great Bear Truffle Burger", - "description": "Double dry-aged Angus beef patties, molten cheddar cheese, thick-cut bacon, caramelized bourbon onions, and black truffle aioli on a toasted brioche bun.", - "price": 525, - "pairing": "American Pale Ale", - "tag": "Burger", - "image": "/images/menu-bear-burger.jpg", - "sort": 5 - }, - { - "uid": "item-chicken-sliders", - "category": "burgers", - "name": "Hot Honey Fried Chicken Sliders (3x)", - "description": "Crispy buttermilk marinated chicken thighs glazed with spicy hot honey, topped with crunchy pickled jalapeño slaw and garlic mayo.", - "price": 495, - "pairing": "Session IPA", - "tag": "Sliders", - "image": "/images/menu-chicken-sliders.jpg", - "sort": 6 - }, - { - "uid": "item-stout-brownie", - "category": "desserts", - "name": "Stout-Infused Chocolate Brownie", - "description": "Warm Valrhona dark chocolate brownie baked with our signature oatmeal stout, served with Madagascar vanilla bean ice cream and salted bourbon caramel.", - "price": 345, - "pairing": "Imperial Stout", - "tag": "Dessert", - "image": "/images/menu-stout-brownie.jpg", - "sort": 7 - }, - { - "uid": "item-tarte-tatin", - "category": "desserts", - "name": "Bourbon Apple Tarte Tatin", - "description": "Caramelized honey-crisp apples baked in flaky puff pastry, flambéed tableside with Kentucky bourbon, accompanied by clotted cream.", - "price": 395, - "pairing": "Belgian Tripel", - "tag": "Dessert", - "image": "/images/menu-tarte-tatin.jpg", - "sort": 8 - } - ], "pairings": [ { "uid": "pairing-hop-heat", diff --git a/app/src/data/menu-data.mjs b/app/src/data/menu-data.mjs new file mode 100644 index 0000000..a647128 --- /dev/null +++ b/app/src/data/menu-data.mjs @@ -0,0 +1,447 @@ +/** + * The Great Bear food menu — single source of truth. + * + * Used by scripts/update-menu.mjs (PocketBase sync) and the site's offline + * fallback (imported by src/lib/pb.ts). Menu content is managed here in code; + * edit this file, commit, then run scripts/update-menu.mjs to refresh the + * live PocketBase data. + * + * group: 'veg' | 'nonveg' | '' (mixed / not applicable) + * price: text so ranges like "449/549" work + */ + +export const MENU_CATEGORIES = [ + { uid: 'cat-brew-bites', slug: 'brew-bites', name: 'Brew Bites', sort: 1 }, + { uid: 'cat-sharing-plates', slug: 'sharing-plates', name: 'Sharing Plates', sort: 2 }, + { uid: 'cat-salads', slug: 'salads', name: 'Salads', sort: 3 }, + { uid: 'cat-platters', slug: 'platters', name: 'Platters', sort: 4 }, + { uid: 'cat-international', slug: 'international-appetizers', name: 'International Appetizers', sort: 5 }, + { uid: 'cat-indian', slug: 'indian-appetizers', name: 'Indian Appetizers', sort: 6 }, + { uid: 'cat-main-course', slug: 'main-course', name: 'Main Course', sort: 7 }, + { uid: 'cat-curries', slug: 'curries', name: 'Curries', sort: 8 }, + { uid: 'cat-biryani-rice-breads', slug: 'biryanis-rice-breads', name: 'Biryanis, Rice & Breads', sort: 9 }, + { uid: 'cat-desserts', slug: 'desserts', name: 'Desserts', sort: 10 }, + { uid: 'cat-pizzas', slug: 'pizzas', name: 'Pizzas', sort: 11 }, +] + +let sortCounter = 0 +function item(category, name, price, description, { group = '', tag = '' } = {}) { + sortCounter += 1 + const slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + return { + uid: `menu-${category}-${slug}`, + category, + name, + description, + price, + pairing: '', + tag, + image: '', + group, + sort: sortCounter, + } +} + +export const MENU_ITEMS = [ + // ---------------------------------------------------------------- Brew Bites + item('brew-bites', 'Chana Chaat', '399', + 'Cooked white chickpeas tossed with onion, tomato, herbs, sweet & spicy chutney & aromatic spices', + { group: 'veg' }), + item('brew-bites', 'French Fries', '389', + 'Fries tossed in your flavour', + { group: 'veg', tag: 'Classic / Gunpowder / Peri-Peri' }), + item('brew-bites', 'Garlic Bread', '239/399', + 'House bake', + { group: 'veg', tag: 'Plain / With Cheese' }), + item('brew-bites', 'Onion Rings', '419', + 'Deep fried onion rings + thousand island', + { group: 'veg' }), + item('brew-bites', 'Peanut Papad Masala', '389', + 'Roasted peanuts tossed with onion, tomato, lemon, fresh coriander & aromatic spices', + { group: 'veg' }), + item('brew-bites', 'Steamed / Spicy Edamame', '389', + 'Edamame in the pod steamed & salted / tempered with garlic & chili', + { group: 'veg', tag: 'Steamed / Spicy' }), + item('brew-bites', 'Cranberry Sev Puri', '', + 'Crispy papdi, spiced potato, chana, sweet-sour cranberry chutney, sweet curd & nylon sev', + { group: 'veg', tag: 'Coming Soon' }), + item('brew-bites', 'Chicken & Cheese Bites', '459', + 'Chicken minced and cheese mix, fried to perfection + chipotle dip', + { group: 'nonveg' }), + + // ------------------------------------------------------------ Sharing Plates + item('sharing-plates', 'Hummus Pita Platter', '449/549', + 'Classic, roasted bell pepper & black beans-cumin hummus, pita, sesame naan & za’atar crostini + arabic pickle', + { tag: 'Veg / Chicken' }), + item('sharing-plates', 'Ultimate Nachos', '599/699', + 'Corn tortilla, beans sauce, pico-de-gallo, cheese sauce, tomato salsa & sour cream', + { tag: 'Veg / Chicken' }), + + // ------------------------------------------------------------------- Salads + item('salads', 'Avocado & Egg Salad', '509', + 'Mixed greens, green beans, cherry tomato, olives, pumpkin seeds + malta vinaigrette', + { group: 'nonveg' }), + item('salads', 'Crispy Lotus Stem & Chickpea Salad', '429', + 'Lettuce, bell peppers, corns, chickpeas, crispy lotus stem + citrus vinaigrette', + { group: 'veg' }), + item('salads', 'Caesar Salad', '449', + 'Iceberg, romaine, parmesan shavings, croutons + veg caesar dressing', + { tag: 'Veg / Chicken' }), + item('salads', 'Chicken Tikka Niçoise Salad', '499', + 'Mixed greens, pulled chicken tikka, boiled egg, potato cubes, green beans, olives + lemon vinaigrette', + { group: 'nonveg' }), + item('salads', 'Quinoa Health Freak Salad', '499/529', + 'Quinoa, lettuce, cucumber, cherry tomato, olives, broccoli + apple cider dressing', + { tag: 'Veg / Chicken' }), + item('salads', 'Roasted Beetroot & Walnut Salad', '449', + 'Mixed lettuce, roasted beet, broccoli, orange segments, cherry tomatoes & feta + balsamic dressing', + { group: 'veg' }), + item('salads', 'Warm Vegetable Salad', '469', + 'Warm exotic vegetables tossed with mustard dressing, topped with pumpkin seeds & roasted almond', + { group: 'veg' }), + + // ----------------------------------------------------------------- Platters + item('platters', 'Chelo Kebab Platter', '1069', + 'Basil chicken seekh, za’atar mutton seekh, chicken shish taouk, sumac chicken breast, saffron rice + tzatziki', + { group: 'nonveg' }), + item('platters', 'Chicken Tikka Platter', '1159', + 'Herb-pepper, malai, parmesan chicken, angara, lehsuni', + { group: 'nonveg' }), + item('platters', 'Mezze Platter Veg', '899', + 'Pan seared cottage cheese, falafel, spinach-ricotta roll, tabbouleh, pita bread, arabic pickle + hummus, labneh, chilli dip', + { group: 'veg' }), + item('platters', 'Mezze Platter Non-Veg', '1159', + 'Cajun fish, peri-peri chicken, lamb roll, chicken kafta, arabic pickle, pita bread + hummus, labneh, chilli dip', + { group: 'nonveg' }), + item('platters', 'Tandoori Veg Platter', '999', + 'Achari paneer tikka, hari mirch paneer tikka, tandoori khumb, roasted pineapple, tandoori soya chaap, quinoa veg seekh, sprouted moong kebab', + { group: 'veg' }), + item('platters', 'Seafood Fritters Platter', '1299', + 'Batter fried prawns, ajwain fish fry, fish fingers, mixed seafood fritters + tartar sauce, mint mayo', + { group: 'nonveg' }), + item('platters', 'Tandoori Meat Platter', '1299', + 'Tandoori prawns, tandoori fish tikka, mutton seekh, tandoori chicken, angara chicken tikka, parmesan chicken tikka, chicken seekh', + { group: 'nonveg' }), + item('platters', 'Sizzler', '699/799', + 'Choose your starch (fried rice / noodles) & your sauce (schezwan / sweet n sour); served with soya sesame vegetables, grilled pineapple slice & fries', + { tag: 'Cottage Cheese / Manchurian / Chicken' }), + + // ------------------------------------------------- International Appetizers + item('international-appetizers', 'Apple & Avocado Salsa', '439', + 'Granny smith apple, khakhra crisp and melba toast', + { group: 'veg' }), + item('international-appetizers', 'Mushroom Pepper Fry', '549', + 'Mushroom cooked with black pepper fry masala + Malabari parotta', + { group: 'veg' }), + item('international-appetizers', 'Corn Salt N Pepper', '519', + 'Crispy fried corn, onions, spring onions', + { group: 'veg' }), + item('international-appetizers', 'Chilli Paneer / Chilli Mushroom', '549', + 'Crispy fried cubes, onion, capsicum, bell peppers + chilli paste', + { group: 'veg', tag: 'Paneer / Mushroom' }), + item('international-appetizers', 'Cottage Cheese Quesadilla', '579', + 'Flour tortilla, mix cheese, beans + tomato salsa, sour cream', + { group: 'veg' }), + item('international-appetizers', 'Falafel', '439', + 'Arabic pickle, chickpea patties + hummus & pita', + { group: 'veg' }), + item('international-appetizers', 'Honey Chilly Cauliflower / Potato', '449', + 'Crispy fried cauliflower/potato, sesame seeds, honey chili sauce', + { group: 'veg', tag: 'Cauliflower / Potato' }), + item('international-appetizers', 'Korean Fried Paneer', '549', + 'Crispy fried paneer tossed in umami Korean sauce', + { group: 'veg' }), + item('international-appetizers', 'Peri-Peri Quinoa Popcorn', '449', + 'Paneer cubes, quinoa crusted + peri peri dust and roasted garlic aioli', + { group: 'veg' }), + item('international-appetizers', 'Exotic Vegetables', '439', + 'Stir fry vegetables with garlic and butter', + { group: 'veg' }), + item('international-appetizers', 'Thai Chilli Crispy Lotus Stems', '479', + 'Crispy fried lotus stems in chilli sauce, spring onion & roasted sesame seeds', + { group: 'veg' }), + item('international-appetizers', 'Veg Manchurian', '449', + 'Fried vegetable balls in sweet & sour, hot manchurian sauce', + { group: 'veg' }), + item('international-appetizers', 'Veg Cigar Roll', '449', + 'Crispy fried cigar rolls stuffed with cheese & fresh vegetables, pickled salad & spicy remoulade', + { group: 'veg' }), + item('international-appetizers', 'Boneless Chicken Wings', '629', + 'Juicy crispy boneless chicken breast + classic hot sauce, ranch dip', + { group: 'nonveg' }), + item('international-appetizers', 'BBQ Pork Ribs', '899', + 'Tender slow cooked BBQ pork ribs served with crispy french fries', + { group: 'nonveg' }), + item('international-appetizers', 'Chicken Quesadilla', '719', + 'Flour tortillas filled with chicken & cheese, mexican rice + pico de gallo, sour cream', + { group: 'nonveg' }), + item('international-appetizers', 'Chilli Chicken', '639', + 'Crispy fried boneless chicken, onion, bell peppers + chilli paste', + { group: 'nonveg' }), + item('international-appetizers', 'Chicken Wings', '599', + 'Crispy fried to perfection', + { group: 'nonveg', tag: 'BBQ / Hot Sauce / Peri-Peri / Grandma Style' }), + item('international-appetizers', 'Chilly Garlic Prawns', '889', + 'Spicy prawns tossed in hot garlic sauce', + { group: 'nonveg' }), + item('international-appetizers', 'Fish and Chips', '799/949', + 'Crispy battered fish served with golden fries + egg tartar sauce, ponzu dip', + { group: 'nonveg', tag: 'Basa / Sole' }), + item('international-appetizers', 'Korean Fried Chicken', '639', + 'Crispy fried chicken tossed in umami Korean sauce', + { group: 'nonveg' }), + item('international-appetizers', 'Moroccan Grilled Chicken', '659', + 'Arabic spice, hung curd, served with grilled veggies + creamy labneh', + { group: 'nonveg' }), + item('international-appetizers', 'Kaffir Lime-Coconut Fish Fillet', '799/949', + 'Thai flavour infused fish with spicy coconut curry', + { group: 'nonveg', tag: 'Basa / Sole' }), + item('international-appetizers', 'Schezwan Chicken Lollipop', '599', + 'Crispy oriental style chicken wings tossed in fiery schezwan sauce', + { group: 'nonveg' }), + item('international-appetizers', 'Spicy Chicken Strips', '599', + 'Spicy strips + sweet chilli sauce', + { group: 'nonveg' }), + + // -------------------------------------------------------- Indian Appetizers + item('indian-appetizers', 'Quinoa Seekh Kebab', '429', + 'Minced vegetables, paneer & quinoa seekh kebab + mint sauce', + { group: 'veg' }), + item('indian-appetizers', 'Sprouted Moong Kebab', '429', + 'Green sprouted moong, fresh coriander, spices & shallow fry + mint sauce', + { group: 'veg' }), + item('indian-appetizers', 'Dahi Kebab', '449', + 'Hung curd, onion, bell pepper, coriander seeds, seasoning', + { group: 'veg' }), + item('indian-appetizers', 'Prune Stuffed Paneer Tikka', '559', + 'Khata meeta prunes stuffed paneer, marinated with tandoori masala + mint sauce', + { group: 'veg' }), + item('indian-appetizers', 'Molten Beetroot & Peanut Kebab', '449', + 'Crumb fried beetroot kebab stuffed with cheese + scallion cream', + { group: 'veg' }), + item('indian-appetizers', 'Paneer Tikka', '549', + 'Paneer marinated with spices, cooked in tandoor, served hot + mint sauce', + { group: 'veg', tag: 'Malai / Hari Mirch / Angara' }), + item('indian-appetizers', 'Sweet Potato & Goat Cheese Tikki', '449', + 'Cranberry-sesame chutney, red chilli-green apple salsa', + { group: 'veg' }), + item('indian-appetizers', 'Roasted Pineapple', '449', + 'Roasted in clay oven with Indian spices + mint sauce', + { group: 'veg' }), + item('indian-appetizers', 'Tandoori Khumb', '529', + 'Spicy mushrooms + mint sauce', + { group: 'veg' }), + item('indian-appetizers', 'Tandoori Soya Chaap', '459', + 'Soya chaap cooked in clay oven + mint sauce', + { group: 'veg', tag: 'Malai / Angara' }), + item('indian-appetizers', 'Maharashtrian Thecha Paneer', '549', + 'Green chili thecha tossed paneer cubes + Malabari paratha', + { group: 'veg' }), + item('indian-appetizers', 'Amritsari Fish Fry', '729/869', + 'Gram flour batter fried fish + mint chutney', + { group: 'nonveg', tag: 'Basa / Sole' }), + item('indian-appetizers', 'Chicken Tikka', '639', + 'Marinated chicken, cooked in tandoor — choose your flavour', + { group: 'nonveg', tag: 'Malai / Parmesan / Lehsuni / Angara / Herb-Pepper' }), + item('indian-appetizers', 'Chicken Seekh', '549', + 'Marinated minced chicken, Indian herbs, spices + mint sauce', + { group: 'nonveg' }), + item('indian-appetizers', 'Fish Tikka', '729/869', + 'Marinated fish cooked in clay oven + mint sauce', + { group: 'nonveg', tag: 'Basa / Sole • Angara / Kasundi' }), + item('indian-appetizers', 'Grilled Tawa Fish', '729/869', + 'Fish marinated in Indian spices, pan grilled to perfection + mint sauce', + { group: 'nonveg', tag: 'Basa / Sole' }), + item('indian-appetizers', 'Mutton Chapli Kebab (1/4 kg)', '759', + 'Minced mutton, chapli masala, cooked to perfection + rogan josh jus', + { group: 'nonveg' }), + item('indian-appetizers', 'Mutton Seekh Kebab', '729', + 'Marinated minced lamb seasoned with Indian herbs & spices', + { group: 'nonveg' }), + item('indian-appetizers', 'Tandoori Chicken', '559/889', + 'Chicken on the bone in cultured yogurt and spices', + { group: 'nonveg' }), + item('indian-appetizers', 'Tandoori Wings', '579', + 'Chicken wings in cultured yogurt and spices', + { group: 'nonveg' }), + item('indian-appetizers', 'Prawns Masala', '949', + 'Spicy onion, tomato masala with curry leaves & mustard, served with uttapam', + { group: 'nonveg' }), + item('indian-appetizers', 'Tawa Masala', '549/779', + 'Punjabi style meat on bone, tawa masala & ground spices', + { group: 'nonveg', tag: 'Chicken / Mutton' }), + item('indian-appetizers', 'Nimbu Hari Mirch ki Tangari', '579', + 'Chicken drumstick, spicy hari mirch nimbu marinated, cooked to perfection + mint sauce', + { group: 'nonveg' }), + + // -------------------------------------------------------------- Main Course + item('main-course', 'Cottage Cheese Steak', '629', + 'Herb quinoa, creamy spinach-corn, sautéed vegetables + roasted pepper coulis', + { group: 'veg' }), + item('main-course', 'Cajun Grilled Fish', '769/899', + 'Quinoa, sautéed vegetables, guacamole', + { group: 'nonveg', tag: 'Basa / Sole' }), + item('main-course', 'Roasted Garlic & Basil Chicken Thigh', '699', + 'Garlic & basil chicken thigh, brown onion pilaf, butter broccoli & spicy pepper coulis', + { group: 'nonveg' }), + item('main-course', 'Grilled Chicken Breast', '699', + 'Mash potato, sautéed vegetables', + { group: 'nonveg', tag: 'Classic Chicken Jus / Hot Peri Peri' }), + item('main-course', 'Penne Arrabiata', '589/639', + 'Pasta with exotic vegetables, basil, chilli flakes, chunky tomatoes + garlic bread', + { tag: 'Veg / Chicken' }), + item('main-course', 'Penne Alfredo', '599/649', + 'With exotic vegetables, creamy cheesy sauce + garlic bread', + { tag: 'Veg / Chicken' }), + item('main-course', 'Penne Salsa Rosa', '599/649', + 'Penne in mix sauce, exotic vegetables + garlic bread', + { tag: 'Veg / Chicken' }), + item('main-course', 'Pan Seared Salmon', '1259', + 'Creamy mashed potato, grilled vegetables + lemon caper sauce', + { group: 'nonveg' }), + item('main-course', 'Spaghetti Aglio e Olio', '549/599', + 'Olive oil, garlic, olives, chili flakes + garlic bread', + { tag: 'Veg / Chicken' }), + item('main-course', 'Chow Mein', '399/429/479', + 'Wok tossed noodles with soy, vinegar & chili sauce', + { tag: 'Veg / Egg / Chicken' }), + item('main-course', 'Fried Rice', '399/429/479', + 'Wok tossed rice with soy, vinegar & chili sauce', + { tag: 'Veg / Egg / Chicken' }), + item('main-course', 'Green / Red Thai Curry', '579/629/659', + 'Veggies cooked in creamy coconut gravy, curry paste + steamed rice', + { tag: 'Veg / Chicken / Prawns' }), + item('main-course', 'Chili Garlic Noodles', '429', + 'Noodles tossed with authentic chili-garlic sauce, spring onion and soya', + { group: 'veg' }), + + // ------------------------------------------------------------------ Curries + item('curries', 'Dal Tadka / Palak Dal / Lasooni Dal', '399', + 'Mix of moong & masoor dal, as you wish', + { group: 'veg', tag: 'Tadka / Palak / Lasooni' }), + item('curries', 'Dal Makhani', '479', + 'Overnight slow cooked black urad dal, tempered with kasoori methi, cream & butter', + { group: 'veg' }), + item('curries', 'Kasoori Subz Milani', '519', + 'Melange of seasonal vegetables tossed in onion masala, dried fenugreek', + { group: 'veg' }), + item('curries', 'Kadhai / Makhani Paneer', '599', + 'Cottage cheese, capsicum, onion & kadhai masala / creamy tomato gravy', + { group: 'veg', tag: 'Kadhai / Makhani' }), + item('curries', 'Methi Palak Paneer', '569', + 'Cubes of cottage cheese, spinach & fenugreek', + { group: 'veg' }), + item('curries', 'Paneer Lababdar', '619', + 'Cubes of cottage cheese, bell pepper, onions & capsicum', + { group: 'veg' }), + item('curries', 'Mushroom Hara Pyaz', '579', + 'Mushroom & hara pyaz tossed in chopped masala', + { group: 'veg' }), + item('curries', 'Punjabi Chana Masala', '459', + 'White chickpeas with fragrant herbs & spices', + { group: 'veg' }), + item('curries', 'Butter Chicken', '779', + 'Chicken on bone + creamy tomato gravy', + { group: 'nonveg' }), + item('curries', 'Egg Curry', '519', + 'Hard boiled eggs in Indian gravy', + { group: 'nonveg' }), + item('curries', 'Punjabi Fish Curry', '759/859/899', + 'Choice of protein cooked in spicy tangy, onion tomato curry', + { group: 'nonveg', tag: 'Basa / Sole / Prawns' }), + item('curries', 'Home Style Chicken Curry', '659', + 'Chicken on bone + home style gravy', + { group: 'nonveg' }), + item('curries', 'Kadhai Chicken', '779', + 'Chicken on bone + kadhai masala', + { group: 'nonveg' }), + item('curries', 'Mutton Rogan Josh', '829', + 'Tender mutton on the bone, Kashmiri style', + { group: 'nonveg' }), + item('curries', 'Kulcha with Curry', '299/499/599', + 'Fresh kulcha served with your choice of curry', + { tag: 'Paneer / Chicken / Mutton' }), + + // --------------------------------------------------- Biryanis, Rice & Breads + item('biryanis-rice-breads', 'Chicken Biryani', '779', + 'Aromatic basmati rice cooked in saffron & spices + mix raita', + { group: 'nonveg' }), + item('biryanis-rice-breads', 'Egg Biryani', '639', + 'Aromatic basmati rice cooked in saffron & spices + mix raita', + { group: 'nonveg' }), + item('biryanis-rice-breads', 'Mutton Biryani', '889', + 'Aromatic basmati rice cooked in saffron & spices + mix raita', + { group: 'nonveg' }), + item('biryanis-rice-breads', 'Vegetable Biryani', '599', + 'Aromatic basmati rice cooked in saffron & spices + mix raita', + { group: 'veg' }), + item('biryanis-rice-breads', 'Jeera Rice', '269', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Steamed Rice', '239', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Dhungari Burani Raita', '199', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Masala Boondi Raita', '189', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Onion, Tomato, Cucumber Raita', '189', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Pudina Raita', '189', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Butter Roti', '79', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Butter Naan', '99', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Garlic / Coriander Naan', '109', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Kachi Haldi Naan', '109', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Laccha / Pudina Parantha', '99', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Missi Roti', '99', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Onion Kulcha', '109', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Paneer Kulcha', '179', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Tandoori Naan', '89', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Tandoori Roti', '69', '', { group: 'veg' }), + item('biryanis-rice-breads', 'Khamiri Roti', '79', '', { group: 'veg' }), + + // ----------------------------------------------------------------- Desserts + item('desserts', 'Baked Cheesecake', '419', + 'Fresh berries compote', + { group: 'veg' }), + item('desserts', 'Churros', '299', + 'Chocolate sauce, caramel sauce & whipped cream', + { group: 'veg' }), + item('desserts', 'Dark Chocolate Brownie with Ice Cream', '339', + 'Brownie topped with vanilla ice cream + chocolate sauce', + { group: 'veg' }), + + // ------------------------------------------------------------------- Pizzas + item('pizzas', 'Burnt Garlic Mushroom Pizza', '589', + 'Sautéed mushrooms, burnt garlic, onions', + { group: 'veg' }), + item('pizzas', 'Garden Fresh Pizza', '579', + 'American corn, onion, tomato, capsicum & jalapeno', + { group: 'veg' }), + item('pizzas', 'Exotic Pizza', '599', + 'Broccoli, zucchini, bell pepper, olives & baby corn', + { group: 'veg' }), + item('pizzas', 'Margherita Pizza', '559', + 'Mozzarella cheese & fresh basil', + { group: 'veg' }), + item('pizzas', 'Paneer Tikka Pizza', '599', + 'Paneer tikka, onion, tomato, capsicum', + { group: 'veg' }), + item('pizzas', 'Sundried-Tomato & Olives Pizza', '589', + 'Homemade sundried tomato, caper & fresh basil', + { group: 'veg' }), + item('pizzas', 'BBQ Pulled Chicken Pizza', '719', + 'Slow cooked pulled chicken, BBQ sauce & jalapeno', + { group: 'nonveg' }), + item('pizzas', 'Chicken Overload Pizza', '729', + 'Chicken tikka, BBQ chicken, herb chicken, onion', + { group: 'nonveg' }), + item('pizzas', 'Chicken Tikka Pizza', '719', + 'Chicken tikka, onion, tomato & capsicum', + { group: 'nonveg' }), + item('pizzas', 'Meat Lover Pizza', '729', + 'Mutton seekh, tikka chicken & chicken salami', + { group: 'nonveg' }), + item('pizzas', 'Pepperoni Pizza', '769', + 'Pork pepperoni, jalapeno, olives & fresh basil', + { group: 'nonveg' }), + item('pizzas', 'Peri-Peri Chicken Pizza', '729', + 'Peri peri marinated chicken, olives & pickled onion', + { group: 'nonveg' }), +] diff --git a/app/src/lib/pb.ts b/app/src/lib/pb.ts index 8e59b4d..ad5d2c4 100644 --- a/app/src/lib/pb.ts +++ b/app/src/lib/pb.ts @@ -7,6 +7,9 @@ * content so the site still renders. */ import seed from '~/data/content.json' +// The food menu lives in its own module (shared with scripts/update-menu.mjs) +// instead of content.json, since it is synced by its own script. +import { MENU_CATEGORIES, MENU_ITEMS } from '~/data/menu-data.mjs' import type { Brew, Experience, @@ -66,11 +69,14 @@ export async function fetchTestimonials(): Promise { } export async function fetchMenuCategories(): Promise { - return orFallback(await pbList('menu_categories'), seed.menu_categories) + return orFallback( + await pbList('menu_categories'), + MENU_CATEGORIES as MenuCategory[], + ) } export async function fetchMenuItems(): Promise { - return orFallback(await pbList('menu_items'), seed.menu_items) + return orFallback(await pbList('menu_items'), MENU_ITEMS as MenuItem[]) } export async function fetchPairings(): Promise { diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index a1c5f45..46e392e 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -70,10 +70,13 @@ export interface MenuItem { category: string name: string description: string - price: number + /** Text so price ranges like "449/549" (Veg/Chicken) work. */ + price: string pairing: string tag: string image: string + /** 'veg' | 'nonveg' | '' — drives the dietary badge on the menu card. */ + group: string sort: number } diff --git a/pb/migrations/1788500000_menu_items_v2.js b/pb/migrations/1788500000_menu_items_v2.js new file mode 100644 index 0000000..1512592 --- /dev/null +++ b/pb/migrations/1788500000_menu_items_v2.js @@ -0,0 +1,70 @@ +/// +migrate( + (app) => { + // The real food menu uses text prices ("449/549" for Veg/Chicken variants) + // and needs a veg/non-veg badge field. The previous menu_items collection + // only held placeholder content, so we recreate it with the final schema. + try { + const oldCollection = app.findCollectionByNameOrId('menu_items') + app.delete(oldCollection) + } catch (e) { + // collection may not exist on a fresh database — nothing to migrate + } + + const collection = new Collection({ + name: 'menu_items', + type: 'base', + listRule: '', + viewRule: '', + createRule: null, + updateRule: null, + deleteRule: null, + fields: [ + { name: 'uid', type: 'text', required: true }, + { name: 'category', type: 'text', required: true }, + { name: 'name', type: 'text', required: true }, + { name: 'description', type: 'text' }, + { name: 'price', type: 'text' }, + { name: 'pairing', type: 'text' }, + { name: 'tag', type: 'text' }, + { name: 'image', type: 'text' }, + { name: 'group', type: 'text' }, + { name: 'sort', type: 'number' }, + ], + indexes: ['CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)'], + }) + return app.save(collection) + }, + (app) => { + // Revert to the original placeholder schema. + try { + const newCollection = app.findCollectionByNameOrId('menu_items') + app.delete(newCollection) + } catch (e) { + // nothing to revert + } + + const collection = new Collection({ + name: 'menu_items', + type: 'base', + listRule: '', + viewRule: '', + createRule: null, + updateRule: null, + deleteRule: null, + fields: [ + { name: 'uid', type: 'text', required: true }, + { name: 'category', type: 'text', required: true }, + { name: 'name', type: 'text', required: true }, + { name: 'description', type: 'text' }, + { name: 'price', type: 'number' }, + { name: 'pairing', type: 'text' }, + { name: 'tag', type: 'text' }, + { name: 'image', type: 'text' }, + { name: 'sort', type: 'number' }, + ], + indexes: ['CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)'], + }) + return app.save(collection) + } +) diff --git a/scripts/update-menu.mjs b/scripts/update-menu.mjs new file mode 100755 index 0000000..0708e0f --- /dev/null +++ b/scripts/update-menu.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +/** + * Menu sync — replaces the PocketBase menu with app/src/data/menu-data.mjs. + * + * Deletes every record in menu_categories / menu_items and inserts the + * current food menu from code. Nothing else is touched. + * + * Works against any PocketBase instance (local dev or production): + * + * # local dev + * PB_URL=http://127.0.0.1:8090 PB_SUPERUSER_EMAIL=dev@greatbear.in \ + * PB_SUPERUSER_PASSWORD=devpass12345 node scripts/update-menu.mjs + * + * # production (admin domain serves the PocketBase API) + * PB_URL=https://admin.greatbear.in PB_SUPERUSER_EMAIL=admin@greatbear.in \ + * PB_SUPERUSER_PASSWORD=... node scripts/update-menu.mjs + */ +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +const { MENU_CATEGORIES, MENU_ITEMS } = await import( + new URL(`file://${join(here, '../app/src/data/menu-data.mjs')}`) +) + +const PB_URL = (process.env.PB_URL ?? 'http://127.0.0.1:8090').replace(/\/$/, '') +const EMAIL = process.env.PB_SUPERUSER_EMAIL +const PASSWORD = process.env.PB_SUPERUSER_PASSWORD + +if (!EMAIL || !PASSWORD) { + console.error( + '[menu] PB_SUPERUSER_EMAIL and PB_SUPERUSER_PASSWORD are required\n' + + ' e.g. PB_URL=https://admin.greatbear.in PB_SUPERUSER_EMAIL=admin@greatbear.in \\\n' + + ' PB_SUPERUSER_PASSWORD=... node scripts/update-menu.mjs', + ) + process.exit(1) +} + +async function auth() { + const res = await fetch(`${PB_URL}/api/collections/_superusers/auth-with-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identity: EMAIL, password: PASSWORD }), + }) + if (!res.ok) throw new Error(`superuser auth failed against ${PB_URL} (HTTP ${res.status})`) + const json = await res.json() + return json.token +} + +async function listAll(collection, token) { + const records = [] + let page = 1 + for (;;) { + const res = await fetch( + `${PB_URL}/api/collections/${collection}/records?sort=sort&perPage=200&page=${page}`, + { headers: { Authorization: token } }, + ) + if (!res.ok) throw new Error(`list ${collection} failed (HTTP ${res.status})`) + const json = await res.json() + records.push(...json.items) + if (json.items.length < json.perPage || page >= json.totalPages) break + page += 1 + } + return records +} + +async function deleteRecord(collection, token, id) { + const res = await fetch(`${PB_URL}/api/collections/${collection}/records/${id}`, { + method: 'DELETE', + headers: { Authorization: token }, + }) + if (!res.ok && res.status !== 404) { + throw new Error(`delete ${collection}/${id} failed (HTTP ${res.status})`) + } +} + +async function createRecord(collection, token, body) { + const res = await fetch(`${PB_URL}/api/collections/${collection}/records`, { + method: 'POST', + headers: { Authorization: token, 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (!res.ok) { + const text = await res.text() + throw new Error(`create ${collection}/${body.uid} failed (HTTP ${res.status}): ${text}`) + } +} + +try { + const token = await auth() + console.log(`[menu] authenticated against ${PB_URL}`) + + for (const [collection, entries] of [ + ['menu_categories', MENU_CATEGORIES], + ['menu_items', MENU_ITEMS], + ]) { + const existing = await listAll(collection, token) + for (const record of existing) { + await deleteRecord(collection, token, record.id) + } + for (const entry of entries) { + await createRecord(collection, token, entry) + } + console.log(`[menu] ${collection}: -${existing.length} old, +${entries.length} new`) + } + + console.log('[menu] done — the live menu now matches app/src/data/menu-data.mjs') +} catch (error) { + console.error(`[menu] failed: ${error.message}`) + process.exit(1) +}