Error executing template "Designs/Scanpan/eCom/Product/product.cshtml"
System.InvalidOperationException: Sequence contains no elements
at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
at CompiledRazorTemplates.Dynamic.RazorEngine_e44cf973f6624e789ceae3117dc918cc.Execute() in E:\dynamicweb.net\Solutions\scanpan-live\Files\Templates\Designs\Scanpan\eCom\Product\product.cshtml:line 328
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 System.Drawing
3 @using Dynamicweb
4 @using Dynamicweb.Admin
5 @using Dynamicweb.Data
6 @inherits RazorTemplateBase<RazorTemplateModel<Template>>
7 @using Dynamicweb.Rendering
8 @using NoZebra.Util
9 @using Dynamicweb.Content;
10 @using Dynamicweb.Content.Items;
11 @using System.Text.RegularExpressions;
12 @* Note that this file will be inserted directly into another one by Dynamicweb IncludeFile
13 before evaluating the Razor code, so keep the syntax right *@
14
15 @functions {
16
17 /**
18 * Custom date format from string input
19 * String needs to be in a date format, though
20 * @param {String} d
21 */
22 string FormatDate(string d) {
23 string output = d;
24 // Language
25 string lang = GetGlobalValue("Global:Area.Lang");
26 // Date format
27 string dateFormat = (lang == "da") ? "d. MMM. yyyy" : "MMM d yyyy";
28 // Try parsing string as DateTime
29 DateTime dateValue;
30 if (DateTime.TryParse(d, out dateValue)) {
31 output = dateValue.ToString(dateFormat);
32 }
33 // Format date input
34 // Return formatted date
35 return output;
36 }
37
38 /**
39 * Format number form string input
40 * Returns a clean number to be used in sorting loops
41 * @param {String} s
42 */
43 string FormatNumber(string s) {
44 Regex rgx = new Regex(@"[^0-9.,]"); // match everything but numbers, comma and period
45 // Replace all letters with whitespace
46 s = rgx.Replace(s, " ");
47 // Remove everything after the first space
48 // This takes care of values like "28 x 28 cm" and "20 l"
49 return s.Split(new[]{" "}, StringSplitOptions.None)[0];
50 }
51
52 /**
53 * Custom number formatting from string input
54 * @param {String} s
55 */
56 decimal NumberFromString(String s) {
57 decimal output = decimal.Parse("0"); // fallback
58 string inputFormatted = FormatNumber(s); // strip input of anything but numbers
59 // Check if item has a value in the field
60 if( !string.IsNullOrEmpty(inputFormatted) ) {
61 decimal parsed;
62 // Try parsing formatted input as a decimal
63 if (decimal.TryParse(inputFormatted, out parsed)) {
64 output = parsed;
65 }
66 }
67 return output;
68 }
69
70 /**
71 * Order product loop by size field
72 * @param {LoopItem} v
73 */
74 decimal OrderProductsBySize(LoopItem v) {
75 return NumberFromString(v.GetString("Ecom:Product:Field.SIZ"));
76 }
77
78 /**
79 * Order product loop by volume field
80 * @param {LoopItem} v
81 */
82 decimal OrderProductsByVolume(LoopItem v) {
83 return NumberFromString(v.GetString("Ecom:Product:Field.VOL"));
84 }
85
86 /**
87 * Check wether a product id exists in a list of ids
88 * @param {List<string>} list
89 * @param {String} productId
90 */
91 bool HasProduct(List<string> list, string productId) {
92 string productIdFormatted = "p_" + productId;
93 return list.Any(x => x == productIdFormatted);
94 }
95
96 /**
97 * Check wether a blog post page has a product selected
98 * in its related products field
99 * @param {Dynamicweb.Content.Page} blogPostAsPage
100 * @param {String} productId
101 */
102 bool BlogPostHasRelatedProduct(Page blogPostAsPage, string productId) {
103 // Get page item
104 Item blogItem = ItemManager.Storage.GetById("BlogPost", blogPostAsPage.ItemId.ToString());
105 // Get products from blog post fields
106 List<string> blogProducts = blogItem["Products"].ToString()
107 .Split(',')
108 .ToList();
109 // Test wether provided product number matches any in the field
110 return HasProduct(blogProducts, productId);
111 }
112
113 bool BlogPostIsRelated(List<LoopItem> currentPageCategories, List<LoopItem> compareCategories) {
114
115 foreach(var i in compareCategories) {
116 if(currentPageCategories.Any(x => (x.GetInteger("ItemPublisher:Item.Categories.Id") == i.GetInteger("ItemPublisher:Item.Categories.Id")))) {
117 return true;
118 }
119 }
120
121 // No match
122 return false;
123 }
124
125 string ConvertPriceToSouthAfricanFormat(string price, int areaId)
126 {
127 if (areaId == 28)
128 {
129 int place = price.LastIndexOf(',');
130
131 if (place == -1)
132 return price;
133
134 string result = price.Remove(place, 1).Insert(place, ".");
135 return result;
136 }
137 return price;
138 }
139
140 }
141
142 @{
143 string productNumber = GetString("Ecom:Product.Number");
144 string productColor = GetString("Ecom:Product:Field.COL");
145 string productSize = GetString("Ecom:Product:Field.SIZ");
146 string productBottomDiameter = GetString("Ecom:Product:Field.ItemBaseDiameter");
147 string productVolume = GetString("Ecom:Product:Field.VOL");
148 string productWeight = GetString("Ecom:Product.Weight");
149
150 // Getting list of products from product picker
151 List<string> selectedProducts = Pageview.Area.Item["ProductDetailCategories"]
152 .ToString()
153 .Split(',')
154 .ToList();
155 // Formatting product id to match that of the list items above
156 string productIdFormatted = "p_" + GetString("Ecom:Product.ID");
157 // Check if current product is in the list
158 bool hasDiameter = selectedProducts.IndexOf(productIdFormatted) == -1;
159
160 Dictionary<string, Dictionary<string, string>> details = new Dictionary<string, Dictionary<string, string>>() {
161 {
162 "weight", new Dictionary<string, string>(){
163 { "text", "Weight" },
164 { "description", null },
165 { "value", productWeight + " kg" },
166 { "is-empty", string.IsNullOrEmpty(productWeight).ToString() }
167 }
168 },
169 {
170 "product-number", new Dictionary<string, string>(){
171 { "text", "Product number" },
172 { "description", null },
173 { "value", productNumber },
174 { "is-empty", "False" }
175 }
176 }
177 };
178
179 /* Is pot or pan */
180 if(hasDiameter) {
181
182 /**
183 * Creating new dictionary to merge
184 * This way we can prepend the new values to the existing dictionary
185 */
186 Dictionary<string, Dictionary<string, string>> addValues = new Dictionary<string, Dictionary<string, string>>() {
187 {
188 "top-diameter", new Dictionary<string, string>(){
189 { "text", "Top diameter" },
190 { "description", Pageview.Area.Item["ProductDetailDiameterTopDescription"].ToString() },
191 { "value", productSize },
192 { "is-empty", string.IsNullOrEmpty(productSize).ToString() }
193 }
194 },
195 {
196 /* TBD: This is not currently in eCom */
197 "bottom-diameter", new Dictionary<string, string>(){
198 { "text", "Bottom diameter" },
199 { "description", Pageview.Area.Item["ProductDetailDiameterBottomDescription"].ToString() },
200 { "value", productBottomDiameter },
201 { "is-empty", string.IsNullOrEmpty(productBottomDiameter).ToString() }
202 }
203 }
204 };
205
206 /* Merge dictionaries */
207 details = addValues.Concat(details).ToDictionary(x=>x.Key,x=>x.Value);
208
209 /* Is not pot or pan */
210 } else {
211
212 /**
213 * Creating new dictionary to merge
214 * This way we can prepend the new values to the existing dictionary
215 */
216 Dictionary<string, Dictionary<string, string>> addValues = new Dictionary<string, Dictionary<string, string>>() {
217 {
218 /**
219 * NOTE:
220 * The same field is used for length and top diameter
221 * Pots/pans use diameter, knives use length
222 */
223 "length", new Dictionary<string, string>(){
224 { "text", "Length" },
225 { "description", Pageview.Area.Item["ProductDetailLengthDescription"].ToString() },
226 { "value", productSize },
227 { "is-empty", string.IsNullOrEmpty(productSize).ToString() }
228 }
229 },
230 {
231 "color", new Dictionary<string, string>(){
232 { "text", "Color" },
233 { "description", null },
234 { "value", productColor },
235 { "is-empty", string.IsNullOrEmpty(productColor).ToString() }
236 }
237 }
238 };
239
240 /* Merge dictionaries */
241 details = addValues.Concat(details).ToDictionary(x=>x.Key,x=>x.Value);
242 }
243 }
244
245
246 @SnippetStart("BodyClose")
247 <script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5418262f1f54ac79"></script>
248 @SnippetEnd("BodyClose")
249 @{
250 string productVariantId = GetString("Ecom:Product.VariantID");
251
252 SetMetaTags(productNumber, productVariantId);
253
254 // Whether eCom features should be hidden in the frontend
255 bool disableEcommerce = Boolean.Parse(Pageview.Area.Item["DisableEcommerce"].ToString());
256
257 string selectedVariantId = GetString("Ecom:Product.SelectedVariantComboID");
258
259 bool isMainProduct = string.IsNullOrEmpty(productVariantId) ? true : false;
260
261 string productName = GetString("Ecom:Product.Name");
262
263 var dimensions = productSize.Split('x');
264
265 string height = string.Empty;
266 string width = string.Empty;
267
268 if (dimensions.Count() > 1)
269 {
270 height = dimensions[0] + "cm";
271 width = dimensions[1];
272 }
273
274 string weight = productWeight + " kg";
275
276 // Price
277 var productPrice = GetString("Ecom:Product.Price.PriceWithVATFormatted");
278
279 var productNameAndVariant = GetString("Ecom:Product.Name") + ", " + GetString("Ecom:Product.SelectedVariantComboName");
280
281 var symbolsLoop = GetLoop("NZNIQSymbols");
282
283 string productVideo = GetString("Ecom:Product:Field.ProductVideo");
284
285 // Media product images
286 string mainImage = GetString("Ecom:Product:Field.ProductImage");
287
288 string imagePath;
289 List<string> imageIds = new List<string>();
290
291 if (!string.IsNullOrEmpty(mainImage))
292 {
293 imagePath = "https://cdn.scanpan.dk/Perfion/Image.aspx?id={{imageId}}&size=1600x1200";
294 imageIds.Add(mainImage);
295 string extraImages = GetString("Ecom:Product:Field.ProductExtraImages");
296 if (!string.IsNullOrEmpty(extraImages))
297 {
298 imageIds.AddRange(extraImages.Split(','));
299 }
300 }
301 else
302 {
303 imagePath = "/Files/Templates/Designs/Scanpan/assets/images/noimage.gif";
304 }
305
306 var numImages = imageIds.Count;
307
308 // Product guide
309
310 var guideId = GetString("Ecom:Product:Field.guide");
311 string guidePath = string.Empty;
312
313 if (!string.IsNullOrEmpty(guideId))
314 {
315 guidePath = string.Format("https://cdn.scanpan.dk/Perfion/File.aspx?id={0}&action=save", guideId);
316 }
317
318
319 // Get variants sorted by size (SIZ field)
320 var variants = GetLoop("VariantCombinations")
321 .OrderBy(v => OrderProductsBySize(v))
322 .ThenBy(v => OrderProductsByVolume(v))
323 .ThenBy(v => v.GetString("Ecom:Product:Field.COL"))
324 .ThenBy(v => v.GetDouble("Ecom:Product.Price.Price"))
325 .ToList();
326
327 // Price
328 var priceWithVat = isMainProduct ? variants.Take(1).First().GetDouble("Ecom:Product.Price.PriceWithVAT") : GetDouble("Ecom:Product.Price.PriceWithVAT");
329 var price = GetDouble("Ecom:Product.Price.PriceWithVAT");
330 // Get product price or lowest variant price
331 var priceFormatted = isMainProduct ? variants.Take(1).First().GetString("Ecom:Product.Price") : GetString("Ecom:Product.Price");
332 var vatPercentage = (GetDouble("Ecom:Product.Price.VATPercent") / 100) + 1;
333 var oldPrice = GetDouble("Ecom:Product:Field.ProductPriceBefore") * vatPercentage;
334 // See http://developer.dynamicweb-cms.com/api8/#Dynamicweb~Dynamicweb.Ecommerce.Prices.Price~GetDoublePriceFormatted.html
335 string oldPriceFormatted = string.Format("{0:0.00}", oldPrice);
336 var savingsPrice = oldPrice - price;
337 string savingsPriceFormatted = string.Format("{0:0.00}", savingsPrice);
338 //var savingsPriceFormatted = Dynamicweb.Ecommerce.Prices.Price.GetDoublePriceFormatted(savingsPrice, Dynamicweb.Ecommerce.Common.Context.Currency, false);
339 string googlePrice = price.ToString().Replace(",", ".");
340
341 double discountPercentage = Math.Round(100 - (100 / oldPrice * price));
342 string discountPercentageFormatted = discountPercentage.ToString() + "%";
343
344 // Add price text to main product
345 var MainProductText = isMainProduct ? "<span class='product__price__text'>From</span>" : null;
346
347 // Determine stock status
348 var stockStatus = GetString("Ecom:Product:Stock.ID");
349 var stockStatusText = GetString("Ecom:Product:Stock.Text");
350 var stockDeliveryText = string.Format("{0} {1}", GetString("Ecom:Product:Stock.DeliveryText"), GetString("Ecom:Product:Stock.DeliveryUnit"));
351 bool isOutOfStock = false;
352 string stockClassModifier = string.Empty;
353 string stockItemPropContent = string.Empty;
354 switch (stockStatus)
355 {
356 case "STOCKSTASTUSLINEID1":
357 case "LANGUAGEVALUE2":
358 stockClassModifier = "product__stock--in-stock";
359 stockItemPropContent = "in_stock";
360 break;
361 case "STOCKSTASTUSLINEID2":
362 case "LANGUAGEVALUE3":
363 stockClassModifier = "product__stock--limited-stock";
364 stockItemPropContent = "in_stock";
365 break;
366 case "STOCKSTASTUSLINEID3":
367 case "LANGUAGEVALUE4":
368 stockClassModifier = "product__stock--sold-out";
369 stockItemPropContent = "out_of_stock";
370 isOutOfStock = true;
371 break;
372 }
373
374 @* Only on Denmark ( area id 1 ) *@
375 var productId = GetString("Ecom:Product.Number");
376 var pageId = GetString("Ecom:Product.PrimaryOrCurrentPageID");
377 var groupId = GetString("Ecom:Product.PrimaryOrFirstGroupID");
378 var path = GetGlobalValue("Global:Pageview.Url").Replace('/', '#').Replace(".aspx", "");
379 var queryCount = path.Split('#').Count();
380 }
381 @if (isMainProduct)
382 {
383 // Disable out of stock
384 isOutOfStock = false;
385 }
386
387 <section class="product" itemscope itemtype="http://schema.org/Product">
388 <div itemprop="brand" itemscope itemtype="http://schema.org/Brand" content="Scanpan">
389 <meta itemprop="name" content="Scanpan">
390 </div>
391
392 @* productNumber found in eCom/partials/product-details.cshtml *@
393 <meta itemprop="sku" content="@productNumber">
394 <meta itemprop="gtin8" content="@productNumber">
395 <meta itemprop="height" content="@height">
396 <meta itemprop="width" content="@width">
397 <meta itemprop="weight" content="@weight">
398 <meta itemprop="description" content="@StripHTML(GetString("Ecom:Product.ShortDescription"))">
399
400 <div class="product__container">
401 <div class="product__container-left">
402 <h1 class="product__name product__name--mobile" itemprop="name">@productName</h1>
403 @if (imageIds.Any() || !string.IsNullOrEmpty(productVideo))
404 {
405 <div class="product__image royalSlider rsUni js-royal-slider is-loading">
406 @if (!string.IsNullOrEmpty(productVideo))
407 {
408 <div class="product__video-slide">
409 <video class="product__video-element js-product-video" autoplay muted controls playsinline>
410 <source src="https://cdn.scanpan.dk/Perfion/File.aspx?id=@productVideo" type="video/mp4">
411 Your browser does not support HTML5 video.
412 </video>
413 <span class="product__video-play js-product-video-play"></span>
414 <img width="85" class="rsTmb" src="/Files/Templates/Designs/Scanpan/assets/images/play-thumbnail.png" alt="@productNameAndVariant">
415 </div>
416 }
417 @foreach (string imageId in imageIds)
418 {
419 string imageSrc = imagePath.Replace("{{imageId}}", imageId);
420 <a class="rsImg" data-rsw="1600" data-rsh="1200" data-rsBigImg="@imageSrc?quality=75" href="@imageSrc.Replace("size=1600x1200", "size=588x441")">
421 <img itemprop="image" width="85" class="rsTmb" src="@imageSrc.Replace("size=1600x1200", "size=85x65")" alt="@productNameAndVariant">
422 </a>
423 }
424 </div>
425 }
426 @* Symbol splash *@
427 @if (symbolsLoop.FirstOrDefault(item => item.GetInteger("NZPriority") == 1) != null)
428 {
429 var splash = symbolsLoop.FirstOrDefault(item => item.GetInteger("NZPriority") == 1).GetString("NZLink");
430 <img class="splat" src="@splash?width=160&format=png&quality=80">
431 }
432 </div>
433 <div class="product__container-right">
434 <h1 class="product__name product__name--desktop" itemprop="name">@productName</h1>
435
436 <div class="product__info">
437 <div class="product__shop-info">
438 <form class="product__form js-product-form" autocomplete="off">
439 @* For adding this product to the cart by submitting the wrapping form *@
440 <input type="hidden" name="CartCmd" id="CartCmd" value="add">
441 @GetString("Ecom:Product.Form.Multi.HiddenFields")
442 <div class="product__form-field">
443 @foreach (LoopItem v in variants)
444 {
445 var variantId = v.GetString("Ecom:VariantCombination.VariantID");
446 if (isMainProduct)
447 {
448 variantId = productVariantId;
449 }
450
451 // Build custom variant text due to some variants missing variant text
452 List<string> variantTexts = new List<string>()
453 {
454 v.GetString("Ecom:Product:Field.SIZ"),
455 v.GetString("Ecom:Product:Field.VOL"),
456 v.GetString("Ecom:Product:Field.COL")
457 };
458
459 var variantText = string.Join(", ", variantTexts.Where(item => !string.IsNullOrEmpty(item)));
460
461 if (String.IsNullOrWhiteSpace(variantText))
462 {
463 continue;
464 }
465
466 bool isChecked = (variantId == selectedVariantId);
467 string isCheckedText = isChecked ? "checked" : null;
468 string isActiveText = isChecked ? "active" : null;
469
470 <div class="product__form-radio-container radio-container js-radio-container @isActiveText">
471 <input type="radio" checked="@isCheckedText" id="@variantText" class="product__form-radio js-product-variant-radio" data-variantlink="@v.GetString("Ecom:VariantCombination.Link.Clean")" name="variant" value="@variantId">
472 <label class="product__form-radio-label" for="@variantText"><span class="product__form-radio-label--text">@variantText.Replace("cm,", "cm<br>")</span></label>
473 </div>
474 }
475 </div>
476 <div class="product__usp">
477 <ul class="product__usp-list">
478 <li class="product__usp-item">Leveringstid 2-5 virkedager</li>
479 <li class="product__usp-item">Gratis frakt fra 500 kr.</li>
480 <li class="product__usp-item">Dansk håndverk siden 1956</li>
481 </ul>
482 </div><!-- .product__usps -->
483 @if (!disableEcommerce)
484 {
485 <div class="product__offer" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
486 <link itemprop="url" href="@GetGlobalValue("Global:Pageview.Url")" />
487 <meta itemprop="priceCurrency" content="@GetString("Ecom:Product.Currency.Code")">
488 <meta itemprop="price" content="@googlePrice" />
489
490 <meta itemprop="availability" content="@(isOutOfStock ? "https://schema.org/OutOfStock" : "https://schema.org/InStock")" />
491
492 <div class="product__price">
493 @if (oldPrice > 0)
494 {
495 <div class="product__price__current">@MainProductText@priceFormatted</div>
496 <span class="product__price__old">@oldPriceFormatted</span>
497 <span class="product__price__savings">Save @savingsPriceFormatted</span>
498
499 }
500 else
501 {
502 <div class="product__price__current">@MainProductText@priceFormatted</div>
503 }
504 </div>
505 @* Alin *@
506 <div>
507 @* Alin -> TODO: Add translation and link to the Shipping terms page @Translate(InklMwStZzgl, "inkl. MwSt. Zzgl.", global) Translate(VersandKosten, "Versandkosten", global) *@
508 @{
509 var ShippingInfoPage = Pageview.Area.Item["ShippingInfoPage"];
510 if (ShippingInfoPage != null && !string.IsNullOrEmpty(ShippingInfoPage.ToString()))
511 {
512 <p class="product-list__extra-text" style="font-size: 0.8em"> <a href="@ShippingInfoPage"> Frakt</a></p>
513 }
514 }
515 </div>
516 <div class="product__stock-wrapper">
517 <span class="product__stock @stockClassModifier">@stockStatusText </span>
518 </div>
519 <div class="product__add-to-cart-container">
520 @*<input class="product__quantity js-quantity-input" type="number" name="Quantity" value="1" />*@
521 <input type="hidden" name="Quantity" value="1" />
522 <button class="js-ga-four-add-to-cart product__form-submit product__add-to-cart js-fbq-add-to-cart @(isMainProduct ? "js-add-to-cart" : null)" type="submit" disabled="@isOutOfStock">Legg i handlekurv@(discountPercentage > 0 ? " (-" + discountPercentageFormatted + ")" : "")</button>
523 </div>
524 <style>
525 </style>
526 @if (!isOutOfStock)
527 {
528 <div class="product__availability">
529 @* Render global message inside product info *@
530 @{
531 string theme = GetString("Item.Area.GlobalMessageTheme");
532
533 int campaignDuration = GetInteger("Item.Area.GlobalMessageCampaignDuration") > 0 ? GetInteger("Item.Area.GlobalMessageCampaignDuration") : 30;
534
535 // Setting default global message
536
537 string name = "defaultMessage";
538
539 int hour = int.Parse(Translate("product-countdown-hour", "13"));
540
541 string message = Translate("product-countdown-default", "Modtag i morgen,");
542
543 DateTime current = DateTime.Now;
544
545 DateTime endDate = DateTime.Now.Date.Add(new TimeSpan(hour, 0, 0));
546
547 bool hideCountDown = current.DayOfWeek == DayOfWeek.Friday && current.TimeOfDay.TotalSeconds >= 60 * 60 * hour
548 || current.DayOfWeek == DayOfWeek.Saturday
549 || current.DayOfWeek == DayOfWeek.Sunday && current.TimeOfDay.TotalSeconds <= 60 * 60 * hour;
550
551 if (hideCountDown)
552 {
553 message = Translate("product-countdown-weekend", "Vi sender din ordre på mandag kl. 13");
554 }
555
556 if (current.TimeOfDay.TotalSeconds > 60 * 60 * hour)
557 {
558 endDate = endDate.AddDays(1); // Hour is passed 13. Adding one day to endDate
559 }
560
561 bool hideDays = false;
562
563 // Overruling default values when activated from website settings
564
565 DateTime campaignEndDate = GetDate("Item.Area.CampaignEndDate");
566
567 bool globalMessageIsActive = GetBoolean("Item.Area.GlobalMessageIsActive");
568
569 if (globalMessageIsActive && campaignEndDate >= DateTime.Now)
570 {
571 message = GetString("Item.Area.GlobalMessageText");
572 endDate = campaignEndDate;
573 hideCountDown = GetBoolean("Item.Area.HideCountDown");
574 if (!string.IsNullOrEmpty(GetString("Item.Area.GlobalMessageCampaignName")))
575 {
576 name = GetString("Item.Area.GlobalMessageCampaignName");
577 }
578 }
579 else
580 {
581 hideDays = true;
582 }
583
584 // Do not render section if cookie set (message dismissed) and area id not equals 1 or 30 or 34
585 if (Dynamicweb.Frontend.PageView.Current().Area.ID == 1 || Dynamicweb.Frontend.PageView.Current().Area.ID == 30 || Dynamicweb.Frontend.PageView.Current().Area.ID == 34)
586 {
587 <!--googleoff: all-->
588 <div class="product__timer js-count-down-active-state" data-id="@name" data-expires="@campaignDuration">
589 <div class="product__timer-content">
590 <div class="product__timer-text">@message</div>
591 @if (!hideCountDown)
592 {
593 <div class="product__timer-counter js-count-down" data-end-date="@(endDate.ToString("dd-MM-yyyy HH:mm:ss"))">
594 <span>@Translate("product-countdown-text", "bestil inden")</span>
595 @if (!hideDays)
596 {
597 <span class="js-count-down-days"></span><span>@Translate("product-countdown-days", "d:")</span>
598 }
599 <span class="js-count-down-hours"></span><span>@Translate("product-countdown-hours", "t:")</span>
600 <span class="js-count-down-minutes"></span><span>@Translate("product-countdown-minutes", "m:")</span>
601 <span class="js-count-down-seconds"></span><span>@Translate("product-countdown-seconds", "s")</span>
602 </div>
603 }
604 </div>
605 </div> <!-- .global-message--@(theme) -->
606 <!--googleon: all-->
607 }
608 }
609
610 </div>
611 }
612
613 @{
614 if (Pageview.Area.ID == 1)
615 {
616 <button style="margin-top:16px" id="ov-onskeskyen-generated-wish-button" class="pill" type="button">Tilføj til Ønskeskyen</button>
617
618 <script type="application/javascript" src="https://xn--nskeskyen-k8a.dk/onskeskyen-wish-button/external-wish-button.js" id="ov-onskeskyen-generate-wish-button-script"></script>
619 }
620 }
621 </div>
622 }
623 </form>
624 </div><!-- .product__shop-info -->
625 </div><!-- .product__info -->
626 </div>
627 <div class="product__container-left">
628 <div class="product__share-container">
629 <h2 class="product__subheader">Share this product</h2>
630 <div class="product__share addthis_sharing_toolbox"></div>
631 </div><!-- .product__share-container -->
632 <div class="js-accordion">
633 <div class="product__tabs-container">
634 <button class="product__tab-button active js-accordion-button" rel="product-tabs" data-target="#accordion-target-id-1">Om produktet</button>
635
636 @if (GetLoop("ProductCategories").Any(item => item.GetLoop("ProductCategoryFields").Where(subitem => subitem.GetString("Ecom:Product.CategoryField.CategoryID") == "specsList").Any()))
637 {
638 <button class="product__tab-button js-accordion-button" rel="product-tabs" href="#accordion-target-id-2">Spesifikasjoner</button>
639 }
640 @if (!string.IsNullOrEmpty(GetString("Ecom:Product:Field.guide")))
641 {
642 <button class="product__tab-button js-accordion-button" rel="product-tabs" href="#accordion-target-id-3">Guides</button>
643 }
644 @if (!string.IsNullOrEmpty(GetString("Ecom:Product:Field.theSeries")))
645 {
646 <button class="product__tab-button js-accordion-button" rel="product-tabs" href="#accordion-target-id-4">Om serien</button>
647 }
648 </div>
649 <div class="active product__tab-content" id="accordion-target-id-1">
650 <div class="product__text">@GetString("Ecom:Product.ShortDescription")</div>
651 <div class="product__features">
652 @if (symbolsLoop.Where(symbol => symbol.GetInteger("NZPriority") == 0 || symbol.GetString("NZName") == "Not_induction_compatible" || symbol.GetString("NZName") == "Ikke_til_induktion").Any())
653 {
654 <h2 class="product__subheader">Egenskaper</h2>
655 <ul class="feature-list">
656
657 @foreach (var symbol in symbolsLoop.Where(symbol => symbol.GetString("NZName") == "Not_induction_compatible" || symbol.GetString("NZName") == "Ikke_til_induktion"))
658 {
659 <li class="feature-list__item">
660 <img class="feature-list__image" width="25" height="25" src="@symbol.GetString("NZLink")?height=25&width=25&quality=80">
661 <span class="feature-list__item-text">@Translate(symbol.GetString("NZName"))</span>
662 </li>
663 }
664 @foreach (var symbol in symbolsLoop.Where(symbol => symbol.GetInteger("NZPriority") == 0).Where(symbol => symbol.GetString("NZName") != "Not_induction_compatible" && symbol.GetString("NZName") != "Ikke_til_induktion"))
665 {
666 <li class="feature-list__item">
667 <img class="feature-list__image" width="25" height="25" src="@symbol.GetString("NZLink")?height=25&width=25&quality=80">
668 <span class="feature-list__item-text">@Translate(symbol.GetString("NZName"))</span>
669 </li>
670 }
671 </ul>
672 }
673 </div>
674 </div>
675 <!-- This is an accordion always -->
676 @if (GetLoop("ProductCategories").Any(item => item.GetLoop("ProductCategoryFields").Where(subitem => subitem.GetString("Ecom:Product.CategoryField.CategoryID") == "specsList").Any()))
677 {
678 <div class="product__tab-content" id="accordion-target-id-2">
679 <dl class="product__specs-list">
680 <div class="product__specs-item">
681 <dt class="product__specs-key">Item number</dt>
682 <dd class="product__specs-value">@GetString("Ecom:Product.Number")</dd>
683 </div>
684 @foreach (var item in GetLoop("ProductCategories").SelectMany(item => item.GetLoop("ProductCategoryFields").Where(subitem => subitem.GetString("Ecom:Product.CategoryField.CategoryID") == "specsList" && !string.IsNullOrEmpty(subitem.GetString("Ecom:Product.CategoryField.Value")))))
685 {
686 string specsKey = item.GetString("Ecom:Product.CategoryField.Label");
687 string toolTipText = string.Concat("ToolTip", specsKey);
688 string toolTipId = item.GetString("Ecom:Product.CategoryField.ID");
689
690 if (item.GetString("Ecom:Product.CategoryField.ID") == "specsWithLid" && (item.GetString("Ecom:Product.CategoryField.Value").ToLower() == "ikke relevant" || item.GetString("Ecom:Product.CategoryField.Value").ToLower() == "not relevant"))
691 {
692 continue;
693 }
694 <div class="product__specs-item">
695 <dt class="product__specs-key">
696 @Translate(specsKey)
697 @if (Translate(toolTipText) != toolTipText)
698 {
699 <span class="product__specs-tooltip-icon js-toggle-class" data-target="#@toolTipId">
700 <span id="@toolTipId" class="product__specs-tooltip js-tooltip">@Translate(toolTipText)</span>
701 </span>
702 }
703 </dt>
704 <dd class="product__specs-value">@Translate(item.GetString("Ecom:Product.CategoryField.Value"))</dd>
705 </div>
706 }
707 </dl>
708 </div>
709 }
710 @if (!string.IsNullOrEmpty(guidePath))
711 {
712 <div class="product__tab-content" id="accordion-target-id-3">
713 <div class="product__text">
714 <a href="@guidePath">
715 Last ned brukerveiledning
716 </a>
717 </div>
718 </div>
719
720 }
721 @if (!string.IsNullOrEmpty(GetString("Ecom:Product:Field.theSeries")))
722 {
723 <div class="product__tab-content" id="accordion-target-id-4">
724 <div class="product__text">@GetString("Ecom:Product:Field.theSeries")</div>
725 </div>
726 }
727 </div>
728 </div>
729
730 </div><!-- .product__container -->
731 @*GA4 tracking*@
732
733 <div style="display:none" class="js-ga-four-product"
734 data-itemprice="@GetDouble("Ecom:Product.Price.PriceWithVAT").ToString().Replace(",", ".")"
735 data-itemvat="@GetDouble("Ecom:Product.Price.VATPercent").ToString().Replace(",", ".")"
736 data-itempriceold="@GetDouble("Ecom:Product:Field.ProductPriceBefore").ToString().Replace(",", ".")"
737 data-item_name="@GetString("Ecom:Product.Name")"
738 data-item_id="@GetString("Ecom:Product.Number")"
739 data-currency="@GetString("Ecom:Product.Currency.Code")">
740 </div>
741
742
743
744 </section><!-- .product -->
745 @functions
746 {
747 struct Language
748 {
749 public string productId;
750 public string productVariantId;
751 public string productLanguage;
752 }
753
754
755 public static string StripHTML(string htmlString)
756 {
757
758 string pattern = @"<(.|\n)*?>";
759
760 return Regex.Replace(htmlString, pattern, string.Empty);
761 }
762
763
764 private void SetMetaTags(string productNumber, string productVariantId)
765 {
766 var relatedLanguages = new List<Language>();
767
768 //Create a DataReader on the default database
769 using (var myDr = Database.CreateDataReader($"SELECT ProductLanguageID, ProductID, ProductVariantID FROM EcomProducts WHERE ProductNumber='{productNumber}' AND ProductVariantID = '{productVariantId}'"))
770 {
771 while (myDr.Read())
772 {
773 relatedLanguages.Add(new Language()
774 {
775 productId = Input.FormatString(myDr["ProductID"]),
776 productLanguage = Input.FormatString(myDr["ProductLanguageID"]),
777 productVariantId = Input.FormatString(myDr["ProductVariantID"]),
778 });
779 }
780 }
781
782 string[] productPagesAndLanguages = ApplicationSettings.Instance.ProductPagesAndLanguageCode?.Split(';');
783
784 if (productPagesAndLanguages == null) return;
785
786 foreach (var rl in relatedLanguages)
787 {
788 var languageService = new Dynamicweb.Ecommerce.International.LanguageService();
789 string languageCode = languageService.GetLanguage(rl.productLanguage).Code2;
790 string page = String.Empty;
791 string culture = String.Empty;
792 var areaService = new Dynamicweb.Content.AreaService();
793 Dynamicweb.Content.Area dkArea = areaService.GetArea(1);
794
795 string[] productPageAndLanguage = productPagesAndLanguages.FirstOrDefault(p => p.StartsWith($"{languageCode}|"))?.Split('|');
796
797 if (productPageAndLanguage == null)
798 {
799 continue;
800 }
801
802 page = productPageAndLanguage[1];
803 culture = productPageAndLanguage[2];
804
805 string url = Dynamicweb.Frontend.SearchEngineFriendlyURLs.GetFriendlyUrl($"Default.aspx?ID={page}&ProductID={rl.productId}&VariantID={rl.productVariantId}");
806 string urlLeftPart = System.Web.HttpContext.Current.Request.Url.GetLeftPart(System.UriPartial.Authority);
807 System.Web.HttpContext.Current.Items["MetaTags"] += $"<link rel=\"alternate\" href=\"{urlLeftPart}{url}\" hreflang=\"{culture}\" />";
808 }
809 string mainImage = GetString("Ecom:Product:Field.ProductImage");
810 System.Web.HttpContext.Current.Items["MetaTags"] += $"<meta property = og:image content = https://cdn.scanpan.dk/Perfion/Image.aspx?id={mainImage}&size=600x315&fit=smart />";
811 }
812 }
813 @SnippetStart("Tracking")
814 <script type="text/javascript">
815 // FB view tracking
816 fbq('track', 'ViewContent', {
817 content_name: '@System.Web.HttpUtility.JavaScriptStringEncode(productName)',
818 content_category: '',
819 content_ids: ['@productNumber'],
820 content_type: 'product',
821 value: '@GetString("Ecom:Product.Price.Price")',
822 currency: '@GetString("Ecom:Product.Currency.Code")'
823 });
824 // Click FB event
825 $('.js-fbq-add-to-cart').click(function () {
826 fbq('track', 'AddToCart', {
827 content_name: '@System.Web.HttpUtility.JavaScriptStringEncode(productName)',
828 content_category: '',
829 content_ids: ['@productNumber'],
830 content_type: 'product',
831 value: '@GetString("Ecom:Product.Price.Price")',
832 currency: '@GetString("Ecom:Product.Currency.Code")'
833 });
834 });
835 </script>
836 <script type="text/javascript">
837 @{
838 int areaId = Pageview.AreaID;
839 string priceTotal = GetString("Ecom:Product.Price.Price");
840
841 string totalValue = areaId == 28 ?
842 System.Text.RegularExpressions.Regex.Replace(priceTotal, @"\s+", "").Replace(",", ".") :
843 priceTotal.Replace(",", ".");
844 }
845
846 var google_tag_params = {
847 ecomm_prodid: @productNumber,
848 ecomm_pagetype: 'product',
849 ecomm_totalvalue: '@totalValue'
850 };
851 </script>
852 <script type="text/javascript">
853 /* <![CDATA[ */
854 var google_conversion_id = 976264288;
855 var google_custom_params = window.google_tag_params;
856 var google_remarketing_only = true;
857 /* ]]> */
858 </script>
859 <script type="text/javascript" src="//www.googleadservices.com/pagead/conversion.js">
860 </script>
861 <noscript>
862 <div style="display:inline;">
863 <img height="1" width="1" style="border-style:none;" alt="" src="//googleads.g.doubleclick.net/pagead/viewthroughconversion/976264288/?guid=ON&script=0" />
864 </div>
865 </noscript>
866 @SnippetEnd("Tracking")
867
868
869 <script>
870
871 window.addEventListener("load", function (event) {
872 window.NzApp.GA4.trackAddToCart(".product")
873 window.NzApp.GA4.trackViewItem(".product")
874 })
875 </script>