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\">×</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\">×</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""> </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\"> </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\"> </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">×</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">×</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"> </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"> </div>
1079
1080 @if (UserContext.Current?.IsLoggedOn == true)
1081 {
1082 <div class="table-cell table-cell-addtocart"> </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