No products in cart

Error compiling template "Designs/Swift-v2/eCom/ProductCatalog/Core_ProductDetail.cshtml"
Line 26: The using directive for 'System.Text.RegularExpressions' appeared previously in this namespace
Line 1269: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 1256: The contextual keyword 'var' may only appear within a local variable declaration or in script code
Line 74: 'IResponse.Redirect(string)' is obsolete: 'Do not use'
Line 95: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 98: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 101: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 104: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 110: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 110: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 584: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 689: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 862: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 905: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 910: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 913: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 916: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.
Line 191: The local function 'RememberVGOption' is declared but never used
Line 1260: The type or namespace name 'SystemLog' does not exist in the namespace 'Dynamicweb.Core' (are you missing an assembly reference?)

1 // <auto-generated/> 2 #pragma warning disable 1591 3 namespace CompiledRazorTemplates.Dynamic 4 { 5 #line hidden 6 using System.Threading.Tasks; 7 using System; 8 using System.Collections.Generic; 9 using Core.Extensions; 10 using Dynamicweb.Ecommerce.ProductCatalog; 11 using Dynamicweb.Rendering; 12 using Core.Services; 13 using Core.ViewModels.Base; 14 using Core.ViewModels.UI; 15 using Dynamicweb; 16 using Dynamicweb.Core.Encoders; 17 using Dynamicweb.Frontend; 18 using Dynamicweb.Security.UserManagement; 19 using NuGet.Protocol; 20 using System.Text.RegularExpressions; 21 using System.Globalization; 22 using ButtonViewModel = Core.ViewModels.UI.ButtonViewModel; 23 using ProductViewModel = Core.ViewModels.Ecommerce.ProductViewModel; 24 using Mennt.Dynamicweb.CustomerProductAliases; 25 using System.Runtime.Caching; 26 using System.Text.RegularExpressions; 27 using System.Linq; 28 internal class RazorEngine_745c2949546a46f18eb65a4ce9b8fc34 : ViewModelTemplate<Core.ViewModels.Ecommerce.ProductViewModel> 29 { 30 #pragma warning disable 1998 31 public async override global::System.Threading.Tasks.Task ExecuteAsync() 32 { 33 WriteLiteral("\r\n\r\n"); 34 WriteLiteral("\r\n"); 35 36 var req = Dynamicweb.Context.Current?.Request; 37 var res = Dynamicweb.Context.Current?.Response; 38 39 var feed = (req?["feed"] ?? "").Equals("true", StringComparison.InvariantCultureIgnoreCase); 40 var getproductinfo = (req?["getproductinfo"] ?? "").Equals("true", StringComparison.InvariantCultureIgnoreCase); 41 var isVariantAjaxRequest = feed || getproductinfo; 42 43 if (req != null && res != null) 44 { 45 46 // Hvis dette er en "ekte" sidevisning (refresh/navigate), skal vi IKKE vise feed-mode. 47 // Sec-Fetch-Mode er ofte "navigate" på refresh/vanlig navigasjon. 48 var secFetchMode = req.Headers["Sec-Fetch-Mode"] ?? ""; 49 var isNavigate = secFetchMode.Equals("navigate", StringComparison.InvariantCultureIgnoreCase); 50 51 // Hvis feed/getproductinfo er med, men det er en vanlig navigasjon => redirect til clean URL. 52 if ((feed || getproductinfo) && isNavigate) 53 { 54 var url = req.Url; 55 var query = url?.Query?.TrimStart('?') ?? ""; 56 var pairs = new List<KeyValuePair<string, string>>(); 57 foreach (var part in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) 58 { 59 var idx = part.IndexOf('='); 60 if (idx < 0) continue; 61 var key = Uri.UnescapeDataString(part.Substring(0, idx)); 62 var val = idx + 1 < part.Length ? Uri.UnescapeDataString(part.Substring(idx + 1)) : ""; 63 if (string.Equals(key, "feed", StringComparison.OrdinalIgnoreCase) || 64 string.Equals(key, "getproductinfo", StringComparison.OrdinalIgnoreCase) || 65 string.Equals(key, "variantCount", StringComparison.OrdinalIgnoreCase)) 66 continue; 67 pairs.Add(new KeyValuePair<string, string>(key, val)); 68 } 69 var newQuery = string.Join("&", pairs.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); 70 var clean = url.AbsolutePath; 71 if (!string.IsNullOrWhiteSpace(newQuery)) 72 clean += "?" + newQuery; 73 74 Dynamicweb.Context.Current.Response.Redirect(clean); 75 } 76 } 77 78 // ---------------------------- 79 // Base data 80 // ---------------------------- 81 var variantCountRaw = Dynamicweb.Context.Current?.Request?["variantCount"]; 82 int alreadyRendered = 0; 83 int.TryParse(variantCountRaw, out alreadyRendered); 84 alreadyRendered = Math.Max(0, alreadyRendered); 85 86 PerfLog("Start"); 87 List<string> variantIdCombinations = Model?.VariantCombinations() ?? new List<string>(); 88 PerfLog("After VariantCombinations"); 89 var fieldGroups = Model?.FieldDisplayGroups; 90 91 var productInfoGroup = fieldGroups?.FirstOrDefault(fdg => fdg.Key == "Produktinformasjon"); 92 var productInfoFields = productInfoGroup?.Value?.Fields; 93 bool hasProductInfo = productInfoFields != null && productInfoFields.Any(); 94 95 CategoryFieldViewModel? techinicalSpecsFieldDisplayGroup = 96 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase)); 97 98 CategoryFieldViewModel? documentsFieldDisplayGroup = 99 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Dokumentasjon", StringComparison.InvariantCultureIgnoreCase)); 100 101 CategoryFieldViewModel? drawingsFieldDisplayGroup = 102 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase)); 103 104 CategoryFieldViewModel? variantInfoFieldDisplayGroup = 105 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 106 107 var variantInfoFields = variantInfoFieldDisplayGroup?.Fields; 108 bool hasVariantInfo = variantInfoFields != null && variantInfoFields.Any(); 109 110 List<CategoryFieldViewModel?> displayGroups = new List<CategoryFieldViewModel?> 111 { 112 techinicalSpecsFieldDisplayGroup, 113 documentsFieldDisplayGroup, 114 drawingsFieldDisplayGroup 115 }; 116 117 bool hasVariants = ((Model?.VariantInfo?.VariantInfo?.Count) ?? 0) > 1 || variantIdCombinations.Count > 0; 118 119 // ---------------------------- 120 // FILTERS (single select) 121 // Query params: 122 // f_vg_<slug>=value 123 // f_vi_<systemName>=value 124 // ---------------------------- 125 126 // FIX #2: VG stores "wanted" which can be OPTION ID (new) OR OPTION NAME (legacy) 127 var activeVG = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); // slug -> wanted (id or name) 128 var activeVI = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); // sys -> value 129 130 if (req != null) 131 { 132 foreach (string key in req.Params.AllKeys) 133 { 134 if (string.IsNullOrWhiteSpace(key)) continue; 135 136 if (key.StartsWith("f_vg_", StringComparison.InvariantCultureIgnoreCase)) 137 { 138 var slug = key.Substring(5); 139 var val = req.Params[key] ?? ""; 140 if (!string.IsNullOrWhiteSpace(slug) && !string.IsNullOrWhiteSpace(val)) 141 activeVG[slug] = val; 142 } 143 else if (key.StartsWith("f_vi_", StringComparison.InvariantCultureIgnoreCase)) 144 { 145 var sys = key.Substring(5); 146 var val = req.Params[key] ?? ""; 147 if (!string.IsNullOrWhiteSpace(sys) && !string.IsNullOrWhiteSpace(val)) 148 activeVI[sys] = val; 149 } 150 } 151 } 152 153 bool HasAnyFilters() => activeVG.Count > 0 || activeVI.Count > 0; 154 155 // Performance test: 156 // On the first normal product page view we skip the expensive full variant/facet build. 157 // On the first feed request without active filters, use fast mode: render only the requested page of variants. 158 bool shouldBuildVariantData = 159 hasVariants && (isVariantAjaxRequest || HasAnyFilters()); 160 161 // Fast mode can be used when: 162 // - this is an AJAX/feed request 163 // - there are no VariantInfo filters active 164 // Variant Group filters can be handled without hydrating every variant. 165 bool fastVariantFeed = 166 shouldBuildVariantData 167 && isVariantAjaxRequest 168 && activeVI.Count == 0; 169 170 bool shouldRunFullVariantAnalysis = 171 shouldBuildVariantData 172 && !fastVariantFeed; 173 174 // FIX #3: blacklist VariantInfo filters we do NOT want (removes TOMU etc) 175 var variantInfoBlacklist = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) 176 { 177 "TOMU", 178 "SACD", 179 "LEAT" 180 }; 181 bool IsAllowedVI(string sys) => !string.IsNullOrWhiteSpace(sys) && !variantInfoBlacklist.Contains(sys); 182 183 var facetCountsAll = new Dictionary<string, Dictionary<string, int>>(StringComparer.InvariantCultureIgnoreCase); 184 var facetCountsCurrent = new Dictionary<string, Dictionary<string, int>>(StringComparer.InvariantCultureIgnoreCase); 185 186 var vgSlugToLabel = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); 187 188 // id->name mapping for VG options so UI can show names even if URL stores id 189 var vgOptionIdToNameBySlug = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase); 190 191 void RememberVGOption(string slug, string optionId, string optionName) 192 { 193 if (string.IsNullOrWhiteSpace(slug) || string.IsNullOrWhiteSpace(optionId)) return; 194 195 if (!vgOptionIdToNameBySlug.TryGetValue(slug, out var map)) 196 vgOptionIdToNameBySlug[slug] = map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); 197 198 if (!map.ContainsKey(optionId)) 199 map[optionId] = string.IsNullOrWhiteSpace(optionName) ? optionId : optionName; 200 } 201 202 void AddFacetValue(Dictionary<string, Dictionary<string, int>> target, string facetKey, string value) 203 { 204 if (string.IsNullOrWhiteSpace(facetKey) || string.IsNullOrWhiteSpace(value)) return; 205 206 if (!target.TryGetValue(facetKey, out var values)) 207 target[facetKey] = values = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase); 208 209 values[value] = values.TryGetValue(value, out var c) ? c + 1 : 1; 210 } 211 212 var productId = Model?.Id ?? ""; 213 var modelVariantGroups = Model?.VariantGroups()?.ToList() ?? new List<VariantGroupViewModel>(); 214 215 void AddVariantDisplayFacetValues(Dictionary<string, Dictionary<string, int>> target, string variantId) 216 { 217 var displayValues = VariantDisplayValues(variantId); 218 219 foreach (var item in modelVariantGroups.Select((group, index) => new { group, index })) 220 { 221 var groupName = item.group?.Name ?? string.Empty; 222 if (groupName.Equals("TOMU", StringComparison.InvariantCultureIgnoreCase)) continue; 223 224 var slug = Slug(groupName); 225 if (string.IsNullOrWhiteSpace(slug)) continue; 226 227 if (!vgSlugToLabel.ContainsKey(slug)) 228 vgSlugToLabel[slug] = groupName; 229 230 var value = item.index < displayValues.Count ? displayValues[item.index] : string.Empty; 231 if (!string.IsNullOrWhiteSpace(value)) 232 AddFacetValue(target, $"VG:{slug}", value); 233 } 234 } 235 236 // Variant group filters use the same display values as the table and sorting. 237 bool MatchesFilters(ProductViewModel v, 238 Dictionary<string, string> vgFilters, 239 Dictionary<string, string> viFilters) 240 { 241 var displayValues = VariantDisplayValues(v.VariantId); 242 243 foreach (var f in vgFilters) 244 { 245 var slug = f.Key; 246 var wanted = NormalizeVI(f.Value); 247 248 var groupIndex = modelVariantGroups.FindIndex(g => 249 g != null && Slug(g.Name).Equals(slug, StringComparison.InvariantCultureIgnoreCase)); 250 251 if (groupIndex < 0 || groupIndex >= displayValues.Count) 252 return false; 253 254 var actual = NormalizeVI(displayValues[groupIndex]); 255 if (!actual.Equals(wanted, StringComparison.InvariantCultureIgnoreCase)) 256 return false; 257 } 258 259 foreach (var f in viFilters) 260 { 261 var sys = f.Key; 262 var wantedVal = NormalizeVI(f.Value); 263 264 var vi = v.FieldDisplayGroups?.Values? 265 .FirstOrDefault(g => string.Equals(g?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 266 267 var fv = vi?.Fields?.Values?.FirstOrDefault(x => string.Equals(x.SystemName, sys, StringComparison.InvariantCultureIgnoreCase)); 268 var actual = NormalizeVI(fv?.Value); 269 270 if (!actual.Equals(wantedVal, StringComparison.InvariantCultureIgnoreCase)) 271 return false; 272 } 273 274 return true; 275 } 276 277 bool MatchesFastVGFilters(string variantId) 278 { 279 var displayValues = VariantDisplayValues(variantId); 280 281 foreach (var f in activeVG) 282 { 283 var slug = f.Key; 284 var wanted = NormalizeVI(f.Value); 285 286 var groupIndex = modelVariantGroups.FindIndex(g => 287 g != null && 288 Slug(g.Name).Equals(slug, StringComparison.InvariantCultureIgnoreCase)); 289 290 if (groupIndex < 0 || groupIndex >= displayValues.Count) 291 return false; 292 293 var actual = NormalizeVI(displayValues[groupIndex]); 294 295 if (!actual.Equals(wanted, StringComparison.InvariantCultureIgnoreCase)) 296 return false; 297 } 298 299 return true; 300 } 301 302 var filteredVariantIds = new List<string>(); 303 304 // Cache all variant view models once per request. 305 // This avoids calling GetProductViewModelById once during facet/filter building and again during rendering. 306 var variantViewModelsById = new Dictionary<string, ProductViewModel>(StringComparer.InvariantCultureIgnoreCase); 307 308 if (shouldRunFullVariantAnalysis && !string.IsNullOrWhiteSpace(productId)) 309 { 310 PerfLog("Before full variant hydrate"); 311 foreach (var vid in variantIdCombinations) 312 { 313 var v = ProductService.Instance.GetProductViewModelById(productId, vid); 314 if (v != null) 315 { 316 variantViewModelsById[vid] = v; 317 } 318 } 319 PerfLog("After full variant hydrate"); 320 } 321 322 if (shouldRunFullVariantAnalysis && !string.IsNullOrWhiteSpace(productId)) 323 { 324 foreach (var vid in variantIdCombinations) 325 { 326 if (!variantViewModelsById.TryGetValue(vid, out var v)) 327 { 328 continue; 329 } 330 331 // ALL counts 332 AddVariantDisplayFacetValues(facetCountsAll, v.VariantId); 333 334 var viGroup = v.FieldDisplayGroups?.Values? 335 .FirstOrDefault(g => string.Equals(g?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 336 337 if (viGroup?.Fields != null) 338 { 339 foreach (var kv in viGroup.Fields) 340 { 341 var fv = kv.Value; 342 if (fv == null) continue; 343 344 var sys = fv.SystemName ?? ""; 345 if (!IsAllowedVI(sys)) continue; 346 347 var val = NormalizeVI(fv.Value); 348 if (string.IsNullOrWhiteSpace(val)) continue; 349 350 AddFacetValue(facetCountsAll, $"VI:{sys}", val); 351 } 352 } 353 354 // FILTERED + CURRENT counts 355 bool shouldIncludeVariant = 356 UserContext.Current?.IsLoggedOn != true 357 || (v.Price != null && v.Price.Price > 0); 358 359 if (shouldIncludeVariant && MatchesFilters(v, activeVG, activeVI)) 360 { 361 filteredVariantIds.Add(vid); 362 363 AddVariantDisplayFacetValues(facetCountsCurrent, v.VariantId); 364 365 if (viGroup?.Fields != null) 366 { 367 foreach (var kv in viGroup.Fields) 368 { 369 var fv = kv.Value; 370 if (fv == null) continue; 371 372 var sys = fv.SystemName ?? ""; 373 if (!IsAllowedVI(sys)) continue; 374 375 var val = NormalizeVI(fv.Value); 376 if (string.IsNullOrWhiteSpace(val)) continue; 377 378 AddFacetValue(facetCountsCurrent, $"VI:{sys}", val); 379 } 380 } 381 } 382 } 383 } 384 385 // Better facet visibility: show if choice exists OR if active 386 bool ShouldShowFacet(string facetKey) 387 { 388 if (!facetCountsAll.TryGetValue(facetKey, out var values)) return false; 389 390 if (values.Keys.Count > 1) return true; 391 392 if (facetKey.StartsWith("VG:", StringComparison.InvariantCultureIgnoreCase)) 393 { 394 var slug = facetKey.Substring(3); 395 return activeVG.ContainsKey(slug); 396 } 397 if (facetKey.StartsWith("VI:", StringComparison.InvariantCultureIgnoreCase)) 398 { 399 var sys = facetKey.Substring(3); 400 return activeVI.ContainsKey(sys); 401 } 402 return false; 403 } 404 405 if (fastVariantFeed) 406 { 407 // Fast path: 408 // Do not hydrate every variant with GetProductViewModelById. 409 // Build lightweight Variant Group facets from variant names. 410 // Supports filtering on Variant Groups without full variant analysis. 411 412 foreach (var vid in variantIdCombinations) 413 { 414 AddVariantDisplayFacetValues(facetCountsAll, vid); 415 416 if (MatchesFastVGFilters(vid)) 417 { 418 filteredVariantIds.Add(vid); 419 AddVariantDisplayFacetValues(facetCountsCurrent, vid); 420 } 421 } 422 } 423 424 // Paging based on filtered list. 425 // When variant data is deferred on the first normal product view, render 0 rows so the page can return quickly. 426 int totalFiltered = shouldBuildVariantData ? filteredVariantIds.Count : variantIdCombinations.Count; 427 int variantCountToSkip = shouldBuildVariantData ? Math.Min(alreadyRendered, totalFiltered) : 0; 428 int variantCountToTake = shouldBuildVariantData ? Math.Min(10, Math.Max(0, totalFiltered - variantCountToSkip)) : 0; 429 430 var sortedFilteredVariantIds = shouldBuildVariantData 431 ? filteredVariantIds 432 .OrderBy(variantId => VariantDisplaySortKey(variantId)) 433 .ThenBy(variantId => variantId) 434 .ToList() 435 : new List<string>(); 436 437 var pageVariantIds = sortedFilteredVariantIds 438 .Skip(variantCountToSkip) 439 .Take(variantCountToTake) 440 .ToList(); 441 442 if (fastVariantFeed && !string.IsNullOrWhiteSpace(productId)) 443 { 444 foreach (var vid in pageVariantIds) 445 { 446 var v = ProductService.Instance.GetProductViewModelById(productId, vid); 447 if (v != null) 448 { 449 variantViewModelsById[vid] = v; 450 } 451 } 452 } 453 WriteLiteral("\r\n<style>\r\n\t.table .btn:hover {\r\n\t\tcolor: #FCC400 !important;\r\n\t}\r\n</style>\r\n\r\n"); 454 WriteLiteral("\r\n\r\n<product-details"); 455 BeginWriteAttribute("product-id", "\r\n\tproduct-id=\"", 22365, "\"", 22390, 1); 456 WriteAttributeValue("", 22380, Model?.Id, 22380, 10, false); 457 EndWriteAttribute(); 458 BeginWriteAttribute("variat-id", "\r\n\tvariat-id=\"", 22391, "\"", 22422, 1); 459 WriteAttributeValue("", 22405, Model?.VariantId, 22405, 17, false); 460 EndWriteAttribute(); 461 BeginWriteAttribute("variant-combinations-count", "\r\n\tvariant-combinations-count=\"", 22423, "\"", 22470, 1); 462 WriteAttributeValue("", 22454, totalFiltered, 22454, 16, false); 463 EndWriteAttribute(); 464 WriteLiteral("\r\n\tdata-is-logged-in=\""); 465 Write(UserContext.Current?.IsLoggedOn == true ? "true" : "false"); 466 WriteLiteral("\"\r\n\tdata-has-variants=\""); 467 Write(hasVariants.ToString().ToLowerInvariant()); 468 WriteLiteral(@"""> 469 <section class=""core-section js-section"" id=""core-section-product-details"" data-dw-colorscheme=""tingstad-dark""> 470 <div class=""container""> 471 <div class=""row justify-content-start""> 472 <div class=""col-12 col-sm-12""> 473 <div class=""row""> 474 <div class=""position-relative""> 475 <div class=""row mx-0 pt-5 pt-lg-6 position-relative custom-product-details-top-info-container"" data-dw-colorscheme=""tingstad-white""> 476 <div class=""col-12""> 477 <div class=""row""> 478 <div class=""d-none d-lg-flex col-4 offset-1 mb-4""> 479 "); 480 if (Model != null) 481 { 482 Write(RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images.cshtml", Model)); 483 484 } 485 WriteLiteral("\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t<div class=\"col-12 col-lg-6 mb-4\">\r\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex align-items-center mb-3\">\r\n"); 486 if (!string.IsNullOrEmpty(Model?.Number)) 487 { 488 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"badge text-bg-dark me-3\">"); 489 Write(Model.Number); 490 WriteLiteral("</div>\r\n"); 491 if (CustomerAlias.Has(Model.Number)) 492 { 493 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"badge text-bg-dark me-3\">"); 494 Write(CustomerAlias.First(Model.Number)); 495 WriteLiteral("</div>\r\n"); 496 } 497 } 498 if (UserContext.Current?.IsLoggedOn == true && Model?.RenderedHtml?.Stock != null) 499 { 500 if (modelVariantGroups.Count == 0 || !string.IsNullOrEmpty(Model.VariantId)) 501 { 502 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"js-core-details-stock\">"); 503 Write(Model.RenderedHtml.Stock); 504 WriteLiteral("</span>\r\n"); 505 } 506 } 507 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 508 if (!string.IsNullOrEmpty(Model?.Name)) 509 { 510 var formattedName = FormatTitleCase(Model.Name); 511 512 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<h1 class=\"h2\">"); 513 Write(formattedName); 514 WriteLiteral("</h1>\r\n"); 515 } 516 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t"); 517 Write(Model?.ShortDescription); 518 WriteLiteral("\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"row d-lg-none\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"col-8 my-4 mx-auto\">\r\n"); 519 if (Model != null) 520 { 521 Write(RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images.cshtml", Model)); 522 523 } 524 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 525 if (hasProductInfo) 526 { 527 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"product-details-specs-list list-top-info mt-3 mb-5\">\r\n"); 528 foreach (var fieldDisplayGroup in productInfoFields!) 529 { 530 var fv = fieldDisplayGroup.Value; 531 if (fv == null) { continue; } 532 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-item\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-label\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 533 Write(fv.Name); 534 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-value\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 535 Write(fv.Value); 536 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 537 } 538 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 539 } 540 WriteLiteral("\r\n"); 541 if (UserContext.Current?.IsLoggedOn == true && (!string.IsNullOrEmpty(Model.VariantId) || modelVariantGroups.Count == 0)) 542 { 543 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<price-and-form>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-block d-md-flex align-items-end justify-content-end\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex flex-grow-1 gap-5 gap-md-2\">\r\n"); 544 if (Model?.RenderedHtml?.UnitPrice != null) 545 { 546 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex flex-column me-lg-auto js-core-details-unit-price\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p class=\"small mb-2\">"); 547 Write(Translate("Pris. pr. stk")); 548 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 549 Write(Model.RenderedHtml.UnitPrice); 550 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 551 } 552 WriteLiteral("\r\n"); 553 if (Model?.RenderedHtml?.Price != null) 554 { 555 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex flex-column align-items-start justify-content-center me-4 mb-4 mb-md-0 me-lg-6 js-core-details-price\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p class=\"small mb-2\">"); 556 Write(Translate("Total")); 557 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 558 Write(Model.RenderedHtml.Price); 559 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 560 } 561 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 562 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t"); 563 Write(Model?.RenderedHtml?.AddToCartForm); 564 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</price-and-form>\r\n"); 565 } 566 else 567 { 568 ButtonViewModel viewMoreButton = new ButtonViewModel 569 { 570 Text = Translate("View variants"), 571 Type = ButtonType.Link, 572 Link = new LinkViewModel { Url = $"{Model.Link}#product-info-section" }, 573 DisplayType = ButtonDisplayType.Secondary, 574 }; 575 Write(viewMoreButton.Render()); 576 577 } 578 WriteLiteral("\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 579 if (hasVariantInfo) 580 { 581 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t<div class=\"col-12 py-4 mt-5 order-3 px-0 d-none d-lg-flex custom-product-details-bottom-info-container\" data-dw-colorscheme=\"tingstad-white\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"product-details-specs-list list-bottom-info\">\r\n"); 582 foreach (var fieldDisplayGroup in variantInfoFields!) 583 { 584 FieldValueViewModel? fv = fieldDisplayGroup.Value; 585 if (fv == null || string.IsNullOrEmpty(fv.ToString())) { continue; } 586 587 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-item\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-label\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 588 Write(Translate($"Product Info Info - Field - {fv.SystemName}", fv.Name)); 589 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"list-value\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 590 Write(fv.Value); 591 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 592 } 593 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 594 } 595 WriteLiteral(@" </div> 596 </div> 597 </div> 598 599 <div class=""row position-relative""> 600 <div class=""custom-product-details-top-info-container--pseudo-shadow""></div> 601 </div> 602 <div class=""row mx-0""> 603 <div class=""custom-product-details-top-info--box-shadow""></div> 604 </div> 605 606 </div> 607 </div> 608 </div> 609 </div> 610 </div> 611 </section> 612 613 <section id=""product-info-section"" class=""core-section js-section pt-0"" data-dw-colorscheme=""tingstad-white""> 614 <div class=""container""> 615 <div class=""row justify-content-start""> 616 <div class=""col-12 col-sm-12""> 617 618 <div class=""d-flex justify-content-center custom-product-details-nav-pills-container""> 619 <button class=""btn btn-secondary d-flex d-lg-none dropdown-toggle mt-5 js-core-specifications-dropdown-btn core-specifications-dropdown-btn"" 620 data-target=""#core-specifications-navigation""> 621 <span class=""js-core-specifications-dropdown-text""> 622 <div class=""btn-content""> 623 "); 624 Write(Translate("Specifications Mobile Button - Text", "View specifications")); 625 WriteLiteral("\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</span>\r\n\t\t\t\t\t\t</button>\r\n\r\n\t\t\t\t\t\t<ul class=\"nav nav-pills justify-content-center p-1\" id=\"core-specifications-navigation\" role=\"tablist\">\r\n"); 626 if (hasVariants) 627 { 628 WriteLiteral("\t\t\t\t\t\t\t\t<li class=\"nav-item\" role=\"presentation\">\r\n\t\t\t\t\t\t\t\t\t<button"); 629 BeginWriteAttribute("class", " class=\"", 29251, "\"", 29308, 2); 630 WriteAttributeValue("", 29259, "nav-link", 29259, 8, true); 631 WriteAttributeValue(" ", 29267, hasVariants ? "active" : string.Empty, 29268, 40, false); 632 EndWriteAttribute(); 633 WriteLiteral(" id=\"pills-variants-tab\" data-bs-toggle=\"pill\"\r\n\t\t\t\t\t\t\t\t\t\t\tdata-bs-target=\"#pills-variants\" type=\"button\" role=\"tab\"\r\n\t\t\t\t\t\t\t\t\t\t\taria-controls=\"pills-variants\"\r\n\t\t\t\t\t\t\t\t\t\t\taria-selected=\"true\">\r\n\t\t\t\t\t\t\t\t\t\t"); 634 Write(Translate("Specifications Variants - Button - Text", "Varianter")); 635 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t</button>\r\n\t\t\t\t\t\t\t\t</li>\r\n"); 636 } 637 WriteLiteral("\r\n"); 638 639 bool displayGroupsFirst = false; 640 foreach (var displayGroup in displayGroups) 641 { 642 if (displayGroup == null || string.IsNullOrEmpty(displayGroup.Id)) { continue; } 643 644 bool isDrawings = string.Equals(displayGroup.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase); 645 bool shouldShow = true; 646 647 if (isDrawings) 648 { 649 var linkField = (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>()) 650 .FirstOrDefault(f => string.Equals(f.Value?.Type, "Link", StringComparison.InvariantCultureIgnoreCase)); 651 652 var linkValueObj = linkField.Value?.Value; 653 string drawingImageUrl = Convert.ToString(linkValueObj) ?? string.Empty; 654 shouldShow = !string.IsNullOrEmpty(drawingImageUrl); 655 } 656 657 if (shouldShow) 658 { 659 WriteLiteral("\t\t\t\t\t\t\t\t\t<li class=\"nav-item\" role=\"presentation\">\r\n\t\t\t\t\t\t\t\t\t\t<button"); 660 BeginWriteAttribute("class", " class=\"", 30567, "\"", 30648, 2); 661 WriteAttributeValue("", 30575, "nav-link", 30575, 8, true); 662 WriteAttributeValue(" ", 30583, !hasVariants && !displayGroupsFirst ? "active" : string.Empty, 30584, 64, false); 663 EndWriteAttribute(); 664 BeginWriteAttribute("id", " id=\"", 30649, "\"", 30680, 3); 665 WriteAttributeValue("", 30654, "pills-", 30654, 6, true); 666 WriteAttributeValue("", 30660, displayGroup.Id, 30660, 16, false); 667 WriteAttributeValue("", 30676, "-tab", 30676, 4, true); 668 EndWriteAttribute(); 669 WriteLiteral(" data-bs-toggle=\"pill\"\r\n\t\t\t\t\t\t\t\t\t\t\t\tdata-bs-target=\"#pills-"); 670 Write(displayGroup.Id); 671 WriteLiteral("\" type=\"button\" role=\"tab\""); 672 BeginWriteAttribute("aria-controls", "\r\n\t\t\t\t\t\t\t\t\t\t\t\taria-controls=\"", 30782, "\"", 30833, 2); 673 WriteAttributeValue("", 30811, "pills-", 30811, 6, true); 674 WriteAttributeValue("", 30817, displayGroup.Id, 30817, 16, false); 675 EndWriteAttribute(); 676 BeginWriteAttribute("aria-selected", " aria-selected=\"", 30834, "\"", 30907, 1); 677 WriteAttributeValue("", 30850, !hasVariants && !displayGroupsFirst ? "true" : "false", 30850, 57, false); 678 EndWriteAttribute(); 679 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t"); 680 Write(displayGroup.Name); 681 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t</button>\r\n\t\t\t\t\t\t\t\t\t</li>\r\n"); 682 } 683 684 displayGroupsFirst = true; 685 } 686 WriteLiteral("\t\t\t\t\t\t</ul>\r\n\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t<div class=\"tab-content custom-product-details-tab-pane-container pt-5 pt-lg-0\" id=\"pills-tabContent\">\r\n"); 687 if (hasVariants) 688 { 689 CategoryFieldViewModel? firstVariantInfoGroup = shouldBuildVariantData 690 ? GetFirstVariantFieldDisplayGroup("VariantInfo", Model.Id, (pageVariantIds?.FirstOrDefault() ?? variantIdCombinations?.FirstOrDefault() ?? string.Empty)) 691 : null; 692 693 WriteLiteral("\t\t\t\t\t\t\t<div"); 694 BeginWriteAttribute("class", " class=\"", 31482, "\"", 31549, 3); 695 WriteAttributeValue("", 31490, "tab-pane", 31490, 8, true); 696 WriteAttributeValue(" ", 31498, "fade", 31499, 5, true); 697 WriteAttributeValue(" ", 31503, hasVariants ? "show active" : string.Empty, 31504, 45, false); 698 EndWriteAttribute(); 699 WriteLiteral(" id=\"pills-variants\" role=\"tabpanel\"\r\n\t\t\t\t\t\t\t\t aria-labelledby=\"pills-variants-tab\" tabindex=\"0\">\r\n\t\t\t\t\t\t\t\t<div class=\"row py-3\">\r\n\t\t\t\t\t\t\t\t\t<div class=\"col-12 position-relative\">\r\n\t\t\t\t\t\t\t\t\t\t<h3 class=\"h2\">"); 700 Write(Translate("Specifications Variants - Header - Text", "Varianter")); 701 WriteLiteral("</h3>\r\n\r\n"); 702 WriteLiteral("\t\t\t\t\t\t\t\t\t\t<div class=\"variant-loading-overlay js-variant-loading\" hidden>\r\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"spinner-border\" role=\"status\" aria-label=\"Laster...\"></div>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 703 if (HasAnyFilters()) 704 { 705 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex flex-wrap gap-2 mb-3\">\r\n"); 706 foreach (var vg in activeVG.OrderBy(k => k.Key)) 707 { 708 var groupLabel = vgSlugToLabel.TryGetValue(vg.Key, out var l) ? l : vg.Key; 709 var wanted = vg.Value; 710 711 string optLabel = wanted; // could be id or legacy name 712 if (vgOptionIdToNameBySlug.TryGetValue(vg.Key, out var map) && map.TryGetValue(wanted, out var nm)) 713 { 714 optLabel = nm; 715 } 716 717 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<button type=\"button\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-outline-secondary btn-sm filter-chip js-filter-chip\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata-param=\"f_vg_"); 718 Write(vg.Key); 719 WriteLiteral("\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 720 Write(groupLabel); 721 WriteLiteral(": "); 722 Write(optLabel); 723 WriteLiteral(" <span aria-hidden=\"true\">&times;</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\r\n"); 724 } 725 foreach (var vi in activeVI.OrderBy(k => k.Key)) 726 { 727 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<button type=\"button\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"btn btn-outline-secondary btn-sm filter-chip js-filter-chip\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdata-param=\"f_vi_"); 728 Write(vi.Key); 729 WriteLiteral("\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 730 Write(vi.Key); 731 WriteLiteral(": "); 732 Write(vi.Value); 733 WriteLiteral(" <span aria-hidden=\"true\">&times;</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</button>\r\n"); 734 } 735 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-secondary btn-sm js-clear-filters\">Fjern filter</button>\r\n\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 736 } 737 738 WriteLiteral("\t\t\t\t\t\t\t\t\t\t<div class=\"row g-3 mb-4\">\r\n"); 739 foreach (var vgEntry in vgSlugToLabel.OrderBy(x => x.Value, StringComparer.InvariantCultureIgnoreCase)) 740 { 741 var slug = vgEntry.Key; 742 var label = vgEntry.Value; 743 744 // extra safety: skip TOMU if it sneaks in as VG label 745 if (label.Equals("TOMU", StringComparison.InvariantCultureIgnoreCase)) { continue; } 746 747 var facetKey = $"VG:{slug}"; 748 if (!ShouldShowFacet(facetKey)) { continue; } 749 750 var paramName = $"f_vg_{slug}"; 751 var selectedWanted = activeVG.TryGetValue(slug, out var val) ? val : ""; 752 753 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"col-12 col-md-4\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<label class=\"form-label\">"); 754 Write(label); 755 WriteLiteral("</label>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<select class=\"form-select js-variant-filter\""); 756 BeginWriteAttribute("name", " name=\"", 34251, "\"", 34268, 1); 757 WriteAttributeValue("", 34258, paramName, 34258, 10, false); 758 EndWriteAttribute(); 759 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option"); 760 BeginWriteAttribute("value", " value=\"", 34293, "\"", 34301, 0); 761 EndWriteAttribute(); 762 WriteLiteral(">Alle</option>\r\n"); 763 if (facetCountsCurrent.TryGetValue(facetKey, out var opts)) 764 { 765 foreach (var opt in opts 766 .OrderBy(o => 767 { 768 var nm = (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(o.Key, out var n)) 769 ? n 770 : o.Key; 771 return SortNumber(nm); 772 }) 773 .ThenBy(o => 774 { 775 var nm = (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(o.Key, out var n)) 776 ? n 777 : o.Key; 778 return nm; 779 }, StringComparer.InvariantCultureIgnoreCase)) 780 { 781 var optId = opt.Key; 782 var optCount = opt.Value; 783 784 var optName = 785 (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(optId, out var nm)) 786 ? nm 787 : optId; 788 789 // selectedWanted can be id OR legacy name 790 var isSelected = 791 optId.Equals(selectedWanted, StringComparison.InvariantCultureIgnoreCase) || 792 optName.Equals(selectedWanted, StringComparison.InvariantCultureIgnoreCase); 793 794 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option"); 795 BeginWriteAttribute("value", " value=\"", 35654, "\"", 35668, 1); 796 WriteAttributeValue("", 35662, optId, 35662, 6, false); 797 EndWriteAttribute(); 798 BeginWriteAttribute("selected", " selected=\"", 35669, "\"", 35713, 1); 799 WriteAttributeValue("", 35680, isSelected ? "selected" : null, 35680, 33, false); 800 EndWriteAttribute(); 801 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 802 Write(optName); 803 WriteLiteral(" ("); 804 Write(optCount); 805 WriteLiteral(")\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\r\n"); 806 } 807 } 808 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 809 } 810 WriteLiteral("\r\n"); 811 foreach (var viFacet in facetCountsAll.Keys 812 .Where(k => k.StartsWith("VI:", StringComparison.InvariantCultureIgnoreCase)) 813 .OrderBy(k => k, StringComparer.InvariantCultureIgnoreCase)) 814 { 815 var sys = viFacet.Substring(3); 816 if (!IsAllowedVI(sys)) { continue; } 817 if (!ShouldShowFacet(viFacet)) { continue; } 818 819 var paramName = $"f_vi_{sys}"; 820 var selectedVal = activeVI.TryGetValue(sys, out var sv) ? sv : ""; 821 822 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"col-12 col-md-4\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<label class=\"form-label\">"); 823 Write(sys); 824 WriteLiteral("</label>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<select class=\"form-select js-variant-filter\""); 825 BeginWriteAttribute("name", " name=\"", 36549, "\"", 36566, 1); 826 WriteAttributeValue("", 36556, paramName, 36556, 10, false); 827 EndWriteAttribute(); 828 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option"); 829 BeginWriteAttribute("value", " value=\"", 36591, "\"", 36599, 0); 830 EndWriteAttribute(); 831 WriteLiteral(">Alle</option>\r\n"); 832 if (facetCountsCurrent.TryGetValue(viFacet, out var opts)) 833 { 834 foreach (var opt in opts 835 .OrderBy(o => SortNumber(o.Key)) 836 .ThenBy(o => o.Key, StringComparer.InvariantCultureIgnoreCase)) 837 { 838 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<option"); 839 BeginWriteAttribute("value", " value=\"", 36921, "\"", 36937, 1); 840 WriteAttributeValue("", 36929, opt.Key, 36929, 8, false); 841 EndWriteAttribute(); 842 BeginWriteAttribute("selected", " selected=\"", 36938, "\"", 37044, 1); 843 WriteAttributeValue("", 36949, opt.Key.Equals(selectedVal, StringComparison.InvariantCultureIgnoreCase) ? "selected" : null, 36949, 95, false); 844 EndWriteAttribute(); 845 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 846 Write(opt.Key); 847 WriteLiteral(" ("); 848 Write(opt.Value); 849 WriteLiteral(")\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</option>\r\n"); 850 } 851 } 852 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t</select>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 853 } 854 WriteLiteral(@" </div> 855 856 <div class=""flexbox-table w-100 mb-4""> 857 <div class=""table-row table-header text-muted""> 858 <div class=""table-cell table-cell-image""><div class=""ratio ratio-1x1""></div></div> 859 <div class=""table-cell table-cell-info"">&nbsp;</div> 860 861 "); 862 foreach (VariantGroupViewModel? variantGroup in modelVariantGroups) 863 { 864 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\"><p"); 865 BeginWriteAttribute("title", " title=\"", 37638, "\"", 37664, 1); 866 WriteAttributeValue("", 37646, variantGroup.Name, 37646, 18, false); 867 EndWriteAttribute(); 868 WriteLiteral(">"); 869 Write(variantGroup.Name); 870 WriteLiteral("</p></div>\r\n"); 871 } 872 WriteLiteral("\r\n"); 873 if (firstVariantInfoGroup != null) 874 { 875 foreach (KeyValuePair<string, FieldValueViewModel> field in firstVariantInfoGroup.Fields) 876 { 877 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p"); 878 BeginWriteAttribute("title", " title=\"", 37954, "\"", 38069, 1); 879 WriteAttributeValue("", 37962, HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name)), 37962, 107, false); 880 EndWriteAttribute(); 881 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 882 Write(HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name))); 883 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 884 } 885 } 886 WriteLiteral("\r\n"); 887 if (UserContext.Current?.IsLoggedOn == true) 888 { 889 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\"><p title=\"Enhpris\">"); 890 Write(Translate("Enhpris")); 891 WriteLiteral("</p></div>\r\n"); 892 } 893 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell table-cell-expand\">&nbsp;</div>\r\n\r\n"); 894 if (UserContext.Current?.IsLoggedOn == true) 895 { 896 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell table-cell-addtocart\">&nbsp;</div>\r\n"); 897 } 898 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"js-core-variant-list\">\r\n"); 899 if (!shouldBuildVariantData && hasVariants) 900 { 901 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"alert alert-light mb-3 js-variant-deferred-placeholder\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tVarianter lastes inn.\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 902 } 903 foreach (string variantId in pageVariantIds) 904 { 905 ProductViewModel? variantViewModel = null; 906 variantViewModelsById.TryGetValue(variantId, out variantViewModel); 907 908 if (variantViewModel != null) 909 { 910 CategoryFieldViewModel? variantInfoVariantFieldDisplayGroup = 911 variantViewModel.FieldDisplayGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 912 913 CategoryFieldViewModel? techinicalSpecsVariantFieldDisplayGroup = 914 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase)); 915 916 ImageViewModel? variantProductImageViewModel = Model?.DefaultImage?.GetImage(); 917 if (variantProductImageViewModel != null) 918 { 919 variantProductImageViewModel.Classes = new ClassList("mb-0 ratio ratio-1x1 flex-shrink-0"); 920 variantProductImageViewModel.ImgTagClasses = new ClassList("h-100 w-100 object-fit-contain"); 921 } 922 923 WriteLiteral(@" <div class=""js-core-variant-list-item""> 924 <div class=""table-row custom-product-list-item ""> 925 <div class=""d-flex d-lg-contents flex-row-reverse flex-xl-row""> 926 <div class=""table-cell table-cell-image collapsed"" 927 data-bs-toggle=""collapse"""); 928 BeginWriteAttribute("href", "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t href=\"", 40450, "\"", 40519, 2); 929 WriteAttributeValue("", 40477, "#", 40477, 1, true); 930 WriteAttributeValue("", 40478, $"product-variant-details-{variantId}", 40478, 41, false); 931 EndWriteAttribute(); 932 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t aria-expanded=\"false\">\r\n"); 933 if (variantProductImageViewModel != null) 934 { 935 Write(variantProductImageViewModel); 936 937 } 938 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell table-cell-info\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"custom-product-list-item--info\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex align-items-center mb-2\">\r\n"); 939 if (!string.IsNullOrEmpty(variantViewModel.Number)) 940 { 941 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"badge text-bg-dark me-3\">"); 942 Write(variantViewModel.Number); 943 WriteLiteral("</div>\r\n"); 944 if (CustomerAlias.Has(variantViewModel.Number)) 945 { 946 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"badge text-bg-dark me-3\">"); 947 Write(CustomerAlias.First(variantViewModel.Number)); 948 WriteLiteral("</div>\r\n"); 949 } 950 951 } 952 if (UserContext.Current?.IsLoggedOn == true) 953 { 954 Write(variantViewModel?.RenderedHtml?.Stock); 955 956 } 957 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 958 if (!string.IsNullOrEmpty(variantViewModel.Name)) 959 { 960 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<h3 class=\"mb-0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a"); 961 BeginWriteAttribute("href", " href=\"", 41754, "\"", 41783, 1); 962 WriteAttributeValue("", 41761, variantViewModel.Link, 41761, 22, false); 963 EndWriteAttribute(); 964 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclass=\"text-decoration-none text-reset\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 965 Write(variantViewModel.Name); 966 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</h3>\r\n"); 967 } 968 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex d-lg-contents table-cell-variable-wrapper\">\r\n"); 969 970 var variantDisplayValues = VariantDisplayValues(variantViewModel.VariantId); 971 WriteLiteral("\r\n"); 972 foreach (var item in modelVariantGroups.Select((group, index) => new { group, index })) 973 { 974 var value = item.index < variantDisplayValues.Count 975 ? variantDisplayValues[item.index] 976 : string.Empty; 977 978 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\" data-title=\""); 979 Write(HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {item.group?.Name}", item.group?.Name))); 980 WriteLiteral("\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 981 Write(value); 982 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 983 } 984 WriteLiteral("\r\n"); 985 if (variantInfoVariantFieldDisplayGroup != null) 986 { 987 foreach (KeyValuePair<string, FieldValueViewModel> field in variantInfoVariantFieldDisplayGroup.Fields) 988 { 989 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\" data-title=\""); 990 Write(HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name))); 991 WriteLiteral("\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p>"); 992 Write(field.Value); 993 WriteLiteral("</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 994 } 995 } 996 else if (firstVariantInfoGroup != null) 997 { 998 foreach (KeyValuePair<string, FieldValueViewModel> field in firstVariantInfoGroup.Fields) 999 { 1000 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\"></div>\r\n"); 1001 } 1002 } 1003 WriteLiteral("\r\n"); 1004 if (UserContext.Current?.IsLoggedOn == true) 1005 { 1006 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell\" data-title=\"Enhpris\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1007 Write(variantViewModel?.RenderedHtml?.UnitPrice); 1008 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 1009 } 1010 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"table-cell table-cell-expand collapsed\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t data-title=\"Utvidet info\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t data-bs-toggle=\"collapse\""); 1011 BeginWriteAttribute("href", "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t href=\"", 44055, "\"", 44123, 2); 1012 WriteAttributeValue("", 44081, "#", 44081, 1, true); 1013 WriteAttributeValue("", 44082, $"product-variant-details-{variantId}", 44082, 41, false); 1014 EndWriteAttribute(); 1015 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t aria-expanded=\"false\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span class=\"icon-2 opacity-75\" title=\"Utvidet info\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1016 Write(ReadFile("/Files/Templates/Designs/Swift-v2/Assets/images/custom-icons/tingstad-chevron-right.svg")); 1017 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 1018 if (UserContext.Current?.IsLoggedOn == true) 1019 { 1020 WriteLiteral(@" <div class=""table-cell table-cell-addtocart""> 1021 <price-and-form> 1022 <div class=""d-flex flex-row core-price-wrapper gap-3 justify-content-between w-100 w-lg-auto""> 1023 <div class=""d-flex core-add-to-favorites d-none d-lg-flex custom-product-list-item--addtofavorites justify-content-end""> 1024 "); 1025 Write(variantViewModel?.RenderedHtml?.AddToFavorites); 1026 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"custom-product-list-item--price d-flex flex-column align-items-start align-items-lg-end justify-content-center\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1027 Write(variantViewModel?.RenderedHtml?.Price); 1028 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"align-items-end\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1029 Write(variantViewModel?.RenderedHtml?.AddToCartForm); 1030 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</price-and-form>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 1031 } 1032 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"collapse\""); 1033 BeginWriteAttribute("id", " id=\"", 45501, "\"", 45547, 1); 1034 WriteAttributeValue("", 45506, $"product-variant-details-{variantId}", 45506, 41, false); 1035 EndWriteAttribute(); 1036 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"py-2 py-lg-4 d-flex flex-row flex-wrap custom-product-list-item__variant-info--extended-info\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"extended-info-header\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<h3 class=\"mb-0 mb-lg-4\">Teknisk informasjon</h3>\r\n"); 1037 if (variantProductImageViewModel != null) 1038 { 1039 Write(variantProductImageViewModel); 1040 1041 } 1042 WriteLiteral(@" </div> 1043 <div class=""other stuff"" style=""flex: 1;""> 1044 <table class=""table custom-info-data-table""> 1045 <tbody> 1046 <tr> 1047 <th>Varenummer</th> 1048 <td>"); 1049 Write(variantViewModel?.Number); 1050 WriteLiteral("</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<th>Navn</th>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td>"); 1051 Write(variantViewModel?.Name); 1052 WriteLiteral("</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n"); 1053 if (techinicalSpecsVariantFieldDisplayGroup != null) 1054 { 1055 foreach (KeyValuePair<string, FieldValueViewModel> field in techinicalSpecsVariantFieldDisplayGroup.Fields) 1056 { 1057 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<th>"); 1058 Write(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name)); 1059 WriteLiteral("</th>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td>"); 1060 Write(field.Value); 1061 WriteLiteral("</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n"); 1062 } 1063 } 1064 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a"); 1065 BeginWriteAttribute("href", " href=\"", 47020, "\"", 47049, 1); 1066 WriteAttributeValue("", 47027, variantViewModel.Link, 47027, 22, false); 1067 EndWriteAttribute(); 1068 BeginWriteAttribute("class", " class=\"", 47050, "\"", 47058, 0); 1069 EndWriteAttribute(); 1070 WriteLiteral(">"); 1071 Write(Translate("Se flere detaljer her")); 1072 WriteLiteral("</a>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 1073 } 1074 } 1075 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 1076 if (totalFiltered > (alreadyRendered + variantCountToTake)) 1077 { 1078 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex justify-content-center my-3\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1079 Write(new ButtonViewModel() 1080 { 1081 Type = ButtonType.Button, 1082 DisplayType = ButtonDisplayType.LazyLoad, 1083 Icon = "/Files/Templates/Designs/Swift-v2/Assets/images/custom-icons/tingstad-load.svg", 1084 Text = shouldBuildVariantData ? Translate("Show more") : Translate("Load variants", "Last varianter"), 1085 Classes = new ClassList("js-core-lazyload-variants-btn") 1086 }); 1087 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 1088 } 1089 WriteLiteral("\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</div>\r\n"); 1090 } 1091 WriteLiteral("\r\n"); 1092 1093 bool displayGroupsFirstContent = false; 1094 foreach (var displayGroup in displayGroups) 1095 { 1096 if (displayGroup == null || string.IsNullOrEmpty(displayGroup.Id)) { continue; } 1097 1098 WriteLiteral("\t\t\t\t\t\t\t<div"); 1099 BeginWriteAttribute("class", " class=\"", 48228, "\"", 48326, 3); 1100 WriteAttributeValue("", 48236, "tab-pane", 48236, 8, true); 1101 WriteAttributeValue(" ", 48244, "fade", 48245, 5, true); 1102 WriteAttributeValue(" ", 48249, !hasVariants && !displayGroupsFirstContent ? "show active" : string.Empty, 48250, 76, false); 1103 EndWriteAttribute(); 1104 BeginWriteAttribute("id", " id=\"", 48327, "\"", 48354, 2); 1105 WriteAttributeValue("", 48332, "pills-", 48332, 6, true); 1106 WriteAttributeValue("", 48338, displayGroup.Id, 48338, 16, false); 1107 EndWriteAttribute(); 1108 WriteLiteral(" role=\"tabpanel\""); 1109 BeginWriteAttribute("aria-labelledby", "\r\n\t\t\t\t\t\t\t\t aria-labelledby=\"", 48371, "\"", 48425, 3); 1110 WriteAttributeValue("", 48399, "pills-", 48399, 6, true); 1111 WriteAttributeValue("", 48405, displayGroup.Id, 48405, 16, false); 1112 WriteAttributeValue("", 48421, "-tab", 48421, 4, true); 1113 EndWriteAttribute(); 1114 WriteLiteral(" tabindex=\"0\">\r\n\t\t\t\t\t\t\t\t<div class=\"row py-3\">\r\n\t\t\t\t\t\t\t\t\t<div class=\"col-12\">\r\n\t\t\t\t\t\t\t\t\t\t<h3 class=\"h2\">"); 1115 Write(displayGroup.Name); 1116 WriteLiteral("</h3>\r\n\r\n"); 1117 1118 ClassList tableContainerClassList = new ClassList("col-12"); 1119 bool isDrawings = string.Equals(displayGroup.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase); 1120 1121 if (isDrawings) 1122 { 1123 tableContainerClassList.Add("col-lg-6"); 1124 } 1125 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t<div class=\"row\">\r\n\t\t\t\t\t\t\t\t\t\t\t<div"); 1126 BeginWriteAttribute("class", " class=\"", 48935, "\"", 48967, 1); 1127 WriteAttributeValue("", 48943, tableContainerClassList, 48943, 24, false); 1128 EndWriteAttribute(); 1129 WriteLiteral(">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<table class=\"table custom-info-data-table mt-4\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n"); 1130 1131 bool isDocuments = string.Equals(displayGroup.Id, "Dokumentasjon", StringComparison.InvariantCultureIgnoreCase); 1132 WriteLiteral("\r\n"); 1133 foreach (var field in (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>())) 1134 { 1135 var fv = field.Value; 1136 if (fv == null) { continue; } 1137 1138 bool isTechnicalInfo = string.Equals(displayGroup.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase); 1139 bool noVariantSelected = string.IsNullOrWhiteSpace(Model?.VariantId); 1140 bool isWeightField = 1141 string.Equals(fv.SystemName, "NEWE", StringComparison.InvariantCultureIgnoreCase) || 1142 string.Equals(fv.Name, "Vekt", StringComparison.InvariantCultureIgnoreCase); 1143 1144 if (isTechnicalInfo && noVariantSelected && isWeightField) 1145 { 1146 continue; 1147 } 1148 1149 if (!isDocuments && string.Equals(fv.Type, "Link", StringComparison.InvariantCultureIgnoreCase)) 1150 { 1151 continue; 1152 } 1153 1154 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<th scope=\"row\" style=\"vertical-align: middle;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1155 Write(Translate($"Product Details - Field Display Group - Label - {fv.SystemName}", fv.Name)); 1156 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</th>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td>\r\n"); 1157 if (isDocuments && string.Equals(fv.Type, "Link", StringComparison.InvariantCultureIgnoreCase)) 1158 { 1159 var url = Convert.ToString(fv.Value) ?? string.Empty; 1160 1161 if (!string.IsNullOrWhiteSpace(url)) 1162 { 1163 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a"); 1164 BeginWriteAttribute("href", " href=\"", 50728, "\"", 50739, 1); 1165 WriteAttributeValue("", 50735, url, 50735, 4, false); 1166 EndWriteAttribute(); 1167 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t class=\"btn btn-primary\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t style=\"width: fit-content;\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t target=\"_blank\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t rel=\"noopener noreferrer\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"); 1168 Write(Translate("Documents - Open here", "Åpne her")); 1169 WriteLiteral("\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</a>\r\n"); 1170 } 1171 } 1172 else 1173 { 1174 Write(fv.Value.ToString().Replace("False", "Nei")); 1175 1176 } 1177 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n"); 1178 } 1179 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\r\n"); 1180 if (isDrawings) 1181 { 1182 var linkField = (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>()) 1183 .FirstOrDefault(f => string.Equals(f.Value?.Type, "Link", StringComparison.InvariantCultureIgnoreCase)); 1184 1185 var linkValueObj = linkField.Value?.Value; 1186 string drawingImageUrl = Convert.ToString(linkValueObj) ?? string.Empty; 1187 1188 if (!string.IsNullOrWhiteSpace(drawingImageUrl)) 1189 { 1190 WriteLiteral("\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"col-12 col-lg-6\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div class=\"d-flex justify-content-center align-items-center\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img class=\"img-fluid\""); 1191 BeginWriteAttribute("src", " src=\"", 51948, "\"", 51970, 1); 1192 WriteAttributeValue("", 51954, drawingImageUrl, 51954, 16, false); 1193 EndWriteAttribute(); 1194 WriteLiteral(" />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\r\n"); 1195 } 1196 } 1197 WriteLiteral("\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</div>\r\n"); 1198 1199 displayGroupsFirstContent = true; 1200 } 1201 WriteLiteral("\t\t\t\t\t</div>\r\n\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t</section>\r\n\r\n"); 1202 if (Model != null) 1203 { 1204 Write(RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images-modal.cshtml", Model)); 1205 1206 } 1207 WriteLiteral("</product-details>\r\n\r\n"); 1208 if (Model?.Price != null) 1209 { 1210 WriteLiteral("\t<script>\r\n\t\twindow.dataLayer.push({\r\n\t\t\tevent: \"view_item\",\r\n\t\t\tcurrency: \""); 1211 Write(Model.Price.CurrencyCode); 1212 WriteLiteral("\",\r\n\t\t\tvalue: "); 1213 Write(Model.Price.ToStringInvariant()); 1214 WriteLiteral(",\r\n\t\t\tecommerce: {\r\n\t\t\t\titems: [\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\titem_id: \""); 1215 Write(Model.Number); 1216 WriteLiteral("\",\r\n\t\t\t\t\t\titem_name: \""); 1217 Write(Dynamicweb.Core.Encoders.HtmlEncoder.JavaScriptStringEncode(Model.Name)); 1218 WriteLiteral("\",\r\n\t\t\t\t\t\tcurrency: \""); 1219 Write(Model.Price.CurrencyCode); 1220 WriteLiteral("\",\r\n\t\t\t\t\t\tprice: "); 1221 Write(Model.Price.ToStringInvariant()); 1222 WriteLiteral("\r\n\t\t\t\t\t}\r\n\t\t\t\t]\r\n\t\t\t}\r\n\t\t})\r\n\t</script>\r\n"); 1223 } 1224 WriteLiteral(@" 1225 <style> 1226 #pills-variants { position: relative; } 1227 .variant-loading-overlay { 1228 position: absolute; 1229 inset: 0; 1230 background: rgba(255,255,255,0.6); 1231 display: flex; 1232 align-items: center; 1233 justify-content: center; 1234 z-index: 50; 1235 border-radius: 8px; 1236 } 1237 </style> 1238 1239 1240 <script> 1241 document.addEventListener(""DOMContentLoaded"", function () { 1242 var placeholder = document.querySelector("".js-variant-deferred-placeholder""); 1243 var button = document.querySelector("".js-core-lazyload-variants-btn""); 1244 1245 if (placeholder && button) { 1246 button.click(); 1247 } 1248 }); 1249 </script> 1250 1251 <script type=""module"" src=""/Files/Templates/Designs/Swift-v2/Custom/js/product-details.js"" defer></script> 1252 "); 1253 } 1254 #pragma warning restore 1998 1255 1256 var perf = System.Diagnostics.Stopwatch.StartNew(); 1257 1258 void PerfLog(string label) 1259 { 1260 Dynamicweb.Core.SystemLog.WriteToLog( 1261 "ProductDetailsPerf", 1262 $"{label}: {perf.ElapsedMilliseconds} ms" 1263 ); 1264 } 1265 1266 private static readonly MemoryCache VariantNameCache = 1267 MemoryCache.Default; 1268 1269 public string CachedVariantName(string? variantId) 1270 { 1271 if (string.IsNullOrWhiteSpace(variantId)) 1272 return string.Empty; 1273 1274 var cacheKey = "variant-name:" + variantId; 1275 1276 var cached = VariantNameCache.Get(cacheKey) as string; 1277 1278 if (!string.IsNullOrWhiteSpace(cached)) 1279 return cached; 1280 1281 var variantName = 1282 Dynamicweb.Ecommerce.Services.Variants.GetVariantName(variantId) 1283 ?? string.Empty; 1284 1285 VariantNameCache.Set( 1286 cacheKey, 1287 variantName, 1288 DateTimeOffset.Now.AddHours(12) 1289 ); 1290 1291 return variantName; 1292 } 1293 1294 #nullable enable 1295 public CategoryFieldViewModel? GetFirstVariantFieldDisplayGroup(string key, string productId, string variantId) 1296 { 1297 CategoryFieldViewModel? returnValue = null; 1298 ProductViewModel? variantViewModel = ProductService.Instance.GetProductViewModelById(productId, variantId); 1299 if (variantViewModel != null) 1300 { 1301 returnValue = 1302 variantViewModel.FieldDisplayGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, key, StringComparison.InvariantCultureIgnoreCase)); 1303 } 1304 return returnValue; 1305 } 1306 1307 public string Slug(string input) 1308 { 1309 if (string.IsNullOrWhiteSpace(input)) return string.Empty; 1310 var s = input.Trim().ToLowerInvariant(); 1311 s = Regex.Replace(s, @"\s+", "_"); 1312 s = Regex.Replace(s, @"[^a-z0-9_æøå\-]", ""); 1313 return s; 1314 } 1315 1316 public decimal SortNumber(string? s) 1317 { 1318 if (string.IsNullOrWhiteSpace(s)) return decimal.MaxValue; 1319 var m = Regex.Match(s, @"-?\d+(?:[.,]\d+)?"); 1320 if (!m.Success) return decimal.MaxValue; 1321 1322 var raw = m.Value.Replace(',', '.'); 1323 if (decimal.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) 1324 return d; 1325 1326 return decimal.MaxValue; 1327 } 1328 1329 // --- FIX #1: exact token-match on VariantId (no Contains) --- 1330 public HashSet<string> VariantTokens(string? variantId) 1331 { 1332 if (string.IsNullOrWhiteSpace(variantId)) 1333 return new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); 1334 1335 return new HashSet<string>( 1336 variantId.Split(new[] { '.', ',' }, StringSplitOptions.RemoveEmptyEntries) 1337 .Select(x => x.Trim()) 1338 .Where(x => !string.IsNullOrEmpty(x)), 1339 StringComparer.InvariantCultureIgnoreCase 1340 ); 1341 } 1342 1343 // Normalize VariantInfo values so matching/counting is stable 1344 public string NormalizeVI(object? value) 1345 { 1346 var s = Convert.ToString(value) ?? string.Empty; 1347 s = s.Trim(); 1348 s = Regex.Replace(s, @"\s+", " "); 1349 return s; 1350 } 1351 1352 public VariantOptionViewModel? GetSelectedVariantOption( 1353 string? variantId, 1354 VariantGroupViewModel? variantGroup) 1355 { 1356 if (variantGroup?.Options == null) 1357 return null; 1358 1359 var tokens = VariantTokens(variantId); 1360 1361 return variantGroup.Options.FirstOrDefault(o => 1362 !string.IsNullOrWhiteSpace(o.Id) && tokens.Contains(o.Id)); 1363 } 1364 1365 public List<string> VariantDisplayValues(string? variantId) 1366 { 1367 if (string.IsNullOrWhiteSpace(variantId)) 1368 return new List<string>(); 1369 1370 var variantName = CachedVariantName(variantId); 1371 1372 return variantName 1373 .Split(new[] { " - " }, StringSplitOptions.None) 1374 .Select(x => x.Trim()) 1375 .Where(x => !string.IsNullOrWhiteSpace(x)) 1376 .ToList(); 1377 } 1378 1379 public string VariantDisplaySortKey(string? variantId) 1380 { 1381 var parts = VariantDisplayValues(variantId); 1382 var sortParts = new List<string>(); 1383 1384 foreach (var part in parts) 1385 { 1386 var numbers = Regex.Matches(part, @"-?\d+(?:[.,]\d+)?") 1387 .Cast<Match>() 1388 .Select(m => 1389 { 1390 var raw = m.Value.Replace(',', '.'); 1391 1392 return decimal.TryParse( 1393 raw, 1394 NumberStyles.Any, 1395 CultureInfo.InvariantCulture, 1396 out var d 1397 ) 1398 ? d 1399 : decimal.MaxValue; 1400 }) 1401 .ToList(); 1402 1403 if (numbers.Any()) 1404 { 1405 sortParts.Add(string.Join( 1406 ":", 1407 numbers.Select(n => n.ToString("0000000000.####", CultureInfo.InvariantCulture)) 1408 )); 1409 } 1410 else 1411 { 1412 sortParts.Add(part); 1413 } 1414 } 1415 1416 return string.Join("|", sortParts); 1417 } 1418 1419 public string FastVariantSortKey(string? variantId) 1420 { 1421 if (string.IsNullOrWhiteSpace(variantId)) 1422 return string.Empty; 1423 1424 var variantName = 1425 Dynamicweb.Ecommerce.Services.Variants.GetVariantName(variantId) 1426 ?? variantId; 1427 1428 var parts = variantName 1429 .Split(new[] { " - " }, StringSplitOptions.None) 1430 .Select(x => x.Trim()) 1431 .Where(x => !string.IsNullOrWhiteSpace(x)) 1432 .ToList(); 1433 1434 var sortParts = new List<string>(); 1435 1436 foreach (var part in parts) 1437 { 1438 var numbers = Regex.Matches(part, @"-?\d+(?:[.,]\d+)?") 1439 .Cast<Match>() 1440 .Select(m => 1441 { 1442 var raw = m.Value.Replace(',', '.'); 1443 1444 return decimal.TryParse( 1445 raw, 1446 NumberStyles.Any, 1447 CultureInfo.InvariantCulture, 1448 out var d 1449 ) 1450 ? d 1451 : decimal.MaxValue; 1452 }) 1453 .ToList(); 1454 1455 if (numbers.Any()) 1456 { 1457 sortParts.Add(string.Join( 1458 ":", 1459 numbers.Select(n => n.ToString("0000000000.####", CultureInfo.InvariantCulture)) 1460 )); 1461 } 1462 else 1463 { 1464 sortParts.Add(part.ToLowerInvariant()); 1465 } 1466 } 1467 1468 return string.Join("|", sortParts); 1469 } 1470 1471 1472 /// <summary>Leaves <paramref name="name"/> unchanged except reserved words from config, which are output in uppercase (from Global Settings, created with default if missing).</summary> 1473 public static string FormatTitleCase(string name, string configKey = "/Globalsettings/Modules/Mennt/Content/TitleCaseUpperWords", string defaultWords = "din,a2,api,crm") 1474 { 1475 if (string.IsNullOrWhiteSpace(name)) return name; 1476 var lowerName = name.ToLower(); 1477 var result = lowerName.Length > 1 ? char.ToUpper(lowerName[0]) + lowerName.Substring(1) : lowerName.ToUpper(); 1478 var sys = Dynamicweb.Configuration.SystemConfiguration.Instance; 1479 var raw = sys.GetValue(configKey); 1480 if (string.IsNullOrWhiteSpace(raw)) { sys.SetValue(configKey, defaultWords); raw = defaultWords; } 1481 var words = raw.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToArray(); 1482 foreach (var word in words) 1483 { 1484 result = Regex.Replace(result, @"\b" + Regex.Escape(word) + @"\b", word.ToUpper(), RegexOptions.IgnoreCase); 1485 } 1486 return result; 1487 } 1488 } 1489 } 1490 #pragma warning restore 1591 1491

1 @inherits ViewModelTemplate<Core.ViewModels.Ecommerce.ProductViewModel> 2 @using Core.Extensions 3 @using Dynamicweb.Ecommerce.ProductCatalog 4 @using Dynamicweb.Rendering 5 @using Core.Services 6 @using Core.ViewModels.Base 7 @using Core.ViewModels.UI 8 @using Dynamicweb 9 @using Dynamicweb.Core.Encoders 10 @using Dynamicweb.Frontend 11 @using Dynamicweb.Security.UserManagement 12 @using NuGet.Protocol 13 @using System.Text.RegularExpressions 14 @using System.Globalization 15 @using ButtonViewModel = Core.ViewModels.UI.ButtonViewModel 16 @using ProductViewModel = Core.ViewModels.Ecommerce.ProductViewModel 17 @using Mennt.Dynamicweb.CustomerProductAliases 18 @using System.Runtime.Caching 19 20 21 @functions 22 { 23 var perf = System.Diagnostics.Stopwatch.StartNew(); 24 25 void PerfLog(string label) 26 { 27 Dynamicweb.Core.SystemLog.WriteToLog( 28 "ProductDetailsPerf", 29 $"{label}: {perf.ElapsedMilliseconds} ms" 30 ); 31 } 32 33 private static readonly MemoryCache VariantNameCache = 34 MemoryCache.Default; 35 36 public string CachedVariantName(string? variantId) 37 { 38 if (string.IsNullOrWhiteSpace(variantId)) 39 return string.Empty; 40 41 var cacheKey = "variant-name:" + variantId; 42 43 var cached = VariantNameCache.Get(cacheKey) as string; 44 45 if (!string.IsNullOrWhiteSpace(cached)) 46 return cached; 47 48 var variantName = 49 Dynamicweb.Ecommerce.Services.Variants.GetVariantName(variantId) 50 ?? string.Empty; 51 52 VariantNameCache.Set( 53 cacheKey, 54 variantName, 55 DateTimeOffset.Now.AddHours(12) 56 ); 57 58 return variantName; 59 } 60 61 #nullable enable 62 public CategoryFieldViewModel? GetFirstVariantFieldDisplayGroup(string key, string productId, string variantId) 63 { 64 CategoryFieldViewModel? returnValue = null; 65 ProductViewModel? variantViewModel = ProductService.Instance.GetProductViewModelById(productId, variantId); 66 if (variantViewModel != null) 67 { 68 returnValue = 69 variantViewModel.FieldDisplayGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, key, StringComparison.InvariantCultureIgnoreCase)); 70 } 71 return returnValue; 72 } 73 74 public string Slug(string input) 75 { 76 if (string.IsNullOrWhiteSpace(input)) return string.Empty; 77 var s = input.Trim().ToLowerInvariant(); 78 s = Regex.Replace(s, @"\s+", "_"); 79 s = Regex.Replace(s, @"[^a-z0-9_æøå\-]", ""); 80 return s; 81 } 82 83 public decimal SortNumber(string? s) 84 { 85 if (string.IsNullOrWhiteSpace(s)) return decimal.MaxValue; 86 var m = Regex.Match(s, @"-?\d+(?:[.,]\d+)?"); 87 if (!m.Success) return decimal.MaxValue; 88 89 var raw = m.Value.Replace(',', '.'); 90 if (decimal.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) 91 return d; 92 93 return decimal.MaxValue; 94 } 95 96 // --- FIX #1: exact token-match on VariantId (no Contains) --- 97 public HashSet<string> VariantTokens(string? variantId) 98 { 99 if (string.IsNullOrWhiteSpace(variantId)) 100 return new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); 101 102 return new HashSet<string>( 103 variantId.Split(new[] { '.', ',' }, StringSplitOptions.RemoveEmptyEntries) 104 .Select(x => x.Trim()) 105 .Where(x => !string.IsNullOrEmpty(x)), 106 StringComparer.InvariantCultureIgnoreCase 107 ); 108 } 109 110 // Normalize VariantInfo values so matching/counting is stable 111 public string NormalizeVI(object? value) 112 { 113 var s = Convert.ToString(value) ?? string.Empty; 114 s = s.Trim(); 115 s = Regex.Replace(s, @"\s+", " "); 116 return s; 117 } 118 119 public VariantOptionViewModel? GetSelectedVariantOption( 120 string? variantId, 121 VariantGroupViewModel? variantGroup) 122 { 123 if (variantGroup?.Options == null) 124 return null; 125 126 var tokens = VariantTokens(variantId); 127 128 return variantGroup.Options.FirstOrDefault(o => 129 !string.IsNullOrWhiteSpace(o.Id) && tokens.Contains(o.Id)); 130 } 131 132 public List<string> VariantDisplayValues(string? variantId) 133 { 134 if (string.IsNullOrWhiteSpace(variantId)) 135 return new List<string>(); 136 137 var variantName = CachedVariantName(variantId); 138 139 return variantName 140 .Split(new[] { " - " }, StringSplitOptions.None) 141 .Select(x => x.Trim()) 142 .Where(x => !string.IsNullOrWhiteSpace(x)) 143 .ToList(); 144 } 145 146 public string VariantDisplaySortKey(string? variantId) 147 { 148 var parts = VariantDisplayValues(variantId); 149 var sortParts = new List<string>(); 150 151 foreach (var part in parts) 152 { 153 var numbers = Regex.Matches(part, @"-?\d+(?:[.,]\d+)?") 154 .Cast<Match>() 155 .Select(m => 156 { 157 var raw = m.Value.Replace(',', '.'); 158 159 return decimal.TryParse( 160 raw, 161 NumberStyles.Any, 162 CultureInfo.InvariantCulture, 163 out var d 164 ) 165 ? d 166 : decimal.MaxValue; 167 }) 168 .ToList(); 169 170 if (numbers.Any()) 171 { 172 sortParts.Add(string.Join( 173 ":", 174 numbers.Select(n => n.ToString("0000000000.####", CultureInfo.InvariantCulture)) 175 )); 176 } 177 else 178 { 179 sortParts.Add(part); 180 } 181 } 182 183 return string.Join("|", sortParts); 184 } 185 186 public string FastVariantSortKey(string? variantId) 187 { 188 if (string.IsNullOrWhiteSpace(variantId)) 189 return string.Empty; 190 191 var variantName = 192 Dynamicweb.Ecommerce.Services.Variants.GetVariantName(variantId) 193 ?? variantId; 194 195 var parts = variantName 196 .Split(new[] { " - " }, StringSplitOptions.None) 197 .Select(x => x.Trim()) 198 .Where(x => !string.IsNullOrWhiteSpace(x)) 199 .ToList(); 200 201 var sortParts = new List<string>(); 202 203 foreach (var part in parts) 204 { 205 var numbers = Regex.Matches(part, @"-?\d+(?:[.,]\d+)?") 206 .Cast<Match>() 207 .Select(m => 208 { 209 var raw = m.Value.Replace(',', '.'); 210 211 return decimal.TryParse( 212 raw, 213 NumberStyles.Any, 214 CultureInfo.InvariantCulture, 215 out var d 216 ) 217 ? d 218 : decimal.MaxValue; 219 }) 220 .ToList(); 221 222 if (numbers.Any()) 223 { 224 sortParts.Add(string.Join( 225 ":", 226 numbers.Select(n => n.ToString("0000000000.####", CultureInfo.InvariantCulture)) 227 )); 228 } 229 else 230 { 231 sortParts.Add(part.ToLowerInvariant()); 232 } 233 } 234 235 return string.Join("|", sortParts); 236 } 237 238 } 239 240 @{ 241 var req = Dynamicweb.Context.Current?.Request; 242 var res = Dynamicweb.Context.Current?.Response; 243 244 var feed = (req?["feed"] ?? "").Equals("true", StringComparison.InvariantCultureIgnoreCase); 245 var getproductinfo = (req?["getproductinfo"] ?? "").Equals("true", StringComparison.InvariantCultureIgnoreCase); 246 var isVariantAjaxRequest = feed || getproductinfo; 247 248 if (req != null && res != null) 249 { 250 251 // Hvis dette er en "ekte" sidevisning (refresh/navigate), skal vi IKKE vise feed-mode. 252 // Sec-Fetch-Mode er ofte "navigate" på refresh/vanlig navigasjon. 253 var secFetchMode = req.Headers["Sec-Fetch-Mode"] ?? ""; 254 var isNavigate = secFetchMode.Equals("navigate", StringComparison.InvariantCultureIgnoreCase); 255 256 // Hvis feed/getproductinfo er med, men det er en vanlig navigasjon => redirect til clean URL. 257 if ((feed || getproductinfo) && isNavigate) 258 { 259 var url = req.Url; 260 var query = url?.Query?.TrimStart('?') ?? ""; 261 var pairs = new List<KeyValuePair<string, string>>(); 262 foreach (var part in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) 263 { 264 var idx = part.IndexOf('='); 265 if (idx < 0) continue; 266 var key = Uri.UnescapeDataString(part.Substring(0, idx)); 267 var val = idx + 1 < part.Length ? Uri.UnescapeDataString(part.Substring(idx + 1)) : ""; 268 if (string.Equals(key, "feed", StringComparison.OrdinalIgnoreCase) || 269 string.Equals(key, "getproductinfo", StringComparison.OrdinalIgnoreCase) || 270 string.Equals(key, "variantCount", StringComparison.OrdinalIgnoreCase)) 271 continue; 272 pairs.Add(new KeyValuePair<string, string>(key, val)); 273 } 274 var newQuery = string.Join("&", pairs.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); 275 var clean = url.AbsolutePath; 276 if (!string.IsNullOrWhiteSpace(newQuery)) 277 clean += "?" + newQuery; 278 279 Dynamicweb.Context.Current.Response.Redirect(clean); 280 } 281 } 282 283 // ---------------------------- 284 // Base data 285 // ---------------------------- 286 var variantCountRaw = Dynamicweb.Context.Current?.Request?["variantCount"]; 287 int alreadyRendered = 0; 288 int.TryParse(variantCountRaw, out alreadyRendered); 289 alreadyRendered = Math.Max(0, alreadyRendered); 290 291 PerfLog("Start"); 292 List<string> variantIdCombinations = Model?.VariantCombinations() ?? new List<string>(); 293 PerfLog("After VariantCombinations"); 294 var fieldGroups = Model?.FieldDisplayGroups; 295 296 var productInfoGroup = fieldGroups?.FirstOrDefault(fdg => fdg.Key == "Produktinformasjon"); 297 var productInfoFields = productInfoGroup?.Value?.Fields; 298 bool hasProductInfo = productInfoFields != null && productInfoFields.Any(); 299 300 CategoryFieldViewModel? techinicalSpecsFieldDisplayGroup = 301 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase)); 302 303 CategoryFieldViewModel? documentsFieldDisplayGroup = 304 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Dokumentasjon", StringComparison.InvariantCultureIgnoreCase)); 305 306 CategoryFieldViewModel? drawingsFieldDisplayGroup = 307 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase)); 308 309 CategoryFieldViewModel? variantInfoFieldDisplayGroup = 310 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 311 312 var variantInfoFields = variantInfoFieldDisplayGroup?.Fields; 313 bool hasVariantInfo = variantInfoFields != null && variantInfoFields.Any(); 314 315 List<CategoryFieldViewModel?> displayGroups = new List<CategoryFieldViewModel?> 316 { 317 techinicalSpecsFieldDisplayGroup, 318 documentsFieldDisplayGroup, 319 drawingsFieldDisplayGroup 320 }; 321 322 bool hasVariants = ((Model?.VariantInfo?.VariantInfo?.Count) ?? 0) > 1 || variantIdCombinations.Count > 0; 323 324 // ---------------------------- 325 // FILTERS (single select) 326 // Query params: 327 // f_vg_<slug>=value 328 // f_vi_<systemName>=value 329 // ---------------------------- 330 331 // FIX #2: VG stores "wanted" which can be OPTION ID (new) OR OPTION NAME (legacy) 332 var activeVG = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); // slug -> wanted (id or name) 333 var activeVI = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); // sys -> value 334 335 if (req != null) 336 { 337 foreach (string key in req.Params.AllKeys) 338 { 339 if (string.IsNullOrWhiteSpace(key)) continue; 340 341 if (key.StartsWith("f_vg_", StringComparison.InvariantCultureIgnoreCase)) 342 { 343 var slug = key.Substring(5); 344 var val = req.Params[key] ?? ""; 345 if (!string.IsNullOrWhiteSpace(slug) && !string.IsNullOrWhiteSpace(val)) 346 activeVG[slug] = val; 347 } 348 else if (key.StartsWith("f_vi_", StringComparison.InvariantCultureIgnoreCase)) 349 { 350 var sys = key.Substring(5); 351 var val = req.Params[key] ?? ""; 352 if (!string.IsNullOrWhiteSpace(sys) && !string.IsNullOrWhiteSpace(val)) 353 activeVI[sys] = val; 354 } 355 } 356 } 357 358 bool HasAnyFilters() => activeVG.Count > 0 || activeVI.Count > 0; 359 360 // Performance test: 361 // On the first normal product page view we skip the expensive full variant/facet build. 362 // On the first feed request without active filters, use fast mode: render only the requested page of variants. 363 bool shouldBuildVariantData = 364 hasVariants && (isVariantAjaxRequest || HasAnyFilters()); 365 366 // Fast mode can be used when: 367 // - this is an AJAX/feed request 368 // - there are no VariantInfo filters active 369 // Variant Group filters can be handled without hydrating every variant. 370 bool fastVariantFeed = 371 shouldBuildVariantData 372 && isVariantAjaxRequest 373 && activeVI.Count == 0; 374 375 bool shouldRunFullVariantAnalysis = 376 shouldBuildVariantData 377 && !fastVariantFeed; 378 379 // FIX #3: blacklist VariantInfo filters we do NOT want (removes TOMU etc) 380 var variantInfoBlacklist = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) 381 { 382 "TOMU", 383 "SACD", 384 "LEAT" 385 }; 386 bool IsAllowedVI(string sys) => !string.IsNullOrWhiteSpace(sys) && !variantInfoBlacklist.Contains(sys); 387 388 var facetCountsAll = new Dictionary<string, Dictionary<string, int>>(StringComparer.InvariantCultureIgnoreCase); 389 var facetCountsCurrent = new Dictionary<string, Dictionary<string, int>>(StringComparer.InvariantCultureIgnoreCase); 390 391 var vgSlugToLabel = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); 392 393 // id->name mapping for VG options so UI can show names even if URL stores id 394 var vgOptionIdToNameBySlug = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase); 395 396 void RememberVGOption(string slug, string optionId, string optionName) 397 { 398 if (string.IsNullOrWhiteSpace(slug) || string.IsNullOrWhiteSpace(optionId)) return; 399 400 if (!vgOptionIdToNameBySlug.TryGetValue(slug, out var map)) 401 vgOptionIdToNameBySlug[slug] = map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); 402 403 if (!map.ContainsKey(optionId)) 404 map[optionId] = string.IsNullOrWhiteSpace(optionName) ? optionId : optionName; 405 } 406 407 void AddFacetValue(Dictionary<string, Dictionary<string, int>> target, string facetKey, string value) 408 { 409 if (string.IsNullOrWhiteSpace(facetKey) || string.IsNullOrWhiteSpace(value)) return; 410 411 if (!target.TryGetValue(facetKey, out var values)) 412 target[facetKey] = values = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase); 413 414 values[value] = values.TryGetValue(value, out var c) ? c + 1 : 1; 415 } 416 417 var productId = Model?.Id ?? ""; 418 var modelVariantGroups = Model?.VariantGroups()?.ToList() ?? new List<VariantGroupViewModel>(); 419 420 void AddVariantDisplayFacetValues(Dictionary<string, Dictionary<string, int>> target, string variantId) 421 { 422 var displayValues = VariantDisplayValues(variantId); 423 424 foreach (var item in modelVariantGroups.Select((group, index) => new { group, index })) 425 { 426 var groupName = item.group?.Name ?? string.Empty; 427 if (groupName.Equals("TOMU", StringComparison.InvariantCultureIgnoreCase)) continue; 428 429 var slug = Slug(groupName); 430 if (string.IsNullOrWhiteSpace(slug)) continue; 431 432 if (!vgSlugToLabel.ContainsKey(slug)) 433 vgSlugToLabel[slug] = groupName; 434 435 var value = item.index < displayValues.Count ? displayValues[item.index] : string.Empty; 436 if (!string.IsNullOrWhiteSpace(value)) 437 AddFacetValue(target, $"VG:{slug}", value); 438 } 439 } 440 441 // Variant group filters use the same display values as the table and sorting. 442 bool MatchesFilters(ProductViewModel v, 443 Dictionary<string, string> vgFilters, 444 Dictionary<string, string> viFilters) 445 { 446 var displayValues = VariantDisplayValues(v.VariantId); 447 448 foreach (var f in vgFilters) 449 { 450 var slug = f.Key; 451 var wanted = NormalizeVI(f.Value); 452 453 var groupIndex = modelVariantGroups.FindIndex(g => 454 g != null && Slug(g.Name).Equals(slug, StringComparison.InvariantCultureIgnoreCase)); 455 456 if (groupIndex < 0 || groupIndex >= displayValues.Count) 457 return false; 458 459 var actual = NormalizeVI(displayValues[groupIndex]); 460 if (!actual.Equals(wanted, StringComparison.InvariantCultureIgnoreCase)) 461 return false; 462 } 463 464 foreach (var f in viFilters) 465 { 466 var sys = f.Key; 467 var wantedVal = NormalizeVI(f.Value); 468 469 var vi = v.FieldDisplayGroups?.Values? 470 .FirstOrDefault(g => string.Equals(g?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 471 472 var fv = vi?.Fields?.Values?.FirstOrDefault(x => string.Equals(x.SystemName, sys, StringComparison.InvariantCultureIgnoreCase)); 473 var actual = NormalizeVI(fv?.Value); 474 475 if (!actual.Equals(wantedVal, StringComparison.InvariantCultureIgnoreCase)) 476 return false; 477 } 478 479 return true; 480 } 481 482 bool MatchesFastVGFilters(string variantId) 483 { 484 var displayValues = VariantDisplayValues(variantId); 485 486 foreach (var f in activeVG) 487 { 488 var slug = f.Key; 489 var wanted = NormalizeVI(f.Value); 490 491 var groupIndex = modelVariantGroups.FindIndex(g => 492 g != null && 493 Slug(g.Name).Equals(slug, StringComparison.InvariantCultureIgnoreCase)); 494 495 if (groupIndex < 0 || groupIndex >= displayValues.Count) 496 return false; 497 498 var actual = NormalizeVI(displayValues[groupIndex]); 499 500 if (!actual.Equals(wanted, StringComparison.InvariantCultureIgnoreCase)) 501 return false; 502 } 503 504 return true; 505 } 506 507 var filteredVariantIds = new List<string>(); 508 509 // Cache all variant view models once per request. 510 // This avoids calling GetProductViewModelById once during facet/filter building and again during rendering. 511 var variantViewModelsById = new Dictionary<string, ProductViewModel>(StringComparer.InvariantCultureIgnoreCase); 512 513 if (shouldRunFullVariantAnalysis && !string.IsNullOrWhiteSpace(productId)) 514 { 515 PerfLog("Before full variant hydrate"); 516 foreach (var vid in variantIdCombinations) 517 { 518 var v = ProductService.Instance.GetProductViewModelById(productId, vid); 519 if (v != null) 520 { 521 variantViewModelsById[vid] = v; 522 } 523 } 524 PerfLog("After full variant hydrate"); 525 } 526 527 if (shouldRunFullVariantAnalysis && !string.IsNullOrWhiteSpace(productId)) 528 { 529 foreach (var vid in variantIdCombinations) 530 { 531 if (!variantViewModelsById.TryGetValue(vid, out var v)) 532 { 533 continue; 534 } 535 536 // ALL counts 537 AddVariantDisplayFacetValues(facetCountsAll, v.VariantId); 538 539 var viGroup = v.FieldDisplayGroups?.Values? 540 .FirstOrDefault(g => string.Equals(g?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 541 542 if (viGroup?.Fields != null) 543 { 544 foreach (var kv in viGroup.Fields) 545 { 546 var fv = kv.Value; 547 if (fv == null) continue; 548 549 var sys = fv.SystemName ?? ""; 550 if (!IsAllowedVI(sys)) continue; 551 552 var val = NormalizeVI(fv.Value); 553 if (string.IsNullOrWhiteSpace(val)) continue; 554 555 AddFacetValue(facetCountsAll, $"VI:{sys}", val); 556 } 557 } 558 559 // FILTERED + CURRENT counts 560 bool shouldIncludeVariant = 561 UserContext.Current?.IsLoggedOn != true 562 || (v.Price != null && v.Price.Price > 0); 563 564 if (shouldIncludeVariant && MatchesFilters(v, activeVG, activeVI)) 565 { 566 filteredVariantIds.Add(vid); 567 568 AddVariantDisplayFacetValues(facetCountsCurrent, v.VariantId); 569 570 if (viGroup?.Fields != null) 571 { 572 foreach (var kv in viGroup.Fields) 573 { 574 var fv = kv.Value; 575 if (fv == null) continue; 576 577 var sys = fv.SystemName ?? ""; 578 if (!IsAllowedVI(sys)) continue; 579 580 var val = NormalizeVI(fv.Value); 581 if (string.IsNullOrWhiteSpace(val)) continue; 582 583 AddFacetValue(facetCountsCurrent, $"VI:{sys}", val); 584 } 585 } 586 } 587 } 588 } 589 590 // Better facet visibility: show if choice exists OR if active 591 bool ShouldShowFacet(string facetKey) 592 { 593 if (!facetCountsAll.TryGetValue(facetKey, out var values)) return false; 594 595 if (values.Keys.Count > 1) return true; 596 597 if (facetKey.StartsWith("VG:", StringComparison.InvariantCultureIgnoreCase)) 598 { 599 var slug = facetKey.Substring(3); 600 return activeVG.ContainsKey(slug); 601 } 602 if (facetKey.StartsWith("VI:", StringComparison.InvariantCultureIgnoreCase)) 603 { 604 var sys = facetKey.Substring(3); 605 return activeVI.ContainsKey(sys); 606 } 607 return false; 608 } 609 610 if (fastVariantFeed) 611 { 612 // Fast path: 613 // Do not hydrate every variant with GetProductViewModelById. 614 // Build lightweight Variant Group facets from variant names. 615 // Supports filtering on Variant Groups without full variant analysis. 616 617 foreach (var vid in variantIdCombinations) 618 { 619 AddVariantDisplayFacetValues(facetCountsAll, vid); 620 621 if (MatchesFastVGFilters(vid)) 622 { 623 filteredVariantIds.Add(vid); 624 AddVariantDisplayFacetValues(facetCountsCurrent, vid); 625 } 626 } 627 } 628 629 // Paging based on filtered list. 630 // When variant data is deferred on the first normal product view, render 0 rows so the page can return quickly. 631 int totalFiltered = shouldBuildVariantData ? filteredVariantIds.Count : variantIdCombinations.Count; 632 int variantCountToSkip = shouldBuildVariantData ? Math.Min(alreadyRendered, totalFiltered) : 0; 633 int variantCountToTake = shouldBuildVariantData ? Math.Min(10, Math.Max(0, totalFiltered - variantCountToSkip)) : 0; 634 635 var sortedFilteredVariantIds = shouldBuildVariantData 636 ? filteredVariantIds 637 .OrderBy(variantId => VariantDisplaySortKey(variantId)) 638 .ThenBy(variantId => variantId) 639 .ToList() 640 : new List<string>(); 641 642 var pageVariantIds = sortedFilteredVariantIds 643 .Skip(variantCountToSkip) 644 .Take(variantCountToTake) 645 .ToList(); 646 647 if (fastVariantFeed && !string.IsNullOrWhiteSpace(productId)) 648 { 649 foreach (var vid in pageVariantIds) 650 { 651 var v = ProductService.Instance.GetProductViewModelById(productId, vid); 652 if (v != null) 653 { 654 variantViewModelsById[vid] = v; 655 } 656 } 657 } 658 } 659 660 <style> 661 .table .btn:hover { 662 color: #FCC400 !important; 663 } 664 </style> 665 666 @* Include once per template: [Include file 'Helpers/TitleCaseHelper.cshtml' not found in 'Templates/Designs/Swift-v2/Helpers/TitleCaseHelper.cshtml'] then use: FormatTitleCase(Model.Name) *@ 667 @using System.Text.RegularExpressions 668 @using System.Linq 669 @functions { 670 /// <summary>Leaves <paramref name="name"/> unchanged except reserved words from config, which are output in uppercase (from Global Settings, created with default if missing).</summary> 671 public static string FormatTitleCase(string name, string configKey = "/Globalsettings/Modules/Mennt/Content/TitleCaseUpperWords", string defaultWords = "din,a2,api,crm") 672 { 673 if (string.IsNullOrWhiteSpace(name)) return name; 674 var lowerName = name.ToLower(); 675 var result = lowerName.Length > 1 ? char.ToUpper(lowerName[0]) + lowerName.Substring(1) : lowerName.ToUpper(); 676 var sys = Dynamicweb.Configuration.SystemConfiguration.Instance; 677 var raw = sys.GetValue(configKey); 678 if (string.IsNullOrWhiteSpace(raw)) { sys.SetValue(configKey, defaultWords); raw = defaultWords; } 679 var words = raw.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToArray(); 680 foreach (var word in words) 681 { 682 result = Regex.Replace(result, @"\b" + Regex.Escape(word) + @"\b", word.ToUpper(), RegexOptions.IgnoreCase); 683 } 684 return result; 685 } 686 } 687 688 689 <product-details 690 product-id="@Model?.Id" 691 variat-id="@Model?.VariantId" 692 variant-combinations-count="@(totalFiltered)" 693 data-is-logged-in="@(UserContext.Current?.IsLoggedOn == true ? "true" : "false")" 694 data-has-variants="@(hasVariants.ToString().ToLowerInvariant())"> 695 <section class="core-section js-section" id="core-section-product-details" data-dw-colorscheme="tingstad-dark"> 696 <div class="container"> 697 <div class="row justify-content-start"> 698 <div class="col-12 col-sm-12"> 699 <div class="row"> 700 <div class="position-relative"> 701 <div class="row mx-0 pt-5 pt-lg-6 position-relative custom-product-details-top-info-container" data-dw-colorscheme="tingstad-white"> 702 <div class="col-12"> 703 <div class="row"> 704 <div class="d-none d-lg-flex col-4 offset-1 mb-4"> 705 @if (Model != null) 706 { 707 @RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images.cshtml", Model) 708 } 709 </div> 710 <div class="col-12 col-lg-6 mb-4"> 711 <div class="d-flex align-items-center mb-3"> 712 @if (!string.IsNullOrEmpty(Model?.Number)) 713 { 714 <div class="badge text-bg-dark me-3">@Model.Number</div> 715 @if (CustomerAlias.Has(Model.Number)) 716 { 717 <div class="badge text-bg-dark me-3">@CustomerAlias.First(Model.Number)</div> 718 } 719 } 720 @if (UserContext.Current?.IsLoggedOn == true && Model?.RenderedHtml?.Stock != null) 721 { 722 if (modelVariantGroups.Count == 0 || !string.IsNullOrEmpty(Model.VariantId)) 723 { 724 <span class="js-core-details-stock">@Model.RenderedHtml.Stock</span> 725 } 726 } 727 </div> 728 @if (!string.IsNullOrEmpty(Model?.Name)) 729 { 730 var formattedName = FormatTitleCase(Model.Name); 731 732 <h1 class="h2">@formattedName</h1> 733 } 734 @Model?.ShortDescription 735 736 <div class="row d-lg-none"> 737 <div class="col-8 my-4 mx-auto"> 738 @if (Model != null) 739 { 740 @RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images.cshtml", Model) 741 } 742 </div> 743 </div> 744 745 @if (hasProductInfo) 746 { 747 <div class="product-details-specs-list list-top-info mt-3 mb-5"> 748 @foreach (var fieldDisplayGroup in productInfoFields!) 749 { 750 var fv = fieldDisplayGroup.Value; 751 if (fv == null) { continue; } 752 <div class="list-item"> 753 <div class="list-label"> 754 <p>@fv.Name</p> 755 </div> 756 <div class="list-value"> 757 <p>@fv.Value</p> 758 </div> 759 </div> 760 } 761 </div> 762 } 763 764 @if (UserContext.Current?.IsLoggedOn == true && (!string.IsNullOrEmpty(Model.VariantId) || modelVariantGroups.Count == 0)) 765 { 766 <price-and-form> 767 <div class="d-block d-md-flex align-items-end justify-content-end"> 768 <div class="d-flex flex-grow-1 gap-5 gap-md-2"> 769 @if (Model?.RenderedHtml?.UnitPrice != null) 770 { 771 <div class="d-flex flex-column me-lg-auto js-core-details-unit-price"> 772 <p class="small mb-2">@Translate("Pris. pr. stk")</p> 773 @Model.RenderedHtml.UnitPrice 774 </div> 775 } 776 777 @if (Model?.RenderedHtml?.Price != null) 778 { 779 <div class="d-flex flex-column align-items-start justify-content-center me-4 mb-4 mb-md-0 me-lg-6 js-core-details-price"> 780 <p class="small mb-2">@Translate("Total")</p> 781 @Model.RenderedHtml.Price 782 </div> 783 } 784 </div> 785 786 @* Viktig: hvis AddToCartForm er avhengig av variantvalg kan du beholde den slik, 787 men pris vises uansett *@ 788 @Model?.RenderedHtml?.AddToCartForm 789 </div> 790 </price-and-form> 791 } 792 else 793 { 794 ButtonViewModel viewMoreButton = new ButtonViewModel 795 { 796 Text = Translate("View variants"), 797 Type = ButtonType.Link, 798 Link = new LinkViewModel { Url = $"{Model.Link}#product-info-section" }, 799 DisplayType = ButtonDisplayType.Secondary, 800 }; 801 @viewMoreButton.Render() 802 } 803 </div> 804 805 @if (hasVariantInfo) 806 { 807 <div class="col-12 py-4 mt-5 order-3 px-0 d-none d-lg-flex custom-product-details-bottom-info-container" data-dw-colorscheme="tingstad-white"> 808 <div class="product-details-specs-list list-bottom-info"> 809 @foreach (var fieldDisplayGroup in variantInfoFields!) 810 { 811 FieldValueViewModel? fv = fieldDisplayGroup.Value; 812 if (fv == null || string.IsNullOrEmpty(fv.ToString())) { continue; } 813 814 <div class="list-item"> 815 <div class="list-label"> 816 <p>@Translate($"Product Info Info - Field - {fv.SystemName}", fv.Name)</p> 817 </div> 818 <div class="list-value"> 819 <p>@fv.Value</p> 820 </div> 821 </div> 822 } 823 </div> 824 </div> 825 } 826 </div> 827 </div> 828 </div> 829 830 <div class="row position-relative"> 831 <div class="custom-product-details-top-info-container--pseudo-shadow"></div> 832 </div> 833 <div class="row mx-0"> 834 <div class="custom-product-details-top-info--box-shadow"></div> 835 </div> 836 837 </div> 838 </div> 839 </div> 840 </div> 841 </div> 842 </section> 843 844 <section id="product-info-section" class="core-section js-section pt-0" data-dw-colorscheme="tingstad-white"> 845 <div class="container"> 846 <div class="row justify-content-start"> 847 <div class="col-12 col-sm-12"> 848 849 <div class="d-flex justify-content-center custom-product-details-nav-pills-container"> 850 <button class="btn btn-secondary d-flex d-lg-none dropdown-toggle mt-5 js-core-specifications-dropdown-btn core-specifications-dropdown-btn" 851 data-target="#core-specifications-navigation"> 852 <span class="js-core-specifications-dropdown-text"> 853 <div class="btn-content"> 854 @Translate("Specifications Mobile Button - Text", "View specifications") 855 </div> 856 </span> 857 </button> 858 859 <ul class="nav nav-pills justify-content-center p-1" id="core-specifications-navigation" role="tablist"> 860 @if (hasVariants) 861 { 862 <li class="nav-item" role="presentation"> 863 <button class="nav-link @(hasVariants ? "active" : string.Empty)" id="pills-variants-tab" data-bs-toggle="pill" 864 data-bs-target="#pills-variants" type="button" role="tab" 865 aria-controls="pills-variants" 866 aria-selected="true"> 867 @Translate("Specifications Variants - Button - Text", "Varianter") 868 </button> 869 </li> 870 } 871 872 @{ 873 bool displayGroupsFirst = false; 874 } 875 @foreach (var displayGroup in displayGroups) 876 { 877 if (displayGroup == null || string.IsNullOrEmpty(displayGroup.Id)) { continue; } 878 879 bool isDrawings = string.Equals(displayGroup.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase); 880 bool shouldShow = true; 881 882 if (isDrawings) 883 { 884 var linkField = (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>()) 885 .FirstOrDefault(f => string.Equals(f.Value?.Type, "Link", StringComparison.InvariantCultureIgnoreCase)); 886 887 var linkValueObj = linkField.Value?.Value; 888 string drawingImageUrl = Convert.ToString(linkValueObj) ?? string.Empty; 889 shouldShow = !string.IsNullOrEmpty(drawingImageUrl); 890 } 891 892 if (shouldShow) 893 { 894 <li class="nav-item" role="presentation"> 895 <button class="nav-link @(!hasVariants && !displayGroupsFirst ? "active" : string.Empty)" id="pills-@displayGroup.Id-tab" data-bs-toggle="pill" 896 data-bs-target="#pills-@displayGroup.Id" type="button" role="tab" 897 aria-controls="pills-@displayGroup.Id" aria-selected="@(!hasVariants && !displayGroupsFirst ? "true" : "false")"> 898 @displayGroup.Name 899 </button> 900 </li> 901 } 902 903 displayGroupsFirst = true; 904 } 905 </ul> 906 </div> 907 908 <div class="tab-content custom-product-details-tab-pane-container pt-5 pt-lg-0" id="pills-tabContent"> 909 @if (hasVariants) 910 { 911 CategoryFieldViewModel? firstVariantInfoGroup = shouldBuildVariantData 912 ? GetFirstVariantFieldDisplayGroup("VariantInfo", Model.Id, (pageVariantIds?.FirstOrDefault() ?? variantIdCombinations?.FirstOrDefault() ?? string.Empty)) 913 : null; 914 915 <div class="tab-pane fade @(hasVariants ? "show active" : string.Empty)" id="pills-variants" role="tabpanel" 916 aria-labelledby="pills-variants-tab" tabindex="0"> 917 <div class="row py-3"> 918 <div class="col-12 position-relative"> 919 <h3 class="h2">@Translate("Specifications Variants - Header - Text", "Varianter")</h3> 920 921 @* Loading overlay *@ 922 <div class="variant-loading-overlay js-variant-loading" hidden> 923 <div class="spinner-border" role="status" aria-label="Laster..."></div> 924 </div> 925 926 @* Active filter chips *@ 927 @if (HasAnyFilters()) 928 { 929 <div class="d-flex flex-wrap gap-2 mb-3"> 930 @foreach (var vg in activeVG.OrderBy(k => k.Key)) 931 { 932 var groupLabel = vgSlugToLabel.TryGetValue(vg.Key, out var l) ? l : vg.Key; 933 var wanted = vg.Value; 934 935 string optLabel = wanted; // could be id or legacy name 936 if (vgOptionIdToNameBySlug.TryGetValue(vg.Key, out var map) && map.TryGetValue(wanted, out var nm)) 937 { 938 optLabel = nm; 939 } 940 941 <button type="button" 942 class="btn btn-outline-secondary btn-sm filter-chip js-filter-chip" 943 data-param="f_vg_@vg.Key"> 944 @groupLabel: @optLabel <span aria-hidden="true">&times;</span> 945 </button> 946 } 947 @foreach (var vi in activeVI.OrderBy(k => k.Key)) 948 { 949 <button type="button" 950 class="btn btn-outline-secondary btn-sm filter-chip js-filter-chip" 951 data-param="f_vi_@vi.Key"> 952 @vi.Key: @vi.Value <span aria-hidden="true">&times;</span> 953 </button> 954 } 955 956 <button type="button" class="btn btn-secondary btn-sm js-clear-filters">Fjern filter</button> 957 </div> 958 } 959 960 @* Select filters *@ 961 <div class="row g-3 mb-4"> 962 @foreach (var vgEntry in vgSlugToLabel.OrderBy(x => x.Value, StringComparer.InvariantCultureIgnoreCase)) 963 { 964 var slug = vgEntry.Key; 965 var label = vgEntry.Value; 966 967 // extra safety: skip TOMU if it sneaks in as VG label 968 if (label.Equals("TOMU", StringComparison.InvariantCultureIgnoreCase)) { continue; } 969 970 var facetKey = $"VG:{slug}"; 971 if (!ShouldShowFacet(facetKey)) { continue; } 972 973 var paramName = $"f_vg_{slug}"; 974 var selectedWanted = activeVG.TryGetValue(slug, out var val) ? val : ""; 975 976 <div class="col-12 col-md-4"> 977 <label class="form-label">@label</label> 978 <select class="form-select js-variant-filter" name="@paramName"> 979 <option value="">Alle</option> 980 @if (facetCountsCurrent.TryGetValue(facetKey, out var opts)) 981 { 982 foreach (var opt in opts 983 .OrderBy(o => 984 { 985 var nm = (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(o.Key, out var n)) 986 ? n 987 : o.Key; 988 return SortNumber(nm); 989 }) 990 .ThenBy(o => 991 { 992 var nm = (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(o.Key, out var n)) 993 ? n 994 : o.Key; 995 return nm; 996 }, StringComparer.InvariantCultureIgnoreCase)) 997 { 998 var optId = opt.Key; 999 var optCount = opt.Value; 1000 1001 var optName = 1002 (vgOptionIdToNameBySlug.TryGetValue(slug, out var map) && map.TryGetValue(optId, out var nm)) 1003 ? nm 1004 : optId; 1005 1006 // selectedWanted can be id OR legacy name 1007 var isSelected = 1008 optId.Equals(selectedWanted, StringComparison.InvariantCultureIgnoreCase) || 1009 optName.Equals(selectedWanted, StringComparison.InvariantCultureIgnoreCase); 1010 1011 <option value="@optId" selected="@(isSelected ? "selected" : null)"> 1012 @optName (@optCount) 1013 </option> 1014 } 1015 } 1016 </select> 1017 </div> 1018 } 1019 1020 @foreach (var viFacet in facetCountsAll.Keys 1021 .Where(k => k.StartsWith("VI:", StringComparison.InvariantCultureIgnoreCase)) 1022 .OrderBy(k => k, StringComparer.InvariantCultureIgnoreCase)) 1023 { 1024 var sys = viFacet.Substring(3); 1025 if (!IsAllowedVI(sys)) { continue; } 1026 if (!ShouldShowFacet(viFacet)) { continue; } 1027 1028 var paramName = $"f_vi_{sys}"; 1029 var selectedVal = activeVI.TryGetValue(sys, out var sv) ? sv : ""; 1030 1031 <div class="col-12 col-md-4"> 1032 <label class="form-label">@sys</label> 1033 <select class="form-select js-variant-filter" name="@paramName"> 1034 <option value="">Alle</option> 1035 @if (facetCountsCurrent.TryGetValue(viFacet, out var opts)) 1036 { 1037 foreach (var opt in opts 1038 .OrderBy(o => SortNumber(o.Key)) 1039 .ThenBy(o => o.Key, StringComparer.InvariantCultureIgnoreCase)) 1040 { 1041 <option value="@opt.Key" selected="@(opt.Key.Equals(selectedVal, StringComparison.InvariantCultureIgnoreCase) ? "selected" : null)"> 1042 @opt.Key (@opt.Value) 1043 </option> 1044 } 1045 } 1046 </select> 1047 </div> 1048 } 1049 </div> 1050 1051 <div class="flexbox-table w-100 mb-4"> 1052 <div class="table-row table-header text-muted"> 1053 <div class="table-cell table-cell-image"><div class="ratio ratio-1x1"></div></div> 1054 <div class="table-cell table-cell-info">&nbsp;</div> 1055 1056 @foreach (VariantGroupViewModel? variantGroup in modelVariantGroups) 1057 { 1058 <div class="table-cell"><p title="@variantGroup.Name">@variantGroup.Name</p></div> 1059 } 1060 1061 @if (firstVariantInfoGroup != null) 1062 { 1063 foreach (KeyValuePair<string, FieldValueViewModel> field in firstVariantInfoGroup.Fields) 1064 { 1065 <div class="table-cell"> 1066 <p title="@HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name))"> 1067 @HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name)) 1068 </p> 1069 </div> 1070 } 1071 } 1072 1073 @if (UserContext.Current?.IsLoggedOn == true) 1074 { 1075 <div class="table-cell"><p title="Enhpris">@Translate("Enhpris")</p></div> 1076 } 1077 1078 <div class="table-cell table-cell-expand">&nbsp;</div> 1079 1080 @if (UserContext.Current?.IsLoggedOn == true) 1081 { 1082 <div class="table-cell table-cell-addtocart">&nbsp;</div> 1083 } 1084 </div> 1085 1086 <div class="js-core-variant-list"> 1087 @if (!shouldBuildVariantData && hasVariants) 1088 { 1089 <div class="alert alert-light mb-3 js-variant-deferred-placeholder"> 1090 Varianter lastes inn. 1091 </div> 1092 } 1093 @foreach (string variantId in pageVariantIds) 1094 { 1095 ProductViewModel? variantViewModel = null; 1096 variantViewModelsById.TryGetValue(variantId, out variantViewModel); 1097 1098 if (variantViewModel != null) 1099 { 1100 CategoryFieldViewModel? variantInfoVariantFieldDisplayGroup = 1101 variantViewModel.FieldDisplayGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "VariantInfo", StringComparison.InvariantCultureIgnoreCase)); 1102 1103 CategoryFieldViewModel? techinicalSpecsVariantFieldDisplayGroup = 1104 fieldGroups?.Values?.FirstOrDefault(c => string.Equals(c?.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase)); 1105 1106 ImageViewModel? variantProductImageViewModel = Model?.DefaultImage?.GetImage(); 1107 if (variantProductImageViewModel != null) 1108 { 1109 variantProductImageViewModel.Classes = new ClassList("mb-0 ratio ratio-1x1 flex-shrink-0"); 1110 variantProductImageViewModel.ImgTagClasses = new ClassList("h-100 w-100 object-fit-contain"); 1111 } 1112 1113 <div class="js-core-variant-list-item"> 1114 <div class="table-row custom-product-list-item "> 1115 <div class="d-flex d-lg-contents flex-row-reverse flex-xl-row"> 1116 <div class="table-cell table-cell-image collapsed" 1117 data-bs-toggle="collapse" 1118 href="#@($"product-variant-details-{variantId}")" 1119 aria-expanded="false"> 1120 @if (variantProductImageViewModel != null) 1121 { 1122 @variantProductImageViewModel 1123 } 1124 </div> 1125 1126 <div class="table-cell table-cell-info"> 1127 <div class="custom-product-list-item--info"> 1128 <div class="d-flex align-items-center mb-2"> 1129 @if (!string.IsNullOrEmpty(variantViewModel.Number)) 1130 { 1131 <div class="badge text-bg-dark me-3">@variantViewModel.Number</div> 1132 @if (CustomerAlias.Has(variantViewModel.Number)) 1133 { 1134 <div class="badge text-bg-dark me-3">@CustomerAlias.First(variantViewModel.Number)</div> 1135 } 1136 1137 } 1138 @if (UserContext.Current?.IsLoggedOn == true) 1139 { 1140 @variantViewModel?.RenderedHtml?.Stock 1141 } 1142 </div> 1143 @if (!string.IsNullOrEmpty(variantViewModel.Name)) 1144 { 1145 <h3 class="mb-0"> 1146 <a href="@variantViewModel.Link" 1147 class="text-decoration-none text-reset"> 1148 @variantViewModel.Name 1149 </a> 1150 </h3> 1151 } 1152 </div> 1153 </div> 1154 </div> 1155 1156 <div class="d-flex d-lg-contents table-cell-variable-wrapper"> 1157 @{ 1158 var variantDisplayValues = VariantDisplayValues(variantViewModel.VariantId); 1159 } 1160 1161 @foreach (var item in modelVariantGroups.Select((group, index) => new { group, index })) 1162 { 1163 var value = item.index < variantDisplayValues.Count 1164 ? variantDisplayValues[item.index] 1165 : string.Empty; 1166 1167 <div class="table-cell" data-title="@HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {item.group?.Name}", item.group?.Name))"> 1168 <p>@value</p> 1169 </div> 1170 } 1171 1172 @if (variantInfoVariantFieldDisplayGroup != null) 1173 { 1174 foreach (KeyValuePair<string, FieldValueViewModel> field in variantInfoVariantFieldDisplayGroup.Fields) 1175 { 1176 <div class="table-cell" data-title="@HtmlEncoder.HtmlAttributeEncode(Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name))"> 1177 <p>@field.Value</p> 1178 </div> 1179 } 1180 } 1181 else if (firstVariantInfoGroup != null) 1182 { 1183 foreach (KeyValuePair<string, FieldValueViewModel> field in firstVariantInfoGroup.Fields) 1184 { 1185 <div class="table-cell"></div> 1186 } 1187 } 1188 1189 @if (UserContext.Current?.IsLoggedOn == true) 1190 { 1191 <div class="table-cell" data-title="Enhpris"> 1192 @variantViewModel?.RenderedHtml?.UnitPrice 1193 </div> 1194 } 1195 </div> 1196 1197 <div class="table-cell table-cell-expand collapsed" 1198 data-title="Utvidet info" 1199 data-bs-toggle="collapse" 1200 href="#@($"product-variant-details-{variantId}")" 1201 aria-expanded="false"> 1202 <span class="icon-2 opacity-75" title="Utvidet info"> 1203 @ReadFile("/Files/Templates/Designs/Swift-v2/Assets/images/custom-icons/tingstad-chevron-right.svg") 1204 </span> 1205 </div> 1206 1207 @if (UserContext.Current?.IsLoggedOn == true) 1208 { 1209 <div class="table-cell table-cell-addtocart"> 1210 <price-and-form> 1211 <div class="d-flex flex-row core-price-wrapper gap-3 justify-content-between w-100 w-lg-auto"> 1212 <div class="d-flex core-add-to-favorites d-none d-lg-flex custom-product-list-item--addtofavorites justify-content-end"> 1213 @variantViewModel?.RenderedHtml?.AddToFavorites 1214 </div> 1215 <div class="custom-product-list-item--price d-flex flex-column align-items-start align-items-lg-end justify-content-center"> 1216 @variantViewModel?.RenderedHtml?.Price 1217 </div> 1218 <div class="align-items-end"> 1219 @variantViewModel?.RenderedHtml?.AddToCartForm 1220 </div> 1221 </div> 1222 </price-and-form> 1223 </div> 1224 } 1225 </div> 1226 1227 <div class="collapse" id="@($"product-variant-details-{variantId}")"> 1228 <div class="py-2 py-lg-4 d-flex flex-row flex-wrap custom-product-list-item__variant-info--extended-info"> 1229 <div class="extended-info-header"> 1230 <h3 class="mb-0 mb-lg-4">Teknisk informasjon</h3> 1231 @if (variantProductImageViewModel != null) 1232 { 1233 @variantProductImageViewModel 1234 } 1235 </div> 1236 <div class="other stuff" style="flex: 1;"> 1237 <table class="table custom-info-data-table"> 1238 <tbody> 1239 <tr> 1240 <th>Varenummer</th> 1241 <td>@variantViewModel?.Number</td> 1242 </tr> 1243 <tr> 1244 <th>Navn</th> 1245 <td>@variantViewModel?.Name</td> 1246 </tr> 1247 @if (techinicalSpecsVariantFieldDisplayGroup != null) 1248 { 1249 foreach (KeyValuePair<string, FieldValueViewModel> field in techinicalSpecsVariantFieldDisplayGroup.Fields) 1250 { 1251 <tr> 1252 <th>@Translate($"Variant Info - Field - {field.Value.Name}", field.Value.Name)</th> 1253 <td>@field.Value</td> 1254 </tr> 1255 } 1256 } 1257 </tbody> 1258 </table> 1259 <div> 1260 <a href="@variantViewModel.Link" class="">@Translate("Se flere detaljer her")</a> 1261 </div> 1262 </div> 1263 </div> 1264 </div> 1265 </div> 1266 } 1267 } 1268 </div> 1269 1270 @if (totalFiltered > (alreadyRendered + variantCountToTake)) 1271 { 1272 <div class="d-flex justify-content-center my-3"> 1273 @(new ButtonViewModel() 1274 { 1275 Type = ButtonType.Button, 1276 DisplayType = ButtonDisplayType.LazyLoad, 1277 Icon = "/Files/Templates/Designs/Swift-v2/Assets/images/custom-icons/tingstad-load.svg", 1278 Text = shouldBuildVariantData ? Translate("Show more") : Translate("Load variants", "Last varianter"), 1279 Classes = new ClassList("js-core-lazyload-variants-btn") 1280 }) 1281 </div> 1282 } 1283 </div> 1284 1285 </div> 1286 </div> 1287 </div> 1288 } 1289 1290 @{ 1291 bool displayGroupsFirstContent = false; 1292 } 1293 @foreach (var displayGroup in displayGroups) 1294 { 1295 if (displayGroup == null || string.IsNullOrEmpty(displayGroup.Id)) { continue; } 1296 1297 <div class="tab-pane fade @(!hasVariants && !displayGroupsFirstContent ? "show active" : string.Empty)" id="pills-@displayGroup.Id" role="tabpanel" 1298 aria-labelledby="pills-@displayGroup.Id-tab" tabindex="0"> 1299 <div class="row py-3"> 1300 <div class="col-12"> 1301 <h3 class="h2">@displayGroup.Name</h3> 1302 1303 @{ 1304 ClassList tableContainerClassList = new ClassList("col-12"); 1305 bool isDrawings = string.Equals(displayGroup.Id, "Mltegning", StringComparison.InvariantCultureIgnoreCase); 1306 1307 if (isDrawings) 1308 { 1309 tableContainerClassList.Add("col-lg-6"); 1310 } 1311 } 1312 1313 <div class="row"> 1314 <div class="@tableContainerClassList"> 1315 <table class="table custom-info-data-table mt-4"> 1316 <tbody> 1317 @{ 1318 bool isDocuments = string.Equals(displayGroup.Id, "Dokumentasjon", StringComparison.InvariantCultureIgnoreCase); 1319 } 1320 1321 @foreach (var field in (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>())) 1322 { 1323 var fv = field.Value; 1324 if (fv == null) { continue; } 1325 1326 bool isTechnicalInfo = string.Equals(displayGroup.Id, "Teknisk_informasjon", StringComparison.InvariantCultureIgnoreCase); 1327 bool noVariantSelected = string.IsNullOrWhiteSpace(Model?.VariantId); 1328 bool isWeightField = 1329 string.Equals(fv.SystemName, "NEWE", StringComparison.InvariantCultureIgnoreCase) || 1330 string.Equals(fv.Name, "Vekt", StringComparison.InvariantCultureIgnoreCase); 1331 1332 if (isTechnicalInfo && noVariantSelected && isWeightField) 1333 { 1334 continue; 1335 } 1336 1337 if (!isDocuments && string.Equals(fv.Type, "Link", StringComparison.InvariantCultureIgnoreCase)) 1338 { 1339 continue; 1340 } 1341 1342 <tr> 1343 <th scope="row" style="vertical-align: middle;"> 1344 @Translate($"Product Details - Field Display Group - Label - {fv.SystemName}", fv.Name) 1345 </th> 1346 <td> 1347 @if (isDocuments && string.Equals(fv.Type, "Link", StringComparison.InvariantCultureIgnoreCase)) 1348 { 1349 var url = Convert.ToString(fv.Value) ?? string.Empty; 1350 1351 if (!string.IsNullOrWhiteSpace(url)) 1352 { 1353 <a href="@url" 1354 class="btn btn-primary" 1355 style="width: fit-content;" 1356 target="_blank" 1357 rel="noopener noreferrer"> 1358 @Translate("Documents - Open here", "Åpne her") 1359 </a> 1360 } 1361 } 1362 else 1363 { 1364 @fv.Value.ToString().Replace("False", "Nei") 1365 } 1366 </td> 1367 </tr> 1368 } 1369 </tbody> 1370 </table> 1371 </div> 1372 1373 @if (isDrawings) 1374 { 1375 var linkField = (displayGroup.Fields ?? new Dictionary<string, FieldValueViewModel>()) 1376 .FirstOrDefault(f => string.Equals(f.Value?.Type, "Link", StringComparison.InvariantCultureIgnoreCase)); 1377 1378 var linkValueObj = linkField.Value?.Value; 1379 string drawingImageUrl = Convert.ToString(linkValueObj) ?? string.Empty; 1380 1381 if (!string.IsNullOrWhiteSpace(drawingImageUrl)) 1382 { 1383 <div class="col-12 col-lg-6"> 1384 <div class="d-flex justify-content-center align-items-center"> 1385 <img class="img-fluid" src="@drawingImageUrl" /> 1386 </div> 1387 </div> 1388 } 1389 } 1390 </div> 1391 </div> 1392 </div> 1393 </div> 1394 1395 displayGroupsFirstContent = true; 1396 } 1397 </div> 1398 1399 </div> 1400 </div> 1401 </div> 1402 </section> 1403 1404 @if (Model != null) 1405 { 1406 @RenderingService.Instance.PartialView("/eCom/ProductCatalog/partials/detail/images-modal.cshtml", Model) 1407 } 1408 </product-details> 1409 1410 @if (Model?.Price != null) 1411 { 1412 <script> 1413 window.dataLayer.push({ 1414 event: "view_item", 1415 currency: "@Model.Price.CurrencyCode", 1416 value: @Model.Price.ToStringInvariant(), 1417 ecommerce: { 1418 items: [ 1419 { 1420 item_id: "@Model.Number", 1421 item_name: "@Dynamicweb.Core.Encoders.HtmlEncoder.JavaScriptStringEncode(Model.Name)", 1422 currency: "@Model.Price.CurrencyCode", 1423 price: @Model.Price.ToStringInvariant() 1424 } 1425 ] 1426 } 1427 }) 1428 </script> 1429 } 1430 1431 <style> 1432 #pills-variants { position: relative; } 1433 .variant-loading-overlay { 1434 position: absolute; 1435 inset: 0; 1436 background: rgba(255,255,255,0.6); 1437 display: flex; 1438 align-items: center; 1439 justify-content: center; 1440 z-index: 50; 1441 border-radius: 8px; 1442 } 1443 </style> 1444 1445 1446 <script> 1447 document.addEventListener("DOMContentLoaded", function () { 1448 var placeholder = document.querySelector(".js-variant-deferred-placeholder"); 1449 var button = document.querySelector(".js-core-lazyload-variants-btn"); 1450 1451 if (placeholder && button) { 1452 button.click(); 1453 } 1454 }); 1455 </script> 1456 1457 <script type="module" src="/Files/Templates/Designs/Swift-v2/Custom/js/product-details.js" defer></script> 1458