@colabcommerce/elements 0.0.4 → 0.9.1

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 (99) hide show
  1. package/.pnp.cjs +16484 -0
  2. package/.pnp.loader.mjs +2126 -0
  3. package/.yarn/install-state.gz +0 -0
  4. package/.yarn/releases/yarn-4.12.0.cjs +942 -0
  5. package/.yarnrc.yml +1 -0
  6. package/README.md +60 -41
  7. package/cypress/fixtures/example.json +5 -0
  8. package/cypress/support/commands.js +25 -0
  9. package/cypress/support/component-index.html +15 -0
  10. package/cypress/support/component.js +26 -0
  11. package/cypress.config.js +10 -0
  12. package/eslint.config.js +32 -0
  13. package/index.html +13 -0
  14. package/package.json +91 -67
  15. package/playground/index.html +14 -0
  16. package/playground/main.jsx +36 -0
  17. package/public/vite.svg +1 -0
  18. package/src/App.css +0 -0
  19. package/src/App.jsx +65 -0
  20. package/src/components/CollapsibleStoreHours/index.jsx +269 -0
  21. package/src/components/HoursList/index.jsx +225 -0
  22. package/src/components/LeadForm/index.jsx +241 -0
  23. package/src/components/MessageDialog/index.jsx +169 -0
  24. package/src/components/QuoteForm/index.jsx +82 -0
  25. package/src/components/QuoteFormSearch/index.jsx +276 -0
  26. package/src/components/QuoteFormStoreList/index.jsx +65 -0
  27. package/src/components/QuoteFormStoreListItem/index.jsx +134 -0
  28. package/src/components/QuoteLeadForm/index.jsx +16 -0
  29. package/src/components/QuoteMap/index.jsx +96 -0
  30. package/src/components/QuoteMapMarker/index.jsx +56 -0
  31. package/src/components/StaticMap/index.jsx +24 -0
  32. package/src/components/Store/index.jsx +44 -0
  33. package/src/components/StoreContact/index.jsx +96 -0
  34. package/src/components/StoreInfo/index.jsx +50 -0
  35. package/src/components/StoreList/index.jsx +59 -0
  36. package/src/components/StoreListItem/index.jsx +99 -0
  37. package/src/components/StoreListItem/indexStoreListItem.cy.jsx +30 -0
  38. package/src/components/StoreListNoneFound/index.jsx +16 -0
  39. package/src/components/StoreLocator/index.jsx +43 -0
  40. package/src/components/StoreLocatorMap/index.jsx +93 -0
  41. package/src/components/StoreLocatorMapMarker/index.jsx +55 -0
  42. package/src/components/StoreLocatorMessageDialog/index.jsx +20 -0
  43. package/src/components/StoreLocatorSearch/index.jsx +316 -0
  44. package/src/components/StoreMap/index.jsx +30 -0
  45. package/src/components/StoreMeta/index.jsx +7 -0
  46. package/src/components/StoreProducts/index.jsx +112 -0
  47. package/src/components/ui/Badge/index.jsx +46 -0
  48. package/src/components/ui/Button/index.jsx +56 -0
  49. package/src/components/ui/Button/indexButton.cy.jsx +9 -0
  50. package/src/components/ui/Card/index.jsx +90 -0
  51. package/src/components/ui/Input/index.jsx +19 -0
  52. package/src/components/ui/Input/indexInput.cy.jsx +9 -0
  53. package/src/components/ui/LoadingPuff/index.jsx +10 -0
  54. package/src/components/ui/Panel/index.jsx +23 -0
  55. package/src/components/ui/PhoneNumberInput/index.jsx +17 -0
  56. package/src/contexts/quote-form.jsx +94 -0
  57. package/src/contexts/store-locator.jsx +83 -0
  58. package/src/contexts/store.jsx +59 -0
  59. package/src/contexts/translations.jsx +11 -0
  60. package/src/dist.css +229 -0
  61. package/src/entries/QuoteForm.js +2 -0
  62. package/src/entries/Store.js +2 -0
  63. package/src/entries/StoreLocator.js +2 -0
  64. package/src/entries/StoreLocatorProvider.js +2 -0
  65. package/src/entries/styles.js +2 -0
  66. package/src/entries/useStoreLocator.js +2 -0
  67. package/src/i18n/defaultResources.js +19 -0
  68. package/src/i18n/index.js +44 -0
  69. package/src/i18n/mergeResources.js +22 -0
  70. package/src/index.css +214 -0
  71. package/src/lib/addressComponentsToAddress.js +43 -0
  72. package/src/lib/productSchema.js +6 -0
  73. package/src/lib/useGeolocation.js +266 -0
  74. package/src/lib/useHours.js +205 -0
  75. package/src/lib/usePlacesAutocomplete.js +288 -0
  76. package/src/lib/useProductAvailability.js +38 -0
  77. package/src/lib/useRudderAnalytics.js +50 -0
  78. package/src/lib/useSearchResults.js +102 -0
  79. package/src/lib/useStoreLocatorConfig.js +50 -0
  80. package/src/lib/utils/cn.js +6 -0
  81. package/src/lib/utils/measure.js +31 -0
  82. package/src/locales/en/default.json +58 -0
  83. package/src/locales/es/default.json +58 -0
  84. package/src/locales/fr/default.json +58 -0
  85. package/src/locales/it/default.json +58 -0
  86. package/src/main.jsx +10 -0
  87. package/vite.config.js +60 -53
  88. package/dist/CartForm.js +0 -617
  89. package/dist/Container-CU_WrBOi.js +0 -22
  90. package/dist/Modal-DTBKy_6d.js +0 -863
  91. package/dist/ProductForm.js +0 -343
  92. package/dist/Retailer.js +0 -3637
  93. package/dist/StoreLocator.js +0 -797
  94. package/dist/addressComponentsToAddress-DCL-K8mn.js +0 -1932
  95. package/dist/browser-ponyfill-DcK7_cJB.js +0 -339
  96. package/dist/globals-B8-hYoIU.js +0 -8518
  97. package/dist/index-CqSfhXDd.js +0 -137
  98. package/dist/index-FM02Uq_P.js +0 -100
  99. package/dist/style.css +0 -1
@@ -0,0 +1,44 @@
1
+ import StoreInfo from '@/components/StoreInfo'
2
+ import StoreContact from '@/components/StoreContact'
3
+ import StoreMap from '@/components/StoreMap'
4
+ import StoreProducts from '@/components/StoreProducts'
5
+ import { StoreProvider } from '@/contexts/store'
6
+ import TranslationProvider from '@/contexts/translations'
7
+
8
+ const Store = ({ storeId, locale = "en", initialData = null }) => {
9
+ return (
10
+ <TranslationProvider options={{ lng: locale }}>
11
+ <StoreProvider id={storeId} initialData={initialData}>
12
+ <div className="cc">
13
+ <main className="flex-1">
14
+ {/* Store Hero Section */}
15
+ <StoreInfo />
16
+
17
+ {/* Store Details Grid */}
18
+ <section className="py-12 lg:py-16 bg-muted/30">
19
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8">
20
+ <div className="grid lg:grid-cols-3 gap-8">
21
+ <div className="lg:col-span-2">
22
+ <StoreContact />
23
+ </div>
24
+ <div>
25
+ <StoreMap />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </section>
30
+
31
+ {/* Products Section */}
32
+ <section className="py-12 lg:py-16">
33
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8">
34
+ <StoreProducts />
35
+ </div>
36
+ </section>
37
+ </main>
38
+ </div>
39
+ </StoreProvider>
40
+ </TranslationProvider>
41
+ )
42
+ }
43
+
44
+ export default Store
@@ -0,0 +1,96 @@
1
+ "use client"
2
+
3
+ import { Phone, Mail, MapPin, Clock, Calendar, Globe, MessageSquare } from "lucide-react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Card } from "@/components/ui/card"
6
+ import HoursList from "@/components/HoursList"
7
+ import { useStore } from "@/contexts/store"
8
+
9
+ export function StoreContact() {
10
+
11
+ const { store } = useStore()
12
+
13
+ const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${store?.name}&destination_place_id=${store?.placeId}`
14
+
15
+ return (
16
+ <div className="space-y-6">
17
+ <Card className="p-6">
18
+ <h2 className="text-2xl font-bold mb-6">Contact Information</h2>
19
+ <div className="space-y-4">
20
+ <div className="flex items-start gap-4">
21
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
22
+ <MapPin className="w-5 h-5 text-primary" />
23
+ </div>
24
+ <div>
25
+ <div className="font-semibold mb-1">Address</div>
26
+ <div className="text-muted-foreground">
27
+ {store?.address?.street_line_one} {store?.address?.street_line_two && (`, ${store.address.street_line_two}`)}
28
+ <br />
29
+ {store?.address?.city}, {store?.address?.province} {store?.address?.postal_code}
30
+ </div>
31
+ <a className="px-0 h-auto mt-2 text-primary hover:underline py-3 " href={directionsUrl} target="_blank" rel="noopener noreferrer">
32
+ Get Directions →
33
+ </a>
34
+ </div>
35
+ </div>
36
+
37
+ {store?.phone_number &&
38
+ <div className="flex items-start gap-4">
39
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
40
+ <Phone className="w-5 h-5 text-primary" />
41
+ </div>
42
+ <div>
43
+ <div className="font-semibold mb-1">Phone</div>
44
+ <a href={`tel:${store?.phone_number?.phone_number}`} className="text-primary hover:underline">
45
+ {store?.phone_number?.formatted}
46
+ </a>
47
+ </div>
48
+ </div>
49
+ }
50
+
51
+ <div className="flex items-start gap-4">
52
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
53
+ <Globe className="w-5 h-5 text-primary" />
54
+ </div>
55
+ <div>
56
+ <div className="font-semibold mb-1">Website</div>
57
+ <a
58
+ href={store?.website}
59
+ className="text-primary hover:underline"
60
+ target="_blank"
61
+ rel="noopener noreferrer"
62
+ >
63
+ {store?.website}
64
+ </a>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div className="flex flex-wrap gap-3 mt-6 pt-6 border-t">
70
+ <Button className="flex-1 sm:flex-none">
71
+ <Phone className="w-4 h-4 mr-2" />
72
+ Call Now
73
+ </Button>
74
+ <Button variant="outline" className="flex-1 sm:flex-none bg-transparent">
75
+ <MessageSquare className="w-4 h-4 mr-2" />
76
+ Send Message
77
+ </Button>
78
+ </div>
79
+ </Card>
80
+
81
+ <Card className="p-6">
82
+ <div className="flex items-center gap-3 mb-6">
83
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
84
+ <Clock className="w-5 h-5 text-primary" />
85
+ </div>
86
+ <h2 className="text-2xl font-bold">Hours of Operation</h2>
87
+ </div>
88
+ <div className="space-y-3">
89
+ <HoursList hours={store?.retailer_location_hours} />
90
+ </div>
91
+ </Card>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ export default StoreContact
@@ -0,0 +1,50 @@
1
+ import { Star, MapPin, Clock, Award, TrendingUp } from "lucide-react"
2
+ import { Badge } from "@/components/ui/badge"
3
+ import { useTranslation } from "react-i18next"
4
+ import { useStore } from "@/contexts/store"
5
+ import useHours from "@/lib/useHours"
6
+
7
+ export function StoreInfo() {
8
+
9
+ const { t } = useTranslation()
10
+
11
+ const { store } = useStore()
12
+
13
+ const { computed } = useHours({
14
+ hours: store?.retailer_location_hours,
15
+ t,
16
+ })
17
+
18
+ return (
19
+ <section className="bg-gradient-to-b from-primary/5 to-background py-12 lg:py-16">
20
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8">
21
+ <div className="max-w-4xl">
22
+ <div className="flex flex-wrap items-center gap-3 mb-4">
23
+ <Badge variant="secondary" className="text-sm">
24
+ {t(`default:store.types.${store?.store_type?.toLowerCase()}`)}
25
+ </Badge>
26
+ </div>
27
+
28
+ <h1 className="text-4xl lg:text-5xl font-bold mb-4 text-balance">{store?.retailer_name}</h1>
29
+
30
+ <div className="flex flex-wrap items-center gap-4 mb-6">
31
+ <div className="flex items-center gap-2 text-muted-foreground">
32
+ <MapPin className="w-4 h-4" />
33
+ <span className="text-sm">{store?.address?.city}, {store?.address?.province}</span>
34
+ </div>
35
+ <div className="flex items-center gap-2 text-muted-foreground">
36
+ <Clock className="w-4 h-4" />
37
+ <span className={`text-sm font-medium ${computed.statusLabel === t('default:open') ? 'text-green-600' : 'text-red-600'}`}>{computed.statusLabel}</span>
38
+ </div>
39
+ </div>
40
+
41
+ <p className="text-lg text-muted-foreground leading-relaxed mb-6">
42
+ {t('default:store_romance', { name: store?.retailer_name || 'our store' })}
43
+ </p>
44
+ </div>
45
+ </div>
46
+ </section>
47
+ )
48
+ }
49
+
50
+ export default StoreInfo
@@ -0,0 +1,59 @@
1
+ import { useState } from "react"
2
+ import { MapPin, Phone, Clock, Navigation, Star } from "lucide-react"
3
+ import { Button } from "@/components/ui/button"
4
+ import StoreListItem from '@/components/StoreListItem'
5
+ import StoreListNoneFound from "@/components/StoreListNoneFound"
6
+ import LoadingPuff from "@/components/ui/LoadingPuff"
7
+ import { distanceBetweenPoints } from '@/lib/utils/measure'
8
+ import { useStoreLocator } from '@/contexts/store-locator'
9
+
10
+ export function StoreList() {
11
+
12
+ const { stores, location, selectedStoreId, setSelectedStoreId, setMessageDialogOpen, setMessageStoreId, searchIsLoading, searchError, baseUrl } = useStoreLocator()
13
+
14
+ const handleMessageClick = (storeId) => {
15
+ setMessageStoreId(storeId)
16
+ setMessageDialogOpen(true)
17
+ }
18
+
19
+ return (
20
+ <div className="space-y-4">
21
+ {!searchIsLoading && !searchError && stores.length === 0 && (
22
+ <StoreListNoneFound />
23
+ )}
24
+ {searchIsLoading && (
25
+ <div className="flex items-center justify-center h-48">
26
+ <LoadingPuff />
27
+ </div>
28
+ )}
29
+ {!searchIsLoading && !searchError && stores.map((store) => (
30
+ <StoreListItem
31
+ key={store.id}
32
+ id={store.id}
33
+ name={store.retailer_name}
34
+ placeId={store.place_id}
35
+ addressLineOne={store.address?.street_line_one}
36
+ addressLineTwo={store.address?.street_line_two}
37
+ city={store.address?.city}
38
+ province={store.address?.province}
39
+ postalCode={store.address?.postal_code}
40
+ country={store.address?.country}
41
+ distance={distanceBetweenPoints(location.latitude, location.longitude, store.address?.latitude, store.address?.longitude)}
42
+ phone={store.phone_number}
43
+ hours={store.retailer_location_hours}
44
+ services={[store.store_type]}
45
+ isSelected={selectedStoreId === store.id}
46
+ handleSelect={setSelectedStoreId}
47
+ href={`${baseUrl}/${store.id}`}
48
+ onMessageClick={handleMessageClick}
49
+ />
50
+ ))}
51
+ {!searchIsLoading && searchError && (
52
+ <div className="text-red-600 text-center">
53
+ An error occurred while searching for stores. Please try again.
54
+ </div>
55
+ )}
56
+ </div>
57
+ )
58
+ }
59
+ export default StoreList
@@ -0,0 +1,99 @@
1
+ import { MapPin, Phone, Clock, Navigation, MessageSquareText } from "lucide-react"
2
+ import { Button } from "@/components/ui/button"
3
+ import { metersToMiles } from "@/lib/utils/measure"
4
+ import CollapsibleStoreHours from "@/components/CollapsibleStoreHours"
5
+ import { useTranslation } from "react-i18next"
6
+
7
+ const StoreListItem = ({
8
+ id,
9
+ name,
10
+ placeId,
11
+ addressLineOne,
12
+ addressLineTwo,
13
+ city,
14
+ province,
15
+ postalCode,
16
+ country,
17
+ phone,
18
+ hours,
19
+ distance,
20
+ services = [],
21
+ isSelected,
22
+ handleSelect,
23
+ onMessageClick,
24
+ href,
25
+ }) => {
26
+ const { t } = useTranslation()
27
+
28
+ const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${name}&destination_place_id=${placeId}`
29
+
30
+ return (
31
+ <div
32
+ className={`bg-card border rounded-xl p-6 transition-all hover:shadow-lg cursor-pointer ${isSelected ? "ring-2 ring-primary shadow-lg" : ""
33
+ }`}
34
+ onClick={() => handleSelect(id)}
35
+ >
36
+ <div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
37
+ <div className="flex-1">
38
+ <div className="flex items-start justify-between gap-4 mb-3">
39
+ <div>
40
+ <h3 className="font-bold text-lg text-left mb-1">{name}</h3>
41
+ </div>
42
+ </div>
43
+
44
+ <div className="space-y-2 text-sm">
45
+ <div className="flex items-start gap-2">
46
+ <MapPin className="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
47
+ <div>
48
+ <p className="font-medium text-left">{addressLineOne}</p>
49
+ <p className="text-muted-foreground text-left">{city}, {province} {postalCode}</p>
50
+ <p className="text-primary font-medium mt-1 text-left">{t('default:distance_away', { distance: metersToMiles(distance).toFixed(1), distanceUnit: 'miles' })}</p>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="flex items-center gap-2">
55
+ <Phone className="w-4 h-4 text-muted-foreground" />
56
+ <a href={`tel:${phone?.phone_number}`} className="hover:text-primary transition-colors">
57
+ {phone?.formatted}
58
+ </a>
59
+ </div>
60
+
61
+ <div className="flex items-top gap-2">
62
+ <div className="mt-2.5">
63
+ <Clock className="w-4 h-4 text-muted-foreground" />
64
+ </div>
65
+ <span className="text-muted-foreground"><CollapsibleStoreHours hours={hours} /></span>
66
+ </div>
67
+ </div>
68
+
69
+ <div className="flex flex-wrap gap-2 mt-4">
70
+ {services.map((service) => (
71
+ <span
72
+ key={service}
73
+ className="px-3 py-1 bg-primary/10 text-primary text-xs font-medium rounded-full"
74
+ >
75
+ {service}
76
+ </span>
77
+ ))}
78
+ </div>
79
+ </div>
80
+
81
+ <div className="flex md:flex-col gap-2">
82
+ <a href={directionsUrl} target="_blank" rel="noopener noreferrer" className="flex-1 md:flex-none inline-flex items-center justify-center gap-2 rounded-md bg-primary text-primary-foreground text-sm font-medium h-8 px-4 hover:bg-primary/90 transition-colors">
83
+ <Navigation className="w-4 h-4" />
84
+ {t('default:get_directions')}
85
+ </a>
86
+ <Button variant="outline" className="flex-1 md:flex-none bg-transparent" size="sm" onClick={(e) => { e.stopPropagation(); onMessageClick(id); }}>
87
+ <MessageSquareText className="w-4 h-4" />
88
+ {t('default:message')}
89
+ </Button>
90
+ <a href={href} target="_blank" className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-secondary text-secondary-foreground hover:bg-secondary/80 flex-1 md:flex-none bg-transparent" size="sm">
91
+ {t('default:view_details')}
92
+ </a>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ export default StoreListItem
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+ import StoreListItem from './index'
3
+
4
+ describe('<StoreListItem />', () => {
5
+ it('renders', () => {
6
+ // see: https://on.cypress.io/mounting-react
7
+ cy.mount(<StoreListItem
8
+ id="test-id"
9
+ name="Test Store"
10
+ addressLine1="123 Main St"
11
+ city="Anytown"
12
+ province="CA"
13
+ postalCode="12345"
14
+ distance="2.5 mi"
15
+ phone="(123) 456-7890"
16
+ hours={[
17
+ { day: 0, open: true, open_time: "09:00", close_time: "17:00" },
18
+ { day: 1, open: true, open_time: "09:00", close_time: "17:00" },
19
+ { day: 2, open: true, open_time: "09:00", close_time: "17:00" },
20
+ { day: 3, open: true, open_time: "09:00", close_time: "17:00" },
21
+ { day: 4, open: true, open_time: "09:00", close_time: "17:00" },
22
+ { day: 5, open: false },
23
+ { day: 6, open: false },
24
+ ]}
25
+ isSelected={false}
26
+ handleSelect={() => { }}
27
+ onMessageClick={() => { }}
28
+ />)
29
+ })
30
+ })
@@ -0,0 +1,16 @@
1
+ import LeadForm from '@/components/LeadForm'
2
+
3
+ const StoreListNoneFound = () => {
4
+ return (
5
+ <div className="text-center py-1">
6
+ <div className="pb-5">
7
+ <p className="text-muted-foreground">No stores found matching your criteria.</p>
8
+ <p className="text-sm">You can try adjusting your search filters or expanding the search radius.</p>
9
+ </div>
10
+ <h4 className="mb-4 text-lg font-semibold">Contact Our Authorized Online Retailer</h4>
11
+ <LeadForm storeName="our authorized online retailer" />
12
+ </div>
13
+ )
14
+ }
15
+
16
+ export default StoreListNoneFound
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright (c) 2025 Colab Commerce <https://colabcommerce.com>
3
+ * All rights reserved.
4
+ *
5
+ * This is the main Store Locator component which ties together the search, list, map, and details components.
6
+ * Usage:
7
+ * <StoreLocator organizationId="org_123" locale="en-US" />
8
+ */
9
+
10
+ import { StoreLocatorProvider } from '@/contexts/store-locator'
11
+ import { APIProvider } from '@vis.gl/react-google-maps'
12
+ import StoreLocatorMap from '@/components/StoreLocatorMap'
13
+ import StoreLocatorSearch from '@/components/StoreLocatorSearch'
14
+ import StoreLocatorMessageDialog from '@/components/StoreLocatorMessageDialog'
15
+ import TranslationsProvider from '@/contexts/translations'
16
+ import StoreList from '@/components/StoreList'
17
+
18
+ const StoreLocator = ({ organizationId, locale, baseUrl = '/retailers' }) => {
19
+ return (
20
+ <TranslationsProvider options={{ lng: locale }}>
21
+ <StoreLocatorProvider organizationId={organizationId} locale={locale} baseUrl={baseUrl}>
22
+ <section className="py-12 lg:py-16 cc">
23
+ <div className="container mx-auto">
24
+ <div className="grid lg:grid-cols-2 gap-8">
25
+ <APIProvider apiKey={'AIzaSyAnJmWEU1r63DiRWHkjczxzHyIEq3dhj4M'}>
26
+ <StoreLocatorMap />
27
+ </APIProvider>
28
+ <div className="lg:order-first">
29
+ <div className="mb-8">
30
+ <StoreLocatorSearch />
31
+ </div>
32
+ <StoreList />
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </section>
37
+ <StoreLocatorMessageDialog />
38
+ </StoreLocatorProvider>
39
+ </TranslationsProvider>
40
+ )
41
+ }
42
+
43
+ export default StoreLocator
@@ -0,0 +1,93 @@
1
+ // The import part of the map is that you need to let the map managed its own center
2
+ // and zoom state internally. Using external state management (like React state) can lead to
3
+ // performance issues and a suboptimal user experience.
4
+ import { useEffect } from 'react'
5
+ import { MapPin } from 'lucide-react'
6
+ import { Map, useMap, AdvancedMarker } from '@vis.gl/react-google-maps'
7
+ import StoreLocatorMapMarker from '@/components/StoreLocatorMapMarker'
8
+ import { useStoreLocator } from '@/contexts/store-locator'
9
+
10
+ const StoreLocatorMap = () => {
11
+
12
+ const { location, stores, selectedStoreId, setSelectedStoreId } = useStoreLocator()
13
+
14
+ const map = useMap()
15
+
16
+ const zoomIn = () => {
17
+ if (!map) return;
18
+ const currentZoom = map.getZoom();
19
+ map.setZoom(currentZoom + 1);
20
+ }
21
+
22
+ const zoomOut = () => {
23
+ if (!map) return;
24
+ const currentZoom = map.getZoom();
25
+ map.setZoom(currentZoom - 1);
26
+ }
27
+
28
+ useEffect(() => {
29
+ if (!map) return;
30
+ if (location?.latitude && location?.longitude) {
31
+ map.panTo({ lat: location.latitude, lng: location.longitude })
32
+ map.setZoom(9)
33
+ }
34
+ }, [location, map]);
35
+
36
+ useEffect(() => {
37
+ if (!map || !selectedStoreId) return;
38
+ const selectedStore = stores.find(store => store.id === selectedStoreId)
39
+ if (selectedStore && selectedStore.address) {
40
+ map.panTo({ lat: selectedStore.address.latitude * 1, lng: selectedStore.address.longitude * 1 })
41
+ }
42
+ }, [selectedStoreId, stores, map]);
43
+
44
+ const handleMarkerClick = (storeId) => {
45
+ setSelectedStoreId(storeId)
46
+ }
47
+
48
+ return (
49
+ <div className="relative">
50
+ <div className="bg-muted rounded-xl overflow-hidden border shadow-md h-[600px] lg:sticky lg:top-24">
51
+ {/* Map placeholder - Replace with actual map integration (Google Maps, Mapbox, etc.) */}
52
+ <div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/5 to-muted">
53
+ <div className="text-center w-full">
54
+
55
+ <Map
56
+ style={{ width: '100%', height: '600px' }}
57
+ defaultCenter={{ lat: 22.54992, lng: 0 }}
58
+ defaultZoom={3}
59
+ gestureHandling='greedy'
60
+ disableDefaultUI
61
+ mapId="b1e4f5f5f8c7e2d9"
62
+ >
63
+ {stores.map((store) => (
64
+ <StoreLocatorMapMarker key={store.id} store={store} isSelected={selectedStoreId === store.id} onClick={handleMarkerClick} />
65
+ ))}
66
+ </Map>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Map controls overlay */}
71
+ <div className="absolute top-4 right-4 flex flex-col gap-2">
72
+ <button className="bg-card border shadow-md rounded-lg p-2 hover:bg-accent transition-colors" onClick={zoomIn}>
73
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
74
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
75
+ </svg>
76
+ </button>
77
+ <button className="bg-card border shadow-md rounded-lg p-2 hover:bg-accent transition-colors" onClick={zoomOut}>
78
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
79
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
80
+ </svg>
81
+ </button>
82
+ </div>
83
+
84
+ {/* Results count badge */}
85
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-card border shadow-lg rounded-full px-4 py-2 text-sm font-medium">
86
+ <span className="text-primary">{stores.length}</span> stores found
87
+ </div>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export default StoreLocatorMap
@@ -0,0 +1,55 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { MapPin, Navigation } from 'lucide-react'
3
+ import { AdvancedMarker, InfoWindow, useAdvancedMarkerRef } from '@vis.gl/react-google-maps'
4
+ import { Button } from "@/components/ui/button"
5
+ import { info } from 'autoprefixer'
6
+
7
+ const StoreLocatorMapMarker = ({ store, isSelected, onClick }) => {
8
+
9
+ const [infoWindowShown, setInfoWindowShown] = useState(false)
10
+ const [markerRef, marker] = useAdvancedMarkerRef()
11
+
12
+ const handleMarkerClick = useCallback(() => {
13
+ onClick && onClick(store.id)
14
+ }, [store.id, onClick])
15
+
16
+ const handleClose = useCallback(() => setInfoWindowShown(false), [])
17
+
18
+ const showInfoWindow = isSelected
19
+
20
+ return (
21
+ <AdvancedMarker
22
+ position={{
23
+ lat: store.address?.latitude * 1,
24
+ lng: store.address?.longitude * 1,
25
+ }}
26
+ ref={markerRef}
27
+ title={store.name}
28
+ onClick={handleMarkerClick}
29
+ >
30
+ <MapPin className={`w-8 h-8 hover:cursor-pointer ${isSelected ? 'text-primary fill-white z-10' : 'z-1 fill-white text-muted-foreground'}`} size={32} />
31
+ {showInfoWindow && (
32
+ <InfoWindow anchor={marker} headerContent={<h3 className="font-bold text-sm align-center text-left mb-2">{store.retailer_name}</h3>} onClose={handleClose}>
33
+ <p className="text-sm text-left text-muted-foreground">
34
+ {store.address?.street_line_one}
35
+ {store.address?.street_line_two ? `, ${store.address?.street_line_two}` : ''}
36
+ </p>
37
+ <p className="text-sm text-left text-muted-foreground mb-4">
38
+ {store.address?.city}, {store.address?.province} {store.address?.postal_code}
39
+ </p>
40
+ <div className="flex flex-row gap-2 mb-1">
41
+ <Button className="flex-1 md:flex-row" size="sm">
42
+ <Navigation className="w-4 h-4" />
43
+ Directions
44
+ </Button>
45
+ <Button variant="outline" className="flex-1 md:flex-none bg-transparent" size="sm">
46
+ View Details
47
+ </Button>
48
+ </div>
49
+ </InfoWindow>
50
+ )}
51
+ </AdvancedMarker>
52
+ )
53
+ }
54
+
55
+ export default StoreLocatorMapMarker
@@ -0,0 +1,20 @@
1
+ import MessageDialog from "@/components/MessageDialog"
2
+ import { useStoreLocator } from "@/contexts/store-locator"
3
+
4
+ const StoreLocatorMessageDialog = () => {
5
+
6
+ const { messageDialogOpen, setMessageDialogOpen, messageStoreId, setMessageStoreId, stores } = useStoreLocator()
7
+
8
+ const store = stores.find(store => store.id === messageStoreId)
9
+ const storeName = store ? store.retailer_name : null
10
+
11
+ const handleClose = () => {
12
+ setMessageDialogOpen(false)
13
+ setMessageStoreId(null)
14
+ }
15
+ return (
16
+ <MessageDialog isOpen={messageDialogOpen} onClose={handleClose} storeName={storeName} />
17
+ )
18
+ }
19
+
20
+ export default StoreLocatorMessageDialog