Error executing template "/Designs/Swift/Paragraph/Swift_ProductListGridView_Custom.cshtml"
System.NullReferenceException: Object reference not set to an instance of an object.
at Dynamicweb.Ecommerce.Orders.Discounts.Discount.get_Extender()
at Dynamicweb.Ecommerce.Orders.Discounts.DiscountProvider.GetValidDiscountsForProduct(IEnumerable`1 discounts, Product product, Language language, PriceContext priceContext, Boolean ignoreOrderConditions)
at Dynamicweb.Ecommerce.Orders.Discounts.DiscountInfoCollection.LoadDiscounts()
at Dynamicweb.Ecommerce.ProductCatalog.ViewEngine.GetDiscountInfo(PriceViewModelSettings settings, Product product)
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at Dynamicweb.Ecommerce.ProductCatalog.ViewEngine.<>c__DisplayClass3_2.<BulkCreateView>b__46()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at Dynamicweb.Ecommerce.ProductCatalog.ViewEngine.<>c__DisplayClass3_2.<BulkCreateView>b__48()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at Dynamicweb.Ecommerce.ProductCatalog.ViewEngine.GetPrice(PriceViewModelSettings settings, IList`1 products, Boolean& pricesHasBeenPrepared, Object lock, Lazy`1 priceInfo)
at Dynamicweb.Ecommerce.ProductCatalog.ViewEngine.<>c__DisplayClass3_2.<BulkCreateView>b__49()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at CompiledRazorTemplates.Dynamic.RazorEngine_2be893513f5349e9bf163bc58bf747ce.<RenderProductList>b__10_0(TextWriter __razor_helper_writer) in C:\inetpub\solutions\Goecker-2022-Staging\Files\Templates\Designs\Swift\Paragraph\Swift_ProductListGridView_Custom.cshtml:line 285
at CompiledRazorTemplates.Dynamic.RazorEngine_2be893513f5349e9bf163bc58bf747ce.Execute() in C:\inetpub\solutions\Goecker-2022-Staging\Files\Templates\Designs\Swift\Paragraph\Swift_ProductListGridView_Custom.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 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel>
2 @using Dynamicweb.Ecommerce.ProductCatalog
3 @using Dynamicweb.Ecommerce.CustomerExperienceCenter.Favorites
4 @using System.Linq
5 @using Dynamicweb.Core
6 @using Dynamicweb.Environment
7 @using NLWI.Core.Factory
8 @using NLWI.Platforms.Dynamicweb9.Specs.ViewModels
9 @using NLWI.Platforms.Dynamicweb9.Specs.Services
10 @using NORRIQ.CustomCode.Razor
11 @using NORRIQ.CustomCode.StockLocations
12 @using System.Text;
13 @using System.Web;
14 @using Dynamicweb;
15 @using Dynamicweb.Core.Encoders
16 @using System.Globalization
17
18 @* CUSTOMIZED STANDARD SWIFT (v1.15.0) TEMPLATE *@
19
20 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel>
21
22 @helper RenderAddedModal(bool isRental = false)
23 {
24 string productsFeedUrl = $"/Default.aspx?ID={GetPageIdByNavigationTag("productsfeed")}&feed=true&redirect=false";
25 string checkoutUrl = isRental ? $"/Default.aspx?ID={GetPageIdByNavigationTag("rentalcheckout")}" : $"/Default.aspx?ID={GetPageIdByNavigationTag("checkout")}";
26 string cartUrl = "/Default.aspx?ID=" + (GetPageIdByNavigationTag("CartService"));
27 if (!cartUrl.Contains("LayoutTemplate"))
28 {
29 cartUrl += cartUrl.Contains("?") ? "&LayoutTemplate=Swift_MiniCart.cshtml" : "?LayoutTemplate=Swift_MiniCart.cshtml";
30 }
31
32 <div class="modal fade product-added-modal js-product-added-modal" data-productsfeedurl="@productsFeedUrl" tabindex="-1" role="dialog">
33 <input type="hidden" class="js-translation choose" value="@HttpUtility.HtmlEncode(Translate("Smartpage.Product.AddedModal.Choose", "Vælg"))" />
34 <input type="hidden" class="js-translation period-price" value="@HttpUtility.HtmlAttributeEncode(Translate("Smartpage:Rental.PeriodPrice", "Pris for periode:"))" />
35 <input type="hidden" class="js-translation daily-price" value="@HttpUtility.HtmlAttributeEncode(Translate("Smartpage:Rental.DailyPrice", "Dagspris:"))" />
36
37 <div class="modal-dialog" role="document">
38 <div class="modal-content">
39 <div class="modal-header">
40 <div class="d-flex flex-column">
41 <h3 class="modal-title">@Translate("Smartpage.Product.AddedModal.Header", "Tilføjet til kurv")</h3>
42 @if (isRental)
43 {
44 <p>
45 @Translate("Smartpage.Rental.PeriodDays", "Skydedage")
46 <span class="js-rental-periodfrom"></span> - <span class="js-rental-periodto"></span>
47 </p>
48 }
49 </div>
50 <div class="cross-icon cursor-pointer" onclick="hideModal(this)"></div>
51 </div>
52 <div class="spinner-border js-loader powerstep-loader text-dark d-none">
53 <span class="visually-hidden">Loading..</span>
54 </div>
55 <div class="modal-body container pt-2 pb-0 px-3 m-0 text-center text-lg-start">
56 <div class="js-product-modal-orderline row gap-2 gap-lg-0 d-flex flex-column flex-lg-row justify-content-between pb-2 border-bottom">
57 <div class="col-lg-6 col-12 d-flex align-items-center flex-column flex-lg-row gap-3 gap-lg-0">
58 <img class="js-product-modal-orderline-image mr-2" />
59 <a class="js-product-modal-orderline-name text-decoration-none fw-bold px-md-3 px-lg-0">
60 </a>
61 </div>
62 <div class="col-lg-6 col-12 d-flex flex-column align-items-center align-items-lg-end justify-content-center">
63 @if (!isRental)
64 {
65 <div class="d-flex mr-2">
66 <span class="js-product-modal-orderline-price"></span>
67 </div>
68 }
69 else
70 {
71 <div class="d-flex mr-2">
72 <span class="me-2">@Translate("Smartpage:Rental.DailyPrice", "Dagspris:")</span> <span class="js-product-modal-orderline-dailyprice"></span>
73 </div>
74 <div class="d-flex">
75 <span class="me-2">@Translate("Smartpage:Rental.PeriodPrice", "Pris for periode:")</span> <span class="js-product-modal-orderline-periodprice"></span>
76 </div>
77 }
78 </div>
79 </div>
80 <div class="row border-bottom">
81 <h5 class="fw-bold py-3 fs-sm-7 fs-md-6">@Translate("Smartpage.Product.AddedModal.Recommendation", "Til ovenstående produkt anbefaler vi:")</h5>
82 </div>
83 <div class="js-product-modal-relatedproducts d-flex flex-column"></div>
84 </div>
85
86 <div class="modal-footer border-top-0 justify-content-end">
87 @if (!isRental)
88 {
89 <form method="post" action="@cartUrl" class="js-relatedproducts-form">
90 <input type="hidden" name="cartcmd" value="addmulti" />
91 <input type="hidden" name="redirect" value="false" />
92
93 <div class="js-products-inputs"></div>
94
95 <button class="btn btn-primary js-add-to-cart-button disabled" onclick="swift.Cart.Update(event)">@Translate("Tilføj til kurv")</button>
96 </form>
97 }
98 else
99 {
100 <form class="js-relatedproducts-form">
101 <div class="js-products-inputs"></div>
102 <button class="btn btn-primary js-add-to-rental disabled" onclick="addRelatedRentalProducts(event, this)">@Translate("Tilføj til kurv")</button>
103 </form>
104 }
105 </div>
106 <div class="modal-footer justify-content-between">
107 <button type="button" class="btn btn-primary" onclick="hideModal(this)">@Translate("Smartpage.Product.AddedModal.BackToShop", "Tilbage til shop")</button>
108 <a href="@checkoutUrl" class="btn btn-primary">@Translate("Smartpage.Product.AddedModal.Checkout", "Til checkout")</a>
109 </div>
110 </div>
111 </div>
112 </div>
113 }
114 @using NLWI.Platforms.Dynamicweb9.Specs.ViewModels
115
116 @functions {
117 string GetProductImage(ProductSpecifications specs)
118 {
119 string imageUrl = "/Files/Images/Perfion/default.png";
120
121 if (specs.HasKey("Primærbillede"))
122 {
123 imageUrl = specs.GetByKey("Primærbillede").Value;
124 }
125
126 return imageUrl;
127 }
128
129 Dictionary<string, string> GetAllProductImages(ProductSpecifications specs)
130 {
131 Dictionary<string, string> images = new Dictionary<string, string>();
132
133 foreach (var img in specs.GetByGroup("Billeder"))
134 {
135 if (!images.ContainsKey(img.Value) && img.Key != "Brandlogo")
136 {
137 images.Add(img.Value, img.Caption);
138 }
139 }
140
141 return images;
142 }
143 }
144
145 @functions
146 {
147 bool isLazyLoadingForProductInfoEnabled = Dynamicweb.Ecommerce.DynamicwebLiveIntegration.TemplatesHelper.IsLazyLoadingForProductInfoEnabled;
148 string liveInfoClass = "";
149 string productInfoFeed = "";
150
151 string showPricesWithVat = "";
152 bool neverShowVat = false;
153
154 bool isDetailPage = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("ProductID"));
155
156 ProductListViewModel productList = new ProductListViewModel();
157 }
158
159 @{
160 if (Dynamicweb.Context.Current.Items.Contains("ProductList"))
161 {
162 productList = (ProductListViewModel)Dynamicweb.Context.Current.Items["ProductList"];
163 }
164
165 showPricesWithVat = HttpContext.Current.Session["Smartpage:ShowPricesWithVat"] != null ? Convert.ToString(HttpContext.Current.Session["Smartpage:ShowPricesWithVat"]) : Pageview.Area.EcomPricesWithVat.ToLower();
166 neverShowVat = string.IsNullOrEmpty(showPricesWithVat);
167
168 if (isLazyLoadingForProductInfoEnabled)
169 {
170 if (Dynamicweb.Context.Current.Items.Contains("ProductInfoFeed"))
171 {
172 productInfoFeed = Dynamicweb.Context.Current.Items["ProductInfoFeed"]?.ToString();
173 if (!string.IsNullOrEmpty(productInfoFeed))
174 {
175 productInfoFeed = $"data-product-info-feed=\"{productInfoFeed}\"";
176 }
177 }
178 liveInfoClass = "js-live-info";
179 }
180
181 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : "";
182 string themePadding = theme != string.Empty ? "p-3" : string.Empty;
183 }
184
185 @if (!isDetailPage)
186 {
187 if (!string.IsNullOrEmpty(theme))
188 {
189 <div class="h-100@(theme) @themePadding item_@Model.Item.SystemName.ToLower()" @productInfoFeed>
190 @RenderProductList()
191 </div>
192 }
193 else
194 {
195 <div class="pt-3 item_@Model.Item.SystemName.ToLower()" @productInfoFeed>
196 @RenderProductList()
197 </div>
198 }
199 }
200
201 @helper RenderProductList()
202 {
203 var productSpecificationService = ObjectFactory.GetInstance<IProductSpecificationService>();
204 var stockService = ObjectFactory.GetInstance<StockService>();
205 var featurePostFix = Pageview.AreaSettings.GetItem("CustomSettings").GetRawValueString("FeaturePostFix_Custom", "");
206
207 string anonymousUsersLimitations = Pageview.AreaSettings.GetRawValueString("AnonymousUsers", "");
208 bool anonymousUser = Pageview.User == null;
209 bool hidePrice = anonymousUsersLimitations.Contains("price") && anonymousUser || Pageview.AreaSettings.GetBoolean("ErpDownHidePrices") && !Dynamicweb.Ecommerce.DynamicwebLiveIntegration.TemplatesHelper.IsWebServiceConnectionAvailable();
210
211 string productTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ProductTheme")) ? " theme " + Model.Item.GetRawValueString("ProductTheme").Replace(" ", "").Trim().ToLower() : "";
212 string productThemePadding = productTheme != string.Empty ? "p-3" : string.Empty;
213
214 string url = Dynamicweb.Context.Current.Request.RawUrl;
215 bool hideFavoritesSelector = !string.IsNullOrEmpty(Model.Item.GetString("HideFavoritesSelector")) ? Model.Item.GetBoolean("HideFavoritesSelector") : false;
216 bool showFavoritesSelectorMasterProduct = !string.IsNullOrEmpty(Model.Item.GetString("ShowFavoritesSelectorMasterProduct")) ? Model.Item.GetBoolean("ShowFavoritesSelectorMasterProduct") : false;
217 string staticVariantsLayout = Model.Item.GetRawValueString("StaticVariantsLayout", "hide");
218
219 string groupId = productList?.Group?.Id != null ? productList.Group.Id : "";
220
221 string cartUrl = "/Default.aspx?ID=" + (GetPageIdByNavigationTag("CartService"));
222 if (!cartUrl.Contains("LayoutTemplate"))
223 {
224 cartUrl += cartUrl.Contains("?") ? "&LayoutTemplate=Swift_MiniCart.cshtml" : "?LayoutTemplate=Swift_MiniCart.cshtml";
225 }
226
227 var badgeParms = new Dictionary<string, object>();
228 badgeParms.Add("saleBadgeType", Model.Item.GetRawValue("SaleBadgeType"));
229 badgeParms.Add("saleBadgeCssClassName", Model.Item.GetRawValue("SaleBadgeDesign"));
230 badgeParms.Add("newBadgeCssClassName", Model.Item.GetRawValue("NewBadgeDesign"));
231 badgeParms.Add("newPublicationDays", Model.Item.GetInt32("NewPublicationDays"));
232 badgeParms.Add("campaignBadgesValues", Model.Item.GetRawValueString("CampaignBadges"));
233
234 bool saleBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("SaleBadgeDesign")) && Model.Item.GetRawValueString("SaleBadgeDesign") != "none" ? true : false;
235 bool newBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("NewBadgeDesign")) && Model.Item.GetRawValueString("NewBadgeDesign") != "none" ? true : false;
236
237 string googleAnalyticsTrackingID = Pageview.AreaSettings.GetString("GoogleAnalyticsTrackingID");
238 string googleAnalyticsMeasurementID = Pageview.AreaSettings.GetString("GoogleAnalyticsMeasurementID");
239 var cookieOptInLevel = CookieManager.GetCookieOptInLevel();
240 bool allowTracking = cookieOptInLevel == CookieOptInLevel.All || (cookieOptInLevel == CookieOptInLevel.Functional && CookieManager.GetCookieOptInCategories().Contains("Statistical"));
241
242 var favoriteParameters = new Dictionary<string, object>();
243 if (!anonymousUser && !hideFavoritesSelector)
244 {
245 int defaultFavoriteListId = 0;
246
247 IEnumerable<FavoriteList> favoreiteLists = Pageview.User.GetFavoriteLists();
248 if (favoreiteLists.Count() == 1)
249 {
250 foreach (FavoriteList list in favoreiteLists)
251 {
252 defaultFavoriteListId = list.ListId;
253 }
254 }
255
256 favoriteParameters.Add("ListId", defaultFavoriteListId);
257 }
258
259 if (productList.TotalProductsCount > 0)
260 {
261 int pageSizeSetting = 30;
262 int pageSize = productList.PageSize;
263 pageSize += pageSizeSetting;
264
265 int loadedProducts = productList.PageSize > productList.TotalProductsCount ? productList.TotalProductsCount : productList.PageSize;
266
267 <div class="grid grid-2 grid-lg-3">
268 @*
269 20250512: Removed by AE
270 @RenderAddedModal(false)
271 *@
272
273 <script>
274 window.addEventListener('CookieInformationConsentGiven', function (event) {
275 if (CookieInformation.getConsentGivenFor('cookie_cat_statistic')) {
276 gtag("event", "view_item_list", {
277 item_list_id: "product_list_gridview",
278 item_list_name: "Product list (Gridview)",
279 items: [
280 @foreach (ProductViewModel product in productList.Products)
281 {
282 <text>{
283 item_id: "@product.Number",
284 item_name: "@product.Name",
285 currency: "@product.Price.CurrencyCode",
286 price: @product.Price.Price
287 },</text>
288 }
289 ]
290 });
291 }
292 }, false);
293 </script>
294
295
296 @foreach (ProductViewModel product in productList.Products)
297 {
298 var specs = product.GetSpecifications();
299 var availableStockLocations = product.StockUnits.Where(a => a.StockLevel > 0).Select(a => a.StockLocationName).ToList();
300 var inStock = availableStockLocations.Any();
301
302 string variantIdForLink = !string.IsNullOrEmpty(product.VariantId) ? $"&VariantID={product.VariantId}" : "";
303 variantIdForLink = string.IsNullOrEmpty(variantIdForLink) && !string.IsNullOrEmpty(product.DefaultVariantId) ? $"&VariantID={product.DefaultVariantId}" : variantIdForLink;
304
305 string link = "Default.aspx?ID=" + GetPageIdByNavigationTag("Shop");
306 link += $"&GroupID={product.PrimaryOrDefaultGroup.Id}";
307 link += $"&ProductID={product.Id}";
308 link += variantIdForLink;
309 link = Dynamicweb.Frontend.SearchEngineFriendlyURLs.GetFriendlyUrl(link);
310
311 string imagePath = product?.DefaultImage?.Value ?? "";
312 imagePath = Dynamicweb.Context.Current.Server.UrlEncode(imagePath);
313
314 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", "");
315 ratio = ratio != "0" ? ratio : "";
316 string ratioCssClass = ratio != "" ? " ratio" : "";
317 string ratioVariable = ratio != "" ? "--bs-aspect-ratio: " + ratio : "";
318
319 string imagePathXs = "/Admin/Public/GetImage.ashx?width=" + 480 + "&image=" + imagePath + "&format=webp";
320 string imagePathS = "/Admin/Public/GetImage.ashx?width=" + 640 + "&image=" + imagePath + "&format=webp";
321 string imagePathFallBack = "/Admin/Public/GetImage.ashx?width=" + 640 + "&image=" + imagePath + "&format=webp";
322
323 string imageTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ImageTheme")) ? " theme " + Model.Item.GetRawValueString("ImageTheme").Replace(" ", "").Trim().ToLower() : "";
324 string imageThemePadding = imageTheme != string.Empty ? "p-3" : string.Empty;
325 string imageOutlineStyle = imageTheme == string.Empty ? "style=\"border: 1px solid transparent\"" : string.Empty;
326
327 string imageId = "ProductImage_" + product.Id + product.VariantId;
328 string priceId = "ProductPrice_" + product.Id + product.VariantId;
329
330 @* Alternative image *@
331 var supportedImageFormats = new string[] { ".jpg", ".webp", ".png", ".gif" };
332 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : "";
333 var selectedAssetCategories = Model.Item.GetRawValueString("AlternativeImageAssets");
334 IEnumerable<MediaViewModel> alternativeImagesList = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets);
335
336 if (alternativeImagesList.FirstOrDefault() != null)
337 {
338 alternativeImagesList = alternativeImagesList.OrderByDescending(x => x.Value.Equals(defaultImage));
339
340 if (alternativeImagesList.First().Value == defaultImage)
341 {
342 alternativeImagesList = alternativeImagesList.Skip(1);
343 }
344 }
345
346 string alternativeImage = alternativeImagesList.FirstOrDefault() != null ? alternativeImagesList.FirstOrDefault().Value : "";
347 alternativeImage = !string.IsNullOrEmpty(alternativeImage) ? "/Admin/Public/GetImage.ashx?width=" + 640 + "&image=" + alternativeImage + "&format=webp" : "";
348
349 @* Badges *@
350 DateTime createdDate = product.Created.Value;
351 bool showBadges = saleBadgeEnabled && product.Discount.Price != 0 ? true : false;
352 showBadges = (newBadgeEnabled && Model.Item.GetInt32("NewPublicationDays") == 0) || (newBadgeEnabled && (createdDate.AddDays(Model.Item.GetInt32("NewPublicationDays")) > DateTime.Now)) ? true : showBadges;
353 showBadges = !string.IsNullOrEmpty(Model.Item.GetRawValueString("CampaignBadges")) ? true : showBadges;
354
355 @* Main features *@
356 IEnumerable<string> selectedDisplayGroups = Model.Item.GetRawValueString("MainFeatures").Split(',').ToList();
357 List<CategoryFieldViewModel> mainFeatures = new List<CategoryFieldViewModel>();
358
359 foreach (var selection in selectedDisplayGroups)
360 {
361 foreach (CategoryFieldViewModel group in product.FieldDisplayGroups.Values)
362 {
363 if (selection == group.Id)
364 {
365 mainFeatures.Add(group);
366 }
367 }
368 }
369
370 //CUSTOM
371 var disablePrice = product.ProductFields.ContainsKey("Custom_DisablePrice") ? Convert.ToBoolean(product.ProductFields["Custom_DisablePrice"].Value) : false;
372 var disablePurchase = product.ProductFields.ContainsKey("Custom_DisablePurchase") ? Convert.ToBoolean(product.ProductFields["Custom_DisablePurchase"].Value) : false;
373 var disableStock = product.ProductFields.ContainsKey("Custom_DisableStock") ? Convert.ToBoolean(product.ProductFields["Custom_DisableStock"].Value) : false;
374 var enableContact = product.ProductFields.ContainsKey("Custom_EnableContact") ? Convert.ToBoolean(product.ProductFields["Custom_EnableContact"].Value) : false;
375 var enableBookDemo = product.ProductFields.ContainsKey("Custom_EnableBookDemo") ? Convert.ToBoolean(product.ProductFields["Custom_EnableBookDemo"].Value) : false;
376 //--CUSTOM
377
378 <article class="position-relative@(productTheme) product-list-item js-product @liveInfoClass" data-product-id="@product.Id" itemscope itemtype="https://schema.org/Product">
379
380 @if (specs.HasKey("Ikon" + featurePostFix))
381 {
382 <div class="product-label">
383 @foreach (var label in specs.GetAllByKey("Ikon" + featurePostFix))
384 {
385 var referenceSpecfication = label.GetReferenceSpecification();
386 var txtcolor = "#ffffff";
387 if (!string.IsNullOrEmpty(referenceSpecfication.GetByKey("LinkFarvekode").Value))
388 {
389 txtcolor = referenceSpecfication.GetByKey("LinkFarvekode").Value;
390 }
391 if (referenceSpecfication.HasKey("LinkURL"))
392 {
393 <a href="@(referenceSpecfication.GetByKey("LinkURL").Value)" class="product-label-item" style="background-color:@(referenceSpecfication.GetByKey("Farvekode").Value);color:@txtcolor;">
394 @referenceSpecfication.GetByKey("Label").Value
395 </a>
396 }
397 else
398 {
399 <span class="product-label-item" style="background-color:@(referenceSpecfication.GetByKey("Farvekode").Value);color:@txtcolor;">
400 @referenceSpecfication.GetByKey("Label").Value
401 </span>
402 }
403 }
404 </div>
405 }
406
407 @if (!anonymousUser)
408 {
409 if (!hideFavoritesSelector && product.VariantInfo.VariantInfo == null)
410 {
411 <div class="position-absolute top-0 end-0 my-3" style="z-index: 2">
412 @RenderPartial("Components/ToggleFavorite.cshtml", product, favoriteParameters)
413 </div>
414 }
415 else if (showFavoritesSelectorMasterProduct)
416 {
417 <div class="position-absolute top-0 end-0 my-3" style="z-index: 2">
418 @RenderPartial("Components/ToggleFavorite.cshtml", product, favoriteParameters)
419 </div>
420 }
421 }
422
423 @if (showBadges)
424 {
425 <div class="position-absolute top-0 left-0 p-1 p-lg-2 ps-0 ps-lg-0" style="z-index: 2">
426 @RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)
427 </div>
428 }
429
430 <div class="d-flex flex-column d-block h-100 text-center">
431 @{
432 string clickProductLink = string.Empty;
433 if (allowTracking)
434 {
435 clickProductLink = "onclick=\"return clickProductLink('" + @product.Id + "', '" + @product.Name + "', '" + @product.VariantName + "', '" + @product.Price.CurrencyCode + "', '" + @product.Price.Price + "')\"";
436 }
437 }
438 <a href="@link" class="text-decoration-none d-flex flex-column" @clickProductLink>
439 @if (allowTracking)
440 {
441 <script>
442 function clickProductLink(productId, productName, productVariant, productCurrency, productPrice) {
443 if (typeof gtag !== "undefined") {
444 gtag("event", "select_item", {
445 item_list_id: "product_list_gridview",
446 item_list_name: "Product list (Gridview)",
447 items: [
448 {
449 item_id: productId,
450 item_name: productName,
451 currency: productCurrency,
452 item_list_id: "product_list_gridview",
453 item_list_name: "Product list (Gridview)",
454 item_variant: productVariant,
455 price: productPrice
456 }
457 ]
458 });
459 }
460 }
461 </script>
462 }
463
464 <div class="@productThemePadding order-2">
465 <div class="flex-grow-1">
466 <h3 class="h6 mb-0 text-break">
467 @product.Name @if (!string.IsNullOrEmpty(product.VariantName))
468 {<text>(@product.VariantName)</text>}
469 </h3>
470 @if (!Model.Item.GetBoolean("HideProductNumber"))
471 {
472 <p class="fs-7 opacity-85 mb-2">@product.Number</p>
473 }
474 @if (mainFeatures.Count > 0)
475 {
476 <ul class="p-0 lh-sm opacity-75" style="list-style-position: inside">
477 @foreach (CategoryFieldViewModel mainFeatureGroup in mainFeatures)
478 {
479 foreach (var field in mainFeatureGroup.Fields)
480 {
481 @RenderField(field.Value)
482 }
483 }
484 </ul>
485 }
486 </div>
487 </div>
488
489 <div class="overflow-hidden order-1 @(imageTheme)" @imageOutlineStyle>
490 <div class="ratio" style="@(ratioVariable)">
491 <div class="d-flex justify-content-center align-items-center">
492 @if (string.IsNullOrEmpty(alternativeImage))
493 {
494 <img id="@imageId"
495 srcset="
496 @imagePathXs 480w,
497 @imagePathS 640w"
498 sizes="(min-width: 992px) 33vw, 50vw"
499 src="@imagePathFallBack"
500 loading="lazy"
501 decoding="async"
502 class="mw-100 mh-100 @imageThemePadding"
503 alt="@product.Name">
504 }
505 else
506 {
507 <img id="@imageId"
508 src="@imagePathFallBack"
509 loading="lazy"
510 decoding="async"
511 class="mw-100 mh-100 @imageThemePadding"
512 alt="@product.Name"
513 onmouseover="this.src='@alternativeImage'"
514 onmouseout="this.src='@imagePathFallBack'">
515 }
516 </div>
517 </div>
518
519 @if (product.VariantInfo.VariantInfo != null && staticVariantsLayout == "images")
520 {
521 int variantGroupCount = 0;
522 int showMaxVariantGroups = 2;
523 int showMaxVariants = 3;
524 var productVariantTheme = productTheme != "" ? productTheme : "bg-white";
525
526 <div class="position-relative">
527 <div id="StaticVariants_@product.Id" class="static-variants w-100 d-none d-lg-block position-absolute left-0 bottom-0 @productTheme" style="pointer-events: none;">
528
529 @foreach (var variantGroup in product.VariantGroups())
530 {
531 int variantsCount = 0;
532
533 <div class="d-flex gap-2 mb-2">
534 @foreach (var variant in variantGroup.Options)
535 {
536 if (variantGroupCount < showMaxVariantGroups)
537 {
538 var optionsCount = variantGroup.Options.Count();
539
540 if (variantsCount < showMaxVariants)
541 {
542 string optionWidth = !string.IsNullOrEmpty(variant.Color) ? "w-25" : "";
543
544 <article class="static-variants-option @optionWidth @(productVariantTheme)" title="@product.Name @variant.Name" style="pointer-events: initial;">
545 @if (!string.IsNullOrEmpty(variant.Color))
546 {
547 string defaultProductImage = Dynamicweb.Context.Current.Server.UrlEncode(product.DefaultImage.Value);
548 string variantImage = Dynamicweb.Context.Current.Server.UrlEncode(variant.Image.Value);
549 string defaultPrice = !hidePrice ? product.Price.PriceFormatted : "0";
550 string variantPrice = !hidePrice ? product.Price.PriceFormatted : "0";
551
552 if (isLazyLoadingForProductInfoEnabled)
553 {
554 <figure class="w-100 d-block m-0" data-price-formatted="" onmouseover="swift.StaticVariants.SwitchProduct(event, '@product.Id', this.getAttribute('data-price-formatted'), '@variantImage')" onmouseout="swift.StaticVariants.SwitchProduct(event, '@product.Id', this.getAttribute('data-price-formatted'), '@defaultProductImage')">
555 <div class="d-flex align-items-center justify-content-center">
556 <img src="/admin/public/GetImage.ashx?image=@variantImage&width=75&height=75&crop=5&FillCanvas=true&format=webp&Quality=70" height="75" width="75" class="p-1 text-small" loading="lazy" decoding="async" alt="@product.Name, @variant.Name">
557 </div>
558 </figure>
559 }
560 else
561 {
562 <figure class="w-100 d-block m-0" onmouseover="swift.StaticVariants.SwitchProduct(event, '@product.Id', '@defaultPrice', '@variantImage')" onmouseout="swift.StaticVariants.SwitchProduct(event, '@product.Id', '@variantPrice', '@defaultProductImage')">
563 <div class="d-flex align-items-center justify-content-center">
564 <img src="/admin/public/GetImage.ashx?image=@variantImage&width=75&height=75&crop=5&FillCanvas=true&format=webp&Quality=70" height="75" width="75" class="p-1 text-small" loading="lazy" decoding="async" alt="@product.Name, @variant.Name">
565 </div>
566 </figure>
567 }
568 }
569 else
570 {
571 <div class="d-flex align-items-center justify-content-center">
572 @variant.Name
573 </div>
574 }
575 <div class="visually-hidden">
576 <h4>@product.Name, @variant.Name</h4>
577 @if (!hidePrice)
578 {
579 if (isLazyLoadingForProductInfoEnabled)
580 {
581 <span class="text-price js-text-price"></span>
582 }
583 else
584 {
585 <span class="text-price">@product.Price.PriceFormatted</span>
586 }
587 }
588 </div>
589 </article>
590 }
591
592 variantsCount++;
593
594 if (variantsCount == showMaxVariants && optionsCount != showMaxVariants)
595 {
596 int left = optionsCount - showMaxVariants;
597 <div class="variant-option ms-1 d-flex justify-content-center align-items-center">
598 <span>+@left</span>
599 </div>
600 }
601 }
602 }
603 </div>
604
605 variantGroupCount++;
606 }
607 </div>
608 </div>
609 }
610 </div>
611 </a>
612 <div class="@productThemePadding mt-auto pt-0">
613
614 @if (!disableStock) //CUSTOM
615 {
616 <div>
617 @if (inStock && availableStockLocations.Any())
618 {
619 <span class="icon-stock icon-green position-relative" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="@HttpUtility.HtmlAttributeEncode(string.Join("<br />", availableStockLocations))" data-bs-html="true">@Translate("label_in_Stock", "På lager")</span>
620 }
621 else
622 {
623 <span class="icon-stock icon-red position-relative">@Translate("label_not_in_stock", "Ikke på lager")</span>
624 }
625 </div>
626
627 <script>
628 window.addEventListener('DOMContentLoaded', function () {
629 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
630 var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
631 return new bootstrap.Tooltip(tooltipTriggerEl)
632 })
633 }, false);
634 </script>
635 }
636
637 @if (!hidePrice && !disablePrice) //CUSTOM
638 {
639 string priceMin = "";
640 string priceMax = "";
641
642 <div class="mt-2">
643 <div>
644 <span itemprop="priceCurrency" content="@product.Price.CurrencyCode" class="d-none"></span>
645
646 @if (showPricesWithVat == "false" && !neverShowVat)
647 {
648 if (isLazyLoadingForProductInfoEnabled)
649 {
650 <span itemprop="price" content="" class="d-none"></span>
651 <span class="text-decoration-line-through js-text-decoration-line-through opacity-75 me-3 text-price js-text-price d-none" data-show-if="LiveProductInfo.product.Price.Price != LiveProductInfo.product.PriceBeforeDiscount.Price"></span>
652 }
653 else
654 {
655 string beforePrice = product.PriceBeforeDiscount.PriceWithoutVatFormatted;
656 string informativePrice = product.PriceInformative.PriceWithoutVatFormatted;
657
658 <span itemprop="price" content="@product.Price.PriceWithoutVat" class="d-none"></span>
659 if (product.Price.Price != product.PriceBeforeDiscount.Price)
660 {
661 <span class="text-decoration-line-through opacity-75 me-3 text-price">@beforePrice</span>
662 }
663 else if (product.PriceInformative.Price > 0)
664 {
665 <span class="text-decoration-line-through opacity-75 me-3 text-price">@informativePrice</span>
666 }
667 }
668 }
669 else
670 {
671 if (isLazyLoadingForProductInfoEnabled)
672 {
673 <span itemprop="price" content="" class="d-none"></span>
674 <span class="text-decoration-line-through js-text-decoration-line-through opacity-75 me-3 text-price js-text-price d-none" data-show-if="LiveProductInfo.product.Price.Price != LiveProductInfo.product.PriceBeforeDiscount.Price"></span>
675 }
676 else
677 {
678 string beforePrice = product.PriceBeforeDiscount.PriceFormatted;
679 string informativePrice = product.PriceInformative.PriceWithVatFormatted;
680
681 <span itemprop="price" content="@product.Price.Price" class="d-none"></span>
682 if (product.Price.Price != product.PriceBeforeDiscount.Price)
683 {
684 <span class="text-decoration-line-through opacity-75 me-3 text-price">@beforePrice</span>
685 }
686 else if (product.PriceInformative.Price > 0)
687 {
688 <span class="text-decoration-line-through opacity-75 me-3 text-price">@informativePrice</span>
689 }
690 }
691 }
692
693 @if (showPricesWithVat == "false" && !neverShowVat)
694 {
695 if (isLazyLoadingForProductInfoEnabled)
696 {
697 <span class="text-price js-text-price"><div class="spinner-border" role="status"></div></span>
698 }
699 else
700 {
701 string price = product.Price.PriceWithoutVatFormatted;
702 if (product?.VariantInfo?.VariantInfo != null)
703 {
704 priceMin = product?.VariantInfo?.PriceMin?.PriceWithoutVatFormatted != null ? product.VariantInfo.PriceMin.PriceWithoutVatFormatted : "";
705 priceMax = product?.VariantInfo?.PriceMax?.PriceWithoutVatFormatted != null ? product.VariantInfo.PriceMax.PriceWithoutVatFormatted : "";
706 }
707 if (priceMin != priceMax)
708 {
709 price = priceMin + " - " + priceMax;
710 }
711 <span class="text-price">@price</span>
712 }
713 }
714 else
715 {
716 if (isLazyLoadingForProductInfoEnabled)
717 {
718 <span class="text-price js-text-price"><div class="spinner-border" role="status"></div></span>
719 }
720 else
721 {
722 string price = product.Price.PriceFormatted;
723 if (product?.VariantInfo?.VariantInfo != null)
724 {
725 priceMin = product?.VariantInfo?.PriceMin?.PriceFormatted != null ? product.VariantInfo.PriceMin.PriceFormatted : "";
726 priceMax = product?.VariantInfo?.PriceMax?.PriceFormatted != null ? product.VariantInfo.PriceMax.PriceFormatted : "";
727 }
728 if (priceMin != priceMax)
729 {
730 price = priceMin + " - " + priceMax;
731 }
732 <span class="text-price">@price</span>
733 }
734 }
735 </div>
736 @if (showPricesWithVat == "false" && !neverShowVat)
737 {
738 if (isLazyLoadingForProductInfoEnabled)
739 {
740 <div class="fs-7 opacity-85 text-price js-text-price-with-vat d-none" data-suffix="@Translate("Incl. VAT")"></div>
741 }
742 else
743 {
744 string price = product.Price.PriceWithVatFormatted;
745 if (product?.VariantInfo?.VariantInfo != null)
746 {
747 priceMin = product?.VariantInfo?.PriceMin?.PriceWithVatFormatted != null ? product.VariantInfo.PriceMin.PriceWithVatFormatted : "";
748 priceMax = product?.VariantInfo?.PriceMax?.PriceWithVatFormatted != null ? product.VariantInfo.PriceMax.PriceWithVatFormatted : "";
749 }
750 if (priceMin != priceMax)
751 {
752 price = priceMin + " - " + priceMax;
753 }
754 <div class="fs-7 opacity-85 text-price">@price @Translate("Incl. VAT")</div>
755 }
756 }
757 </div>
758 }
759
760 <div class="icon-compare mt-2 d-none d-sm-flex justify-content-center align-items-center fs-7"
761 data-number="@HttpUtility.HtmlAttributeEncode(product.Number)"
762 data-image="@HttpUtility.HtmlAttributeEncode(imagePath)"
763 data-name="@HttpUtility.HtmlAttributeEncode(product.Name)"
764 data-link="@HttpUtility.HtmlAttributeEncode(link)"
765 onclick="toggleCompareProduct(event, this)">
766 <span class="square-icon js-square-icon d-flex" data-product-number="@HttpUtility.HtmlAttributeEncode(product.Number)"></span>
767 <span>@Translate("Sammenlign")</span>
768 </div>
769
770 @if (product.VariantInfo.VariantInfo != null && staticVariantsLayout == "swatches")
771 {
772 var optionCount = product.VariantInfo.VariantInfo.Count();
773 var showMaxVariants = 5;
774
775 <div class="d-flex flex-row gap-1 align-items-center">
776 @foreach (VariantInfoViewModel variant in product.VariantInfo.VariantInfo.Take(showMaxVariants))
777 {
778 <span class="colorbox colorbox-sm rounded-circle border me-1" style="background-color: @variant.OptionColor"></span>
779 }
780 @if (optionCount > showMaxVariants)
781 {
782 int left = optionCount - showMaxVariants;
783 <span class="ms-2">+@left</span>
784 }
785 </div>
786 }
787
788 <div class="mt-3">
789 <form method="post" action="@cartUrl">
790 <input type="hidden" name="redirect" value="false" />
791 <input type="hidden" name="ProductId" value="@product.Id" />
792 <input type="hidden" name="ProductName" value="@HtmlEncoder.HtmlEncode(product.Name)" />
793 <input type="hidden" name="ProductVariantName" value="@HtmlEncoder.HtmlEncode(product.VariantName)" />
794 <input type="hidden" name="ProductCurrency" value="@Dynamicweb.Ecommerce.Common.Context.Currency.Code" />
795 <input type="hidden" name="ProductPrice" value="@(showPricesWithVat == "false" && !neverShowVat ? product.Price.PriceWithoutVat.ToString("0.00", CultureInfo.InvariantCulture) : product.Price.Price.ToString("0.00", CultureInfo.InvariantCulture))" />
796 <input type="hidden" name="ProductReferer" value="component_ProductListGridView_Custom">
797 <input type="hidden" name="cartcmd" value="add" />
798 <input type="hidden" name="quantity" value="1" />
799 <input type="hidden" class="js-product-id" value="@product.Id" />
800
801 @if (!string.IsNullOrEmpty(product.VariantId))
802 {
803 <input type="hidden" name="VariantId" value="@product.VariantId" />
804 }
805
806 @*//CUSTOM*@
807 <div class="d-flex flex-column gap-2">
808 @if (enableContact)
809 {
810 var enableContactlink = Pageview.AreaSettings.GetItem("CustomSettings").GetLink("Custom_EnableContactLink");
811 if (enableContactlink != null)
812 {
813 <a href="@(enableContactlink.Url)" class="btn btn-tertiary flex-fill" title="@HttpUtility.HtmlAttributeEncode(Translate("Contact us about price and delivery >>"))">
814 @Translate("Contact us about price and delivery >>")
815 </a>
816 }
817 }
818
819 @if (enableBookDemo)
820 {
821 var enableBookDemolink = Pageview.AreaSettings.GetItem("CustomSettings").GetLink("Custom_EnableBookDemoLink");
822 if (enableBookDemolink != null)
823 {
824 <a href="@(enableBookDemolink.Url)" class="btn btn-tertiary flex-fill" title="@HttpUtility.HtmlAttributeEncode(Translate("Contact us about book demo >>"))">
825 @Translate("Contact us about book demo >>")
826 </a>
827 }
828 }
829
830 @if (!disablePurchase)
831 {
832 if (!specs.GetByKey("Preorder").Value.IsYes())
833 {
834 <button type="button" onclick="swift.Cart.Update(event)" class="btn btn-primary flex-fill js-add-to-cart-button" title="@HttpUtility.HtmlAttributeEncode(Translate("Add to cart"))" id="AddToCartButton@(product.Id)">
835 @Translate("Add to cart")
836 </button>
837 }
838 else
839 {
840 <button type="button" class="btn btn-secondary flex-fill" onclick="Form(this).addData('@HttpUtility.HtmlEncode(product.Name)','@product.Id')" data-bs-toggle="modal" data-bs-target="#preorderModal">
841 @Translate("Preorder")
842 </button>
843 }
844 }
845 </div>
846 @*//CUSTOM*@
847 </form>
848 </div>
849
850 </div>
851
852 </div>
853 </article>
854 }
855 </div>
856
857 <div class="my-3" id="LoadMoreButton">
858 <div class="text-center d-flex flex-column gap-3">
859 <div class="opacity-85">@loadedProducts @Translate("out of") @productList.TotalProductsCount @Translate("products")</div>
860 @if (productList.PageCount != 1)
861 {
862 string sortBySelection = Dynamicweb.Context.Current.Request?.Form["SortBy"] ?? "";
863 sortBySelection = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("SortBy")) ? Dynamicweb.Context.Current.Request.QueryString.Get("SortBy") : sortBySelection;
864
865 string searchQuery = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("q")) ? Dynamicweb.Context.Current.Request.QueryString.Get("q") : "";
866 string searchLayout = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("SearchLayout")) ? Dynamicweb.Context.Current.Request.QueryString.Get("SearchLayout") : "";
867
868 <form method="get" action="@url" data-response-target-element="content" class="w-100">
869 @foreach (FacetGroupViewModel facetGroup in productList.FacetGroups)
870 {
871 foreach (FacetViewModel facetItem in facetGroup.Facets)
872 {
873 foreach (FacetOptionViewModel facetOption in facetItem.Options)
874 {
875 if (facetOption.Selected)
876 {
877 <input type="hidden" name="@facetItem.QueryParameter" value="[@facetOption.Value]">
878 }
879 }
880 }
881 }
882
883 <input type="hidden" name="PageSize" value="@pageSize">
884 <input type="hidden" name="SortBy" value="@sortBySelection">
885 <input type="hidden" name="RequestType" value="UpdateList">
886
887 @if (!string.IsNullOrEmpty(searchQuery))
888 {
889 <input type="hidden" name="q" value="@HttpUtility.HtmlAttributeEncode(searchQuery)">
890 <input type="hidden" name="SearchLayout" value="@searchLayout">
891 }
892
893 @{
894 string nextPageLink = "/Default.aspx?ID=" + Pageview.Page.ID + "&PageSize=" + pageSize + "&SortBy=" + sortBySelection;
895
896 foreach (FacetGroupViewModel facetGroup in productList.FacetGroups)
897 {
898 foreach (FacetViewModel facetItem in facetGroup.Facets)
899 {
900 foreach (FacetOptionViewModel facetOption in facetItem.Options)
901 {
902 if (facetOption.Selected)
903 {
904 nextPageLink += "&" + facetItem.QueryParameter + "=[" + facetOption.Value + "]";
905 }
906 }
907 }
908 }
909
910 nextPageLink += !string.IsNullOrEmpty(searchQuery) ? "&q=" + searchQuery : "";
911 }
912
913 <a href="@nextPageLink" class="btn btn-primary" onclick="swift.ProductList.Update(event)" id="LoadMoreButton_@Model.ID">@Translate("Load more products")</a>
914 </form>
915 }
916 </div>
917 </div>
918 }
919 else
920 {
921 if (!Pageview.IsVisualEditorMode)
922 {
923 <div class="alert alert-dark m-0">
924 @Translate("We did not find anything matching your search result")
925 </div>
926 }
927 else
928 {
929 <div class="alert alert-dark m-0" role="alert">
930 <span>@Translate("Product list: The list will be shown here, if any")</span>
931 </div>
932 }
933 }
934 }
935
936 @helper RenderField(FieldValueViewModel field)
937 {
938 string fieldValue = field?.Value != null ? field.Value.ToString() : "";
939
940 if (fieldValue != "")
941 {
942 fieldValue = fieldValue == "False" ? Translate("No") : fieldValue;
943 fieldValue = fieldValue == "True" ? Translate("Yes") : fieldValue;
944
945 if (field.Value.GetType() == typeof(System.Collections.Generic.List<FieldOptionValueViewModel>))
946 {
947 fieldValue = "";
948
949 foreach (FieldOptionValueViewModel option in field.Value as System.Collections.Generic.List<FieldOptionValueViewModel>)
950 {
951 fieldValue = option.Name;
952 }
953 }
954
955 bool isColor = false;
956 if (fieldValue.Contains("#") && (Translate(field.Name) == Translate("Color") || Translate(field.Name) == Translate("Colour")))
957 {
958 isColor = true;
959 }
960
961 if (!string.IsNullOrEmpty(fieldValue))
962 {
963 if (!isColor)
964 {
965 <li>@(field.Name): @fieldValue</li>
966 }
967 else
968 {
969 <li class="position-relative">
970 <span class="colorbox-sm" style="background-color: @fieldValue"></span>
971 </li>
972 }
973 }
974 }
975 }
976