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 }