Error executing template "Designs/Keflico/eCom/Productlist/products.cshtml"
System.NullReferenceException: Object reference not set to an instance of an object.
at CompiledRazorTemplates.Dynamic.RazorEngine_172e1b5539624ad1bf1baf74f966a77b.FindTopGroup(Group group) in D:\dynamicweb.net\Solutions\keflico.live\Files\Templates\Designs\Keflico\eCom\Productlist\products.cshtml:line 1484
at CompiledRazorTemplates.Dynamic.RazorEngine_172e1b5539624ad1bf1baf74f966a77b.Execute() in D:\dynamicweb.net\Solutions\keflico.live\Files\Templates\Designs\Keflico\eCom\Productlist\products.cshtml:line 190
at RazorEngine.Templating.TemplateBase.RazorEngine.Templating.ITemplate.Run(ExecuteContext context, TextWriter reader)
at RazorEngine.Templating.RazorEngineService.RunCompile(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag)
at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass16_0.<RunCompile>b__0(TextWriter writer)
at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter)
at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template)
at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template)
at Dynamicweb.Rendering.Template.RenderRazorTemplate()
1 @using System.Web;
2 @using Dynamicweb.Ecommerce;
3 @using Dynamicweb.Ecommerce.CustomerExperienceCenter.Favorites
4 @using Dynamicweb.Ecommerce.Variants;
5 @using Keflico.Website.Custom
6 @{
7 string AllProductsPage = Pageview.Area.Item["AllProductsPage"].ToString();
8 string VariantsLookup = "/" + Pageview.Area.Item["Variantslookup"].ToString();
9 string BundleLookup = "/" + Pageview.Area.Item["Bundlelookup"].ToString();
10 bool isloggedin = false;
11
12
13 var allProductsList = new List<string>(); // Use a List to build your JSON string.
14
15 foreach (var product in GetLoop("Products"))
16 {
17 var primaryGroup = product.GetString("Ecom:Product.PrimaryGroupID").ToLower();
18 var productId = product.GetString("Ecom:Product.ID");
19
20 var productObject = $"{{ \"id\": \"{productId}\", \"primaryGroup\": \"{primaryGroup}\" }}";
21 allProductsList.Add(productObject);
22 }
23
24 // Join all the product objects with commas and wrap them in brackets to form a JSON array.
25 var allProductNumbersString = "[" + string.Join(",", allProductsList) + "]";
26
27
28
29 string sortBy = GetString("Ecom:ProductList.SortBy");
30 string sortOrder = GetString("Ecom:ProductList.SortOrder");
31
32 bool isStandardSorting = string.IsNullOrWhiteSpace(HttpContext.Current.Request.QueryString["Sortby"]);
33
34
35 if (!string.IsNullOrWhiteSpace(GetGlobalValue("Global:Extranet.UserName").ToString()))
36 {
37 isloggedin = true;
38 }
39
40
41 FavoriteList favoriteList = null;
42 var service = new FavoriteProductService();
43 if (isloggedin)
44 {
45 var user = Dynamicweb.Security.UserManagement.User.GetCurrentFrontendUser();
46 favoriteList = user.GetFavoriteLists().FirstOrDefault();
47
48 }
49
50 var shouldRefreshFavorite = (isloggedin && !Dynamicweb.Security.UserManagement.User.GetCurrentFrontendUser().UserHasFavoriteList()).ToString().ToLower();
51
52 }
53
54 <header class="header header-overview module module-sand-light">
55 <div class="breadcrumbs">
56 @RenderNavigation(new
57 {
58 StartLevel = 1,
59 EndLevel = 4,
60 ExpandMode = "Pathonly",
61 Template = "breadcrumbs.xslt"
62 })
63 </div>
64 @if (String.IsNullOrWhiteSpace(GetString("Ecom:ProductList:Page.GroupID")))
65 {
66 <h1 class="header__title">@Translate("Translate_TopLevelGroup_Headline")</h1>
67 <p>@Translate("Translate_TopLevelGroup_Text")</p>
68 }
69 else
70 {
71 <h1 class="header__title">@GetString("Ecom:Group.Name")</h1>
72
73 if (!String.IsNullOrWhiteSpace(GetString("Ecom:Group.Description")))
74 {
75 @GetString("Ecom:Group.Description")
76 }
77 }
78
79 </header>
80
81 @if (GetLoop("ProductGroups").Count() > 0 && String.IsNullOrWhiteSpace(GetString("Ecom:ProductList:Page.GroupID")))
82 {
83 <section class="category-page module module-sand">
84 <article class="card-list__wrapper">
85 <a href="@AllProductsPage" class="all-link">
86 @Translate("Translate_ProductList_SeeAllProducts")
87 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.006 11.031">
88 <g class="arrow" transform="translate(441 901.014) rotate(180)">
89 <path class="angle" d="M-17182.074-20447.988l4.809-4.809,4.809,4.809" transform="translate(20875.289 -16281.768) rotate(-90)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" />
90 <line class="line" x2="17" transform="translate(423.5 895.5)" stroke-linecap="round" stroke-width="1" />
91 </g>
92 </svg>
93 </a>
94 <ul class="card-list">
95 @foreach (var group in GetLoop("ProductGroups"))
96 {
97 bool show = group.GetBoolean("Ecom:Group.ShowInMenu");
98 if (show)
99 {
100 string groupLink = "/Default.aspx?Id=" + Pageview.Page.ID + "&GroupID=" + group.GetString("Ecom:Group.ID");
101 string groupName = group.GetString("Ecom:Group.Name");
102 string groupImage = group.GetString("Ecom:Group.LargeImage");
103 string groupImageLg = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=665&Height=665&Crop=0&Compression=100";
104 string groupImageMd = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=512&Height=512&Crop=0&Compression=100";
105 string groupImageSm = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=415&Height=415&Crop=0&Compression=100";
106 string groupImageDefault = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=620&Height=620&Crop=0&Compression=100";
107
108 <li class="card-list__card">
109 <a href="@groupLink" class="card__wrapper">
110 <picture class="card__picture">
111 <source srcset="@groupImageLg" media="(min-width: 1536px)">
112 <source srcset="@groupImageMd" media="(min-width: 992px)">
113 <source srcset="@groupImageSm" media="(min-width: 768px)">
114 <img src="@groupImageDefault" alt="@groupName">
115 </picture>
116 <ul class="card__info-list">
117 <li class="card__info-list__item">
118 <span class="info info--large">@groupName</span>
119 </li>
120 </ul>
121 </a>
122 </li>
123 }
124
125 }
126 </ul>
127 </article>
128 </section>
129 }
130 else if (GetLoop("Subgroups").Count() > 0)
131 {
132 var currentGroup = Dynamicweb.Ecommerce.Services.ProductGroups.GetGroup(GetString("Ecom:Group.ID"));
133 var currentToplevel = FindTopGroup(currentGroup);
134 string allLink = AllProductsPage + "&Group=" + currentGroup.Name;
135
136 <section class="category-page module module-sand">
137 <article class="card-list__wrapper">
138 <a href="@allLink" class="all-link">
139 @Translate("Translate_ProductList_SeeAllProducts")
140 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.006 11.031">
141 <g class="arrow" transform="translate(441 901.014) rotate(180)">
142 <path class="angle" d="M-17182.074-20447.988l4.809-4.809,4.809,4.809" transform="translate(20875.289 -16281.768) rotate(-90)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" />
143 <line class="line" x2="17" transform="translate(423.5 895.5)" stroke-linecap="round" stroke-width="1" />
144 </g>
145 </svg>
146 </a>
147 <ul class="card-list">
148 @foreach (var group in GetLoop("Subgroups"))
149 {
150 bool show = group.GetBoolean("Ecom:Group.ShowInMenu");
151 if (show)
152 {
153 string groupLink = "/Default.aspx?Id=" + Pageview.Page.ID + "&GroupID=" + group.GetString("Ecom:Group.ID");
154 string groupName = group.GetString("Ecom:Group.Name");
155 string groupImage = group.GetString("Ecom:Group.LargeImage");
156 string groupImageLg = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=665&Height=665&Crop=0&Compression=100";
157 string groupImageMd = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=512&Height=512&Crop=0&Compression=100";
158 string groupImageSm = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=415&Height=415&Crop=0&Compression=100";
159 string groupImageDefault = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(groupImage) + "&Width=620&Height=620&Crop=0&Compression=100";
160
161 if (groupName.ToLower().Contains("alle") || groupName.ToLower().Contains("all"))
162 {
163 groupLink = allLink;
164 }
165
166 <li class="card-list__card">
167 <a href="@groupLink" class="card__wrapper">
168 <picture class="card__picture">
169 <source srcset="@groupImageLg" media="(min-width: 1536px)">
170 <source srcset="@groupImageMd" media="(min-width: 992px)">
171 <source srcset="@groupImageSm" media="(min-width: 768px)">
172 <img src="@groupImageDefault" alt="@groupName">
173 </picture>
174 <ul class="card__info-list">
175 <li class="card__info-list__item">
176 <span class="info info--large">@groupName</span>
177 </li>
178 </ul>
179 </a>
180 </li>
181 }
182 }
183 </ul>
184 </article>
185 </section>
186 }
187 else
188 {
189 var currentGroup = Dynamicweb.Ecommerce.Services.ProductGroups.GetGroup(GetString("Ecom:Group.ID"));
190 var currentToplevel = FindTopGroup(currentGroup);
191
192 <section class="module module-sand product-overview" id="productsFlowApp">
193 <aside id="productFilters" class="filter-section" data-group="@currentGroup.Id" data-top-group="@currentToplevel.Name" data-all="false">
194 <div class="filter-section__header">
195 <h3>@Translate("Translate_ProductList_FilterHeadline")</h3>
196
197 <div class="filter-section__exit" id="filter-article-close">
198 <svg xmlns="http://www.w3.org/2000/svg" width="10.819" height="10.819" viewBox="0 0 10.819 10.819">
199 <g id="Group_1017" data-name="Group 1017" transform="translate(-1.591 2.559)">
200 <rect id="Rectangle_702" data-name="Rectangle 702" width="14" height="1.3" rx="0.65" transform="translate(1.591 7.341) rotate(-45)" />
201 <rect id="Rectangle_703" data-name="Rectangle 703" width="14" height="1.3" rx="0.65" transform="translate(2.51 -2.559) rotate(45)" />
202 </g>
203 </svg>
204 </div>
205 </div>
206 <ul v-if="showFilters" class="filter-section__filter-list">
207 <li v-for="facet in facets" v-if="((facet.QueryParameter != 'Group' && currentGroup && !allProducts) || allProducts) && sortedOptions(facet.Options).length > 1" :class="'filter-item filter-item--' + facet.RenderType.toLowerCase()">
208 <template v-if="facet.RenderType.toLowerCase() != 'range'">
209 <input class="filter-item__checkbox-toggle" type="checkbox" :id="facet.QueryParameter" aria-checked="true">
210 <label class="filter-item__header" :for="facet.QueryParameter">
211 <span class="filter-item__header__title">{{ translation('Translate_ProductList_Filter_' + facet.QueryParameter) }}</span>
212 </label>
213 </template>
214
215 <div class="filter-item__body">
216 <ul class="filter-item__input-list">
217 <template v-if="facet.RenderType.toLowerCase() == 'range'">
218 <li class="input-list__filter">
219 <label for="filter-length">{{ translation('Translate_ProductList_Filter_' + facet.QueryParameter.replace('Start', '')) }}</label>
220 <div class="duo-range-slider">
221 <input class="duo-range-slider__range filter-option" :data-name="facet.QueryParameter" :name="facet.QueryParameter" :value="getLowestValue(facet.Options)" :min="getLowestValue(facet.Options)" :max="getHighestValue(facet.Options)" step="1" type="range">
222 <input class="duo-range-slider__range filter-option" :data-name="facet.QueryParameter.replace('Start', 'End')" :name="facet.QueryParameter.replace('Start', 'End')" :value="getHighestValue(facet.Options)" :min="getLowestValue(facet.Options)" :max="getHighestValue(facet.Options)" step="1" type="range">
223 <div class="duo-range-slider__values">
224 <div class="duo-range-slider__values-min"><span>{{ facet.Options[0].Value }}</span> mm</div>
225 <div class="duo-range-slider__values-max"><span>{{ facet.Options.at(-1).Value }}</span> mm</div>
226 </div>
227 </div>
228 </li>
229 </template>
230 <template v-else-if="facet.RenderType.toLowerCase() == 'select'">
231 <li class="input-list__filter">
232 <div class="search-select">
233 <div class="search-select__search">
234 <input class="search-select__search-input" type="search" :placeholder="translation('Translate_General_SearchFor')">
235 <button class="search-select__search-clear" type="button">@Translate("Translate_Close")</button>
236 </div>
237 <div class="search-select__dropdown">
238 <label v-for="option in sortedOptions(facet.Options)" class="search-select__dropdown-item" :for="facet.QueryParameter + '_' + option.Value">{{ option.Value }}</label>
239 </div>
240 <div class="search-select__tags">
241 <template v-for="option in sortedOptions(facet.Options)">
242 <input type="checkbox" class="filter-option" :data-name="facet.QueryParameter" :data-parameter-type="facet.QueryParameterType" :name="facet.QueryParameter + addition(facet.QueryParameterType)" :value="option.Value" :id="facet.QueryParameter + '_' + option.Value">
243 <label :for="facet.QueryParameter + '_' + option.Value">{{ option.Label }}</label>
244 </template>
245 </div>
246 </div>
247 </li>
248 </template>
249 <template v-else>
250 <li class="input-list__filter">
251 <template v-for="option in sortedOptions(facet.Options)">
252 <div class="form__fieldset" v-if="facet.QueryParameter != 'Type'">
253 <div class="form__field-wrap">
254 <input type="checkbox" class="form__field form__field--checkbox filter-option" :data-name="facet.QueryParameter" :name="facet.QueryParameter + addition(facet.QueryParameterType)" :value="option.Value" :id="facet.QueryParameter + '_' + option.Value">
255 <label :for="facet.QueryParameter + '_' + option.Value">{{ option.Label }}</label>
256 </div>
257 </div>
258 <div class="form__fieldset" v-if="facet.QueryParameter == 'Type' && !(option.Label == 'kunde' || option.Label == 'relatordre')">
259 <div class="form__field-wrap">
260 <input type="checkbox" class="form__field form__field--checkbox filter-option" :data-name="facet.QueryParameter" :name="facet.QueryParameter + addition(facet.QueryParameterType)" :value="option.Value" :id="facet.QueryParameter + '_' + option.Value">
261 <label :for="facet.QueryParameter + '_' + option.Value">{{ translation(`Translate_Filter_ProductType_${option.Label}`) }}</label>
262 </div>
263 </div>
264 </template>
265 </li>
266 </template>
267 </ul>
268 </div>
269 </li>
270 </ul>
271 <div class="filter-section__footer">
272 <div class="footer-controls">
273 <button id="clearFilterButton" class="btn btn-link">@Translate("Translate_ProductList_ResetFilters")</button>
274 <button id="applyFilterButton" class="btn btn-secondary btn--small">@Translate("Translate_ProductList_ApplyFilters")</button>
275 </div>
276 </div>
277 </aside>
278 <article class="card-list__wrapper">
279 <div class="card-list__header">
280 <div class="form__fieldset mobile-only">
281 <button class="btn btn-secondary" id="toggle-filter-btn">
282 @Translate("Translate_Filter")
283 </button>
284 </div>
285 <div class="form__fieldset">
286 <label for="sorting" class="select-label">@Translate("Translate_ProductList_SortBy")</label>
287 <select class="form__field--narrow form__field-wrap--select" id="sorting">
288 <option value="default">@Translate("Translate_ProductList_SortOption_Highlighted")</option>
289 <option value="asc" @(!isStandardSorting && sortOrder.ToLower() == "asc" ? "selected" : "")>@Translate("Translate_ProductList_SortOption_Alphabetic")</option>
290 <option value="desc" @(!isStandardSorting && sortOrder.ToLower() == "desc" ? "selected" : "")>@Translate("Translate_ProductList_SortOption_ReverseAlpabetic")</option>
291 </select>
292 </div>
293 </div>
294 <ul class="card-list">
295 @foreach (var product in GetLoop("Products"))
296 {
297 string link = product.GetString("Ecom:Product.Link.Clean");
298 string name = product.GetString("Ecom:Product.Name");
299 string teaser = product.GetString("Ecom:Product:Field.Name2.Value.Clean");
300 string productNumber = product.GetString("Ecom:Product.Number");
301 string dbNumberLabel = product.GetString("Ecom:Product:Field.ProductDbNumber.Name");
302 string dbNumber = product.GetString("Ecom:Product:Field.ProductDbNumber.Value");
303 string primaryGroup = product.GetString("Ecom:Product.PrimaryGroupID").ToLower();
304 var productLayout = !string.IsNullOrWhiteSpace(product.GetString("Ecom:Product:Field.ProductLayout")) ? product.GetString("Ecom:Product:Field.ProductLayout") : primaryGroup;
305
306 var combination = new VariantCombination(product.GetString("Ecom:Product.ID"));
307 IList<VariantCombination> productVariants = new List<VariantCombination>();
308 var singleVariant = new VariantCombination();
309
310 if (combination != null && combination.Product != null)
311 {
312 productVariants = combination.Product.VariantCombinations;
313 }
314
315 if (productVariants.Count == 1)
316 {
317 singleVariant = productVariants.FirstOrDefault();
318 }
319
320 string labelCode = product.GetString("Ecom:Product:Field.ProductPurchaseCode");
321
322 bool isBundleOnly = System.Convert.ToBoolean(product.GetString("Ecom:Product:Field.BundleOnly"));
323 bool usePriceExplanation = false;
324
325 string primaryPrice = product.GetString("Ecom:Product.Price.PriceWithoutVAT");
326 string secondaryPrice = "";
327 string tertiaryPrice = "";
328 string primaryPriceDescription = "";
329 string secondaryPriceDescription = "";
330 string tertiaryPriceDescription = "";
331
332 //product.GetRawTags();
333
334 var totalStock = product.GetDouble("Ecom:Product.Stock");
335 totalStock += product.GetDouble("Ecom:Product:Field.StockSea.Value");
336
337 int totalStockWareHouse = product.GetInteger("Ecom:Product.Stock");
338 int totalStockSea = product.GetInteger("Ecom:Product:Field.StockSea.Value");
339
340
341 if (primaryGroup != "group32")
342 {
343 totalStock += product.GetDouble("Ecom:Product:Field.ProductPieceOnPurchase.Value");
344 totalStockSea += product.GetInteger("Ecom:Product:Field.ProductPieceOnPurchase.Value");
345 }
346
347 bool isSingleVariant = product.GetInteger("Ecom:Product.VariantCount") == 1 ? true : false;
348
349 double singleVariantStock = 0;
350 int singleVariantStockSea = 0;
351
352 if (isSingleVariant)
353 {
354 singleVariantStock = singleVariant.Product.UnitStock;
355 singleVariantStockSea = !String.IsNullOrWhiteSpace(singleVariant.Product.ProductFieldValues.GetProductFieldValue("ProductPieceOnPurchase").Value.ToString()) ? Convert.ToInt32(singleVariant.Product.ProductFieldValues.GetProductFieldValue("ProductPieceOnPurchase").Value.ToString().Replace(",", "")) : 0;
356 totalStockWareHouse = (int)singleVariantStock;
357 totalStock += singleVariantStock;
358 totalStock += singleVariantStockSea;
359 }
360
361 string priceCurrencySymbol = product.GetString("Ecom:Product.Currency.Symbol");
362 string salesUnit = product.GetString("Ecom:Product.DefaultUnitName").ToLower();
363 string salesUnitCode = product.GetString("Ecom:Product:Field.ProductLengthUnitCode");
364
365 string productImage = product.GetString("Ecom:Product.ImageDefault.Clean");
366 string productImageLg = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(productImage) + "&Width=416&Height=371&Crop=0&Compression=100";
367 string productImageMd = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(productImage) + "&Width=310&Height=277&Crop=0&Compression=100";
368 string productImageSm = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(productImage) + "&Width=230&Height=205&Crop=0&Compression=100";
369 string productImageDefault = "/admin/public/getimage.ashx?Image=" + HttpUtility.UrlEncode(productImage) + "&Width=340&Height=304&Crop=0&Compression=100";
370 bool isFavorite = favoriteList != null && service.GetFavoriteProducts(favoriteList.ListId).Any(x => x.ProductId == productNumber);
371 switch (productLayout)
372 {
373 case "group1":
374 if (isBundleOnly)
375 {
376 primaryPrice = "";
377 }
378
379 secondaryPrice = product.GetString("Ecom:Product:Field.ProductPriceBundle.Value").Replace(",", "-").Replace(".", ",").Replace("-", ".");
380
381 primaryPriceDescription = Translate("Translate_ProductDecription_Hardwood_Anbrud");
382 secondaryPriceDescription = Translate("Translate_ProductDecription_Hardwood_Bundt");
383
384 if (secondaryPrice.Contains(","))
385 {
386 string[] sp = secondaryPrice.Split(',');
387
388 if (sp[1].Length == 1)
389 {
390 secondaryPrice += "0";
391 }
392 }
393 else
394 {
395 secondaryPrice += ",00";
396 }
397
398 break;
399
400 case "group32":
401 secondaryPrice = product.GetString("Ecom:Product:Field.ProductPriceAbove100SQM");
402
403 primaryPriceDescription = Translate("Translate_ProductDecription_Terrase_Anbrud");
404 secondaryPriceDescription = Translate("Translate_ProductDecription_Terrase_Above");
405
406 usePriceExplanation = true;
407
408 if (secondaryPrice.Contains(","))
409 {
410 string[] sp = secondaryPrice.Split(',');
411
412 if (sp[1].Length == 1)
413 {
414 secondaryPrice += "0";
415 }
416 }
417 else
418 {
419 secondaryPrice += ",00";
420 }
421
422 break;
423
424 case "group73":
425 secondaryPrice = product.GetString("Ecom:Product:Field.ProductPriceAbove100SQM");
426 tertiaryPrice = product.GetString("Ecom:Product:Field.ProductPriceAbove3000M.Value").Replace(".", ",");
427
428 primaryPriceDescription = Translate("Translate_ProductDecription_Facade_Under1000");
429 secondaryPriceDescription = Translate("Translate_ProductDecription_Facade_Between1000and3000");
430 tertiaryPriceDescription = Translate("Translate_ProductDecription_Facade_Above3000");
431
432 usePriceExplanation = true;
433
434 if (secondaryPrice.Contains(","))
435 {
436 string[] sp = secondaryPrice.Split(',');
437
438 if (sp[1].Length == 1)
439 {
440 secondaryPrice += "0";
441 }
442 }
443 else
444 {
445 secondaryPrice += ",00";
446 }
447
448 if (tertiaryPrice.Contains(","))
449 {
450 string[] tp = tertiaryPrice.Split(',');
451
452 if (tp[1].Length == 1)
453 {
454 tertiaryPrice += "0";
455 }
456 }
457 else
458 {
459 tertiaryPrice += ",00";
460 }
461
462 break;
463
464 case "group52":
465 case "group66":
466 secondaryPrice = product.GetString("Ecom:Product:Field.ProductPriceHalfParcel").Replace(",", "-").Replace(".", ",").Replace("-", ".");
467 tertiaryPrice = product.GetString("Ecom:Product:Field.ProductPriceCompleteParcel").Replace(",", "-").Replace(".", ",").Replace("-", ".");
468
469 primaryPriceDescription = Translate("Translate_ProductDecription_Plader_Anbrud");
470 secondaryPriceDescription = Translate("Translate_ProductDecription_Plader_Half");
471 tertiaryPriceDescription = Translate("Translate_ProductDecription_Plader_Full");
472
473 if (secondaryPrice.Contains(","))
474 {
475 string[] sp = secondaryPrice.Split(',');
476
477 if (sp[1].Length == 1)
478 {
479 secondaryPrice += "0";
480 }
481 }
482 else
483 {
484 secondaryPrice += ",00";
485 }
486
487 if (tertiaryPrice.Contains(","))
488 {
489 string[] tp = tertiaryPrice.Split(',');
490
491 if (tp[1].Length == 1)
492 {
493 tertiaryPrice += "0";
494 }
495 }
496 else
497 {
498 tertiaryPrice += ",00";
499 }
500
501 break;
502
503 case "group126":
504 primaryPriceDescription = Translate("Translate_ProductDecription_Accessories");
505 break;
506
507 default:
508 primaryPriceDescription = Translate("Translate_ProductDecription_General_Description");
509 break;
510 }
511
512 <li class="card-list__card product-card">
513
514 <a href="@link" class="card__wrapper">
515
516 @switch (labelCode.ToLower())
517 {
518 case "prøver":
519 <div class="card-list__label-code label-code label-code--samples">@Translate("LabelCode_" + labelCode.ToLower().Replace("ø", "oe"))</div>
520 break;
521 case "skaffe":
522 case "relatordre":
523 <div class="card-list__label-code label-code">@Translate("LabelCode_" + labelCode.ToLower())</div>
524 break;
525 default:
526 break;
527 }
528
529 @if (!String.IsNullOrWhiteSpace(productImage))
530 {
531 <picture class="card__picture">
532 <source srcset="@productImageLg" media="(min-width: 1536px)">
533 <source srcset="@productImageMd" media="(min-width: 992px)">
534 <source srcset="@productImageSm" media="(min-width: 768px)">
535 <img src="@productImageDefault" alt="@name.Replace("\"", """)">
536 @if (isloggedin)
537 {
538 <button id="favoriteAdd-@productNumber" style="@(isFavorite ? "display:none" : "")" class="card-favorite" @@click.prevent="favoriteAdd('@productNumber')">
539 <svg id="Lag_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
540 <!-- Generator: Adobe Illustrator 29.5.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 137) -->
541 <path d="M50,92.9c-.2,0-.5,0-.6-.3L10,53.2c-4.9-4.9-7.5-11.3-7.5-18.2s2.7-13.4,7.5-18.2c4.9-4.9,11.3-7.5,18.2-7.5s13.4,2.7,18.2,7.5l3.5,3.5,3.5-3.5c4.9-4.9,11.3-7.5,18.2-7.5s13.4,2.7,18.2,7.5c4.9,4.9,7.5,11.3,7.5,18.2s-2.7,13.4-7.5,18.2l-39.4,39.4c-.2.2-.4.3-.6.3ZM50.1,90.8l38.8-38.8c4.5-4.5,7-10.6,7-17s-2.5-12.5-7-17c-4.5-4.5-10.6-7-17-7s-12.5,2.5-17,7l-4.2,4.2c-.2.2-.4.3-.6.3s-.5,0-.6-.3l-4.2-4.2c-4.5-4.5-10.6-7-17-7s-12.5,2.5-17,7c-4.5,4.5-7,10.6-7,17s2.5,12.5,7,17l.4.4,38.4,38.4Z"/>
542 </svg>
543
544 </button>
545 <button id="favoriteRemove-@productNumber" style="@(isFavorite ? "" : "display:none")" class="card-favorite card-favorite--filled" @@click.prevent="favoriteRemove('@productNumber')">
546 <svg id="Lag_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
547 <!-- Generator: Adobe Illustrator 29.5.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 137) -->
548 <path d="M50,92.9c-.2,0-.5,0-.6-.3L10,53.2c-4.9-4.9-7.5-11.3-7.5-18.2s2.7-13.4,7.5-18.2c4.9-4.9,11.3-7.5,18.2-7.5s13.4,2.7,18.2,7.5l3.5,3.5,3.5-3.5c4.9-4.9,11.3-7.5,18.2-7.5s13.4,2.7,18.2,7.5c4.9,4.9,7.5,11.3,7.5,18.2s-2.7,13.4-7.5,18.2l-39.4,39.4c-.2.2-.4.3-.6.3Z"/>
549 </svg>
550 </button>
551 }
552
553 </picture>
554 }
555 else
556 {
557 <div class="card__picture card__picture--dummie">
558 @if (isloggedin)
559 {
560 <button id="favoriteAdd-@productNumber" style="@(isFavorite ? "display:none" : "")" class="card-favorite" @@click.prevent="favoriteAdd('@productNumber')">
561 <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.57" style="enable-background:new 0 0 122.88 109.57" xml:space="preserve">
562 <g fill="gray">
563 <path d="M65.46,19.57c-0.68,0.72-1.36,1.45-2.2,2.32l-2.31,2.41l-2.4-2.33c-0.71-0.69-1.43-1.4-2.13-2.09 c-7.42-7.3-13.01-12.8-24.52-12.95c-0.45-0.01-0.93,0-1.43,0.02c-6.44,0.23-12.38,2.6-16.72,6.65c-4.28,4-7.01,9.67-7.1,16.57 c-0.01,0.43,0,0.88,0.02,1.37c0.69,19.27,19.13,36.08,34.42,50.01c2.95,2.69,5.78,5.27,8.49,7.88l11.26,10.85l14.15-14.04 c2.28-2.26,4.86-4.73,7.62-7.37c4.69-4.5,9.91-9.49,14.77-14.52c3.49-3.61,6.8-7.24,9.61-10.73c2.76-3.42,5.02-6.67,6.47-9.57 c2.38-4.76,3.13-9.52,2.62-13.97c-0.5-4.39-2.23-8.49-4.82-11.99c-2.63-3.55-6.13-6.49-10.14-8.5C96.5,7.29,91.21,6.2,85.8,6.82 C76.47,7.9,71.5,13.17,65.46,19.57L65.46,19.57z M60.77,14.85C67.67,7.54,73.4,1.55,85.04,0.22c6.72-0.77,13.3,0.57,19.03,3.45 c4.95,2.48,9.27,6.1,12.51,10.47c3.27,4.42,5.46,9.61,6.1,15.19c0.65,5.66-0.29,11.69-3.3,17.69c-1.7,3.39-4.22,7.03-7.23,10.76 c-2.95,3.66-6.39,7.44-10,11.17C97.2,74.08,91.94,79.12,87.2,83.66c-2.77,2.65-5.36,5.13-7.54,7.29L63.2,107.28l-2.31,2.29 l-2.34-2.25l-13.6-13.1c-2.49-2.39-5.37-5.02-8.36-7.75C20.38,71.68,0.81,53.85,0.02,31.77C0,31.23,0,30.67,0,30.09 c0.12-8.86,3.66-16.18,9.21-21.36c5.5-5.13,12.97-8.13,21.01-8.42c0.55-0.02,1.13-0.03,1.74-0.02C46,0.48,52.42,6.63,60.77,14.85 L60.77,14.85z" />
564 </g>
565 </svg>
566 </button>
567 <button id="favoriteRemove-@productNumber" style="@(isFavorite ? "" : "display:none")" class="card-favorite card-favorite--filled" @@click.prevent="favoriteRemove('@productNumber')">
568 <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 107.39">
569 <defs>
570
571 </defs>
572 <title>red-heart</title>
573 <path class="cls-1" d="M60.83,17.18c8-8.35,13.62-15.57,26-17C110-2.46,131.27,21.26,119.57,44.61c-3.33,6.65-10.11,14.56-17.61,22.32-8.23,8.52-17.34,16.87-23.72,23.2l-17.4,17.26L46.46,93.55C29.16,76.89,1,55.92,0,29.94-.63,11.74,13.73.08,30.25.29c14.76.2,21,7.54,30.58,16.89Z" />
574 </svg>
575 </button>
576 }
577
578
579 </div>
580 }
581
582 <div class="card__info-list__item">
583 <span class="info info--small info--header">@Translate("Translate_Product_Page_ProductNumber"): @productNumber</span>
584 @if (!string.IsNullOrWhiteSpace(dbNumber))
585 {
586 <span class="info info--small info--header">@dbNumberLabel: @dbNumber</span>
587 }
588 </div>
589 <div class="card__info-list__item ">
590 <span class="info info--title">@name</span>
591 </div>
592 <div class="card__info-list__item">
593 <span class="info info--small product-teaser">@teaser</span>
594 </div>
595 @if (isloggedin && ((!String.IsNullOrWhiteSpace(primaryPrice) && primaryPrice != "0,00" && primaryPrice != ",00") || (!String.IsNullOrWhiteSpace(secondaryPrice) && secondaryPrice != "0,00" && secondaryPrice != ",00") || (!String.IsNullOrWhiteSpace(tertiaryPrice) && tertiaryPrice != "0,00" && tertiaryPrice != ",00")))
596 {
597 <div class="card__info-list__item column">
598 @if (usePriceExplanation)
599 {
600 <div class="price-line price-line--explanation">
601 @Translate("Translate_PriceExplanation_" + primaryGroup)
602 </div>
603 }
604 @if (!String.IsNullOrWhiteSpace(primaryPrice) && primaryPrice != "0,00" && primaryPrice != ",00")
605 {
606 <div class="price-line">
607 <div class="definition">@primaryPriceDescription</div>
608 <div class="price">@primaryPrice <text> </text> @priceCurrencySymbol <text>pr.</text> @salesUnit</div>
609 </div>
610 }
611
612 @if (!String.IsNullOrWhiteSpace(secondaryPrice) && secondaryPrice != "0,00" && secondaryPrice != ",00")
613 {
614 <div class="price-line">
615 <div class="definition">@secondaryPriceDescription</div>
616 <div class="price">@secondaryPrice <text> </text> @priceCurrencySymbol <text>pr.</text> @salesUnit</div>
617 </div>
618 }
619
620 @if (!String.IsNullOrWhiteSpace(tertiaryPrice) && tertiaryPrice != "0,00" && tertiaryPrice != ",00")
621 {
622 <div class="price-line">
623 <div class="definition">@tertiaryPriceDescription</div>
624 <div class="price">@tertiaryPrice <text> </text> @priceCurrencySymbol <text>pr.</text> @salesUnit</div>
625 </div>
626 }
627 </div>
628 }
629 @*@if (isSingleVariant)
630 {
631 <div class="card__info-list__item">
632 <span class="info info--small" style="font-size: 15px;margin-top:15px;">
633 @if (totalStockWareHouse > 0)
634 {
635 <text>
636 <svg width="30px" height="30px" viewBox="0 -0.5 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#12ca30">
637 <g id="SVGRepo_bgCarrier" stroke-width="0" />
638 <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
639 <g id="SVGRepo_iconCarrier"> <path d="M4 12.6111L8.92308 17.5L20 6.5" stroke="#24a84b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </g>
640 </svg>
641 @Translate("Translate_ProductPage_StockOnWarehouse_OnStock")
642 </text>
643 }
644 else
645 {
646 <text>
647 <svg width="30px" height="30px" viewBox="0 -0.5 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
648 <g id="SVGRepo_bgCarrier" stroke-width="0" />
649 <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
650 <g id="SVGRepo_iconCarrier"> <path d="M6.96967 16.4697C6.67678 16.7626 6.67678 17.2374 6.96967 17.5303C7.26256 17.8232 7.73744 17.8232 8.03033 17.5303L6.96967 16.4697ZM13.0303 12.5303C13.3232 12.2374 13.3232 11.7626 13.0303 11.4697C12.7374 11.1768 12.2626 11.1768 11.9697 11.4697L13.0303 12.5303ZM11.9697 11.4697C11.6768 11.7626 11.6768 12.2374 11.9697 12.5303C12.2626 12.8232 12.7374 12.8232 13.0303 12.5303L11.9697 11.4697ZM18.0303 7.53033C18.3232 7.23744 18.3232 6.76256 18.0303 6.46967C17.7374 6.17678 17.2626 6.17678 16.9697 6.46967L18.0303 7.53033ZM13.0303 11.4697C12.7374 11.1768 12.2626 11.1768 11.9697 11.4697C11.6768 11.7626 11.6768 12.2374 11.9697 12.5303L13.0303 11.4697ZM16.9697 17.5303C17.2626 17.8232 17.7374 17.8232 18.0303 17.5303C18.3232 17.2374 18.3232 16.7626 18.0303 16.4697L16.9697 17.5303ZM11.9697 12.5303C12.2626 12.8232 12.7374 12.8232 13.0303 12.5303C13.3232 12.2374 13.3232 11.7626 13.0303 11.4697L11.9697 12.5303ZM8.03033 6.46967C7.73744 6.17678 7.26256 6.17678 6.96967 6.46967C6.67678 6.76256 6.67678 7.23744 6.96967 7.53033L8.03033 6.46967ZM8.03033 17.5303L13.0303 12.5303L11.9697 11.4697L6.96967 16.4697L8.03033 17.5303ZM13.0303 12.5303L18.0303 7.53033L16.9697 6.46967L11.9697 11.4697L13.0303 12.5303ZM11.9697 12.5303L16.9697 17.5303L18.0303 16.4697L13.0303 11.4697L11.9697 12.5303ZM13.0303 11.4697L8.03033 6.46967L6.96967 7.53033L11.9697 12.5303L13.0303 11.4697Z" fill="#c81919" /> </g>
651 </svg>
652 @Translate("Translate_ProductPage_StockOnWarehouse_NotOnStock")
653 </text>
654 }
655
656 @if (totalStockSea > 0)
657 {
658 <text>
659 <svg width="30px" height="30px" viewBox="0 -0.5 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#12ca30">
660 <g id="SVGRepo_bgCarrier" stroke-width="0" />
661 <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
662 <g id="SVGRepo_iconCarrier"> <path d="M4 12.6111L8.92308 17.5L20 6.5" stroke="#24a84b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> </g>
663 </svg>
664 @Translate("Translate_ProductPage_StockOnSea_InRoute")
665 </text>
666 }
667 else
668 {
669 <text>
670 <svg width="30px" height="30px" viewBox="0 -0.5 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
671 <g id="SVGRepo_bgCarrier" stroke-width="0" />
672 <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
673 <g id="SVGRepo_iconCarrier"> <path d="M6.96967 16.4697C6.67678 16.7626 6.67678 17.2374 6.96967 17.5303C7.26256 17.8232 7.73744 17.8232 8.03033 17.5303L6.96967 16.4697ZM13.0303 12.5303C13.3232 12.2374 13.3232 11.7626 13.0303 11.4697C12.7374 11.1768 12.2626 11.1768 11.9697 11.4697L13.0303 12.5303ZM11.9697 11.4697C11.6768 11.7626 11.6768 12.2374 11.9697 12.5303C12.2626 12.8232 12.7374 12.8232 13.0303 12.5303L11.9697 11.4697ZM18.0303 7.53033C18.3232 7.23744 18.3232 6.76256 18.0303 6.46967C17.7374 6.17678 17.2626 6.17678 16.9697 6.46967L18.0303 7.53033ZM13.0303 11.4697C12.7374 11.1768 12.2626 11.1768 11.9697 11.4697C11.6768 11.7626 11.6768 12.2374 11.9697 12.5303L13.0303 11.4697ZM16.9697 17.5303C17.2626 17.8232 17.7374 17.8232 18.0303 17.5303C18.3232 17.2374 18.3232 16.7626 18.0303 16.4697L16.9697 17.5303ZM11.9697 12.5303C12.2626 12.8232 12.7374 12.8232 13.0303 12.5303C13.3232 12.2374 13.3232 11.7626 13.0303 11.4697L11.9697 12.5303ZM8.03033 6.46967C7.73744 6.17678 7.26256 6.17678 6.96967 6.46967C6.67678 6.76256 6.67678 7.23744 6.96967 7.53033L8.03033 6.46967ZM8.03033 17.5303L13.0303 12.5303L11.9697 11.4697L6.96967 16.4697L8.03033 17.5303ZM13.0303 12.5303L18.0303 7.53033L16.9697 6.46967L11.9697 11.4697L13.0303 12.5303ZM11.9697 12.5303L16.9697 17.5303L18.0303 16.4697L13.0303 11.4697L11.9697 12.5303ZM13.0303 11.4697L8.03033 6.46967L6.96967 7.53033L11.9697 12.5303L13.0303 11.4697Z" fill="#c81919" /> </g>
674 </svg>
675 @Translate("Translate_ProductPage_StockOnSea_NotInRoute")
676 </text>
677 }
678
679 </span>
680 </div>
681 }
682 else
683 {*@
684 <div class="card__info-list__item">
685 <span class="info info--small info--flex-wrap">
686 <div style="display:none;" id="RequestProductText_@(product.GetString("Ecom:Product.ID"))">
687 <div style="display: inline-flex; align-items: center;">
688 <span style="width: 8px; height: 8px; background-color: orange; border-radius: 50%; margin-right: 8px;"></span>
689 @Translate("Translate_ProductPage_RequestProduct")
690 </div>
691 </div>
692 <div id="StockOnWarehouse_OnStock_@(product.GetString("Ecom:Product.ID"))" class="info--stock" style="display:none;" >
693 <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#24a84b" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>
694 <span class="info--stock--text">@Translate("Translate_ProductPage_StockOnWarehouse_OnStock")</span>
695 </div>
696 <div id="StockOnWarehouse_NotOnStock_@(product.GetString("Ecom:Product.ID"))" class="info--stock" style="display:none;">
697 <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#c81919" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"/></svg>
698 <span class="info--stock--text">@Translate("Translate_ProductPage_StockOnWarehouse_NotOnStock")</span>
699 </div>
700 <div id="StockOnSea_InRoute_@(product.GetString("Ecom:Product.ID"))" class="info--stock" style="display:none;">
701 <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#24a84b" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>
702 <span class="info--stock--text">@Translate("Translate_ProductPage_StockOnSea_InRoute")</span>
703 </div>
704 <div id="StockOnSea_NotInRoute_@(product.GetString("Ecom:Product.ID"))" class="info--stock" style="display:none;">
705 <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#c81919" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"/></svg>
706 <span class="info--stock--text">@Translate("Translate_ProductPage_StockOnSea_NotInRoute")</span>
707 </div>
708 <div class="info--stock skeleton_@(product.GetString("Ecom:Product.ID"))" v-if="isLoading">
709 <div class="info--stock--skeleton--icon"></div>
710 <div class="info--stock--skeleton--text"></div>
711 </div>
712 <div class="info--stock skeleton_@(product.GetString("Ecom:Product.ID"))" v-if="isLoading">
713 <div class="info--stock--skeleton--icon"></div>
714 <div class="info--stock--skeleton--text"></div>
715 </div>
716 </span>
717 </div>
718 @*}*@
719
720 </a>
721 </li>
722 }
723 </ul>
724 @if (GetInteger("Ecom:ProductList.TotalPages") > 1)
725 {
726 string prevClass = "";
727 string nextClass = "";
728 string extraPaginationClass = "";
729
730 if (String.IsNullOrWhiteSpace(GetString("Ecom:ProductList.PrevPage.Clean")))
731 {
732 prevClass = " disabled";
733 }
734
735 if (String.IsNullOrWhiteSpace(GetString("Ecom:ProductList.NextPage.Clean")))
736 {
737 nextClass = " disabled";
738 }
739
740 if (GetInteger("Ecom:ProductList.TotalPages") > 5)
741 {
742 extraPaginationClass = "navigation__pagination--overload";
743 }
744
745 <div class="card-list__footer">
746 <div class="card-list__navigation">
747 <a href="@GetString("Ecom:ProductList.PrevPage.Clean")" class="navigation__arrow back@(prevClass)">
748 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 6.503"><path d="M10.94,6.5,14,3.249,10.94,0H9.693L12.4,2.8H0v.9H12.4L9.693,6.5Z" /></svg>
749 </a>
750
751 <div class="navigation__pagination @extraPaginationClass">
752 <span class="navigation__pagination__title"> @Translate("Translate_ProductList_BreadCrumbPage") </span>
753 @for (int i = 0; i < GetInteger("Ecom:ProductList.TotalPages"); i++)
754 {
755 string currentClass = (i + 1) == GetInteger("Ecom:ProductList.CurrentPage") ? " active" : "";
756 string url = GetString("Ecom:Group.Link.Clean") + "&PageNum=" + (i + 1);
757
758 foreach (var query in GetLoop("Query.Parameters"))
759 {
760 if (!String.IsNullOrWhiteSpace(query.GetString("Parameter.Value")))
761 {
762 url += "&" + query.GetString("Parameter.Name") + "=" + query.GetString("Parameter.Value");
763 }
764 }
765
766 if (!isStandardSorting)
767 {
768 url += "&Sortby=NameForSort" + "&SortOrder=" + sortOrder;
769 }
770
771 <a href="@url" class="navigation__pagination__link@(currentClass)">@(i + 1)</a>
772 }
773 </div>
774 <a href="@GetString("Ecom:ProductList.NextPage.Clean")" class="navigation__arrow forward@(nextClass)">
775 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 6.503"><path d="M10.94,6.5,14,3.249,10.94,0H9.693L12.4,2.8H0v.9H12.4L9.693,6.5Z" /></svg>
776 </a>
777 </div>
778 </div>
779 }
780 </article>
781
782 </section>
783 <script async type="module">
784 let activeFilters = {};
785 let delay;
786
787 new Vue({
788 el: '#productsFlowApp',
789 name: 'Products Flow',
790 components: {
791
792 },
793 computed: {
794
795 },
796 mounted() {
797 var allProductNumbers = @allProductNumbersString;
798 const fetchPromises = [];
799
800 for (const productNumberObjects of allProductNumbers) {
801 fetchPromises.push(this.getVariants(productNumberObjects.id));
802 }
803
804 // Wait for all fetch operations to complete
805 Promise.all(fetchPromises)
806 .then(() => {
807 // This block will run after all variants have been successfully fetched
808 this.isLoading = false;
809 this.afterFetchingVariants();
810 this.isLoading = false;
811 });
812
813 @*@if(primaryGroup == "group1") {
814 <text>
815 this.lookUpBundle(@(productNumber));
816 </text>
817 }
818
819 this.getAccessories();
820
821 if(!this.enquireProduct && this.isLoggedIn) {
822 this.orderButtonSpinner = setTimeout(() => {
823 document.querySelector('#product-order-button .loader').style.display = "inline-block";
824 }, 3000);
825 } else {
826 document.querySelector('#product-order-button .product-order-form-button').style.display = "block";
827 }*@
828 },
829 data() {
830 return {
831 isLoggedIn: @isloggedin.ToString().ToLower(),
832 variants: [],
833 isLoading: true,
834 ongoingOperations: {},
835
836 showFilters: false,
837 facets: null,
838 translations: null,
839 allProducts: false,
840 currentGroup: '',
841 }
842 },
843 created() {
844 this.allProducts = (document.getElementById('productFilters').getAttribute('data-all')
845 .toLowerCase() === 'true');
846
847 if (!this.allProducts) {
848 this.currentGroup = `&Group=${document.getElementById('productFilters').getAttribute('data-top-group')}&GroupID=${document.getElementById('productFilters').getAttribute('data-group')}`;
849 } else {
850 const urlSearchParams = new URLSearchParams(window.location.search);
851 const params = Object.fromEntries(urlSearchParams.entries());
852
853 let group = '';
854
855 if (params.Group) {
856 group = `&Group=${params.Group}`;
857 }
858
859 this.currentGroup = group;
860 }
861
862 fetch('/dwapi/translations/area/1')
863 .then(response => response.json())
864 .then(response => {
865 this.translations = response;
866 })
867 .catch(error => {
868 console.log(error);
869 });
870
871 fetch(`/dwapi/ecommerce/products/search?RepositoryName=Products&QueryName=FilterQuery&ProductSettings.FilledProperties=Name&FilledProperties=Products,FacetGroups,TotalProductsCount${this.currentGroup}`)
872 .then(response => response.json())
873 .then(response => {
874 this.facets = response.FacetGroups[0].Facets;
875 this.showFilters = true;
876
877 this.$nextTick(() => {
878 setUpFilters();
879 setupSearchSelects();
880 setupDuoRangeSliders();
881 });
882 });
883 },
884 methods: {
885 addition(paramType) {
886 let addition = '';
887
888 if (paramType == 'System.String[]' || paramType == 'System.Double[]') {
889 addition = '[]';
890 }
891
892 return addition;
893 },
894 translation(key) {
895 return this.translations.find(x => x.Key == key) ? this.translations.find(x => x.Key == key).Value : key;
896 },
897 sortedOptions(options) {
898 const newList = options.filter(x => x.Count && x.Count > 0 && parseInt(x.Value) != -1);
899 return newList;
900 },
901 getLowestValue(options) {
902 const newList = options.filter(x => x.Count && x.Count > 0);
903 newList.sort((a, b) => a - b);
904
905 return newList[0].Value;
906
907 },
908 getHighestValue(options) {
909 const newList = options.filter(x => x.Count && x.Count > 0);
910 newList.sort((a, b) => a - b);
911
912 return newList.at(-1).Value;
913 },
914 favoriteRemove(productNumber) {
915 if (this.ongoingOperations[productNumber]) {
916 return;
917 }
918 this.ongoingOperations[productNumber] = true;
919 let url = location.protocol + '//' + location.host; //RemoveProductFromFavoriteList
920 url += "?FavoriteCmd=RemoveProductFromFavoriteList&ProductId=" + productNumber;
921 fetch(url).then(response => {
922 document.getElementById("favoriteRemove-" + productNumber).style = "display:none"
923 document.getElementById("favoriteAdd-" + productNumber).style = "";
924
925 let favorite = document.getElementsByClassName("favorite-qty");
926 if (favorite.length == 1) {
927 let qty = favorite[0];
928 let currentCount = Number(qty.getAttribute("data-count"));
929 let count = currentCount === 0 ? currentCount : currentCount - 1;
930 qty.setAttribute("data-count", count)
931 qty.innerHTML = count;
932
933 }
934 }).finally(() => {
935 this.ongoingOperations[productNumber] = false; // Reset the flag when the operation is complete
936 });
937 },
938 favoriteAdd(productNumber) {
939 if (this.ongoingOperations[productNumber]) {
940 return;
941 }
942 this.ongoingOperations[productNumber] = true;
943 let url = location.protocol + '//' + location.host;
944 url += "?FavoriteCmd=addproducttofavoritelist&ProductId=" + productNumber;
945 let loc = location;
946 fetch(url).then(response => {
947 if ('@shouldRefreshFavorite' === 'true') {
948 loc.reload()
949 } else {
950 document.getElementById("favoriteAdd-" + productNumber).style = "display:none"
951 document.getElementById("favoriteRemove-" + productNumber).style = "";
952 //simple just assume it went ok
953 let favorite = document.getElementsByClassName("favorite-qty");
954 if (favorite.length == 1) {
955 let qty = favorite[0];
956 let count = Number(qty.getAttribute("data-count")) + 1;
957 qty.setAttribute("data-count", count)
958 qty.innerHTML = count;
959
960 }
961 }
962 }).finally(() => {
963 this.ongoingOperations[productNumber] = false; // Reset the flag when the operation is complete
964 });
965 },
966 doesCookieExist(cookieName) {
967 const cookies = document.cookie.split('; ');
968 const cookieExists = cookies.some(cookie => cookie.startsWith(cookieName + '='));
969 return cookieExists;
970 },
971 setCookie(name, value, days) {
972 let expires = "";
973 if (days) {
974 const date = new Date();
975 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
976 expires = "; expires=" + date.toUTCString();
977 }
978 document.cookie = name + "=" + (value || "") + expires + "; path=/";
979 },
980 deleteCookie(name) {
981 document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
982 },
983 getVariants: async function(productNo, url) {
984 this.loadingVariants = true;
985 return new Promise((resolve, reject) => {
986 let requestUrl = `@(VariantsLookup)&ICC_itemId=${productNo}`;
987
988 if (url && url != "") {
989 requestUrl = url;
990 }
991
992 if (!this.isLoggedIn) {
993 if (requestUrl.indexOf('username') < 0) {
994 requestUrl += '&username=marketing@keflico.com&password=Keflico100%';
995 }
996 if (!this.doesCookieExist('tempLogin')) {
997 this.setCookie('tempLogin', true, 1);
998 }
999 }
1000
1001 let credentials = this.isLoggedIn ? "same-origin" : "omit"
1002
1003 fetch(requestUrl, {
1004 credentials: credentials
1005 })
1006 .then(response => response.json())
1007 .then(response => {
1008 if (response.variants && response.variants.length > 0) {
1009 this.variants = this.variants.concat(response.variants);
1010
1011 if (response.nextPage != "") {
1012 this.getVariants(productNo, response.nextPage).then(resolve);
1013 } else {
1014 resolve(); // Resolve the promise when the last page is fetched
1015 }
1016 } else {
1017 // There is not variants, so we just show it's a request product.
1018 Array.from(document.getElementsByClassName(`skeleton_${productNo}`) || []).forEach(el => el && (el.style.display = "none"));
1019 document.getElementById(`RequestProductText_${productNo}`).style.display = "";
1020 resolve(); // Resolve if there are no variants to fetch
1021 }
1022 })
1023 .catch(error => {
1024 console.error("Error fetching variants:", error);
1025 reject(error); // Reject the promise if there's an error
1026 });
1027 });
1028 },
1029 afterFetchingVariants() {
1030 var allProductNumbers = @allProductNumbersString;
1031 // Code to execute after all variants have been fetched
1032 //console.debug("All variants have been fetched!");
1033
1034 // Example: Group by id prefix or perform other operations
1035 const groupedStock = {};
1036 this.variants.forEach(variant => {
1037 const idPrefix = variant.id.split('_')[0];
1038 if (!groupedStock[idPrefix]) {
1039 groupedStock[idPrefix] = [];
1040 }
1041 groupedStock[idPrefix].push({
1042 warehouse: variant.stock.warehouse,
1043 sea: variant.stock.sea,
1044 purchase: variant.stock.purchase
1045 });
1046 });
1047
1048 for (const idPrefix in groupedStock) {
1049 if (groupedStock.hasOwnProperty(idPrefix)) {
1050 //console.log("ID Prefix:", idPrefix);
1051 var totalWarehouse = 0;
1052 var totalSea = 0;
1053
1054 // Access the array of stock objects for this group
1055 const stocks = groupedStock[idPrefix];
1056
1057 const productObject = allProductNumbers.find(product => product.id === idPrefix);
1058 const group = productObject.primaryGroup;
1059
1060 // Loop through the array of stock entries for this idPrefix
1061 stocks.forEach(stock => {
1062 //console.log("Warehouse:", stock.warehouse, "Sea:", stock.sea);
1063 totalSea += stock.sea
1064 totalWarehouse += stock.warehouse
1065
1066 if (group != "group32") {
1067 totalSea += Number(stock.purchase)
1068 totalWarehouse += Number(stock.purchase)
1069 }
1070
1071 });
1072
1073 if (totalSea == 0 && totalWarehouse == 0) {
1074 document.getElementById(`RequestProductText_${idPrefix}`).style.display = "";
1075 }
1076 else {
1077
1078 if (totalWarehouse > 0) {
1079 if (document.getElementById(`StockOnWarehouse_OnStock_${idPrefix}`) != null)
1080 document.getElementById(`StockOnWarehouse_OnStock_${idPrefix}`).style.display = "";
1081
1082 } else {
1083 if (document.getElementById(`StockOnWarehouse_NotOnStock_${idPrefix}`) != null)
1084 document.getElementById(`StockOnWarehouse_NotOnStock_${idPrefix}`).style.display = "";
1085
1086
1087 }
1088
1089 if (totalSea > 0) {
1090 if (document.getElementById(`StockOnSea_InRoute_${idPrefix}`) != null)
1091 document.getElementById(`StockOnSea_InRoute_${idPrefix}`).style.display = "";
1092
1093
1094 } else {
1095 if (document.getElementById(`StockOnSea_NotInRoute_${idPrefix}`) != null)
1096 document.getElementById(`StockOnSea_NotInRoute_${idPrefix}`).style.display = "";
1097
1098 }
1099 }
1100
1101
1102 }
1103 }
1104 }
1105 },
1106 watch: {
1107
1108 }
1109 });
1110
1111
1112 function setUpFilters() {
1113 const filterSection = document.querySelector('.filter-section');
1114 const sorting = document.getElementById('sorting');
1115
1116 if (filterSection) {
1117 const applyBtn = document.getElementById('applyFilterButton');
1118 const clearBtn = document.getElementById('clearFilterButton');
1119
1120 const filters = filterSection.querySelectorAll('.filter-option');
1121 setupFromQueryString();
1122
1123 Array.from(filters).forEach(filter => {
1124 const filterName = filter.getAttribute('data-name');
1125
1126 filter.addEventListener('input', event => {
1127 clearTimeout(delay);
1128
1129 switch (filter.type) {
1130 case 'range':
1131 activeFilters[filterName] = filter.value;
1132 break;
1133
1134 case 'checkbox':
1135 default:
1136 if (filter.checked) {
1137 if (activeFilters[filterName]) {
1138 activeFilters[filterName].push(filter.value);
1139 } else {
1140 activeFilters[filterName] = [filter.value];
1141 }
1142 } else {
1143 if (activeFilters[filterName]) {
1144 const valueIndex = activeFilters[filterName].indexOf(filter.value);
1145 activeFilters[filterName].splice(valueIndex, 1);
1146
1147 if (activeFilters[filterName].length == 0) {
1148 delete activeFilters[filterName];
1149 }
1150 }
1151 }
1152 break;
1153 }
1154 });
1155 });
1156
1157 clearBtn.addEventListener('click', event => {
1158 if (activeFilters['Sortby']) {
1159 const tempBy = activeFilters['Sortby'];
1160 const tempOrder = activeFilters['SortOrder'];
1161
1162 activeFilters = {};
1163
1164 activeFilters['Sortby'] = tempBy;
1165 activeFilters['SortOrder'] = tempOrder;
1166 } else {
1167 activeFilters = {};
1168 }
1169
1170 buildQueryString();
1171 });
1172
1173 applyBtn.addEventListener('click', buildQueryString);
1174 }
1175
1176 if (sorting) {
1177 sorting.addEventListener('change', event => {
1178 const sortingValue = sorting.value;
1179
1180 if (sortingValue == 'default') {
1181 delete activeFilters['Sortby'];
1182 delete activeFilters['SortOrder'];
1183 } else {
1184 activeFilters['Sortby'] = 'NameForSort';
1185 activeFilters['SortOrder'] = sortingValue.toUpperCase();
1186 }
1187
1188 buildQueryString();
1189 });
1190 }
1191 }
1192
1193 function buildQueryString() {
1194 // Create a new object to hold the processed filters
1195 const processedFilters = {};
1196
1197 // Process each filter
1198 Object.keys(activeFilters).forEach(key => {
1199 if (activeFilters[key] && activeFilters[key].length > 0) {
1200 // Get the first input with this key to check if it's a numeric filter
1201 const input = document.querySelector(`[data-name="${key}"]`);
1202 const isNumeric = input && input.getAttribute('data-parameter-type') === 'System.Double[]';
1203 processedFilters[key] = activeFilters[key].join(',');
1204 }
1205 });
1206
1207 // Build the query string
1208 const queryString = new URLSearchParams(processedFilters).toString();
1209 const currentPath = window.location.pathname;
1210
1211 if (queryString.length > 0) {
1212 window.location.href = `${currentPath}?${queryString.replace(/PageNum=\d+&/g, '')}`;
1213 } else {
1214 window.location.href = currentPath;
1215 }
1216 }
1217
1218 function setupFromQueryString() {
1219 const urlSearchParams = new URLSearchParams(window.location.search);
1220 const params = Object.fromEntries(urlSearchParams.entries());
1221
1222 Object.keys(params).forEach(key => {
1223 const value = params[key];
1224 if (!activeFilters[key]) {
1225 activeFilters[key] = [];
1226 }
1227
1228 // Get all inputs with this key to check if it's a numeric filter
1229 const inputsWithKey = document.querySelectorAll(`[data-name="${key}"]`);
1230 const isNumeric = inputsWithKey.length > 0 &&
1231 inputsWithKey[0].getAttribute('data-parameter-type') === 'System.Double[]';
1232
1233 // Get all possible numeric values from the filter options
1234 const allNumericOptions = Array.from(inputsWithKey).map(input => input.value);
1235
1236 // For numeric filters, we need special handling
1237 if (isNumeric) {
1238 // First, split the value into parts
1239 const parts = value.split(',');
1240 const processedIndices = new Set(); // Track which parts we've processed
1241
1242 // Check for decimal values first (values that contain a comma in our options)
1243 for (let i = 0; i < parts.length - 1; i++) {
1244 if (processedIndices.has(i)) continue; // Skip if already processed
1245
1246 const potentialDecimal = parts[i] + ',' + parts[i + 1];
1247
1248 // If this combination exists as an option, add it
1249 if (allNumericOptions.includes(potentialDecimal)) {
1250 activeFilters[key].push(potentialDecimal);
1251 processedIndices.add(i);
1252 processedIndices.add(i + 1);
1253 }
1254 }
1255
1256 // Now add any remaining individual values
1257 parts.forEach((part, index) => {
1258 if (!processedIndices.has(index) && allNumericOptions.includes(part)) {
1259 activeFilters[key].push(part);
1260 }
1261 });
1262 } else {
1263 // For non-numeric values, keep the original behavior
1264 if (value.indexOf(',') > -1) {
1265 value.split(',').forEach(entry => {
1266 activeFilters[key].push(entry);
1267 });
1268 } else {
1269 activeFilters[key].push(value);
1270 }
1271 }
1272
1273 // Update the checkboxes based on active filters
1274 if (key != 'GroupID' && key != 'PageNum' && key != 'Sortby' && key != 'SortOrder') {
1275 Array.from(inputsWithKey).forEach(input => {
1276 switch (input.type) {
1277 case 'range':
1278 input.value = value;
1279 break;
1280 case 'checkbox':
1281 default:
1282 // Check if the input value is in the active filters
1283 if (activeFilters[key].includes(input.value)) {
1284 input.checked = true;
1285 const toggle = input.closest('.filter-item')?.querySelector('.filter-item__checkbox-toggle');
1286 if (toggle) toggle.checked = true;
1287 }
1288 break;
1289 }
1290 });
1291 }
1292 });
1293 }
1294
1295 function setupSearchSelects() {
1296 if (document.querySelector('.search-select')) {
1297 // Add CSS for hiding partial matches
1298 const style = document.createElement('style');
1299 style.textContent = `
1300 .search-select__tags label.partial-match {
1301 display: none !important;
1302 }
1303 `;
1304 document.head.appendChild(style);
1305
1306 Array.from(document.querySelectorAll('.search-select')).forEach(select => {
1307 const input = select.querySelector('.search-select__search-input');
1308 const dropdown = select.querySelector('.search-select__dropdown');
1309 const options = select.querySelectorAll('.search-select__tags input');
1310 const optionsLabels = select.querySelectorAll('.search-select__dropdown-item');
1311 const clearBtn = select.querySelector('.search-select__search-clear');
1312
1313 // Determine if this is a numeric filter based only on parameter type
1314 const isNumeric = options.length > 0 &&
1315 options[0].getAttribute('data-parameter-type') == 'System.Double[]';
1316
1317 input.addEventListener('focus', open);
1318 clearBtn.addEventListener('click', clear);
1319 input.addEventListener('input', search);
1320
1321 // Fix the visual representation of selected tags
1322 function updateVisualTags() {
1323 // Only process numeric filters
1324 if (!isNumeric) return;
1325
1326 // Get the actual selected values from the URL
1327 const urlSearchParams = new URLSearchParams(window.location.search);
1328 const params = Object.fromEntries(urlSearchParams.entries());
1329
1330 if (options.length > 0) {
1331 const filterName = options[0].getAttribute('data-name');
1332
1333 // Only process if this filter is in the URL
1334 if (params[filterName]) {
1335 // For numeric filters, don't split the value
1336 const selectedValues = [params[filterName]];
1337
1338 // Remove partial-match class from all labels
1339 select.querySelectorAll('.search-select__tags label').forEach(label => {
1340 label.classList.remove('partial-match');
1341 });
1342
1343 // For each selected value, mark exact matches
1344 selectedValues.forEach(selectedValue => {
1345 Array.from(options).forEach(option => {
1346 // For numeric filters, we want exact matches only
1347 if (option.value === selectedValue) {
1348 const label = select.querySelector(`label[for="${option.id}"]`);
1349 if (label) {
1350 label.classList.add('selected');
1351 }
1352 }
1353 });
1354 });
1355 }
1356 }
1357 }
1358
1359 // Call this when page loads
1360 updateVisualTags();
1361
1362 Array.from(options).forEach(option => {
1363 option.addEventListener('change', e => {
1364 if (isNumeric && option.checked) {
1365 // When a numeric option is checked, hide partial matches
1366 const selectedValue = option.value;
1367 Array.from(options).forEach(otherOption => {
1368 if (otherOption !== option) {
1369 const isPartialMatch = selectedValue.includes(otherOption.value) &&
1370 selectedValue !== otherOption.value;
1371
1372 if (isPartialMatch) {
1373 const label = select.querySelector(`label[for="${otherOption.id}"]`);
1374 if (label) {
1375 label.classList.add('partial-match');
1376 }
1377 }
1378 }
1379 });
1380 }
1381 });
1382 });
1383
1384 function clear() {
1385 dropdown.classList.remove('search-select__dropdown--active');
1386 clearBtn.classList.remove('search-select__search-clear--active');
1387 input.value = '';
1388 resetList();
1389 }
1390
1391 function open() {
1392 dropdown.classList.add('search-select__dropdown--active');
1393 clearBtn.classList.add('search-select__search-clear--active');
1394 }
1395
1396 function search() {
1397 const searchTerm = input.value;
1398
1399 if (searchTerm.length > 0) {
1400 const searchWords = searchTerm.trim().split(' ');
1401
1402 Array.from(optionsLabels).forEach(option => {
1403 let hasMatch = false;
1404
1405 searchWords.forEach(word => {
1406 const searchRegEx = new RegExp(word, 'ig');
1407 const matches = option.innerText.match(searchRegEx);
1408
1409 if (matches && matches.length > 0) {
1410 hasMatch = true;
1411 option.innerHTML = option.innerHTML.replaceAll(/\<span class\=\"js-highlight\"\>(.*?)\<\/span\>/gi, '$1').replaceAll(searchRegEx, `<span class="js-highlight">${matches[0]}</span>`);
1412 }
1413 });
1414
1415 if (!hasMatch) {
1416 option.classList.add('js-no-match');
1417 } else {
1418 option.classList.remove('js-no-match');
1419 }
1420 });
1421 } else {
1422 resetList();
1423 }
1424 }
1425
1426 function resetList() {
1427 Array.from(optionsLabels).forEach(option => {
1428 option.classList.remove('js-no-match');
1429 option.innerHTML = option.innerText;
1430 });
1431 }
1432 });
1433 }
1434 }
1435
1436 function setupDuoRangeSliders() {
1437 if (document.querySelector('.duo-range-slider')) {
1438 Array.from(document.querySelectorAll('.duo-range-slider')).forEach(rangeSlider => {
1439 Array.from(rangeSlider.querySelectorAll('.duo-range-slider__range')).forEach(slider => {
1440 slider.oninput = updateValues;
1441 slider.oninput();
1442 });
1443
1444 function updateValues() {
1445 const parent = this.parentNode;
1446 const slides = parent.getElementsByTagName('input');
1447 let slide1 = parseFloat(slides[0].value);
1448 let slide2 = parseFloat(slides[1].value);
1449 const displayMin = parent.querySelector('.duo-range-slider__values-min span');
1450 const displayMax = parent.querySelector('.duo-range-slider__values-max span');
1451
1452 if (slide1 > slide2) {
1453 const temp = slide2;
1454
1455 slide2 = slide1;
1456 slide1 = temp;
1457 }
1458
1459 displayMin.innerText = slide1;
1460 displayMax.innerText = slide2;
1461 }
1462 });
1463 }
1464 }
1465 </script>
1466 }
1467
1468 @{
1469 string groupSeoText = GetString("Ecom:Group:Field.SEOText");
1470
1471 if (!string.IsNullOrWhiteSpace(groupSeoText))
1472 {
1473 <article class="module module-sand-light">
1474 <div class="rich-text">
1475 @groupSeoText
1476 </div>
1477 </article>
1478 }
1479 }
1480
1481 @functions {
1482 Dynamicweb.Ecommerce.Products.Group FindTopGroup(Dynamicweb.Ecommerce.Products.Group group)
1483 {
1484 if (group.IsTopGroup)
1485 {
1486 return group;
1487 }
1488 else if (group.ParentGroups != null && group.ParentGroups.Count > 0)
1489 {
1490 foreach (var parentGroup in group.ParentGroups)
1491 {
1492 Dynamicweb.Ecommerce.Products.Group topLevelGroup = FindTopGroup(parentGroup);
1493 if (topLevelGroup != null)
1494 {
1495 return topLevelGroup;
1496 }
1497 }
1498 }
1499 return null;
1500 }
1501 }
Template:BaseUrl | System.String | /Files/Templates/Designs/Keflico/QueryPublisher/ |
Template:DesignBaseUrl | System.String | /Files/Templates/Designs/Keflico/ |
Loops | |
Facet Groups
Parameters
QueryResult
Page of
TemplateTags() in code (Designs\Keflico\QueryPublisher/List.cshtml). Remove before going live...