Error executing template "/Designs/Swift/Paragraph/Swift_ProductListGridView_Custom.cshtml"
System.InvalidCastException: Unable to cast object of type 'System.DBNull' to type 'System.String'.
at NLWI.Platforms.Dynamicweb9.Specs.Repositories.ProductSpecificationRepository.GetSpecificationObjectIdentifiers(HashSet`1 autoIds)
at NLWI.Platforms.Dynamicweb9.Specs.Repositories.ProductSpecificationRepository.GetProductAutoIdMap(HashSet`1 autoIds)
at NLWI.Platforms.Dynamicweb9.Specs.Services.CachedProductSpecificationService.<GetAutoIdMap>b__14_1(HashSet`1 k)
at NLWI.Core.Platform.Caching.DictionaryCaching.GetOrInsert[TKey,TVal](HashSet`1 keys, Func`2 buildCacheKey, Func`2 lookupMethod, CacheOptions cacheOptions)
at NLWI.Platforms.Dynamicweb9.Specs.Services.CachedProductSpecificationService.GetAutoIdMap(HashSet`1 productAutoIds)
at NLWI.Platforms.Dynamicweb9.Specs.Services.CachedProductSpecificationService.GetProductSpecifications(HashSet`1 productAutoIds)
at NLWI.Platforms.Dynamicweb9.Specs.Services.CachedProductSpecificationService.GetProductSpecifications(Int64 productAutoId)
at CompiledRazorTemplates.Dynamic.RazorEngine_dfcffeb1681942f4b6ce2dcd36fefbb6.<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 298
at CompiledRazorTemplates.Dynamic.RazorEngine_dfcffeb1681942f4b6ce2dcd36fefbb6.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
377 var productDiscountInfo = Custom.Discounts.Services.DiscountInfoService.GetDiscountExtenderInfoByProductId(product.Id)?.Where(x => !string.IsNullOrEmpty(x.CampaignLabel));
378 //--CUSTOM
379
380 <article class="position-relative@(productTheme) product-list-item js-product @liveInfoClass" data-product-id="@product.Id" itemscope itemtype="https://schema.org/Product">
381
382 @if (specs.HasKey("Ikon" + featurePostFix)
383 || (productDiscountInfo != null && productDiscountInfo.Any()))
384 {
385 <div class="product-label">
386 @foreach (var label in specs.GetAllByKey("Ikon" + featurePostFix))
387 {
388 var referenceSpecfication = label.GetReferenceSpecification();
389 var txtcolor = "#ffffff";
390 if (!string.IsNullOrEmpty(referenceSpecfication.GetByKey("LinkFarvekode").Value))
391 {
392 txtcolor = referenceSpecfication.GetByKey("LinkFarvekode").Value;
393 }
394 if (referenceSpecfication.HasKey("LinkURL"))
395 {
396 <a href="@(referenceSpecfication.GetByKey("LinkURL").Value)" class="product-label-item" style="background-color:@(referenceSpecfication.GetByKey("Farvekode").Value);color:@txtcolor;">
397 @referenceSpecfication.GetByKey("Label").Value
398 </a>
399 }
400 else
401 {
402 <span class="product-label-item" style="background-color:@(referenceSpecfication.GetByKey("Farvekode").Value);color:@txtcolor;">
403 @referenceSpecfication.GetByKey("Label").Value
404 </span>
405 }
406 }
407
408 @*CUSTOM*@
409 @if (productDiscountInfo != null)
410 {
411 foreach (var discount in productDiscountInfo)
412 {
413 <span class="product-label-item" style="background-color: @discount.CampaignLabelColor; color: @(!string.IsNullOrEmpty(discount.CampaignLabelTextColor) ? discount.CampaignLabelTextColor : "#ffffff");">
414 @discount.CampaignLabel
415 </span>
416 }
417 }
418 @*CUSTOM*@
419
420 </div>
421 }
422
423 @if (!anonymousUser)
424 {
425 if (!hideFavoritesSelector && product.VariantInfo.VariantInfo == null)
426 {
427 <div class="position-absolute top-0 end-0 my-3" style="z-index: 2">
428 @RenderPartial("Components/ToggleFavorite.cshtml", product, favoriteParameters)
429 </div>
430 }
431 else if (showFavoritesSelectorMasterProduct)
432 {
433 <div class="position-absolute top-0 end-0 my-3" style="z-index: 2">
434 @RenderPartial("Components/ToggleFavorite.cshtml", product, favoriteParameters)
435 </div>
436 }
437 }
438
439 @if (showBadges)
440 {
441 <div class="position-absolute top-0 left-0 p-1 p-lg-2 ps-0 ps-lg-0" style="z-index: 2">
442 @RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)
443 </div>
444 }
445
446 <div class="d-flex flex-column d-block h-100 text-center">
447 @{
448 string clickProductLink = string.Empty;
449 if (allowTracking)
450 {
451 clickProductLink = "onclick=\"return clickProductLink('" + @product.Id + "', '" + @product.Name + "', '" + @product.VariantName + "', '" + @product.Price.CurrencyCode + "', '" + @product.Price.Price + "')\"";
452 }
453 }
454 <a href="@link" class="text-decoration-none d-flex flex-column" @clickProductLink>
455 @if (allowTracking)
456 {
457 <script>
458 function clickProductLink(productId, productName, productVariant, productCurrency, productPrice) {
459 if (typeof gtag !== "undefined") {
460 gtag("event", "select_item", {
461 item_list_id: "product_list_gridview",
462 item_list_name: "Product list (Gridview)",
463 items: [
464 {
465 item_id: productId,
466 item_name: productName,
467 currency: productCurrency,
468 item_list_id: "product_list_gridview",
469 item_list_name: "Product list (Gridview)",
470 item_variant: productVariant,
471 price: productPrice
472 }
473 ]
474 });
475 }
476 }
477 </script>
478 }
479
480 <div class="@productThemePadding order-2">
481 <div class="flex-grow-1">
482 <h3 class="h6 mb-0 text-break" itemprop="name">
483 @product.Name @if (!string.IsNullOrEmpty(product.VariantName))
484 {<text>(@product.VariantName)</text>}
485 </h3>
486 @if (!Model.Item.GetBoolean("HideProductNumber"))
487 {
488 <p class="fs-7 opacity-85 mb-2">@product.Number</p>
489 }
490 @if (mainFeatures.Count > 0)
491 {
492 <ul class="p-0 lh-sm opacity-75" style="list-style-position: inside">
493 @foreach (CategoryFieldViewModel mainFeatureGroup in mainFeatures)
494 {
495 foreach (var field in mainFeatureGroup.Fields)
496 {
497 @RenderField(field.Value)
498 }
499 }
500 </ul>
501 }
502 </div>
503 </div>
504
505 <div class="overflow-hidden order-1 @(imageTheme)" @imageOutlineStyle>
506 <div class="ratio" style="@(ratioVariable)">
507 <div class="d-flex justify-content-center align-items-center">
508 @if (string.IsNullOrEmpty(alternativeImage))
509 {
510 <img id="@imageId" itemprop="image"
511 srcset="
512 @imagePathXs 480w,
513 @imagePathS 640w"
514 sizes="(min-width: 992px) 33vw, 50vw"
515 src="@imagePathFallBack"
516 loading="lazy"
517 decoding="async"
518 class="mw-100 mh-100 @imageThemePadding"
519 alt="@product.Name">
520 }
521 else
522 {
523 <img id="@imageId" itemprop="image"
524 src="@imagePathFallBack"
525 loading="lazy"
526 decoding="async"
527 class="mw-100 mh-100 @imageThemePadding"
528 alt="@product.Name"
529 onmouseover="this.src='@alternativeImage'"
530 onmouseout="this.src='@imagePathFallBack'">
531 }
532 </div>
533 </div>
534
535 @if (product.VariantInfo.VariantInfo != null && staticVariantsLayout == "images")
536 {
537 int variantGroupCount = 0;
538 int showMaxVariantGroups = 2;
539 int showMaxVariants = 3;
540 var productVariantTheme = productTheme != "" ? productTheme : "bg-white";
541
542 <div class="position-relative">
543 <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;">
544
545 @foreach (var variantGroup in product.VariantGroups())
546 {
547 int variantsCount = 0;
548
549 <div class="d-flex gap-2 mb-2">
550 @foreach (var variant in variantGroup.Options)
551 {
552 if (variantGroupCount < showMaxVariantGroups)
553 {
554 var optionsCount = variantGroup.Options.Count();
555
556 if (variantsCount < showMaxVariants)
557 {
558 string optionWidth = !string.IsNullOrEmpty(variant.Color) ? "w-25" : "";
559
560 <article class="static-variants-option @optionWidth @(productVariantTheme)" title="@product.Name @variant.Name" style="pointer-events: initial;">
561 @if (!string.IsNullOrEmpty(variant.Color))
562 {
563 string defaultProductImage = Dynamicweb.Context.Current.Server.UrlEncode(product.DefaultImage.Value);
564 string variantImage = Dynamicweb.Context.Current.Server.UrlEncode(variant.Image.Value);
565 string defaultPrice = !hidePrice ? product.Price.PriceFormatted : "0";
566 string variantPrice = !hidePrice ? product.Price.PriceFormatted : "0";
567
568 if (isLazyLoadingForProductInfoEnabled)
569 {
570 <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')">
571 <div class="d-flex align-items-center justify-content-center">
572 <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">
573 </div>
574 </figure>
575 }
576 else
577 {
578 <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')">
579 <div class="d-flex align-items-center justify-content-center">
580 <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">
581 </div>
582 </figure>
583 }
584 }
585 else
586 {
587 <div class="d-flex align-items-center justify-content-center">
588 @variant.Name
589 </div>
590 }
591 <div class="visually-hidden">
592 <h4>@product.Name, @variant.Name</h4>
593 @if (!hidePrice)
594 {
595 if (isLazyLoadingForProductInfoEnabled)
596 {
597 <span class="text-price js-text-price"></span>
598 }
599 else
600 {
601 <span class="text-price">@product.Price.PriceFormatted</span>
602 }
603 }
604 </div>
605 </article>
606 }
607
608 variantsCount++;
609
610 if (variantsCount == showMaxVariants && optionsCount != showMaxVariants)
611 {
612 int left = optionsCount - showMaxVariants;
613 <div class="variant-option ms-1 d-flex justify-content-center align-items-center">
614 <span>+@left</span>
615 </div>
616 }
617 }
618 }
619 </div>
620
621 variantGroupCount++;
622 }
623 </div>
624 </div>
625 }
626 </div>
627 </a>
628 <div class="@productThemePadding mt-auto pt-0">
629
630 @if (!disableStock) //CUSTOM
631 {
632 <div>
633 @if (inStock && availableStockLocations.Any())
634 {
635 <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>
636 }
637 else
638 {
639 <span class="icon-stock icon-red position-relative">@Translate("label_not_in_stock", "Ikke på lager")</span>
640 }
641 </div>
642
643 <script>
644 window.addEventListener('DOMContentLoaded', function () {
645 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
646 var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
647 return new bootstrap.Tooltip(tooltipTriggerEl)
648 })
649 }, false);
650 </script>
651 }
652
653 @if (!hidePrice && !disablePrice) //CUSTOM
654 {
655 string priceMin = "";
656 string priceMax = "";
657
658 <div class="mt-2">
659 <div itemprop="offers" itemscope itemtype="https://schema.org/Offer">
660 <span itemprop="priceCurrency" content="@product.Price.CurrencyCode" class="d-none"></span>
661
662 @if (showPricesWithVat == "false" && !neverShowVat)
663 {
664 if (isLazyLoadingForProductInfoEnabled)
665 {
666 <span itemprop="price" content="" class="d-none"></span>
667 <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>
668 }
669 else
670 {
671 string beforePrice = product.PriceBeforeDiscount.PriceWithoutVatFormatted;
672 string informativePrice = product.PriceInformative.PriceWithoutVatFormatted;
673
674 <span itemprop="price" content="@product.Price.PriceWithoutVat.ToString("0.00", CultureInfo.InvariantCulture)" class="d-none"></span>
675 if (product.Price.Price != product.PriceBeforeDiscount.Price)
676 {
677 <span class="text-decoration-line-through opacity-75 me-3 text-price">@beforePrice</span>
678 }
679 else if (product.PriceInformative.Price > 0)
680 {
681 <span class="text-decoration-line-through opacity-75 me-3 text-price">@informativePrice</span>
682 }
683 }
684 }
685 else
686 {
687 if (isLazyLoadingForProductInfoEnabled)
688 {
689 <span itemprop="price" content="" class="d-none"></span>
690 <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>
691 }
692 else
693 {
694 string beforePrice = product.PriceBeforeDiscount.PriceFormatted;
695 string informativePrice = product.PriceInformative.PriceWithVatFormatted;
696
697 <span itemprop="price" content="@product.Price.Price.ToString("0.00", CultureInfo.InvariantCulture)" class="d-none"></span>
698 if (product.Price.Price != product.PriceBeforeDiscount.Price)
699 {
700 <span class="text-decoration-line-through opacity-75 me-3 text-price">@beforePrice</span>
701 }
702 else if (product.PriceInformative.Price > 0)
703 {
704 <span class="text-decoration-line-through opacity-75 me-3 text-price">@informativePrice</span>
705 }
706 }
707 }
708
709 @if (showPricesWithVat == "false" && !neverShowVat)
710 {
711 if (isLazyLoadingForProductInfoEnabled)
712 {
713 <span class="text-price js-text-price"><div class="spinner-border" role="status"></div></span>
714 }
715 else
716 {
717 string price = product.Price.PriceWithoutVatFormatted;
718 if (product?.VariantInfo?.VariantInfo != null)
719 {
720 priceMin = product?.VariantInfo?.PriceMin?.PriceWithoutVatFormatted != null ? product.VariantInfo.PriceMin.PriceWithoutVatFormatted : "";
721 priceMax = product?.VariantInfo?.PriceMax?.PriceWithoutVatFormatted != null ? product.VariantInfo.PriceMax.PriceWithoutVatFormatted : "";
722 }
723 if (priceMin != priceMax)
724 {
725 price = priceMin + " - " + priceMax;
726 }
727 <span class="text-price">@price</span>
728 }
729 }
730 else
731 {
732 if (isLazyLoadingForProductInfoEnabled)
733 {
734 <span class="text-price js-text-price"><div class="spinner-border" role="status"></div></span>
735 }
736 else
737 {
738 string price = product.Price.PriceFormatted;
739 if (product?.VariantInfo?.VariantInfo != null)
740 {
741 priceMin = product?.VariantInfo?.PriceMin?.PriceFormatted != null ? product.VariantInfo.PriceMin.PriceFormatted : "";
742 priceMax = product?.VariantInfo?.PriceMax?.PriceFormatted != null ? product.VariantInfo.PriceMax.PriceFormatted : "";
743 }
744 if (priceMin != priceMax)
745 {
746 price = priceMin + " - " + priceMax;
747 }
748 <span class="text-price">@price</span>
749 }
750 }
751 </div>
752 @if (showPricesWithVat == "false" && !neverShowVat)
753 {
754 if (isLazyLoadingForProductInfoEnabled)
755 {
756 <div class="fs-7 opacity-85 text-price js-text-price-with-vat d-none" data-suffix="@Translate("Incl. VAT")"></div>
757 }
758 else
759 {
760 string price = product.Price.PriceWithVatFormatted;
761 if (product?.VariantInfo?.VariantInfo != null)
762 {
763 priceMin = product?.VariantInfo?.PriceMin?.PriceWithVatFormatted != null ? product.VariantInfo.PriceMin.PriceWithVatFormatted : "";
764 priceMax = product?.VariantInfo?.PriceMax?.PriceWithVatFormatted != null ? product.VariantInfo.PriceMax.PriceWithVatFormatted : "";
765 }
766 if (priceMin != priceMax)
767 {
768 price = priceMin + " - " + priceMax;
769 }
770 <div class="fs-7 opacity-85 text-price">@price @Translate("Incl. VAT")</div>
771 }
772 }
773 </div>
774 }
775
776 <div class="icon-compare mt-2 d-none d-sm-flex justify-content-center align-items-center fs-7"
777 data-number="@HttpUtility.HtmlAttributeEncode(product.Number)"
778 data-image="@HttpUtility.HtmlAttributeEncode(imagePath)"
779 data-name="@HttpUtility.HtmlAttributeEncode(product.Name)"
780 data-link="@HttpUtility.HtmlAttributeEncode(link)"
781 onclick="toggleCompareProduct(event, this)">
782 <span class="square-icon js-square-icon d-flex" data-product-number="@HttpUtility.HtmlAttributeEncode(product.Number)"></span>
783 <span>@Translate("Sammenlign")</span>
784 </div>
785
786 @if (product.VariantInfo.VariantInfo != null && staticVariantsLayout == "swatches")
787 {
788 var optionCount = product.VariantInfo.VariantInfo.Count();
789 var showMaxVariants = 5;
790
791 <div class="d-flex flex-row gap-1 align-items-center">
792 @foreach (VariantInfoViewModel variant in product.VariantInfo.VariantInfo.Take(showMaxVariants))
793 {
794 <span class="colorbox colorbox-sm rounded-circle border me-1" style="background-color: @variant.OptionColor"></span>
795 }
796 @if (optionCount > showMaxVariants)
797 {
798 int left = optionCount - showMaxVariants;
799 <span class="ms-2">+@left</span>
800 }
801 </div>
802 }
803
804 <div class="mt-3">
805 <form method="post" action="@cartUrl">
806 <input type="hidden" name="redirect" value="false" />
807 <input type="hidden" name="ProductId" value="@product.Id" />
808 <input type="hidden" name="ProductName" value="@HtmlEncoder.HtmlEncode(product.Name)" />
809 <input type="hidden" name="ProductVariantName" value="@HtmlEncoder.HtmlEncode(product.VariantName)" />
810 <input type="hidden" name="ProductCurrency" value="@Dynamicweb.Ecommerce.Common.Context.Currency.Code" />
811 <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))" />
812 <input type="hidden" name="ProductReferer" value="component_ProductListGridView_Custom">
813 <input type="hidden" name="cartcmd" value="add" />
814 <input type="hidden" name="quantity" value="1" />
815 <input type="hidden" class="js-product-id" value="@product.Id" />
816
817 @if (!string.IsNullOrEmpty(product.VariantId))
818 {
819 <input type="hidden" name="VariantId" value="@product.VariantId" />
820 }
821
822 @*//CUSTOM*@
823 <div class="d-flex flex-column gap-2">
824 @if (enableContact)
825 {
826 var enableContactlink = Pageview.AreaSettings.GetItem("CustomSettings").GetLink("Custom_EnableContactLink");
827 if (enableContactlink != null)
828 {
829 <a href="@(enableContactlink.Url)" class="btn btn-tertiary flex-fill" title="@HttpUtility.HtmlAttributeEncode(Translate("Contact us about price and delivery >>"))">
830 @Translate("Contact us about price and delivery >>")
831 </a>
832 }
833 }
834
835 @if (enableBookDemo)
836 {
837 var enableBookDemolink = Pageview.AreaSettings.GetItem("CustomSettings").GetLink("Custom_EnableBookDemoLink");
838 if (enableBookDemolink != null)
839 {
840 <a href="@(enableBookDemolink.Url)" class="btn btn-tertiary flex-fill" title="@HttpUtility.HtmlAttributeEncode(Translate("Contact us about book demo >>"))">
841 @Translate("Contact us about book demo >>")
842 </a>
843 }
844 }
845
846 @if (!disablePurchase)
847 {
848 if (!specs.GetByKey("Preorder").Value.IsYes())
849 {
850 <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)">
851 @Translate("Add to cart")
852 </button>
853 }
854 else
855 {
856 <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">
857 @Translate("Preorder")
858 </button>
859 }
860 }
861 </div>
862 @*//CUSTOM*@
863 </form>
864 </div>
865
866 </div>
867
868 </div>
869 </article>
870 }
871 </div>
872
873 <div class="my-3" id="LoadMoreButton">
874 <div class="text-center d-flex flex-column gap-3">
875 <div class="opacity-85">@loadedProducts @Translate("out of") @productList.TotalProductsCount @Translate("products")</div>
876 @if (productList.PageCount != 1)
877 {
878 string sortBySelection = Dynamicweb.Context.Current.Request?.Form["SortBy"] ?? "";
879 sortBySelection = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("SortBy")) ? Dynamicweb.Context.Current.Request.QueryString.Get("SortBy") : sortBySelection;
880
881 string searchQuery = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("q")) ? Dynamicweb.Context.Current.Request.QueryString.Get("q") : "";
882 string searchLayout = !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("SearchLayout")) ? Dynamicweb.Context.Current.Request.QueryString.Get("SearchLayout") : "";
883
884 <form method="get" action="@url" data-response-target-element="content" class="w-100">
885 @foreach (FacetGroupViewModel facetGroup in productList.FacetGroups)
886 {
887 foreach (FacetViewModel facetItem in facetGroup.Facets)
888 {
889 foreach (FacetOptionViewModel facetOption in facetItem.Options)
890 {
891 if (facetOption.Selected)
892 {
893 <input type="hidden" name="@facetItem.QueryParameter" value="[@facetOption.Value]">
894 }
895 }
896 }
897 }
898
899 <input type="hidden" name="PageSize" value="@pageSize">
900 <input type="hidden" name="SortBy" value="@sortBySelection">
901 <input type="hidden" name="RequestType" value="UpdateList">
902
903 @if (!string.IsNullOrEmpty(searchQuery))
904 {
905 <input type="hidden" name="q" value="@HttpUtility.HtmlAttributeEncode(searchQuery)">
906 <input type="hidden" name="SearchLayout" value="@searchLayout">
907 }
908
909 @{
910 string nextPageLink = "/Default.aspx?ID=" + Pageview.Page.ID + "&PageSize=" + pageSize + "&SortBy=" + sortBySelection;
911
912 foreach (FacetGroupViewModel facetGroup in productList.FacetGroups)
913 {
914 foreach (FacetViewModel facetItem in facetGroup.Facets)
915 {
916 foreach (FacetOptionViewModel facetOption in facetItem.Options)
917 {
918 if (facetOption.Selected)
919 {
920 nextPageLink += "&" + facetItem.QueryParameter + "=[" + facetOption.Value + "]";
921 }
922 }
923 }
924 }
925
926 nextPageLink += !string.IsNullOrEmpty(searchQuery) ? "&q=" + searchQuery : "";
927 }
928
929 <a href="@nextPageLink" class="btn btn-primary" onclick="swift.ProductList.Update(event)" id="LoadMoreButton_@Model.ID">@Translate("Load more products")</a>
930 </form>
931 }
932 </div>
933 </div>
934 }
935 else
936 {
937 if (!Pageview.IsVisualEditorMode)
938 {
939 <div class="alert alert-dark m-0">
940 @Translate("We did not find anything matching your search result")
941 </div>
942 }
943 else
944 {
945 <div class="alert alert-dark m-0" role="alert">
946 <span>@Translate("Product list: The list will be shown here, if any")</span>
947 </div>
948 }
949 }
950 }
951
952 @helper RenderField(FieldValueViewModel field)
953 {
954 string fieldValue = field?.Value != null ? field.Value.ToString() : "";
955
956 if (fieldValue != "")
957 {
958 fieldValue = fieldValue == "False" ? Translate("No") : fieldValue;
959 fieldValue = fieldValue == "True" ? Translate("Yes") : fieldValue;
960
961 if (field.Value.GetType() == typeof(System.Collections.Generic.List<FieldOptionValueViewModel>))
962 {
963 fieldValue = "";
964
965 foreach (FieldOptionValueViewModel option in field.Value as System.Collections.Generic.List<FieldOptionValueViewModel>)
966 {
967 fieldValue = option.Name;
968 }
969 }
970
971 bool isColor = false;
972 if (fieldValue.Contains("#") && (Translate(field.Name) == Translate("Color") || Translate(field.Name) == Translate("Colour")))
973 {
974 isColor = true;
975 }
976
977 if (!string.IsNullOrEmpty(fieldValue))
978 {
979 if (!isColor)
980 {
981 <li>@(field.Name): @fieldValue</li>
982 }
983 else
984 {
985 <li class="position-relative">
986 <span class="colorbox-sm" style="background-color: @fieldValue"></span>
987 </li>
988 }
989 }
990 }
991 }