@zorgo/next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +19 -0
  2. package/dist/cli.d.mts +5 -0
  3. package/dist/cli.d.ts +5 -0
  4. package/dist/cli.js +2 -0
  5. package/dist/cli.mjs +2 -0
  6. package/dist/components.d.mts +337 -0
  7. package/dist/components.d.ts +337 -0
  8. package/dist/components.js +1996 -0
  9. package/dist/components.js.map +1 -0
  10. package/dist/components.mjs +1962 -0
  11. package/dist/components.mjs.map +1 -0
  12. package/dist/index.d.mts +305 -0
  13. package/dist/index.d.ts +305 -0
  14. package/dist/index.js +2 -0
  15. package/dist/index.mjs +2 -0
  16. package/dist/seo.d.mts +30 -0
  17. package/dist/seo.d.ts +30 -0
  18. package/dist/seo.js +82 -0
  19. package/dist/seo.js.map +1 -0
  20. package/dist/seo.mjs +56 -0
  21. package/dist/seo.mjs.map +1 -0
  22. package/dist/slugs.d.mts +1 -0
  23. package/dist/slugs.d.ts +1 -0
  24. package/dist/slugs.js +47 -0
  25. package/dist/slugs.js.map +1 -0
  26. package/dist/slugs.mjs +19 -0
  27. package/dist/slugs.mjs.map +1 -0
  28. package/dist/tree/.env.gen.ts +27 -0
  29. package/dist/tree/README.md +0 -0
  30. package/dist/tree/app/api/zorgo-token/route.ts +11 -0
  31. package/dist/tree/app/apple-icon.tsx.gen.ts +54 -0
  32. package/dist/tree/app/checkout/page.tsx +10 -0
  33. package/dist/tree/app/components/Footer.tsx +50 -0
  34. package/dist/tree/app/components/Header.tsx +128 -0
  35. package/dist/tree/app/components/ModalHost.tsx +56 -0
  36. package/dist/tree/app/components/ModalRegistry.tsx +113 -0
  37. package/dist/tree/app/components/TestCard.tsx +35 -0
  38. package/dist/tree/app/components/index.ts +3 -0
  39. package/dist/tree/app/error.tsx +10 -0
  40. package/dist/tree/app/global-error.tsx +12 -0
  41. package/dist/tree/app/globals.css.gen.ts +123 -0
  42. package/dist/tree/app/icon.tsx.gen.ts +87 -0
  43. package/dist/tree/app/layout.tsx.gen.ts +55 -0
  44. package/dist/tree/app/locations/page.tsx +30 -0
  45. package/dist/tree/app/manifest.ts.gen.ts +39 -0
  46. package/dist/tree/app/menu/[slug]/ItemCustomizationClient.tsx +18 -0
  47. package/dist/tree/app/menu/[slug]/page.tsx +78 -0
  48. package/dist/tree/app/menu/page.tsx +36 -0
  49. package/dist/tree/app/not-found.tsx +8 -0
  50. package/dist/tree/app/opengraph-image.tsx.gen.ts +82 -0
  51. package/dist/tree/app/page.tsx +19 -0
  52. package/dist/tree/app/privacy/page.tsx.gen.ts +15 -0
  53. package/dist/tree/app/robots.ts +9 -0
  54. package/dist/tree/app/sitemap.ts +26 -0
  55. package/dist/tree/app/terms/page.tsx.gen.ts +16 -0
  56. package/dist/tree/gitignore +44 -0
  57. package/dist/tree/lib/zorgoSiteToken.ts +20 -0
  58. package/dist/tree/next.config.ts +14 -0
  59. package/dist/tree/open-next.config.ts +4 -0
  60. package/dist/tree/package.json.gen.ts +24 -0
  61. package/dist/tree/postcss.config.mjs +7 -0
  62. package/dist/tree/scripts/prebuild.ts +448 -0
  63. package/dist/tree/tsconfig.json +35 -0
  64. package/package.json +72 -0
package/dist/seo.d.mts ADDED
@@ -0,0 +1,30 @@
1
+ import { Menu } from '@zorgo/universal/utils/menuUtils';
2
+ import { MenuItem } from '@zorgo/universal/interfaces';
3
+
4
+ /** Franchise-level facts for the Restaurant node. Everything but `name` is
5
+ * optional so the schema degrades gracefully when a field is missing. */
6
+ interface RestaurantInfo {
7
+ name: string;
8
+ url?: string;
9
+ image?: string;
10
+ description?: string;
11
+ servesCuisine?: string[];
12
+ }
13
+ /**
14
+ * Builds a standalone schema.org JSON-LD object for a single menu item, for
15
+ * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`
16
+ * (the same node shape used inside the Restaurant menu) and adds the
17
+ * `@context` a top-level node needs. Pure — safe to run at build time.
18
+ */
19
+ declare function buildMenuItemJsonLd(item: MenuItem): {
20
+ "@context": string;
21
+ };
22
+ /**
23
+ * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and
24
+ * franchise info. Categories become MenuSections; the items in each become
25
+ * MenuItems. Pure — same inputs, same output — so it can run at prebuild time
26
+ * and be embedded straight into the site's <head>.
27
+ */
28
+ declare function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo): Record<string, unknown>;
29
+
30
+ export { type RestaurantInfo, buildMenuItemJsonLd, buildRestaurantJsonLd };
package/dist/seo.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Menu } from '@zorgo/universal/utils/menuUtils';
2
+ import { MenuItem } from '@zorgo/universal/interfaces';
3
+
4
+ /** Franchise-level facts for the Restaurant node. Everything but `name` is
5
+ * optional so the schema degrades gracefully when a field is missing. */
6
+ interface RestaurantInfo {
7
+ name: string;
8
+ url?: string;
9
+ image?: string;
10
+ description?: string;
11
+ servesCuisine?: string[];
12
+ }
13
+ /**
14
+ * Builds a standalone schema.org JSON-LD object for a single menu item, for
15
+ * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`
16
+ * (the same node shape used inside the Restaurant menu) and adds the
17
+ * `@context` a top-level node needs. Pure — safe to run at build time.
18
+ */
19
+ declare function buildMenuItemJsonLd(item: MenuItem): {
20
+ "@context": string;
21
+ };
22
+ /**
23
+ * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and
24
+ * franchise info. Categories become MenuSections; the items in each become
25
+ * MenuItems. Pure — same inputs, same output — so it can run at prebuild time
26
+ * and be embedded straight into the site's <head>.
27
+ */
28
+ declare function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo): Record<string, unknown>;
29
+
30
+ export { type RestaurantInfo, buildMenuItemJsonLd, buildRestaurantJsonLd };
package/dist/seo.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/seo.ts
21
+ var seo_exports = {};
22
+ __export(seo_exports, {
23
+ buildMenuItemJsonLd: () => buildMenuItemJsonLd,
24
+ buildRestaurantJsonLd: () => buildRestaurantJsonLd
25
+ });
26
+ module.exports = __toCommonJS(seo_exports);
27
+ function priceString(cents) {
28
+ return (cents / 100).toFixed(2);
29
+ }
30
+ function variantOffer(variant) {
31
+ return {
32
+ "@type": "Offer",
33
+ price: priceString(variant.display_price ?? variant.price),
34
+ priceCurrency: "USD"
35
+ };
36
+ }
37
+ function menuItemNode(item) {
38
+ const node = {
39
+ "@type": "MenuItem",
40
+ name: item.name
41
+ };
42
+ if (item.description) node.description = item.description;
43
+ const offers = item.variants.map(variantOffer);
44
+ if (offers.length === 1) node.offers = offers[0];
45
+ else if (offers.length > 1) node.offers = offers;
46
+ return node;
47
+ }
48
+ function buildMenuItemJsonLd(item) {
49
+ return { "@context": "https://schema.org", ...menuItemNode(item) };
50
+ }
51
+ function buildRestaurantJsonLd(menu, info) {
52
+ const hasMenuSection = menu.categories.map((category) => {
53
+ const section = {
54
+ "@type": "MenuSection",
55
+ name: category.name
56
+ };
57
+ const items = menu.items.filter((item) => item.category_id === category.category_id).map(menuItemNode);
58
+ if (items.length) section.hasMenuItem = items;
59
+ return section;
60
+ });
61
+ const restaurant = {
62
+ "@context": "https://schema.org",
63
+ "@type": "Restaurant",
64
+ name: info.name
65
+ };
66
+ if (info.url) restaurant.url = info.url;
67
+ if (info.image) restaurant.image = info.image;
68
+ if (info.description) restaurant.description = info.description;
69
+ if (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;
70
+ restaurant.hasMenu = {
71
+ "@type": "Menu",
72
+ name: "Menu",
73
+ hasMenuSection
74
+ };
75
+ return restaurant;
76
+ }
77
+ // Annotate the CommonJS export names for ESM import in node:
78
+ 0 && (module.exports = {
79
+ buildMenuItemJsonLd,
80
+ buildRestaurantJsonLd
81
+ });
82
+ //# sourceMappingURL=seo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/seo.ts"],"sourcesContent":["import type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport type { ItemVariant, MenuItem } from \"@zorgo/universal/interfaces\";\n\n/** Franchise-level facts for the Restaurant node. Everything but `name` is\n * optional so the schema degrades gracefully when a field is missing. */\nexport interface RestaurantInfo {\n\tname: string;\n\turl?: string;\n\timage?: string;\n\tdescription?: string;\n\tservesCuisine?: string[];\n}\n\n// cents -> \"5.49\" (schema.org Offer.price is a string in major currency units)\nfunction priceString(cents: number): string {\n\treturn (cents / 100).toFixed(2);\n}\n\nfunction variantOffer(variant: ItemVariant) {\n\treturn {\n\t\t\"@type\": \"Offer\",\n\t\tprice: priceString(variant.display_price ?? variant.price),\n\t\tpriceCurrency: \"USD\",\n\t};\n}\n\nfunction menuItemNode(item: MenuItem) {\n\tconst node: Record<string, unknown> = {\n\t\t\"@type\": \"MenuItem\",\n\t\tname: item.name,\n\t};\n\tif (item.description) node.description = item.description;\n\t// one Offer for a single price, an array when the item has size/variant prices\n\tconst offers = item.variants.map(variantOffer);\n\tif (offers.length === 1) node.offers = offers[0];\n\telse if (offers.length > 1) node.offers = offers;\n\treturn node;\n}\n\n/**\n * Builds a standalone schema.org JSON-LD object for a single menu item, for\n * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`\n * (the same node shape used inside the Restaurant menu) and adds the\n * `@context` a top-level node needs. Pure — safe to run at build time.\n */\nexport function buildMenuItemJsonLd(item: MenuItem) {\n\treturn { \"@context\": \"https://schema.org\", ...menuItemNode(item) };\n}\n\n/**\n * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and\n * franchise info. Categories become MenuSections; the items in each become\n * MenuItems. Pure — same inputs, same output — so it can run at prebuild time\n * and be embedded straight into the site's <head>.\n */\nexport function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo) {\n\tconst hasMenuSection = menu.categories.map((category) => {\n\t\tconst section: Record<string, unknown> = {\n\t\t\t\"@type\": \"MenuSection\",\n\t\t\tname: category.name,\n\t\t};\n\t\tconst items = menu.items\n\t\t\t.filter((item) => item.category_id === category.category_id)\n\t\t\t.map(menuItemNode);\n\t\tif (items.length) section.hasMenuItem = items;\n\t\treturn section;\n\t});\n\n\tconst restaurant: Record<string, unknown> = {\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"Restaurant\",\n\t\tname: info.name,\n\t};\n\tif (info.url) restaurant.url = info.url;\n\tif (info.image) restaurant.image = info.image;\n\tif (info.description) restaurant.description = info.description;\n\tif (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;\n\trestaurant.hasMenu = {\n\t\t\"@type\": \"Menu\",\n\t\tname: \"Menu\",\n\t\thasMenuSection,\n\t};\n\n\treturn restaurant;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,SAAS,YAAY,OAAuB;AAC3C,UAAQ,QAAQ,KAAK,QAAQ,CAAC;AAC/B;AAEA,SAAS,aAAa,SAAsB;AAC3C,SAAO;AAAA,IACN,SAAS;AAAA,IACT,OAAO,YAAY,QAAQ,iBAAiB,QAAQ,KAAK;AAAA,IACzD,eAAe;AAAA,EAChB;AACD;AAEA,SAAS,aAAa,MAAgB;AACrC,QAAM,OAAgC;AAAA,IACrC,SAAS;AAAA,IACT,MAAM,KAAK;AAAA,EACZ;AACA,MAAI,KAAK,YAAa,MAAK,cAAc,KAAK;AAE9C,QAAM,SAAS,KAAK,SAAS,IAAI,YAAY;AAC7C,MAAI,OAAO,WAAW,EAAG,MAAK,SAAS,OAAO,CAAC;AAAA,WACtC,OAAO,SAAS,EAAG,MAAK,SAAS;AAC1C,SAAO;AACR;AAQO,SAAS,oBAAoB,MAAgB;AACnD,SAAO,EAAE,YAAY,sBAAsB,GAAG,aAAa,IAAI,EAAE;AAClE;AAQO,SAAS,sBAAsB,MAAY,MAAsB;AACvE,QAAM,iBAAiB,KAAK,WAAW,IAAI,CAAC,aAAa;AACxD,UAAM,UAAmC;AAAA,MACxC,SAAS;AAAA,MACT,MAAM,SAAS;AAAA,IAChB;AACA,UAAM,QAAQ,KAAK,MACjB,OAAO,CAAC,SAAS,KAAK,gBAAgB,SAAS,WAAW,EAC1D,IAAI,YAAY;AAClB,QAAI,MAAM,OAAQ,SAAQ,cAAc;AACxC,WAAO;AAAA,EACR,CAAC;AAED,QAAM,aAAsC;AAAA,IAC3C,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,KAAK;AAAA,EACZ;AACA,MAAI,KAAK,IAAK,YAAW,MAAM,KAAK;AACpC,MAAI,KAAK,MAAO,YAAW,QAAQ,KAAK;AACxC,MAAI,KAAK,YAAa,YAAW,cAAc,KAAK;AACpD,MAAI,KAAK,eAAe,OAAQ,YAAW,gBAAgB,KAAK;AAChE,aAAW,UAAU;AAAA,IACpB,SAAS;AAAA,IACT,MAAM;AAAA,IACN;AAAA,EACD;AAEA,SAAO;AACR;","names":[]}
package/dist/seo.mjs ADDED
@@ -0,0 +1,56 @@
1
+ // src/seo.ts
2
+ function priceString(cents) {
3
+ return (cents / 100).toFixed(2);
4
+ }
5
+ function variantOffer(variant) {
6
+ return {
7
+ "@type": "Offer",
8
+ price: priceString(variant.display_price ?? variant.price),
9
+ priceCurrency: "USD"
10
+ };
11
+ }
12
+ function menuItemNode(item) {
13
+ const node = {
14
+ "@type": "MenuItem",
15
+ name: item.name
16
+ };
17
+ if (item.description) node.description = item.description;
18
+ const offers = item.variants.map(variantOffer);
19
+ if (offers.length === 1) node.offers = offers[0];
20
+ else if (offers.length > 1) node.offers = offers;
21
+ return node;
22
+ }
23
+ function buildMenuItemJsonLd(item) {
24
+ return { "@context": "https://schema.org", ...menuItemNode(item) };
25
+ }
26
+ function buildRestaurantJsonLd(menu, info) {
27
+ const hasMenuSection = menu.categories.map((category) => {
28
+ const section = {
29
+ "@type": "MenuSection",
30
+ name: category.name
31
+ };
32
+ const items = menu.items.filter((item) => item.category_id === category.category_id).map(menuItemNode);
33
+ if (items.length) section.hasMenuItem = items;
34
+ return section;
35
+ });
36
+ const restaurant = {
37
+ "@context": "https://schema.org",
38
+ "@type": "Restaurant",
39
+ name: info.name
40
+ };
41
+ if (info.url) restaurant.url = info.url;
42
+ if (info.image) restaurant.image = info.image;
43
+ if (info.description) restaurant.description = info.description;
44
+ if (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;
45
+ restaurant.hasMenu = {
46
+ "@type": "Menu",
47
+ name: "Menu",
48
+ hasMenuSection
49
+ };
50
+ return restaurant;
51
+ }
52
+ export {
53
+ buildMenuItemJsonLd,
54
+ buildRestaurantJsonLd
55
+ };
56
+ //# sourceMappingURL=seo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/seo.ts"],"sourcesContent":["import type { Menu } from \"@zorgo/universal/utils/menuUtils\";\nimport type { ItemVariant, MenuItem } from \"@zorgo/universal/interfaces\";\n\n/** Franchise-level facts for the Restaurant node. Everything but `name` is\n * optional so the schema degrades gracefully when a field is missing. */\nexport interface RestaurantInfo {\n\tname: string;\n\turl?: string;\n\timage?: string;\n\tdescription?: string;\n\tservesCuisine?: string[];\n}\n\n// cents -> \"5.49\" (schema.org Offer.price is a string in major currency units)\nfunction priceString(cents: number): string {\n\treturn (cents / 100).toFixed(2);\n}\n\nfunction variantOffer(variant: ItemVariant) {\n\treturn {\n\t\t\"@type\": \"Offer\",\n\t\tprice: priceString(variant.display_price ?? variant.price),\n\t\tpriceCurrency: \"USD\",\n\t};\n}\n\nfunction menuItemNode(item: MenuItem) {\n\tconst node: Record<string, unknown> = {\n\t\t\"@type\": \"MenuItem\",\n\t\tname: item.name,\n\t};\n\tif (item.description) node.description = item.description;\n\t// one Offer for a single price, an array when the item has size/variant prices\n\tconst offers = item.variants.map(variantOffer);\n\tif (offers.length === 1) node.offers = offers[0];\n\telse if (offers.length > 1) node.offers = offers;\n\treturn node;\n}\n\n/**\n * Builds a standalone schema.org JSON-LD object for a single menu item, for\n * embedding in that item's `/menu/[slug]` page `<head>`. Reuses `menuItemNode`\n * (the same node shape used inside the Restaurant menu) and adds the\n * `@context` a top-level node needs. Pure — safe to run at build time.\n */\nexport function buildMenuItemJsonLd(item: MenuItem) {\n\treturn { \"@context\": \"https://schema.org\", ...menuItemNode(item) };\n}\n\n/**\n * Builds a schema.org Restaurant JSON-LD object from a prebuilt Menu and\n * franchise info. Categories become MenuSections; the items in each become\n * MenuItems. Pure — same inputs, same output — so it can run at prebuild time\n * and be embedded straight into the site's <head>.\n */\nexport function buildRestaurantJsonLd(menu: Menu, info: RestaurantInfo) {\n\tconst hasMenuSection = menu.categories.map((category) => {\n\t\tconst section: Record<string, unknown> = {\n\t\t\t\"@type\": \"MenuSection\",\n\t\t\tname: category.name,\n\t\t};\n\t\tconst items = menu.items\n\t\t\t.filter((item) => item.category_id === category.category_id)\n\t\t\t.map(menuItemNode);\n\t\tif (items.length) section.hasMenuItem = items;\n\t\treturn section;\n\t});\n\n\tconst restaurant: Record<string, unknown> = {\n\t\t\"@context\": \"https://schema.org\",\n\t\t\"@type\": \"Restaurant\",\n\t\tname: info.name,\n\t};\n\tif (info.url) restaurant.url = info.url;\n\tif (info.image) restaurant.image = info.image;\n\tif (info.description) restaurant.description = info.description;\n\tif (info.servesCuisine?.length) restaurant.servesCuisine = info.servesCuisine;\n\trestaurant.hasMenu = {\n\t\t\"@type\": \"Menu\",\n\t\tname: \"Menu\",\n\t\thasMenuSection,\n\t};\n\n\treturn restaurant;\n}\n"],"mappings":";AAcA,SAAS,YAAY,OAAuB;AAC3C,UAAQ,QAAQ,KAAK,QAAQ,CAAC;AAC/B;AAEA,SAAS,aAAa,SAAsB;AAC3C,SAAO;AAAA,IACN,SAAS;AAAA,IACT,OAAO,YAAY,QAAQ,iBAAiB,QAAQ,KAAK;AAAA,IACzD,eAAe;AAAA,EAChB;AACD;AAEA,SAAS,aAAa,MAAgB;AACrC,QAAM,OAAgC;AAAA,IACrC,SAAS;AAAA,IACT,MAAM,KAAK;AAAA,EACZ;AACA,MAAI,KAAK,YAAa,MAAK,cAAc,KAAK;AAE9C,QAAM,SAAS,KAAK,SAAS,IAAI,YAAY;AAC7C,MAAI,OAAO,WAAW,EAAG,MAAK,SAAS,OAAO,CAAC;AAAA,WACtC,OAAO,SAAS,EAAG,MAAK,SAAS;AAC1C,SAAO;AACR;AAQO,SAAS,oBAAoB,MAAgB;AACnD,SAAO,EAAE,YAAY,sBAAsB,GAAG,aAAa,IAAI,EAAE;AAClE;AAQO,SAAS,sBAAsB,MAAY,MAAsB;AACvE,QAAM,iBAAiB,KAAK,WAAW,IAAI,CAAC,aAAa;AACxD,UAAM,UAAmC;AAAA,MACxC,SAAS;AAAA,MACT,MAAM,SAAS;AAAA,IAChB;AACA,UAAM,QAAQ,KAAK,MACjB,OAAO,CAAC,SAAS,KAAK,gBAAgB,SAAS,WAAW,EAC1D,IAAI,YAAY;AAClB,QAAI,MAAM,OAAQ,SAAQ,cAAc;AACxC,WAAO;AAAA,EACR,CAAC;AAED,QAAM,aAAsC;AAAA,IAC3C,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,MAAM,KAAK;AAAA,EACZ;AACA,MAAI,KAAK,IAAK,YAAW,MAAM,KAAK;AACpC,MAAI,KAAK,MAAO,YAAW,QAAQ,KAAK;AACxC,MAAI,KAAK,YAAa,YAAW,cAAc,KAAK;AACpD,MAAI,KAAK,eAAe,OAAQ,YAAW,gBAAgB,KAAK;AAChE,aAAW,UAAU;AAAA,IACpB,SAAS;AAAA,IACT,MAAM;AAAA,IACN;AAAA,EACD;AAEA,SAAO;AACR;","names":[]}
@@ -0,0 +1 @@
1
+ export { baseItemIdFromSlug, menuItemSlug } from '@zorgo/universal/utils/menuSlugs';
@@ -0,0 +1 @@
1
+ export { baseItemIdFromSlug, menuItemSlug } from '@zorgo/universal/utils/menuSlugs';
package/dist/slugs.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/slugs.ts
21
+ var slugs_exports = {};
22
+ __export(slugs_exports, {
23
+ baseItemIdFromSlug: () => baseItemIdFromSlug,
24
+ menuItemSlug: () => menuItemSlug
25
+ });
26
+ module.exports = __toCommonJS(slugs_exports);
27
+
28
+ // ../zorgo/universal/utils/menuSlugs.ts
29
+ function menuItemNameToSlug(name) {
30
+ return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").trim();
31
+ }
32
+ function menuItemSlug(name, baseItemId) {
33
+ return `${menuItemNameToSlug(name)}-${baseItemId}`;
34
+ }
35
+ function baseItemIdFromSlug(slug) {
36
+ const match = slug.match(/-(\d+)$/);
37
+ if (!match) {
38
+ throw new Error(`No base_item_id found in slug: ${slug}`);
39
+ }
40
+ return Number(match[1]);
41
+ }
42
+ // Annotate the CommonJS export names for ESM import in node:
43
+ 0 && (module.exports = {
44
+ baseItemIdFromSlug,
45
+ menuItemSlug
46
+ });
47
+ //# sourceMappingURL=slugs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/slugs.ts","../../zorgo/universal/utils/menuSlugs.ts"],"sourcesContent":["export { menuItemSlug, baseItemIdFromSlug } from \"@zorgo/universal/utils/menuSlugs\";\n","import { supabase } from \"./supabase\";\n\nexport interface MenuSlugParam {\n\tslug: string;\n}\n\nexport interface BaseMenuSlugRecord {\n\tbaseItemId: number;\n\tname: string;\n\tslug: string;\n\tdescription: string | null;\n\timagePath: string | null;\n}\n\nfunction requireApiKey(apiKey?: string): string {\n\tconst resolvedApiKey = apiKey ?? process.env.ZORGO_API_KEY;\n\tif (!resolvedApiKey) {\n\t\tthrow new Error(\"ZORGO_API_KEY is not set\");\n\t}\n\treturn resolvedApiKey;\n}\n\nexport function menuItemNameToSlug(name: string): string {\n\treturn name\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9\\s-]/g, \"\")\n\t\t.replace(/\\s+/g, \"-\")\n\t\t.trim();\n}\n\n/** Builds the `/menu/[slug]` route segment for an item, e.g. \"chicken-burrito-42\". */\nexport function menuItemSlug(name: string, baseItemId: number): string {\n\treturn `${menuItemNameToSlug(name)}-${baseItemId}`;\n}\n\n/** Parses the trailing numeric base_item_id off a `menuItemSlug` result. */\nexport function baseItemIdFromSlug(slug: string): number {\n\tconst match = slug.match(/-(\\d+)$/);\n\tif (!match) {\n\t\tthrow new Error(`No base_item_id found in slug: ${slug}`);\n\t}\n\treturn Number(match[1]);\n}\n\nexport function buildSlugParam(name: string): MenuSlugParam {\n\treturn { slug: menuItemNameToSlug(name) };\n}\n\nexport function buildSlugParamsFromNames(names: string[]): MenuSlugParam[] {\n\treturn names.map(buildSlugParam);\n}\n\nfunction assertUniqueSlugs(items: Array<{ name: string; slug: string }>): void {\n\tconst duplicates = new Map<string, string[]>();\n\n\tfor (const item of items) {\n\t\tconst matches = duplicates.get(item.slug) ?? [];\n\t\tmatches.push(item.name);\n\t\tduplicates.set(item.slug, matches);\n\t}\n\n\tconst conflicts = Array.from(duplicates.entries()).filter(\n\t\t([, names]) => names.length > 1,\n\t);\n\tif (conflicts.length === 0) return;\n\n\tconst message = conflicts\n\t\t.map(([slug, names]) => `\"${slug}\" => ${names.join(\", \")}`)\n\t\t.join(\"; \");\n\tthrow new Error(`Duplicate menu slugs detected: ${message}`);\n}\n\nasync function getFranchiseIdFromApiKey(apiKey?: string): Promise<number> {\n\tconst keyValue = requireApiKey(apiKey);\n\tconst { data, error } = await supabase\n\t\t.from(\"api_keys\")\n\t\t.select(\"franchise_id\")\n\t\t.eq(\"key_value\", keyValue)\n\t\t.eq(\"is_active\", true)\n\t\t.single();\n\n\tif (error || !data?.franchise_id) {\n\t\tthrow new Error(\"Trouble finding franchise for API key\");\n\t}\n\n\treturn data.franchise_id;\n}\n\nexport async function getBaseMenuSlugRecordsByApiKey(\n\tapiKey?: string,\n\toptions: { assertUniqueSlugs?: boolean } = {},\n): Promise<BaseMenuSlugRecord[]> {\n\tconst { assertUniqueSlugs: shouldAssertUniqueSlugs = true } = options;\n\tconst franchiseId = await getFranchiseIdFromApiKey(apiKey);\n\tconst { data, error } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, name, description, image_path\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.order(\"base_item_id\", { ascending: true });\n\n\tif (error || !data) {\n\t\tthrow new Error(\"Trouble loading base menu items for franchise\");\n\t}\n\n\tconst records = data\n\t\t.filter((item) => typeof item.name === \"string\" && item.name.length > 0)\n\t\t.map((item) => ({\n\t\t\tbaseItemId: item.base_item_id,\n\t\t\tname: item.name,\n\t\t\tslug: menuItemNameToSlug(item.name),\n\t\t\tdescription: item.description ?? null,\n\t\t\timagePath: item.image_path ?? null,\n\t\t}));\n\n\tif (shouldAssertUniqueSlugs) {\n\t\tassertUniqueSlugs(records);\n\t}\n\treturn records;\n}\n\nexport async function getBaseMenuSlugParamsByApiKey(\n\tapiKey?: string,\n): Promise<MenuSlugParam[]> {\n\tconst records = await getBaseMenuSlugRecordsByApiKey(apiKey);\n\treturn records.map(({ slug }) => ({ slug }));\n}\n\nexport async function getBaseMenuSlugRecordBySlug(\n\tslug: string,\n\tapiKey?: string,\n): Promise<BaseMenuSlugRecord | null> {\n\tconst records = await getBaseMenuSlugRecordsByApiKey(apiKey);\n\treturn records.find((record) => record.slug === slug) ?? null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsBO,SAAS,mBAAmB,MAAsB;AACxD,SAAO,KACL,YAAY,EACZ,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACR;AAGO,SAAS,aAAa,MAAc,YAA4B;AACtE,SAAO,GAAG,mBAAmB,IAAI,CAAC,IAAI,UAAU;AACjD;AAGO,SAAS,mBAAmB,MAAsB;AACxD,QAAM,QAAQ,KAAK,MAAM,SAAS;AAClC,MAAI,CAAC,OAAO;AACX,UAAM,IAAI,MAAM,kCAAkC,IAAI,EAAE;AAAA,EACzD;AACA,SAAO,OAAO,MAAM,CAAC,CAAC;AACvB;","names":[]}
package/dist/slugs.mjs ADDED
@@ -0,0 +1,19 @@
1
+ // ../zorgo/universal/utils/menuSlugs.ts
2
+ function menuItemNameToSlug(name) {
3
+ return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").trim();
4
+ }
5
+ function menuItemSlug(name, baseItemId) {
6
+ return `${menuItemNameToSlug(name)}-${baseItemId}`;
7
+ }
8
+ function baseItemIdFromSlug(slug) {
9
+ const match = slug.match(/-(\d+)$/);
10
+ if (!match) {
11
+ throw new Error(`No base_item_id found in slug: ${slug}`);
12
+ }
13
+ return Number(match[1]);
14
+ }
15
+ export {
16
+ baseItemIdFromSlug,
17
+ menuItemSlug
18
+ };
19
+ //# sourceMappingURL=slugs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../zorgo/universal/utils/menuSlugs.ts"],"sourcesContent":["import { supabase } from \"./supabase\";\n\nexport interface MenuSlugParam {\n\tslug: string;\n}\n\nexport interface BaseMenuSlugRecord {\n\tbaseItemId: number;\n\tname: string;\n\tslug: string;\n\tdescription: string | null;\n\timagePath: string | null;\n}\n\nfunction requireApiKey(apiKey?: string): string {\n\tconst resolvedApiKey = apiKey ?? process.env.ZORGO_API_KEY;\n\tif (!resolvedApiKey) {\n\t\tthrow new Error(\"ZORGO_API_KEY is not set\");\n\t}\n\treturn resolvedApiKey;\n}\n\nexport function menuItemNameToSlug(name: string): string {\n\treturn name\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9\\s-]/g, \"\")\n\t\t.replace(/\\s+/g, \"-\")\n\t\t.trim();\n}\n\n/** Builds the `/menu/[slug]` route segment for an item, e.g. \"chicken-burrito-42\". */\nexport function menuItemSlug(name: string, baseItemId: number): string {\n\treturn `${menuItemNameToSlug(name)}-${baseItemId}`;\n}\n\n/** Parses the trailing numeric base_item_id off a `menuItemSlug` result. */\nexport function baseItemIdFromSlug(slug: string): number {\n\tconst match = slug.match(/-(\\d+)$/);\n\tif (!match) {\n\t\tthrow new Error(`No base_item_id found in slug: ${slug}`);\n\t}\n\treturn Number(match[1]);\n}\n\nexport function buildSlugParam(name: string): MenuSlugParam {\n\treturn { slug: menuItemNameToSlug(name) };\n}\n\nexport function buildSlugParamsFromNames(names: string[]): MenuSlugParam[] {\n\treturn names.map(buildSlugParam);\n}\n\nfunction assertUniqueSlugs(items: Array<{ name: string; slug: string }>): void {\n\tconst duplicates = new Map<string, string[]>();\n\n\tfor (const item of items) {\n\t\tconst matches = duplicates.get(item.slug) ?? [];\n\t\tmatches.push(item.name);\n\t\tduplicates.set(item.slug, matches);\n\t}\n\n\tconst conflicts = Array.from(duplicates.entries()).filter(\n\t\t([, names]) => names.length > 1,\n\t);\n\tif (conflicts.length === 0) return;\n\n\tconst message = conflicts\n\t\t.map(([slug, names]) => `\"${slug}\" => ${names.join(\", \")}`)\n\t\t.join(\"; \");\n\tthrow new Error(`Duplicate menu slugs detected: ${message}`);\n}\n\nasync function getFranchiseIdFromApiKey(apiKey?: string): Promise<number> {\n\tconst keyValue = requireApiKey(apiKey);\n\tconst { data, error } = await supabase\n\t\t.from(\"api_keys\")\n\t\t.select(\"franchise_id\")\n\t\t.eq(\"key_value\", keyValue)\n\t\t.eq(\"is_active\", true)\n\t\t.single();\n\n\tif (error || !data?.franchise_id) {\n\t\tthrow new Error(\"Trouble finding franchise for API key\");\n\t}\n\n\treturn data.franchise_id;\n}\n\nexport async function getBaseMenuSlugRecordsByApiKey(\n\tapiKey?: string,\n\toptions: { assertUniqueSlugs?: boolean } = {},\n): Promise<BaseMenuSlugRecord[]> {\n\tconst { assertUniqueSlugs: shouldAssertUniqueSlugs = true } = options;\n\tconst franchiseId = await getFranchiseIdFromApiKey(apiKey);\n\tconst { data, error } = await supabase\n\t\t.from(\"base_items\")\n\t\t.select(\"base_item_id, name, description, image_path\")\n\t\t.eq(\"franchise_id\", franchiseId)\n\t\t.order(\"base_item_id\", { ascending: true });\n\n\tif (error || !data) {\n\t\tthrow new Error(\"Trouble loading base menu items for franchise\");\n\t}\n\n\tconst records = data\n\t\t.filter((item) => typeof item.name === \"string\" && item.name.length > 0)\n\t\t.map((item) => ({\n\t\t\tbaseItemId: item.base_item_id,\n\t\t\tname: item.name,\n\t\t\tslug: menuItemNameToSlug(item.name),\n\t\t\tdescription: item.description ?? null,\n\t\t\timagePath: item.image_path ?? null,\n\t\t}));\n\n\tif (shouldAssertUniqueSlugs) {\n\t\tassertUniqueSlugs(records);\n\t}\n\treturn records;\n}\n\nexport async function getBaseMenuSlugParamsByApiKey(\n\tapiKey?: string,\n): Promise<MenuSlugParam[]> {\n\tconst records = await getBaseMenuSlugRecordsByApiKey(apiKey);\n\treturn records.map(({ slug }) => ({ slug }));\n}\n\nexport async function getBaseMenuSlugRecordBySlug(\n\tslug: string,\n\tapiKey?: string,\n): Promise<BaseMenuSlugRecord | null> {\n\tconst records = await getBaseMenuSlugRecordsByApiKey(apiKey);\n\treturn records.find((record) => record.slug === slug) ?? null;\n}\n"],"mappings":";AAsBO,SAAS,mBAAmB,MAAsB;AACxD,SAAO,KACL,YAAY,EACZ,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,QAAQ,GAAG,EACnB,KAAK;AACR;AAGO,SAAS,aAAa,MAAc,YAA4B;AACtE,SAAO,GAAG,mBAAmB,IAAI,CAAC,IAAI,UAAU;AACjD;AAGO,SAAS,mBAAmB,MAAsB;AACxD,QAAM,QAAQ,KAAK,MAAM,SAAS;AAClC,MAAI,CAAC,OAAO;AACX,UAAM,IAAI,MAAM,kCAAkC,IAAI,EAAE;AAAA,EACzD;AACA,SAAO,OAAO,MAAM,CAAC,CAAC;AACvB;","names":[]}
@@ -0,0 +1,27 @@
1
+ import { SUPABASE_URL } from "@zorgo/universal/constants";
2
+ import type { ScaffoldOptions } from "../../types";
3
+
4
+ // .env is the single source of truth for this site. Everything that varies per
5
+ // project (which API environment, the site URL, the worker name) lives here;
6
+ // scripts/prebuild.ts reads these back to regenerate wrangler.toml on every
7
+ // build, so wrangler.toml is a build artifact — edit .env, not it.
8
+ export default function generateEnvFile(opts: ScaffoldOptions): string {
9
+ const lines = [
10
+ // which Zorgo API this site talks to (an Environments enum value). read by
11
+ // @zorgo constants (client), scripts/prebuild.ts, and lib/zorgoSiteToken.ts.
12
+ `NEXT_PUBLIC_API_BASE_URL=${opts.apiBaseUrl}`,
13
+ // the site's own origin — read by sitemap.ts, robots.ts, layout, stores.
14
+ `NEXT_PUBLIC_SITE_URL=${opts.projectUrl}`,
15
+ `NEXT_PUBLIC_BASE_URL=${opts.projectUrl}`,
16
+ `NEXT_PUBLIC_SUPABASE_URL=${SUPABASE_URL}`,
17
+ // worker name for the generated wrangler.toml.
18
+ `ZORGO_PROJECT_NAME=${opts.projectName}`,
19
+ ];
20
+ if (opts.stripePublishableKey) {
21
+ lines.push(`NEXT_PUBLIC_STRIPE_PUBLIC_KEY=${opts.stripePublishableKey}`);
22
+ }
23
+ if (opts.websiteSecret) {
24
+ lines.push(`ZORGO_SITE_SECRET=${opts.websiteSecret}`);
25
+ }
26
+ return lines.join("\n") + "\n";
27
+ }
File without changes
@@ -0,0 +1,11 @@
1
+ "use server";
2
+ import { NextResponse } from "next/server";
3
+ import { fetchSiteToken } from "../../../lib/zorgoSiteToken";
4
+
5
+ export async function GET() {
6
+ const token = await fetchSiteToken();
7
+ if (!token) {
8
+ return NextResponse.json({ error: "Missing or invalid ZORGO_SITE_SECRET env var" }, { status: 500 });
9
+ }
10
+ return NextResponse.json({ token });
11
+ }
@@ -0,0 +1,54 @@
1
+ import type { ScaffoldOptions } from "../../../types";
2
+
3
+ // Mirrors icon.tsx.gen.ts but at the 180x180 Apple touch-icon size iOS uses for
4
+ // "Add to Home Screen". Kept as its own file because Next.js maps the
5
+ // `apple-icon` convention to <link rel="apple-touch-icon"> separately from `icon`.
6
+ export default function generateAppleIcon(opts: ScaffoldOptions): string {
7
+ const letter = opts.projectName.charAt(0).toUpperCase();
8
+
9
+ return `import { ImageResponse } from "next/og";
10
+ import { readFileSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ export const size = { width: 180, height: 180 };
14
+ export const contentType = "image/png";
15
+
16
+ // Brand palette prebuild drops at public/styles/styles.json. Neutral fallback if
17
+ // it's missing so the icon still renders.
18
+ function loadTheme() {
19
+ try {
20
+ return JSON.parse(
21
+ readFileSync(join(process.cwd(), "public/styles/styles.json"), "utf8"),
22
+ );
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export default function AppleIcon() {
29
+ const theme = loadTheme();
30
+ const background = theme?.primary ?? "#171717";
31
+ const foreground = theme?.primaryForeground ?? "#ffffff";
32
+ return new ImageResponse(
33
+ (
34
+ <div
35
+ style={{
36
+ width: "100%",
37
+ height: "100%",
38
+ display: "flex",
39
+ alignItems: "center",
40
+ justifyContent: "center",
41
+ background,
42
+ color: foreground,
43
+ fontSize: 112,
44
+ fontWeight: 700,
45
+ }}
46
+ >
47
+ ${letter}
48
+ </div>
49
+ ),
50
+ size,
51
+ );
52
+ }
53
+ `;
54
+ }
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { CheckoutForm } from "@zorgo/next/components";
3
+
4
+ export default function CheckoutPage() {
5
+ return (
6
+ <div className="p-8 md:p-16">
7
+ <CheckoutForm />
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,50 @@
1
+ import Link from "next/link";
2
+
3
+ export default function Footer() {
4
+ const pages = [
5
+ {
6
+ name: "Home",
7
+ href: "/",
8
+ },
9
+ {
10
+ name: "Locations",
11
+ href: "/locations",
12
+ },
13
+ {
14
+ name: "Menu",
15
+ href: "/menu",
16
+ },
17
+ {
18
+ name: "Privacy Policy",
19
+ href: "/privacy",
20
+ },
21
+ {
22
+ name: "Terms of Service",
23
+ href: "/terms",
24
+ },
25
+ ];
26
+ return (
27
+ <footer className="w-full p-6 bg-background-recessed flex flex-col gap-4">
28
+ <div className="flex flex-col md:flex-row md:items-center gap-4">
29
+ {pages.map((page) => (
30
+ <Link
31
+ href={page.href}
32
+ key={page.name}
33
+ className="text-md opacity-50 hover:opacity-100 transition-opacity duration-300 font-semibold"
34
+ >
35
+ {page.name}
36
+ </Link>
37
+ ))}
38
+ </div>
39
+ <p className="text-md">
40
+ <span className="opacity-75">Made with ❤️ by </span>
41
+ <Link
42
+ href="https://zorgotech.com"
43
+ className="opacity-75 hover:opacity-100 transition-opacity duration-300 font-semibold"
44
+ >
45
+ Zorgo
46
+ </Link>
47
+ </p>
48
+ </footer>
49
+ );
50
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import Link from "next/link";
3
+ import Image from "next/image";
4
+ import { useLocation, useCart } from "@zorgo/next";
5
+ import { ShoppingCart, MapPin } from "lucide-react";
6
+
7
+ const pages = [
8
+ {
9
+ name: "Home",
10
+ href: "/",
11
+ },
12
+ {
13
+ name: "Locations",
14
+ href: "/locations",
15
+ },
16
+ ];
17
+
18
+ export default function Header() {
19
+ const { currentLocation } = useLocation();
20
+
21
+ return (
22
+ <div className="p-4 pr-12 pl-6 w-full bg-background-elevated flex flex-row justify-between items-center sticky top-0 z-50 text-foreground">
23
+ <Logo />
24
+
25
+ <div className="flex flex-row items-center justify-center gap-6 md:gap-12 h-full">
26
+ <Link
27
+ href="/locations"
28
+ className="flex items-center gap-2 hover:opacity-50 transition-opacity duration-300"
29
+ >
30
+ <MapPin className="w-8 h-8" strokeWidth={2} />
31
+ <p className="text-xl font-semibold hidden md:block">
32
+ {currentLocation?.name ?? "Find a Location"}
33
+ </p>
34
+ </Link>
35
+ <Link href="/menu">
36
+ <div className="bg-primary text-primary-foreground text-xl font-bold p-3 md:px-8 rounded-lg hover:opacity-50 transition-opacity duration-300">
37
+ Order
38
+ </div>
39
+ </Link>
40
+ <CartButton />
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ function NavLinks() {
47
+ return (
48
+ <div className=" h-full flex items-center justify-center">
49
+ <div className="flex-1">
50
+ {pages.map((page) => (
51
+ <Link href={page.href} key={page.name} className="p-4">
52
+ {page.name}
53
+ </Link>
54
+ ))}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ function LocationDropdown() {
61
+ const { availableLocations, currentLocation, setLocation } = useLocation();
62
+
63
+ return (
64
+ <select
65
+ className="text-xl font-semibold"
66
+ value={currentLocation?.location_id ?? ""}
67
+ onChange={(e) => {
68
+ const next = availableLocations.find(
69
+ (location) =>
70
+ location.location_id === parseInt(e.target.value),
71
+ );
72
+ if (next) setLocation(next);
73
+ }}
74
+ >
75
+ <option value="" disabled>
76
+ Select a location
77
+ </option>
78
+ {availableLocations.map((location) => (
79
+ <option
80
+ key={location.location_id}
81
+ value={location.location_id.toString()}
82
+ >
83
+ {location.name}
84
+ </option>
85
+ ))}
86
+ </select>
87
+ );
88
+ }
89
+
90
+ function Logo() {
91
+ return (
92
+ <Link
93
+ href="/"
94
+ className="hover:opacity-50 transition-opacity duration-300"
95
+ >
96
+ {/* bg-primary shows through until logo.png (written by scripts/prebuild.ts) paints */}
97
+ <div className="bg-primary w-16 h-16 rounded-lg overflow-hidden">
98
+ <Image
99
+ src="/images/logo.png"
100
+ alt="Home"
101
+ width={64}
102
+ height={64}
103
+ className="w-full h-full object-cover"
104
+ />
105
+ </div>
106
+ </Link>
107
+ );
108
+ }
109
+
110
+ function CartButton() {
111
+ const { items } = useCart();
112
+ const itemCount = items.length;
113
+ return (
114
+ <Link
115
+ href="/checkout"
116
+ className="relative cursor-pointer hover:opacity-50 transition-opacity duration-300"
117
+ >
118
+ <ShoppingCart className="w-8 h-8" strokeWidth={2} />
119
+ <span
120
+ className={`absolute -top-1.5 -right-2 min-w-5 h-5 px-1 rounded-full flex items-center justify-center text-xs font-semibold text-primary-foreground ${
121
+ itemCount > 0 ? "bg-primary" : "bg-background"
122
+ }`}
123
+ >
124
+ {itemCount}
125
+ </span>
126
+ </Link>
127
+ );
128
+ }