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_e2ee9b1907b8446f8eba8c95d646c442.FindTopGroup(Group group) in D:\dynamicweb.net\Solutions\keflico.live\Files\Templates\Designs\Keflico\eCom\Productlist\products.cshtml:line 1483
at CompiledRazorTemplates.Dynamic.RazorEngine_e2ee9b1907b8446f8eba8c95d646c442.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 });
811
812 @*@if(primaryGroup == "group1") {
813 <text>
814 this.lookUpBundle(@(productNumber));
815 </text>
816 }
817
818 this.getAccessories();
819
820 if(!this.enquireProduct && this.isLoggedIn) {
821 this.orderButtonSpinner = setTimeout(() => {
822 document.querySelector('#product-order-button .loader').style.display = "inline-block";
823 }, 3000);
824 } else {
825 document.querySelector('#product-order-button .product-order-form-button').style.display = "block";
826 }*@
827 },
828 data() {
829 return {
830 isLoggedIn: @isloggedin.ToString().ToLower(),
831 variants: [],
832 isLoading: true,
833 ongoingOperations: {},
834
835 showFilters: false,
836 facets: null,
837 translations: null,
838 allProducts: false,
839 currentGroup: '',
840 }
841 },
842 created() {
843 this.allProducts = (document.getElementById('productFilters').getAttribute('data-all')
844 .toLowerCase() === 'true');
845
846 if (!this.allProducts) {
847 this.currentGroup = `&Group=${document.getElementById('productFilters').getAttribute('data-top-group')}&GroupID=${document.getElementById('productFilters').getAttribute('data-group')}`;
848 } else {
849 const urlSearchParams = new URLSearchParams(window.location.search);
850 const params = Object.fromEntries(urlSearchParams.entries());
851
852 let group = '';
853
854 if (params.Group) {
855 group = `&Group=${params.Group}`;
856 }
857
858 this.currentGroup = group;
859 }
860
861 fetch('/dwapi/translations/area/1')
862 .then(response => response.json())
863 .then(response => {
864 this.translations = response;
865 })
866 .catch(error => {
867 console.log(error);
868 });
869
870 fetch(`/dwapi/ecommerce/products/search?RepositoryName=Products&QueryName=FilterQuery&ProductSettings.FilledProperties=Name&FilledProperties=Products,FacetGroups,TotalProductsCount${this.currentGroup}`)
871 .then(response => response.json())
872 .then(response => {
873 this.facets = response.FacetGroups[0].Facets;
874 this.showFilters = true;
875
876 this.$nextTick(() => {
877 setUpFilters();
878 setupSearchSelects();
879 setupDuoRangeSliders();
880 });
881 });
882 },
883 methods: {
884 addition(paramType) {
885 let addition = '';
886
887 if (paramType == 'System.String[]' || paramType == 'System.Double[]') {
888 addition = '[]';
889 }
890
891 return addition;
892 },
893 translation(key) {
894 return this.translations.find(x => x.Key == key) ? this.translations.find(x => x.Key == key).Value : key;
895 },
896 sortedOptions(options) {
897 const newList = options.filter(x => x.Count && x.Count > 0 && parseInt(x.Value) != -1);
898 return newList;
899 },
900 getLowestValue(options) {
901 const newList = options.filter(x => x.Count && x.Count > 0);
902 newList.sort((a, b) => a - b);
903
904 return newList[0].Value;
905
906 },
907 getHighestValue(options) {
908 const newList = options.filter(x => x.Count && x.Count > 0);
909 newList.sort((a, b) => a - b);
910
911 return newList.at(-1).Value;
912 },
913 favoriteRemove(productNumber) {
914 if (this.ongoingOperations[productNumber]) {
915 return;
916 }
917 this.ongoingOperations[productNumber] = true;
918 let url = location.protocol + '//' + location.host; //RemoveProductFromFavoriteList
919 url += "?FavoriteCmd=RemoveProductFromFavoriteList&ProductId=" + productNumber;
920 fetch(url).then(response => {
921 document.getElementById("favoriteRemove-" + productNumber).style = "display:none"
922 document.getElementById("favoriteAdd-" + productNumber).style = "";
923
924 let favorite = document.getElementsByClassName("favorite-qty");
925 if (favorite.length == 1) {
926 let qty = favorite[0];
927 let currentCount = Number(qty.getAttribute("data-count"));
928 let count = currentCount === 0 ? currentCount : currentCount - 1;
929 qty.setAttribute("data-count", count)
930 qty.innerHTML = count;
931
932 }
933 }).finally(() => {
934 this.ongoingOperations[productNumber] = false; // Reset the flag when the operation is complete
935 });
936 },
937 favoriteAdd(productNumber) {
938 if (this.ongoingOperations[productNumber]) {
939 return;
940 }
941 this.ongoingOperations[productNumber] = true;
942 let url = location.protocol + '//' + location.host;
943 url += "?FavoriteCmd=addproducttofavoritelist&ProductId=" + productNumber;
944 let loc = location;
945 fetch(url).then(response => {
946 if ('@shouldRefreshFavorite' === 'true') {
947 loc.reload()
948 } else {
949 document.getElementById("favoriteAdd-" + productNumber).style = "display:none"
950 document.getElementById("favoriteRemove-" + productNumber).style = "";
951 //simple just assume it went ok
952 let favorite = document.getElementsByClassName("favorite-qty");
953 if (favorite.length == 1) {
954 let qty = favorite[0];
955 let count = Number(qty.getAttribute("data-count")) + 1;
956 qty.setAttribute("data-count", count)
957 qty.innerHTML = count;
958
959 }
960 }
961 }).finally(() => {
962 this.ongoingOperations[productNumber] = false; // Reset the flag when the operation is complete
963 });
964 },
965 doesCookieExist(cookieName) {
966 const cookies = document.cookie.split('; ');
967 const cookieExists = cookies.some(cookie => cookie.startsWith(cookieName + '='));
968 return cookieExists;
969 },
970 setCookie(name, value, days) {
971 let expires = "";
972 if (days) {
973 const date = new Date();
974 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
975 expires = "; expires=" + date.toUTCString();
976 }
977 document.cookie = name + "=" + (value || "") + expires + "; path=/";
978 },
979 deleteCookie(name) {
980 document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
981 },
982 getVariants: async function(productNo, url) {
983 this.loadingVariants = true;
984 return new Promise((resolve, reject) => {
985 let requestUrl = `@(VariantsLookup)&ICC_itemId=${productNo}`;
986
987 if (url && url != "") {
988 requestUrl = url;
989 }
990
991 if (!this.isLoggedIn) {
992 if (requestUrl.indexOf('username') < 0) {
993 requestUrl += '&username=marketing@keflico.com&password=Keflico100%';
994 }
995 if (!this.doesCookieExist('tempLogin')) {
996 this.setCookie('tempLogin', true, 1);
997 }
998 }
999
1000 let credentials = this.isLoggedIn ? "same-origin" : "omit"
1001
1002 fetch(requestUrl, {
1003 credentials: credentials
1004 })
1005 .then(response => response.json())
1006 .then(response => {
1007 if (response.variants && response.variants.length > 0) {
1008 this.variants = this.variants.concat(response.variants);
1009
1010 if (response.nextPage != "") {
1011 this.getVariants(productNo, response.nextPage).then(resolve);
1012 } else {
1013 resolve(); // Resolve the promise when the last page is fetched
1014 }
1015 } else {
1016 // There is not variants, so we just show it's a request product.
1017 Array.from(document.getElementsByClassName(`skeleton_${productNo}`) || []).forEach(el => el && (el.style.display = "none"));
1018 document.getElementById(`RequestProductText_${productNo}`).style.display = "";
1019 resolve(); // Resolve if there are no variants to fetch
1020 }
1021 })
1022 .catch(error => {
1023 console.error("Error fetching variants:", error);
1024 reject(error); // Reject the promise if there's an error
1025 });
1026 });
1027 },
1028 afterFetchingVariants() {
1029 var allProductNumbers = @allProductNumbersString;
1030 // Code to execute after all variants have been fetched
1031 //console.debug("All variants have been fetched!");
1032
1033 // Example: Group by id prefix or perform other operations
1034 const groupedStock = {};
1035 this.variants.forEach(variant => {
1036 const idPrefix = variant.id.split('_')[0];
1037 if (!groupedStock[idPrefix]) {
1038 groupedStock[idPrefix] = [];
1039 }
1040 groupedStock[idPrefix].push({
1041 warehouse: variant.stock.warehouse,
1042 sea: variant.stock.sea,
1043 purchase: variant.stock.purchase
1044 });
1045 });
1046
1047 for (const idPrefix in groupedStock) {
1048 if (groupedStock.hasOwnProperty(idPrefix)) {
1049 //console.log("ID Prefix:", idPrefix);
1050 var totalWarehouse = 0;
1051 var totalSea = 0;
1052
1053 // Access the array of stock objects for this group
1054 const stocks = groupedStock[idPrefix];
1055
1056 const productObject = allProductNumbers.find(product => product.id === idPrefix);
1057 const group = productObject.primaryGroup;
1058
1059 // Loop through the array of stock entries for this idPrefix
1060 stocks.forEach(stock => {
1061 //console.log("Warehouse:", stock.warehouse, "Sea:", stock.sea);
1062 totalSea += stock.sea
1063 totalWarehouse += stock.warehouse
1064
1065 if (group != "group32") {
1066 totalSea += Number(stock.purchase)
1067 totalWarehouse += Number(stock.purchase)
1068 }
1069
1070 });
1071
1072 if (totalSea == 0 && totalWarehouse == 0) {
1073 document.getElementById(`RequestProductText_${idPrefix}`).style.display = "";
1074 }
1075 else {
1076
1077 if (totalWarehouse > 0) {
1078 if (document.getElementById(`StockOnWarehouse_OnStock_${idPrefix}`) != null)
1079 document.getElementById(`StockOnWarehouse_OnStock_${idPrefix}`).style.display = "";
1080
1081 } else {
1082 if (document.getElementById(`StockOnWarehouse_NotOnStock_${idPrefix}`) != null)
1083 document.getElementById(`StockOnWarehouse_NotOnStock_${idPrefix}`).style.display = "";
1084
1085
1086 }
1087
1088 if (totalSea > 0) {
1089 if (document.getElementById(`StockOnSea_InRoute_${idPrefix}`) != null)
1090 document.getElementById(`StockOnSea_InRoute_${idPrefix}`).style.display = "";
1091
1092
1093 } else {
1094 if (document.getElementById(`StockOnSea_NotInRoute_${idPrefix}`) != null)
1095 document.getElementById(`StockOnSea_NotInRoute_${idPrefix}`).style.display = "";
1096
1097 }
1098 }
1099
1100
1101 }
1102 }
1103 }
1104 },
1105 watch: {
1106
1107 }
1108 });
1109
1110
1111 function setUpFilters() {
1112 const filterSection = document.querySelector('.filter-section');
1113 const sorting = document.getElementById('sorting');
1114
1115 if (filterSection) {
1116 const applyBtn = document.getElementById('applyFilterButton');
1117 const clearBtn = document.getElementById('clearFilterButton');
1118
1119 const filters = filterSection.querySelectorAll('.filter-option');
1120 setupFromQueryString();
1121
1122 Array.from(filters).forEach(filter => {
1123 const filterName = filter.getAttribute('data-name');
1124
1125 filter.addEventListener('input', event => {
1126 clearTimeout(delay);
1127
1128 switch (filter.type) {
1129 case 'range':
1130 activeFilters[filterName] = filter.value;
1131 break;
1132
1133 case 'checkbox':
1134 default:
1135 if (filter.checked) {
1136 if (activeFilters[filterName]) {
1137 activeFilters[filterName].push(filter.value);
1138 } else {
1139 activeFilters[filterName] = [filter.value];
1140 }
1141 } else {
1142 if (activeFilters[filterName]) {
1143 const valueIndex = activeFilters[filterName].indexOf(filter.value);
1144 activeFilters[filterName].splice(valueIndex, 1);
1145
1146 if (activeFilters[filterName].length == 0) {
1147 delete activeFilters[filterName];
1148 }
1149 }
1150 }
1151 break;
1152 }
1153 });
1154 });
1155
1156 clearBtn.addEventListener('click', event => {
1157 if (activeFilters['Sortby']) {
1158 const tempBy = activeFilters['Sortby'];
1159 const tempOrder = activeFilters['SortOrder'];
1160
1161 activeFilters = {};
1162
1163 activeFilters['Sortby'] = tempBy;
1164 activeFilters['SortOrder'] = tempOrder;
1165 } else {
1166 activeFilters = {};
1167 }
1168
1169 buildQueryString();
1170 });
1171
1172 applyBtn.addEventListener('click', buildQueryString);
1173 }
1174
1175 if (sorting) {
1176 sorting.addEventListener('change', event => {
1177 const sortingValue = sorting.value;
1178
1179 if (sortingValue == 'default') {
1180 delete activeFilters['Sortby'];
1181 delete activeFilters['SortOrder'];
1182 } else {
1183 activeFilters['Sortby'] = 'NameForSort';
1184 activeFilters['SortOrder'] = sortingValue.toUpperCase();
1185 }
1186
1187 buildQueryString();
1188 });
1189 }
1190 }
1191
1192 function buildQueryString() {
1193 // Create a new object to hold the processed filters
1194 const processedFilters = {};
1195
1196 // Process each filter
1197 Object.keys(activeFilters).forEach(key => {
1198 if (activeFilters[key] && activeFilters[key].length > 0) {
1199 // Get the first input with this key to check if it's a numeric filter
1200 const input = document.querySelector(`[data-name="${key}"]`);
1201 const isNumeric = input && input.getAttribute('data-parameter-type') === 'System.Double[]';
1202 processedFilters[key] = activeFilters[key].join(',');
1203 }
1204 });
1205
1206 // Build the query string
1207 const queryString = new URLSearchParams(processedFilters).toString();
1208 const currentPath = window.location.pathname;
1209
1210 if (queryString.length > 0) {
1211 window.location.href = `${currentPath}?${queryString.replace(/PageNum=\d+&/g, '')}`;
1212 } else {
1213 window.location.href = currentPath;
1214 }
1215 }
1216
1217 function setupFromQueryString() {
1218 const urlSearchParams = new URLSearchParams(window.location.search);
1219 const params = Object.fromEntries(urlSearchParams.entries());
1220
1221 Object.keys(params).forEach(key => {
1222 const value = params[key];
1223 if (!activeFilters[key]) {
1224 activeFilters[key] = [];
1225 }
1226
1227 // Get all inputs with this key to check if it's a numeric filter
1228 const inputsWithKey = document.querySelectorAll(`[data-name="${key}"]`);
1229 const isNumeric = inputsWithKey.length > 0 &&
1230 inputsWithKey[0].getAttribute('data-parameter-type') === 'System.Double[]';
1231
1232 // Get all possible numeric values from the filter options
1233 const allNumericOptions = Array.from(inputsWithKey).map(input => input.value);
1234
1235 // For numeric filters, we need special handling
1236 if (isNumeric) {
1237 // First, split the value into parts
1238 const parts = value.split(',');
1239 const processedIndices = new Set(); // Track which parts we've processed
1240
1241 // Check for decimal values first (values that contain a comma in our options)
1242 for (let i = 0; i < parts.length - 1; i++) {
1243 if (processedIndices.has(i)) continue; // Skip if already processed
1244
1245 const potentialDecimal = parts[i] + ',' + parts[i + 1];
1246
1247 // If this combination exists as an option, add it
1248 if (allNumericOptions.includes(potentialDecimal)) {
1249 activeFilters[key].push(potentialDecimal);
1250 processedIndices.add(i);
1251 processedIndices.add(i + 1);
1252 }
1253 }
1254
1255 // Now add any remaining individual values
1256 parts.forEach((part, index) => {
1257 if (!processedIndices.has(index) && allNumericOptions.includes(part)) {
1258 activeFilters[key].push(part);
1259 }
1260 });
1261 } else {
1262 // For non-numeric values, keep the original behavior
1263 if (value.indexOf(',') > -1) {
1264 value.split(',').forEach(entry => {
1265 activeFilters[key].push(entry);
1266 });
1267 } else {
1268 activeFilters[key].push(value);
1269 }
1270 }
1271
1272 // Update the checkboxes based on active filters
1273 if (key != 'GroupID' && key != 'PageNum' && key != 'Sortby' && key != 'SortOrder') {
1274 Array.from(inputsWithKey).forEach(input => {
1275 switch (input.type) {
1276 case 'range':
1277 input.value = value;
1278 break;
1279 case 'checkbox':
1280 default:
1281 // Check if the input value is in the active filters
1282 if (activeFilters[key].includes(input.value)) {
1283 input.checked = true;
1284 const toggle = input.closest('.filter-item')?.querySelector('.filter-item__checkbox-toggle');
1285 if (toggle) toggle.checked = true;
1286 }
1287 break;
1288 }
1289 });
1290 }
1291 });
1292 }
1293
1294 function setupSearchSelects() {
1295 if (document.querySelector('.search-select')) {
1296 // Add CSS for hiding partial matches
1297 const style = document.createElement('style');
1298 style.textContent = `
1299 .search-select__tags label.partial-match {
1300 display: none !important;
1301 }
1302 `;
1303 document.head.appendChild(style);
1304
1305 Array.from(document.querySelectorAll('.search-select')).forEach(select => {
1306 const input = select.querySelector('.search-select__search-input');
1307 const dropdown = select.querySelector('.search-select__dropdown');
1308 const options = select.querySelectorAll('.search-select__tags input');
1309 const optionsLabels = select.querySelectorAll('.search-select__dropdown-item');
1310 const clearBtn = select.querySelector('.search-select__search-clear');
1311
1312 // Determine if this is a numeric filter based only on parameter type
1313 const isNumeric = options.length > 0 &&
1314 options[0].getAttribute('data-parameter-type') == 'System.Double[]';
1315
1316 input.addEventListener('focus', open);
1317 clearBtn.addEventListener('click', clear);
1318 input.addEventListener('input', search);
1319
1320 // Fix the visual representation of selected tags
1321 function updateVisualTags() {
1322 // Only process numeric filters
1323 if (!isNumeric) return;
1324
1325 // Get the actual selected values from the URL
1326 const urlSearchParams = new URLSearchParams(window.location.search);
1327 const params = Object.fromEntries(urlSearchParams.entries());
1328
1329 if (options.length > 0) {
1330 const filterName = options[0].getAttribute('data-name');
1331
1332 // Only process if this filter is in the URL
1333 if (params[filterName]) {
1334 // For numeric filters, don't split the value
1335 const selectedValues = [params[filterName]];
1336
1337 // Remove partial-match class from all labels
1338 select.querySelectorAll('.search-select__tags label').forEach(label => {
1339 label.classList.remove('partial-match');
1340 });
1341
1342 // For each selected value, mark exact matches
1343 selectedValues.forEach(selectedValue => {
1344 Array.from(options).forEach(option => {
1345 // For numeric filters, we want exact matches only
1346 if (option.value === selectedValue) {
1347 const label = select.querySelector(`label[for="${option.id}"]`);
1348 if (label) {
1349 label.classList.add('selected');
1350 }
1351 }
1352 });
1353 });
1354 }
1355 }
1356 }
1357
1358 // Call this when page loads
1359 updateVisualTags();
1360
1361 Array.from(options).forEach(option => {
1362 option.addEventListener('change', e => {
1363 if (isNumeric && option.checked) {
1364 // When a numeric option is checked, hide partial matches
1365 const selectedValue = option.value;
1366 Array.from(options).forEach(otherOption => {
1367 if (otherOption !== option) {
1368 const isPartialMatch = selectedValue.includes(otherOption.value) &&
1369 selectedValue !== otherOption.value;
1370
1371 if (isPartialMatch) {
1372 const label = select.querySelector(`label[for="${otherOption.id}"]`);
1373 if (label) {
1374 label.classList.add('partial-match');
1375 }
1376 }
1377 }
1378 });
1379 }
1380 });
1381 });
1382
1383 function clear() {
1384 dropdown.classList.remove('search-select__dropdown--active');
1385 clearBtn.classList.remove('search-select__search-clear--active');
1386 input.value = '';
1387 resetList();
1388 }
1389
1390 function open() {
1391 dropdown.classList.add('search-select__dropdown--active');
1392 clearBtn.classList.add('search-select__search-clear--active');
1393 }
1394
1395 function search() {
1396 const searchTerm = input.value;
1397
1398 if (searchTerm.length > 0) {
1399 const searchWords = searchTerm.trim().split(' ');
1400
1401 Array.from(optionsLabels).forEach(option => {
1402 let hasMatch = false;
1403
1404 searchWords.forEach(word => {
1405 const searchRegEx = new RegExp(word, 'ig');
1406 const matches = option.innerText.match(searchRegEx);
1407
1408 if (matches && matches.length > 0) {
1409 hasMatch = true;
1410 option.innerHTML = option.innerHTML.replaceAll(/\<span class\=\"js-highlight\"\>(.*?)\<\/span\>/gi, '$1').replaceAll(searchRegEx, `<span class="js-highlight">${matches[0]}</span>`);
1411 }
1412 });
1413
1414 if (!hasMatch) {
1415 option.classList.add('js-no-match');
1416 } else {
1417 option.classList.remove('js-no-match');
1418 }
1419 });
1420 } else {
1421 resetList();
1422 }
1423 }
1424
1425 function resetList() {
1426 Array.from(optionsLabels).forEach(option => {
1427 option.classList.remove('js-no-match');
1428 option.innerHTML = option.innerText;
1429 });
1430 }
1431 });
1432 }
1433 }
1434
1435 function setupDuoRangeSliders() {
1436 if (document.querySelector('.duo-range-slider')) {
1437 Array.from(document.querySelectorAll('.duo-range-slider')).forEach(rangeSlider => {
1438 Array.from(rangeSlider.querySelectorAll('.duo-range-slider__range')).forEach(slider => {
1439 slider.oninput = updateValues;
1440 slider.oninput();
1441 });
1442
1443 function updateValues() {
1444 const parent = this.parentNode;
1445 const slides = parent.getElementsByTagName('input');
1446 let slide1 = parseFloat(slides[0].value);
1447 let slide2 = parseFloat(slides[1].value);
1448 const displayMin = parent.querySelector('.duo-range-slider__values-min span');
1449 const displayMax = parent.querySelector('.duo-range-slider__values-max span');
1450
1451 if (slide1 > slide2) {
1452 const temp = slide2;
1453
1454 slide2 = slide1;
1455 slide1 = temp;
1456 }
1457
1458 displayMin.innerText = slide1;
1459 displayMax.innerText = slide2;
1460 }
1461 });
1462 }
1463 }
1464 </script>
1465 }
1466
1467 @{
1468 string groupSeoText = GetString("Ecom:Group:Field.SEOText");
1469
1470 if (!string.IsNullOrWhiteSpace(groupSeoText))
1471 {
1472 <article class="module module-sand-light">
1473 <div class="rich-text">
1474 @groupSeoText
1475 </div>
1476 </article>
1477 }
1478 }
1479
1480 @functions {
1481 Dynamicweb.Ecommerce.Products.Group FindTopGroup(Dynamicweb.Ecommerce.Products.Group group)
1482 {
1483 if (group.IsTopGroup)
1484 {
1485 return group;
1486 }
1487 else if (group.ParentGroups != null && group.ParentGroups.Count > 0)
1488 {
1489 foreach (var parentGroup in group.ParentGroups)
1490 {
1491 Dynamicweb.Ecommerce.Products.Group topLevelGroup = FindTopGroup(parentGroup);
1492 if (topLevelGroup != null)
1493 {
1494 return topLevelGroup;
1495 }
1496 }
1497 }
1498 return null;
1499 }
1500 }
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...