Error executing template "Designs/Dynaplus/Paragraph/DynaplusDealerLocatorParagraph.cshtml"
System.NullReferenceException: Object reference not set to an instance of an object.
at CompiledRazorTemplates.Dynamic.RazorEngine_4a2ca198bc354e5792c2c7d89e517095.GetDealersJson(List`1 dealers) in D:\inetpub\wwwroot\www.hoenderdaal-fasteners.nl\Files\Templates\Designs\Dynaplus\Paragraph\DynaplusDealerLocatorParagraph.cshtml:line 745
at CompiledRazorTemplates.Dynamic.RazorEngine_4a2ca198bc354e5792c2c7d89e517095.Execute() in D:\inetpub\wwwroot\www.hoenderdaal-fasteners.nl\Files\Templates\Designs\Dynaplus\Paragraph\DynaplusDealerLocatorParagraph.cshtml:line 42
at RazorEngine.Templating.TemplateBase.RazorEngine.Templating.ITemplate.Run(ExecuteContext context, TextWriter reader)
at RazorEngine.Templating.RazorEngineService.RunCompile(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass16_0.<RunCompile>b__0(TextWriter writer)
at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
at Dynamicweb.Rendering.Template.RenderRazorTemplate()
1 @using System.Text;
2 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel>
3 @{
4 var item = Model.Item;
5 if (item != null && Model.ItemType.Equals(Hoenderdaal.Items.Constants.Dynaplus.ItemConstants.Item_DealerLocatorParagraph))
6 {
7 // globals
8 var pageView = Dynamicweb.Frontend.PageView.Current();
9 var areaId = pageView.Area.ID;
10 var culture = pageView.GlobalTags.GetTagByName("Global:Area.LongLang").Value;
11 var baseUrl = "/Files/Templates/Designs/" + Pageview.Layout.Design.Folder.Name.TrimEnd('/');
12 var showDealerIds = Dynamicweb.Context.Current.Request.QueryString.Get("showdealerids");
13
14 // content
15 var googleMapsID = item.GetString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsID));
16 if (string.IsNullOrEmpty(googleMapsID))
17 {
18 googleMapsID = "766970dc8e26c170"; //default value
19 }
20 var mapZoom = item.GetString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsZoom));
21 var mapsLongitude = item.GetRawValueString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsLongitude)).Replace(",", ".");
22 var mapLatitude = item.GetRawValueString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsLatitude)).Replace(",", ".");
23 var mapRegion = item.GetString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsRegion));
24 if (string.IsNullOrEmpty(mapRegion))
25 {
26 mapRegion = "nl";
27 }
28
29 // settings for search filter
30 var mapZoomSearch = item.GetString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsZoomSearch));
31 var mapZoomSearchRange = item.GetString(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.GoogleMapsSearchRange));
32 if (string.IsNullOrEmpty(mapZoomSearchRange) || mapZoomSearchRange == "0")
33 {
34 mapZoomSearchRange = "25";
35 }
36
37 // map
38 var markerGrouped = item.GetFile(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.MarkerGrouped));
39 var marker = item.GetFile(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.Marker));
40 var markerCertified = item.GetFile(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.CertifiedMarker));
41 var dealers = item.GetField(nameof(Hoenderdaal.Items.Paragraphs.Dynaplus.DynaplusDealerLocatorParagraph.DealerItems))?.GetItems();
42 var dealersJson = GetDealersJson(dealers?.ToList());
43
44 <div id="@($"dealer-locator-{Model.ID}")" class="dealer-locator">
45 <div class="row align-items-center">
46 <div class="col-12 col-md-4 dealer-locator-sidebar">
47 <div class="dealer-list">
48
49 <div class="dealer-search">
50 <input type="search" id="dealer-search-filter-@Model.ID" placeholder="@Translate("Dynaplus:DealerLocator:SearchPlaceholder", "Plaats of postcode")">
51 <button class="search-btn" onclick="filterMarkers()"><svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
52 <g clip-path="url(#clip0_637_2373)">
53 <path d="M49.0153 13.5416V0L3.8763 0V13.5416L49.0153 13.5416Z" fill="white"/>
54 <path d="M35.4736 45.1396H49.0153L49.0153 0.00069046H35.4736L35.4736 45.1396Z" fill="white"/>
55 <path d="M0.00311582 39.4422L9.57849 49.0176L41.4966 17.0995L31.9212 7.52414L0.00311582 39.4422Z" fill="white"/>
56 </g>
57 <defs>
58 <clipPath id="clip0_637_2373">
59 <rect width="50" height="50" fill="white"/>
60 </clipPath>
61 </defs>
62 </svg>
63 </button>
64 </div>
65
66 @foreach (var dealer in Newtonsoft.Json.JsonConvert.DeserializeObject<List<dynamic>>(dealersJson))
67 {
68 <div class="dealer-container">
69 <a href="#" class="dealer-link" data-id="@dealer.Id">
70 <div class="dealer-name">@(!string.IsNullOrEmpty(showDealerIds) ? dealer.Id : string.Empty) @dealer.Name</div>
71
72 @if (Convert.ToBoolean(dealer.Certified) == true)
73 {
74 <div class="dealer-certified"><span class="certified-star"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
75 <path d="M11.6975 5.49191L9.28541 7.64948L10.0203 10.8761C10.0608 11.0513 10.0504 11.235 9.9903 11.404C9.93019 11.573 9.8231 11.7197 9.68257 11.8257C9.54204 11.9316 9.37437 11.992 9.20074 11.9993C9.02711 12.0065 8.85531 11.9602 8.70704 11.8663L6.00011 10.1393L3.29157 11.8663C3.14332 11.9597 2.97172 12.0055 2.79839 11.998C2.62506 11.9905 2.45774 11.93 2.3175 11.8242C2.17727 11.7183 2.07038 11.5718 2.01031 11.4031C1.95023 11.2344 1.93966 11.051 1.97991 10.8761L2.71748 7.64948L0.305367 5.49191C0.174201 5.3744 0.0793386 5.21944 0.032628 5.04637C-0.0140826 4.8733 -0.0105715 4.68981 0.0427227 4.5188C0.096017 4.34779 0.196733 4.19684 0.332291 4.08481C0.46785 3.97278 0.632244 3.90464 0.804944 3.88888L3.9675 3.62439L5.18749 0.563902C5.25353 0.397107 5.36592 0.254435 5.51038 0.154024C5.65484 0.0536135 5.82484 0 5.99877 0C6.1727 0 6.3427 0.0536135 6.48716 0.154024C6.63162 0.254435 6.74401 0.397107 6.81004 0.563902L8.0295 3.62439L11.1921 3.88888C11.3651 3.90405 11.53 3.97182 11.666 4.08369C11.8021 4.19557 11.9033 4.34657 11.9569 4.51779C12.0105 4.68901 12.0142 4.87282 11.9675 5.0462C11.9208 5.21959 11.8258 5.37483 11.6943 5.49247L11.6975 5.49191Z" fill="#CE1435"/>
76 </svg>
77 </span>@Translate("Dynaplus:DealerLocator:CertifiedDealer", "Gecertificeerde dealer")</div>
78 }
79
80 @{
81 // Parse and format the address components
82 var address = dealer.Address.ToString().Trim();
83 string streetName = "";
84 string houseNumber = "";
85 string postalCode = "";
86 string city = "";
87
88 // Check if the address is in the expected format (street, number, postal code, city)
89 var addressParts = address.Split(',');
90 if (addressParts.Length >= 4)
91 {
92 streetName = addressParts[0].Trim();
93 houseNumber = addressParts[1].Trim();
94 postalCode = addressParts[2].Trim();
95 city = addressParts[3].Trim();
96 }
97 }
98
99 <div class="dealer-address">
100 @($"{(string.IsNullOrEmpty(streetName) ? string.Empty : streetName)}" +
101 $"{(string.IsNullOrEmpty(houseNumber) ? string.Empty : " " + houseNumber)}" +
102 $"{(string.IsNullOrEmpty(postalCode) ? string.Empty : ", " + postalCode)}" +
103 $"{(string.IsNullOrEmpty(city) ? string.Empty : ", " + city.Substring(0, 1).ToUpper() + city.Substring(1).ToLower())}")
104 </div>
105 <div class="dealer-status" id="dealer-status-@dealer.Id">
106 <div class="status-placeholder">
107 <!-- These elements will be updated by JavaScript -->
108 <span class="open-status"></span>
109 <span class="status-delimiter"> • </span>
110 <span class="close-time"></span>
111 <div class="dealer-closed"></div>
112 </div>
113 </div>
114 </a>
115 </div>
116 }
117
118 <div class="dealer-container-empty" style="display: none;">
119 <span class="text">@Translate("Dynaplus:DealerLocator:NoDealersFound", $"Er konden geen dealers gevonden worden, binnen een straal van {mapZoomSearchRange} km.")</span>
120 </div>
121
122 </div>
123 </div>
124 <div class="col-12 col-md-8 dealer-locator__container">
125 <div id="map_@(Model.ID)" class="map_canvas" style="width: 100%; height: 900px;"></div>
126 <div id="loader-@Model.ID" class="loader" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="200" height="200" style="shape-rendering: auto; display: block; background: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g transform="rotate(0 50 50)">
127 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
128 <animate repeatCount="indefinite" begin="-0.9166666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
129 </rect>
130 </g><g transform="rotate(30 50 50)">
131 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
132 <animate repeatCount="indefinite" begin="-0.8333333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
133 </rect>
134 </g><g transform="rotate(60 50 50)">
135 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
136 <animate repeatCount="indefinite" begin="-0.75s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
137 </rect>
138 </g><g transform="rotate(90 50 50)">
139 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
140 <animate repeatCount="indefinite" begin="-0.6666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
141 </rect>
142 </g><g transform="rotate(120 50 50)">
143 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
144 <animate repeatCount="indefinite" begin="-0.5833333333333334s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
145 </rect>
146 </g><g transform="rotate(150 50 50)">
147 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
148 <animate repeatCount="indefinite" begin="-0.5s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
149 </rect>
150 </g><g transform="rotate(180 50 50)">
151 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
152 <animate repeatCount="indefinite" begin="-0.4166666666666667s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
153 </rect>
154 </g><g transform="rotate(210 50 50)">
155 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
156 <animate repeatCount="indefinite" begin="-0.3333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
157 </rect>
158 </g><g transform="rotate(240 50 50)">
159 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
160 <animate repeatCount="indefinite" begin="-0.25s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
161 </rect>
162 </g><g transform="rotate(270 50 50)">
163 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
164 <animate repeatCount="indefinite" begin="-0.16666666666666666s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
165 </rect>
166 </g><g transform="rotate(300 50 50)">
167 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
168 <animate repeatCount="indefinite" begin="-0.08333333333333333s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
169 </rect>
170 </g><g transform="rotate(330 50 50)">
171 <rect fill="#ce1435" height="12" width="6" ry="6" rx="3" y="24" x="47">
172 <animate repeatCount="indefinite" begin="0s" dur="1s" keyTimes="0;1" values="1;0" attributeName="opacity"></animate>
173 </rect>
174 </g><g></g></g><!-- [ldio] generated by https://loading.io --></svg></div> <!-- The loader -->
175 </div>
176 </div>
177 </div>
178
179 @SnippetStart("JavaScript")
180 <script>
181 window.onload = async function () {
182
183 const initialValues = construct();
184 let dealers = initialValues.dealers;
185 let mapId = initialValues.mapId;
186 let userLatitude = initialValues.userLatitude;
187 let userLongitude = initialValues.userLongitude;
188 let zoomCenter = initialValues.zoomCenter;
189
190 const searchInput = document.getElementById(`dealer-search-filter-${@(Model.ID)}`);
191
192 searchInput.addEventListener("keypress", function (event) {
193 if (event.key === "Enter") {
194 event.preventDefault();
195 filterMarkers();
196 }
197 });
198
199 if (typeof google !== "undefined") {
200 if ("geolocation" in navigator) {
201 // Async function to handle geolocation
202 const getLocation = async () => {
203 try {
204 const position = await new Promise((resolve, reject) => {
205 navigator.geolocation.getCurrentPosition(resolve, reject);
206 });
207
208 if (position.coords.latitude && position.coords.longitude) {
209 filterMarkers(position.coords.latitude, position.coords.longitude);
210 } else {
211 loadMap(dealers, mapId, zoomCenter, userLatitude, userLongitude);
212 }
213 } catch (error) {
214 console.error("Error getting location: ", error);
215 loadMap(dealers, mapId, zoomCenter, userLatitude, userLongitude);
216 }
217 };
218
219 getLocation();
220 } else {
221 loadMap(dealers, mapId, zoomCenter, userLatitude, userLongitude);
222 }
223 } else {
224 console.error("Google Maps API failed to load.");
225 }
226 };
227
228 document.addEventListener("DOMContentLoaded", async () => {
229 loader(true);
230 });
231
232 function construct() {
233 return {
234 dealers: JSON.parse(`@dealersJson.Replace("\"", "\\\"")`),
235 mapId: '@Model.ID',
236 userLatitude: parseFloat('@mapLatitude'.replace(",", ".")) === 0 ? 52.0842635 : parseFloat('@mapLatitude'.replace(",", ".")),
237 userLongitude: parseFloat('@mapsLongitude'.replace(",", ".")) === 0 ? 5.0000917 : parseFloat('@mapsLongitude'.replace(",", ".")),
238 zoomCenter: parseInt('@mapZoom') === 0 ? 7 : parseInt('@mapZoom'),
239 zoomCenterSearch: parseInt('@mapZoomSearch') === 0 ? 12 : parseInt('@mapZoomSearch'),
240 searchRange: parseInt('@mapZoomSearchRange') === 0 ? 25 : parseInt('@mapZoomSearchRange'),
241 };
242 }
243
244 async function filterMarkers(overrideLatitude, overrideLongitude) {
245 const initialValues = construct();
246 let dealers = initialValues.dealers;
247 let mapId = initialValues.mapId;
248 var userLatitude = initialValues.userLatitude;
249 var userLongitude = initialValues.userLongitude;
250 var zoomCenter = initialValues.zoomCenter;
251 var zoomCenterSearch = initialValues.zoomCenterSearch;
252 var searchRange = initialValues.searchRange;
253
254 //add class for mobile visibility of the sidebar
255 document.querySelector('.dealer-locator-sidebar')?.classList.add('visible');
256
257 let value = document.getElementById(`dealer-search-filter-${@(Model.ID)}`)?.value ?? '';
258
259 if (value.trim() === '') {
260 resetDealerList();
261 loadMap(dealers, mapId, zoomCenter, userLatitude, userLongitude);
262 return;
263 }
264
265 loader(true);
266
267 try {
268 var responseText = '';
269 var response = '';
270 var match = false;
271 const apiUrl = '/hfwebapi/geocoding/filter-markers/@areaId';
272
273 if (overrideLatitude === undefined || overrideLongitude === undefined) {
274
275 const region = getRegion(value);
276
277 response = await fetch(apiUrl, {
278 method: "POST",
279 headers: {
280 "Content-Type": "application/json",
281 },
282 body: JSON.stringify({
283 filter: value,
284 region: region,
285 }),
286 });
287
288 if (!response.ok) {
289 console.log('Failed to fetch coordinates');
290 }
291
292 responseText = await response.text();
293 }
294 else {
295 responseText = "{ Lat = " + overrideLatitude + ", Lng = " + overrideLongitude + " }";
296 }
297
298 match = responseText.match(/Lat\s*=\s*([\d.,-]+),\s*Lng\s*=\s*([\d.,-]+)/);
299
300 if (match) {
301
302 const Lat = parseFloat(match[1].replace(',', '.')); // Replace commas with dots for valid float
303 const Lng = parseFloat(match[2].replace(',', '.'));
304
305 const maxDistance = searchRange;
306 const filteredDealers = dealers
307 .filter(dealer => {
308 // Ensure that each dealer has Latitude and Longitude properties
309 if (dealer.Latitude && dealer.Longitude) {
310 // Calculate the distance between the search location and dealer's location
311 const distance = calculateDistance(Lat, Lng, dealer.Latitude, dealer.Longitude);
312 dealer.distance = distance; // Attach the distance to the dealer object
313 return distance <= maxDistance; // Only include dealers within the search range
314 }
315 return false; // Exclude dealers without valid coordinates
316 })
317 .sort((a, b) => {
318 if (a.distance !== undefined && b.distance !== undefined) {
319 return a.distance - b.distance; // Sort dealers by distance (ascending)
320 }
321 return 0; // If no distance, don't change the order
322 });
323
324 // Load the filtered dealers on the map
325 loadMap(filteredDealers, mapId, zoomCenterSearch, Lat, Lng);
326
327 updateDealerList(filteredDealers, response.status);
328
329 // update dealer status during filtering
330 if (value !== '') {
331 for (let i = 0; i < filteredDealers.length; i++) {
332
333 const details = await getPlaceDetails(filteredDealers[i].Latitude, filteredDealers[i].Longitude);
334
335 await updateDealerStatus(filteredDealers[i], i, details);
336 }
337 }
338
339 } else {
340
341 updateDealerList([], response.status); // Call with empty array on non-200 status, to show empty list of dealers.
342 loader(false);
343
344 console.error("Invalid format in response:", responseText);
345 }
346 } catch (error) {
347
348 updateDealerList([]); // Call with empty array on non-200 status, to show empty list of dealers.
349 loader(false);
350
351 console.error("Error fetching coordinates:", error);
352 }
353 }
354
355 async function loadMap(dealers, mapId, zoom, latitude, longitude) {
356
357 const mapOptions = {
358 zoom,
359 center: new google.maps.LatLng(latitude || '@mapLatitude', longitude || '@mapsLongitude'),
360 mapTypeId: google.maps.MapTypeId.TERRAIN,
361 mapTypeControl: false,
362 mapId: '@googleMapsID', // Map ID is required for advanced markers.
363 };
364
365 const map = new google.maps.Map(document.getElementById(`map_${mapId}`), mapOptions);
366 var infowindow;
367 var markers = [];
368
369 try {
370
371 for (var i = 0; i < dealers.length; i++) {
372 const coordinates = {
373 latitude: dealers[i].Latitude,
374 longitude: dealers[i].Longitude
375 };
376
377 dealers[i].Coordinates = coordinates;
378
379 const certified = dealers[i]["Certified"];
380
381 const markerContent = document.createElement("div");
382 markerContent.innerHTML = `<img src="${certified === 'true' ? '@marker.Path' : '@markerCertified.Path'}">`;
383
384 var marker = new google.maps.marker.AdvancedMarkerElement({
385 position: new google.maps.LatLng(dealers[i].Latitude, dealers[i].Longitude),
386 map: map,
387 title: dealers[i].Name,
388 content: markerContent,
389 });
390
391 marker.id = dealers[i].Id;
392
393 markers.push(marker);
394
395 // Add a click listener to the marker to load details on demand
396 (function (marker, dealer, i) {
397 marker.addListener('click', async () => {
398 if (infowindow) {
399 infowindow.close();
400 }
401 infowindow = new google.maps.InfoWindow();
402
403 try {
404 const details = await getPlaceDetails(dealers[i].Latitude, dealers[i].Longitude);
405
406 infowindow.setContent(`
407 <div class="info-window">
408 <h5>${dealer.Name}</h5>
409 <p class="certified">
410 ${certified === 'true' ? `
411 <span class="certified-star"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
412 <path d="M11.6975 5.49191L9.28541 7.64948L10.0203 10.8761C10.0608 11.0513 10.0504 11.235 9.9903 11.404C9.93019 11.573 9.8231 11.7197 9.68257 11.8257C9.54204 11.9316 9.37437 11.992 9.20074 11.9993C9.02711 12.0065 8.85531 11.9602 8.70704 11.8663L6.00011 10.1393L3.29157 11.8663C3.14332 11.9597 2.97172 12.0055 2.79839 11.998C2.62506 11.9905 2.45774 11.93 2.3175 11.8242C2.17727 11.7183 2.07038 11.5718 2.01031 11.4031C1.95023 11.2344 1.93966 11.051 1.97991 10.8761L2.71748 7.64948L0.305367 5.49191C0.174201 5.3744 0.0793386 5.21944 0.032628 5.04637C-0.0140826 4.8733 -0.0105715 4.68981 0.0427227 4.5188C0.096017 4.34779 0.196733 4.19684 0.332291 4.08481C0.46785 3.97278 0.632244 3.90464 0.804944 3.88888L3.9675 3.62439L5.18749 0.563902C5.25353 0.397107 5.36592 0.254435 5.51038 0.154024C5.65484 0.0536135 5.82484 0 5.99877 0C6.1727 0 6.3427 0.0536135 6.48716 0.154024C6.63162 0.254435 6.74401 0.397107 6.81004 0.563902L8.0295 3.62439L11.1921 3.88888C11.3651 3.90405 11.53 3.97182 11.666 4.08369C11.8021 4.19557 11.9033 4.34657 11.9569 4.51779C12.0105 4.68901 12.0142 4.87282 11.9675 5.0462C11.9208 5.21959 11.8258 5.37483 11.6943 5.49247L11.6975 5.49191Z" fill="#CE1435"/>
413 </svg>
414 </span>
415 @Translate("Dynaplus:DealerLocator:CertifiedDealer", "Gecertificeerde dealer")
416 ` : ''}
417 </p>
418 <p>
419 ${details.address && details.address.trim() ? formatPopupAddress(details.address) : ''}
420 ${details.phone && details.phone.trim() ? `<a href="tel:${details.phone}"><svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
421 <path d="M27.6531 32C27.6446 32 27.6361 32 27.6276 32C23.5278 31.8658 18.7249 27.8922 15.4158 24.5813C12.1023 21.2703 8.12831 16.4658 8.00046 12.3858C7.95359 10.954 11.4673 8.40794 11.5036 8.38237C12.4156 7.74745 13.4277 7.9733 13.8432 8.54856C14.1245 8.93846 16.788 12.9738 17.0778 13.4319C17.3783 13.907 17.3335 14.6144 16.9585 15.3238C16.7518 15.718 16.0657 16.9239 15.7439 17.4864C16.0912 17.9807 17.0096 19.193 18.9061 21.0892C20.8046 22.9855 22.015 23.9059 22.5114 24.2532C23.074 23.9314 24.28 23.2454 24.6742 23.0387C25.3731 22.668 26.0763 22.6211 26.5558 22.9152C27.0459 23.2156 31.071 25.8916 31.4418 26.1494C31.7529 26.3688 31.9532 26.7438 31.9936 27.1806C32.032 27.6216 31.8956 28.0882 31.6122 28.4952C31.5888 28.5293 29.0723 32 27.6531 32Z" fill="currentColor"/>
422 </svg>
423 @Translate("Dynaplus:DealerLocator:PhoneNumber", "Telefoonnummer") </a>` : ''}
424 ${details.website && details.website.trim() ? `<a href="${details.website}" target="_blank"><svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
425 <path d="M19.6939 8C13.2459 8 8 13.2459 8 19.6939C8 26.142 13.2459 31.3879 19.6939 31.3879C20.0892 31.3879 20.4786 31.3671 20.8633 31.3285V30.2185V25.5409C20.8633 24.8942 20.3395 24.3715 19.6939 24.3715H18.5246V22.7339C18.5246 22.4745 18.4224 22.2353 18.2665 22.0327H20.8633V19.6939H16.1858V17.3552H17.3552C18.0007 17.3552 18.5246 16.8324 18.5246 16.1858V13.8744L20.8816 13.8561C22.1609 13.8479 23.2021 12.7978 23.2021 11.5173V11.0263C26.6263 12.4174 29.0491 15.7764 29.0491 19.6939C29.0491 19.708 29.0468 19.721 29.0468 19.7351L31.1869 21.7975C31.312 21.1134 31.3879 20.412 31.3879 19.6939C31.3879 13.2459 26.142 8 19.6939 8ZM19.6939 10.3388C20.0909 10.3388 20.4792 10.3705 20.8633 10.4187V11.5173L17.346 11.5447C16.704 11.5494 16.1858 12.0721 16.1858 12.7141V15.0164H15.0164C14.3709 15.0164 13.847 15.5391 13.847 16.1858V17.5721L11.4853 15.2105C13.076 12.3098 16.159 10.3388 19.6939 10.3388ZM23.2021 17.3552V29.0491L25.8607 26.5207L28.2383 32L30.81 30.8306L28.4142 25.5409H31.7077L23.2021 17.3552ZM10.5786 17.611L16.1858 23.2181V24.3715C16.1858 25.6614 17.2347 26.7103 18.5246 26.7103V28.9692C13.9176 28.3906 10.3388 24.4552 10.3388 19.6939C10.3388 18.9771 10.4252 18.282 10.5786 17.611Z" fill="currentColor"/>
426 </svg>
427 @Translate("Dynaplus:DealerLocator:Website", "Website")</a><br>` : ''}
428 ${details.openingHours && details.openingHours.trim() ? `
429 Openingstijden:
430 <ul>
431 ${details.openingHours.split(',').map(day => {
432 const normalizedDay = day.replace(/[\[\]"]/g, '').replace(/\u202F|\u2009/g, ' ').trim();
433 const [dayName, hours] = normalizedDay.split(/:(.+)/).map(item => item.trim());
434
435 const translation = {
436 "Monday": "@Translate("Dynaplus:DealerLocator:Monday", "Maandag")",
437 "Tuesday": "@Translate("Dynaplus:DealerLocator:Tuesday", "Dinsdag")",
438 "Wednesday": "@Translate("Dynaplus:DealerLocator:Wednesday", "Woensdag")",
439 "Thursday": "@Translate("Dynaplus:DealerLocator:Thursday", "Donderdag")",
440 "Friday": "@Translate("Dynaplus:DealerLocator:Friday", "Vrijdag")",
441 "Saturday": "@Translate("Dynaplus:DealerLocator:Saturday", "Zaterdag")",
442 "Sunday": "@Translate("Dynaplus:DealerLocator:Sunday", "Zondag")"
443 }[dayName] || dayName;
444
445 const timeMatch = hours.match(/(\d{1,2}:\d{2})\s*(AM|PM)?\s*[-–]\s*(\d{1,2}:\d{2})\s*(AM|PM)?/i);
446 if (timeMatch) {
447 const formatTime = (time, period) => {
448 let [hour, minute] = time.split(':').map(Number);
449 if (period && period.toUpperCase() === 'PM' && hour < 12) hour += 12;
450 if (period && period.toUpperCase() === 'AM' && hour === 12) hour = 0;
451 return `${hour}:${minute.toString().padStart(2, '0')}`;
452 };
453
454 const openTime = formatTime(timeMatch[1], timeMatch[2]);
455 const closeTime = formatTime(timeMatch[3], timeMatch[4]);
456 return `<li>${translation}: ${openTime} - ${closeTime}</li>`;
457 } else if (hours.trim().toLowerCase() === "closed") {
458 return `<li>${translation}: @Translate("Dynaplus:DealerLocator:Closed", "Gesloten")</li>`;
459 } else {
460 return `<li>${translation}: @Translate("Dynaplus:DealerLocator:Closed", "Onbekend")</li>`;
461 }
462 }).join('')}
463 </ul>
464 ` : ''}
465 </p>
466 </div>
467 `);
468 infowindow.open(map, marker);
469 } catch (error) {
470 console.error("Error fetching details: ", error);
471 }
472 });
473 })(marker, dealers[i], i);
474 }
475 } catch (error) {
476 console.error("Error loading map: ", error);
477 }
478
479 document.querySelectorAll('.dealer-link').forEach((dealerLink) => {
480
481 dealerLink.addEventListener('click', function (event) {
482 event.preventDefault();
483 const dealerId = this.getAttribute('data-id');
484
485 const marker = markers.find(m => m.id.toString() === dealerId);
486
487 if (marker) {
488 map.setZoom(15);
489 map.panTo(marker.position); // Access the position directly from 'marker.position'
490 google.maps.event.trigger(marker, 'click');
491 }
492 });
493 });
494
495 const dealerIdFromQuery = getQueryStringParam('dealerid');
496
497 if (dealerIdFromQuery) {
498 const dealer = dealers.find(d => d.Id === dealerIdFromQuery);
499
500 if (dealer) {
501 const marker = markers.find(m => m.id === dealerIdFromQuery);
502
503 if (marker) {
504 map.setZoom(15);
505 map.panTo(marker.getPosition());
506 google.maps.event.trigger(marker, 'click');
507 }
508 }
509 }
510
511 const renderer = {
512 render: ({ count, position }) => {
513 // Create a div to hold the custom marker content
514 const markerContent = document.createElement("div");
515 markerContent.className = "clustered-marker";
516
517 markerContent.innerHTML = `
518 <img src="${'@markerGrouped.Path'}" class="clustered-marker-icon" alt="Cluster Marker">
519 <span class="clustered-marker-label">${count}</span>
520 `;
521
522 return new google.maps.marker.AdvancedMarkerElement({
523 position,
524 zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count,
525 content: markerContent, // Use `content` instead of `icon` and `label`
526 });
527 }
528 };
529
530 new markerClusterer.MarkerClusterer({ markers: markers, map: map, renderer: renderer });
531
532 loader(false);
533 }
534
535 function resetDealerList() {
536 document.querySelector('.dealer-container')?.classList.remove('visible');
537 document.querySelector('.dealer-locator-sidebar')?.classList.remove('visible');
538
539 document.querySelectorAll(".dealer-container").forEach(dealerElement => {
540 dealerElement.style.display = "";
541 });
542 }
543
544 function updateDealerList(filteredDealers, statusCode) {
545 const dealerContainers = document.querySelectorAll(".dealer-container");
546 const emptyStateElement = document.querySelector(".dealer-container-empty");
547 const parentElement = document.querySelector(".dealer-list"); // Replace with the actual parent element selector
548
549 let visibleDealersCount = 0;
550
551 // Sort dealers by distance (ascending order)
552 filteredDealers.sort((a, b) => a.distance - b.distance);
553
554 // Clear the parent element and re-add sorted dealers
555 filteredDealers.forEach(dealer => {
556 const dealerElement = Array.from(dealerContainers).find(el =>
557 el.querySelector(".dealer-link").getAttribute("data-id") === dealer.Id
558 );
559
560 if (dealerElement) {
561 // Make the dealer visible
562 dealerElement.style.display = "";
563 dealerElement.classList.add("visible");
564 visibleDealersCount++;
565
566 // Append the sorted dealer container back to the parent
567 parentElement.appendChild(dealerElement);
568 }
569 });
570
571 // Hide any dealers not in the filtered list
572 dealerContainers.forEach(dealerElement => {
573 const dealerId = dealerElement.querySelector(".dealer-link").getAttribute("data-id");
574 if (!filteredDealers.some(dealer => dealer.Id === dealerId)) {
575 dealerElement.style.display = "none";
576 dealerElement.classList.remove("visible");
577 }
578 });
579
580 // Update empty state visibility
581 if (emptyStateElement) {
582
583 if (statusCode === 429) {
584 emptyStateElement.querySelector('.text').textContent = '@Translate("Dynaplus:DealerLocator:ToManyRequests", $"Het filteren kan maar eens per minuut.")';
585 } else {
586 emptyStateElement.querySelector('.text').textContent = '@Translate("Dynaplus:DealerLocator:NoDealersFound", $"Er konden geen dealers gevonden worden, binnen een straal van {mapZoomSearchRange} km.")';
587 }
588
589
590 emptyStateElement.style.display = visibleDealersCount === 0 ? "block" : "none";
591 }
592 }
593
594 function updateDealerStatus(dealer, index, details) {
595 if (details.openingHours !== '') {
596 const dealerStatusElement = document.getElementById(`dealer-status-${dealer.Id}`);
597
598 if (dealerStatusElement) {
599 const statusPlaceholder = dealerStatusElement.querySelector(".status-placeholder");
600 statusPlaceholder.classList.remove("status-open", "status-closed");
601
602 if (details.isOpen) {
603 const openStatusText = "@Translate("Dynaplus:DealerLocator:Open", "Geopend")";
604 const currentDayIndex = new Date().getDay(); // Get current day index (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
605 const closingTimeForToday = getClosingTimeForDay(details.periodsClosed, currentDayIndex);
606
607 const closeTimeText = closingTimeForToday
608 ? `@Translate("Dynaplus:DealerLocator:ClosedAt", "Sluit om") ${formatTime(closingTimeForToday)}`
609 : "@Translate("Dynaplus:DealerLocator:NoClosingTime", "Geen sluitingstijd beschikbaar")";
610
611 statusPlaceholder.classList.add("status-open");
612 statusPlaceholder.querySelector(".open-status").innerText = openStatusText;
613 statusPlaceholder.querySelector(".close-time").innerText = closeTimeText;
614 } else {
615 const closedStatusText = "@Translate("Dynaplus:DealerLocator:Closed", "Gesloten")";
616 statusPlaceholder.classList.add("status-closed");
617 statusPlaceholder.querySelector(".dealer-closed").innerText = closedStatusText;
618 }
619 }
620 }
621 }
622
623 function getClosingTimeForDay(periodsClosed, dayIndex) {
624 // Map JavaScript's day index (0 = Sunday) to your day key (1 = Monday, ..., 7 = Sunday)
625 const adjustedDayIndex = dayIndex === 0 ? 7 : dayIndex; // Map Sunday (0) to 7
626 const period = periodsClosed.find(p => p.Key === String(adjustedDayIndex));
627 return period ? period.Value : null; // Return the closing time or null if not found
628 }
629
630 function formatTime(time) {
631 return time.slice(0, 2) + ":" + time.slice(2, 4); // Format '1800' to '18:00'
632 }
633
634 async function getPlaceDetails(latitude, longitude) {
635 const apiUrl = "/hfwebapi/geocoding/get-place-details/@areaId/@culture.Substring(0, 2)";
636
637 try {
638 const response = await fetch(apiUrl, {
639 method: "POST",
640 headers: {
641 "Content-Type": "application/json",
642 },
643 body: JSON.stringify({
644 Latitude: latitude,
645 Longitude: longitude
646 }),
647 });
648
649 if (!response.ok) {
650 throw new Error(`API error: ${response.statusText}`);
651 }
652
653 const placeDetails = await response.json();
654
655 return {
656 phone: placeDetails.Phone || '',
657 website: placeDetails.Website || '',
658 address: placeDetails.Address || '',
659 openingHours: placeDetails.OpeningHours.replace('[', '').replace(']', '') || '',
660 isOpen: placeDetails.IsOpen || '',
661 periodsOpen: placeDetails.PeriodsStart || '',
662 periodsClosed: placeDetails.PeriodsClose || '',
663 };
664 } catch (error) {
665 console.error("Error fetching place details:", error);
666 throw error;
667 }
668 }
669
670 function loader(visible) {
671 document.getElementById(`loader-${@(Model.ID)}`).style.display = visible === true ? 'block' : 'none';
672 }
673
674 function getQueryStringParam(param) {
675 const urlParams = new URLSearchParams(window.location.search);
676 return urlParams.get(param)?.toLowerCase() || "";
677 }
678
679 function calculateDistance(lat1, lon1, lat2, lon2) {
680 const R = 6371;
681 const dLat = toRad(lat2 - lat1);
682 const dLon = toRad(lon2 - lon1);
683 const a =
684 Math.sin(dLat / 2) * Math.sin(dLat / 2) +
685 Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
686 Math.sin(dLon / 2) * Math.sin(dLon / 2);
687 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
688 return R * c;
689 }
690
691 function toRad(value) {
692 return value * Math.PI / 180;
693 }
694
695 function formatPopupAddress(address) {
696 // Updated regex to handle street names with spaces and extra characters like "T"
697 const regex = /^(.*\d+[A-Za-z]*)\s*,\s*(\d{4}\s?[A-Z]{2})\s([a-zA-Z\s]+),\s*[^,]+$/;
698 const match = address.match(regex);
699
700 if (match) {
701 const street = match[1]; // Street and number (including "T" or other characters)
702 const zip = match[2]; // Zip code
703 const city = match[3]; // City
704 // The country part is ignored
705
706 // Return the formatted address with line breaks
707 return `${street}<br>${zip} ${city}`;
708 }
709 return address; // If no match found, return the address as-is
710 }
711
712 function isDutchZipCode(value) {
713 const dutchZipCodeRegex = /^\d{4}\s?[A-Za-z]{2}$/;
714 return dutchZipCodeRegex.test(value);
715 }
716
717 function isBelgianZipCode(value) {
718 const belgianZipCodeRegex = /^\d{4}$/;
719 return belgianZipCodeRegex.test(value);
720 }
721
722 function getRegion(value) {
723 if (isDutchZipCode(value)) {
724 return 'nl';
725 } else if (isBelgianZipCode(value)) {
726 return 'be';
727 }
728 return 'nl';
729 }
730 </script>
731 @SnippetEnd("JavaScript")
732 }
733 }
734
735
736 @functions{
737 public string GetDealersJson(List<Dynamicweb.Frontend.ItemViewModel> dealers)
738 {
739 StringBuilder stringBuilder = new StringBuilder();
740 stringBuilder.Append("[");
741
742 var processedDealerIds = new HashSet<string>();
743 bool isFirstDealer = true;
744
745 foreach (var dealer in dealers)
746 {
747 var dealerItem = dealer as Dynamicweb.Frontend.ItemViewModel;
748 if (dealerItem != null && !processedDealerIds.Contains(dealerItem.Id))
749 {
750 var active = dealerItem.GetBoolean(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.Active));
751 if (active)
752 {
753 if (!isFirstDealer)
754 {
755 stringBuilder.Append(",");
756 }
757
758 stringBuilder.Append($"{{\"Id\":\"{dealerItem.Id}\",");
759 stringBuilder.Append($"\"Name\":\"{dealerItem.GetString(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.DealerName))}\",");
760 stringBuilder.Append($"\"Certified\":\"{dealerItem.GetBoolean(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.Certified)).ToString().ToLower()}\",");
761 stringBuilder.Append($"\"Latitude\":\"{dealerItem.GetRawValueString(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.Latitude)).ToString().ToLower()}\",");
762 stringBuilder.Append($"\"Longitude\":\"{dealerItem.GetRawValueString(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.Longitude)).ToString().ToLower()}\",");
763 stringBuilder.Append($"\"Address\":\"{dealerItem.GetString(nameof(Hoenderdaal.Items.Partials.Dynaplus.DynaplusDealerItem.Address))}\"}}");
764
765 isFirstDealer = false;
766
767 // Mark this dealer as processed
768 processedDealerIds.Add(dealerItem.Id);
769 }
770 }
771 }
772 stringBuilder.Append("]");
773 return stringBuilder.ToString();
774 }
775 }