Error parsing template "Designs/Swift/Paragraph/Swift_ProductDetailsImage.cshtml"
Line 109: (108:156) - Expected a "{" but found a "@". Block statements must be enclosed in "{" and "}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed:
@if(isLoggedIn)
Hello, @user
Instead, wrap the contents of the block in "{}":
@if(isLoggedIn) {
Hello, @user
}
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel>
2 @using Dynamicweb.Ecommerce.ProductCatalog
3 @using Dynamicweb.Frontend
4 @using System.IO
5 @using System.Text.RegularExpressions;
6
7 @functions {
8 public ProductViewModel product { get; set; } = new ProductViewModel();
9 public string galleryLayout { get; set; }
10 public string[] supportedImageFormats { get; set; }
11 public string[] supportedVideoFormats { get; set; }
12 public string[] supportedDocumentFormats { get; set; }
13 public string[] allSupportedFormats { get; set; }
14
15 public class RatioSettings
16 {
17 public string Ratio { get; set; }
18 public string CssClass { get; set; }
19 public string CssVariable { get; set; }
20 public string Fill { get; set; }
21 }
22
23 public RatioSettings GetRatioSettings(string size = "desktop")
24 {
25 var ratioSettings = new RatioSettings();
26
27 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", "");
28 ratio = ratio != "0" ? ratio : "";
29 string cssClass = ratio != "" && ratio != "fill" ? " ratio" : "";
30 string cssVariable = ratio != "" && ratio != "fill" ? "--bs-aspect-ratio: " + ratio : "";
31 cssClass = ratio == "fill" && size == "mobile" ? " ratio" : cssClass;
32 cssVariable = ratio == "fill" && size == "mobile" ? "--bs-aspect-ratio: 66%" : cssVariable;
33
34 ratioSettings.Ratio = ratio;
35 ratioSettings.CssClass = cssClass;
36 ratioSettings.CssVariable = cssVariable;
37 ratioSettings.Fill = ratio == "fill" ? " h-100" : "";
38
39 return ratioSettings;
40 }
41
42 public string GetArrowsColor()
43 {
44 var invertColor = Model.Item.GetBoolean("InvertModalArrowsColor");
45 var arrowsColor = invertColor ? " carousel-dark" : string.Empty;
46 return arrowsColor;
47 }
48
49 public string GetThumbnailPlacement()
50 {
51 return Model.Item.GetRawValueString("ThumbnailPlacement", "bottom");
52 }
53
54 public string GetThumbnailRowSettingCss()
55 {
56 switch (GetThumbnailPlacement())
57 {
58 case "bottom":
59 return "d-flex flex-wrap";
60 case "left":
61 return "d-flex flex-column order-first";
62 case "right":
63 return "d-flex flex-column order-last";
64 default:
65 return "d-flex flex-wrap";
66 }
67 }
68
69 public Dictionary<string, object> GetVideoParams(MediaViewModel asset, string size)
70 {
71 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Name;
72 string type = GetVideoType(asset.Value);
73 bool openInModal = Model.Item.GetString("OpenVideoInModal") == "true" ? true : false;
74 bool autoPlay = Model.Item.GetBoolean("VideoAutoPlay");
75
76 var videoParams = new Dictionary<string, object>();
77 videoParams.Add("AssetName", asset.Name);
78 videoParams.Add("AssetVideoType", type);
79 videoParams.Add("AssetDisplayName", asset.DisplayName);
80 videoParams.Add("OpenVideoInModal", openInModal);
81 videoParams.Add("VideoAutoPlay", autoPlay);
82 videoParams.Add("Size", size);
83 videoParams.Add("Id", Model.ID);
84 return videoParams;
85
86 }
87
88 public string GetVideoType(string assetValue)
89 {
90 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : string.Empty;
91 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type;
92 type = string.IsNullOrEmpty(type) ? "selfhosted" : type;
93
94 return type;
95 }
96
97 public string GetYoutubeScreenDump(string assetValue, string quality)
98 {
99 var regex = new Regex(@"(?:youtube\.com\/.*[\?&]v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]+)(?:\?.*)?");
100 Match match = regex.Match(assetValue);
101 string videoId = match.Success ? match.Groups[1].Value : string.Empty;
102 string youtubeThumbnail = $"https://img.youtube.com/vi/{videoId}/{quality}.jpg";
103 return youtubeThumbnail;
104 }
105 }
106
107 @{
108 ProductViewModel product = null;
109 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails") && !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("ProductID"))) @*<-Custom code: null check*@
110 {
111 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"];
112 }
113 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode)
114 {
115 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page);
116 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel();
117
118 if (productList?.Products is object)
119 {
120 product = productList.Products[0];
121 }
122 }
123 }
124
125 @if (product is object && product != null)@*<-Custom code: null check*@
126 {
127 @* Supported formats *@
128 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" };
129 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" };
130 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" };
131 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray();
132
133 @* Collect the assets *@
134 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue().OfType<string>();
135 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages");
136
137 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@
138 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : "";
139 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets);
140 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage));
141 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { };
142 assetsList = assetsList.Union(assetsImages);
143 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages) : assetsList;
144 assetsList = includeImagePatternImages && assetsList.Count() == 0 ? assetsList.Append(product.DefaultImage) : assetsList;
145
146 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback");
147 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage");
148
149 int totalAssets = 0;
150 if (showOnlyPrimaryImage == false)
151 {
152 foreach (MediaViewModel asset in assetsList)
153 {
154 var assetValue = asset.Value;
155 foreach (string format in allSupportedFormats)
156 {
157 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0)
158 {
159 totalAssets++;
160 }
161 }
162 }
163 }
164
165 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count() == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback)
166 {
167 assetsList = new List<MediaViewModel>() { product.DefaultImage };
168 totalAssets = 1;
169 }
170
171 @* Theme settings *@
172 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : "";
173
174 var badgeParms = new Dictionary<string, object>();
175 badgeParms.Add("size", "h5");
176 badgeParms.Add("saleBadgeType", Model.Item.GetRawValue("SaleBadgeType"));
177 badgeParms.Add("saleBadgeCssClassName", Model.Item.GetRawValue("SaleBadgeDesign"));
178 badgeParms.Add("newBadgeCssClassName", Model.Item.GetRawValue("NewBadgeDesign"));
179 badgeParms.Add("newPublicationDays", Model.Item.GetInt32("NewPublicationDays"));
180 badgeParms.Add("campaignBadgesValues", Model.Item.GetList("CampaignBadges")?.GetRawValue().OfType<string>().ToList());
181
182 bool saleBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("SaleBadgeDesign")) && Model.Item.GetRawValueString("SaleBadgeDesign") != "none" ? true : false;
183 bool newBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("NewBadgeDesign")) && Model.Item.GetRawValueString("NewBadgeDesign") != "none" ? true : false;
184 DateTime createdDate = product.Created.Value;
185 bool showBadges = saleBadgeEnabled && product.Discount.Price != 0 ? true : false;
186 showBadges = (newBadgeEnabled && Model.Item.GetInt32("NewPublicationDays") == 0) || (newBadgeEnabled && (createdDate.AddDays(Model.Item.GetInt32("NewPublicationDays")) > DateTime.Now)) ? true : showBadges;
187 showBadges = !string.IsNullOrEmpty(Model.Item.GetRawValueString("CampaignBadges")) ? true : showBadges;
188
189 @* Get assets from selected categories or get all assets *@
190 if (totalAssets != 0)
191 {
192 int assetNumber = 0;
193 int thumbnailNumber = 0;
194 int modalAssetNumber = 0;
195 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty;
196
197 <div class="d-flex gap-3 h-100 @(thumbnailAxisCss) @(theme) item_@Model.Item.SystemName.ToLower()">
198 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel">
199 <div class="carousel-inner h-100">
200 @foreach (MediaViewModel asset in assetsList)
201 {
202 var assetValue = asset.Value;
203 foreach (string format in allSupportedFormats)
204 {
205 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0)
206 {
207 string activeSlide = assetNumber == 0 ? "active" : "";
208
209 <div class="carousel-item @activeSlide" data-bs-interval="99999">
210 @{
211 string size = "mobile";
212
213 string imageTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ImageTheme")) ? " theme " + Model.Item.GetRawValueString("ImageTheme").Replace(" ", "").Trim().ToLower() : "";
214
215
216 <div class="h-100 @(imageTheme)">
217 @foreach (string imageFormat in supportedImageFormats)
218 { //Images
219 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
220 {
221 if (product is object)
222 {
223 string productName = product.Name;
224 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
225 string imageLinkPath = Uri.EscapeDataString(imagePath);
226
227 RatioSettings ratioSettings = GetRatioSettings(size);
228
229 var parms = new Dictionary<string, object>();
230 parms.Add("alt", productName + asset.Keywords);
231 parms.Add("itemprop", "image");
232 parms.Add("columns", Model.GridRowColumnCount);
233 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading"));
234 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage"));
235 if (!string.IsNullOrEmpty(asset.DisplayName))
236 {
237 parms.Add("title", asset.DisplayName);
238 }
239
240 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid")
241 {
242 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover");
243 }
244 else
245 {
246 parms.Add("cssClass", "mw-100 mh-100");
247 }
248
249 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID">
250 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber">
251 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
252 </div>
253 </a>
254 }
255 }
256 }
257 @foreach (string videoFormat in supportedVideoFormats)
258 { //Videos
259 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
260 {
261 if (Model.Item.GetString("OpenVideoInModal") == "true")
262 {
263 if (product is object)
264 {
265 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
266
267 string productName = product.Name;
268 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : "";
269 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : "";
270
271 RatioSettings ratioSettings = GetRatioSettings(size);
272
273 string type = GetVideoType(asset.Value);
274
275 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "maxresdefault") : string.Empty;
276 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath;
277 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : "";
278
279
280 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID">
281 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber">
282 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div>
283 @if (type != "selfhosted")
284 {
285 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@productName" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;">
286 }
287 else
288 {
289 string videoType = Path.GetExtension(asset.Value).ToLower();
290
291 <video preload="auto" class="h-100 w-100" style="object-fit: contain;">
292 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")">
293 </video>
294 }
295 </div>
296 </div>
297
298 }
299 }
300 else
301 {
302 if (product is object)
303 {
304 var videoParams = GetVideoParams(asset, size);
305 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value }, videoParams);
306
307 }
308 }
309 }
310 }
311 @foreach (string documentFormat in supportedDocumentFormats)
312 { //Documents
313 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0)
314 {
315 if (product is object)
316 {
317 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
318
319 string productName = product.Name;
320 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
321 string imageLinkPath = Uri.EscapeDataString(imagePath);
322
323 RatioSettings ratioSettings = GetRatioSettings(size);
324
325 var parms = new Dictionary<string, object>();
326 parms.Add("alt", productName + asset.Keywords);
327 parms.Add("itemprop", "image");
328 parms.Add("fullwidth", true);
329 parms.Add("columns", Model.GridRowColumnCount);
330 if (!string.IsNullOrEmpty(asset.DisplayName))
331 {
332 parms.Add("title", asset.DisplayName);
333 }
334
335 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid")
336 {
337 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover");
338 }
339 else
340 {
341 parms.Add("cssClass", "mw-100 mh-100");
342 }
343
344 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@Translate("Download")">
345 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
346 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div>
347 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0)
348 {
349 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
350 }
351 </div>
352 </a>
353 }
354
355 }
356 }
357 </div>
358 }
359
360
361 </div>
362 assetNumber++;
363 }
364 }
365 }
366 </div>
367 @if (showBadges)
368 {
369 <div class="position-absolute top-0 left-0 p-2 p-lg-3">
370 @{@RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)}
371 </div>
372 }
373
374 </div>
375
376 @if (totalAssets > 1)
377 {
378 <div class="@(GetThumbnailRowSettingCss()) gap-3" id="SmallScreenImagesThumbnails_@Model.ID">
379 @foreach (MediaViewModel asset in assetsList)
380 {
381 var assetValue = asset.Value;
382 string assetName = asset.Name;
383 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : "";
384 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : null;
385 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
386
387 string imagePath = Dynamicweb.Context.Current.Server.UrlEncode(assetValue);
388 imagePath = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "https://img.youtube.com/vi/" + assetValue.Substring(assetValue.LastIndexOf('/') + 1) + "/mqdefault.jpg" : imagePath;
389 string imagePathThumb = assetValue.StartsWith("/Files/", StringComparison.OrdinalIgnoreCase) ? imagePath.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0 && imagePath.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0 ? $"/Admin/Public/GetImage.ashx?image={imagePath}&width=180&format=webp" : imagePath : assetValue;
390
391 RatioSettings ratioSettings = GetRatioSettings("desktop");
392
393 <div class="border outline-none @(ratioSettings.CssClass)" style="@(ratioSettings.CssVariable); cursor: pointer; min-width: 7rem; max-width: 8rem;" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber">
394 @foreach (string imageFormat in supportedImageFormats)
395 { //Images
396 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
397 {
398 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 w-100 h-100" style="object-fit: contain;">
399
400 thumbnailNumber++;
401 }
402 }
403
404 @foreach (string videoFormat in supportedVideoFormats)
405 { //Videos
406 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
407 {
408
409 string type = GetVideoType(asset.Value);
410
411 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "mqdefault") : "";
412 videoScreendumpPath = type == "vimeo" ? string.Empty : videoScreendumpPath;
413 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath;
414 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : string.Empty;
415
416 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div>
417
418 if (type != "selfhosted")
419 {
420 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@assetTitle" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;" />
421 }
422 else
423 {
424 string videoType = Path.GetExtension(asset.Value).ToLower();
425
426 <video preload="auto" class="h-100 w-100" style="object-fit: contain;">
427 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")">
428 </video>
429 }
430
431 thumbnailNumber++;
432 }
433 }
434
435 @foreach (string documentFormat in supportedDocumentFormats)
436 { //Documents
437 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0)
438 {
439 <a href="@Uri.EscapeDataString(assetValue)" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 7rem; max-width: 8rem;" download title="@asset.Value">
440 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0)
441 {
442 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
443 <div class="icon-3 position-absolute text-light" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div>
444 </div>
445 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 mw-100 mh-100" style="object-fit: cover;">
446 }
447 else
448 {
449 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
450 <div class="icon-3 position-absolute" style="z-index: 1">@ReadFile(iconPath + "file-text.svg")</div>
451 </div>
452 }
453 </a>
454
455 thumbnailNumber++;
456 }
457 }
458 </div>
459 }
460 </div>
461 }
462 </div>
463
464 @* Modal with slides *@
465 <div class="modal fade swift_products-details-images-modal" id="modal_@Model.ID" tabindex="-1" aria-labelledby="productDetailsGalleryModalTitle_@Model.ID" aria-hidden="true">
466 <div class="modal-dialog modal-dialog-centered modal-xl">
467 <div class="modal-content">
468 <div class="modal-header visually-hidden">
469 <h5 class="modal-title" id="productDetailsGalleryModalTitle_@Model.ID">@product.Title</h5>
470 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
471 </div>
472 <div class="modal-body p-2 p-lg-3 h-100">
473 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel">
474 <div class="carousel-inner h-100 @theme">
475 @foreach (MediaViewModel asset in assetsList)
476 {
477 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
478 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray())
479 {
480 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0)
481 {
482 string imagePath = assetValue;
483 string activeSlide = modalAssetNumber == 0 ? "active" : "";
484
485 var parms = new Dictionary<string, object>();
486 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto");
487 parms.Add("fullwidth", true);
488 parms.Add("columns", Model.GridRowColumnCount);
489
490 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999">
491 @foreach (string imageFormat in supportedImageFormats)
492 { //Images
493 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
494 {
495 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
496 }
497 }
498
499 @foreach (string videoFormat in supportedVideoFormats)
500 { //Videos
501 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
502 {
503 var videoParams = GetVideoParams(asset, "modal");
504 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value }, videoParams)
505 }
506 }
507 </div>
508 modalAssetNumber++;
509 }
510 }
511 }
512 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev">
513 <span class="carousel-control-prev-icon" aria-hidden="true"></span>
514 <span class="visually-hidden">@Translate("Previous")</span>
515 </button>
516 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next">
517 <span class="carousel-control-next-icon" aria-hidden="true"></span>
518 <span class="visually-hidden">@Translate("Next")</span>
519 </button>
520 </div>
521 </div>
522 </div>
523 </div>
524 </div>
525 </div>
526 }
527 else if (Pageview.IsVisualEditorMode)
528 {
529 RatioSettings ratioSettings = GetRatioSettings("desktop");
530
531 <div class="h-100 @theme">
532 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)">
533 <img src="/Files/Images/missing_image.jpg" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;">
534 </div>
535 </div>
536 }
537 }
538 else if (Pageview.IsVisualEditorMode)
539 {
540 <div class="alert alert-dark m-0">@Translate("No products available")</div>
541 }
542
543
544
545
2311014462
E+G Oliestandsglas med prismatisk vindue
- HGFT.13/PR-1/2
Ikke på lager
Ukendt pris
Error parsing template "/Designs/Swift/Paragraph/Swift_ProductSpecification_Custom.cshtml" Line 214: (213:39) - The "string," element was not closed. All elements must be either self-closing or have a matching end tag.
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Ecommerce.ProductCatalog 3 4 @{ 5 ProductViewModel product = null; 6 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails") && !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("ProductID"))) 7 { 8 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 9 } 10 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode) 11 { 12 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page); 13 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel(); 14 15 if (productList?.Products is object) 16 { 17 product = productList.Products[0]; 18 } 19 } 20 } 21 22 @if (product is object && product != null) { 23 { 24 IEnumerable<string> selectedDisplayGroupIds = Model.Item.GetRawValueString("DisplayGroups").Split(',').ToList(); 25 List<CategoryFieldViewModel> displayGroups = new List<CategoryFieldViewModel>(); 26 27 foreach (var selection in selectedDisplayGroupIds) 28 { 29 foreach (CategoryFieldViewModel group in product.FieldDisplayGroups.Values) 30 { 31 if (selection == group.Id) 32 { 33 int fieldsWithNoValueOrZero = 0; 34 35 foreach (var field in group.Fields) 36 { 37 if (string.IsNullOrEmpty(field.Value.Value.ToString())) 38 { 39 fieldsWithNoValueOrZero++; 40 } 41 } 42 43 if (fieldsWithNoValueOrZero != group.Fields.Count) 44 { 45 displayGroups.Add(group); 46 } 47 } 48 } 49 } 50 51 bool showProductFields = Model.Item.GetBoolean("ProductFields"); 52 53 bool hideTitle = Model.Item.GetBoolean("HideTitle"); 54 55 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : ""; 56 57 string titleFontSize = Model.Item.GetRawValueString("TitleFontSize", "display-4"); 58 59 string contentPadding = Model.Item.GetRawValueString("ContentPadding", ""); 60 contentPadding = contentPadding == "none" ? string.Empty : contentPadding; 61 contentPadding = contentPadding == "small" ? " p-2 p-md-3" : contentPadding; 62 contentPadding = contentPadding == "large" ? " p-4 p-md-5" : contentPadding; 63 64 string layout = Model.Item.GetRawValueString("Layout", "list"); 65 string size = Model.Item.GetRawValueString("Size", "full"); 66 string gaps = size == "full" ? " gap-4" : " gap-2"; 67 68 69 if (Pageview.IsVisualEditorMode && displayGroups.Count() == 0) 70 { 71 product.ProductFields.Clear(); 72 product.ProductFields.Add(Translate("Width"), new FieldValueViewModel { Name = Translate("Width"), Value = "99cm" }); 73 product.ProductFields.Add(Translate("Height"), new FieldValueViewModel { Name = Translate("Height"), Value = "195cm" }); 74 showProductFields = true; 75 } 76 77 if (layout == "commas") 78 { 79 gaps = size == "full" ? " gap-4" : " gap-2"; 80 81 } 82 83 <div class="h-100@(gaps)@(theme)@(contentPadding) item_@Model.Item.SystemName.ToLower()"> 84 <div class="grid"> 85 @if ((product.ProductFields != null && Model.Item.GetBoolean("ProductFields")) || (product.ProductCategories != null && Model.Item.GetBoolean("CategoryFields")) || (displayGroups.Count != 0)) { 86 if (!hideTitle) 87 { 88 <h2 class="g-col-12 @titleFontSize">@Model.Item.GetString("Title")</h2> 89 } 90 } 91 92 @if (displayGroups.Count != 0) 93 { 94 if (layout != "accordion") 95 { 96 foreach (var group in displayGroups) 97 { 98 bool hideHeader = Model.Item.GetBoolean("HideGroupHeaders"); 99 100 if (!hideHeader) { 101 <h4 class="g-col-12 h4 mb-0">@group.Name</h4> 102 } 103 104 { @RenderFieldsFromList(group.Fields, layout) } 105 106 } 107 } 108 else 109 { 110 <div class="g-col-12"> 111 <div class="accordion accordion-flush w-100" id="Specifications_@Model.ID"> 112 @foreach (var group in displayGroups) 113 { 114 <div class="accordion-item"> 115 <h2 class="accordion-header" id="SpecificationHeading_@group.Id"> 116 <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#SpecificationItem_@group.Id" aria-expanded="false" aria-controls="SpecificationItem_@group.Id"> 117 @group.Name 118 </button> 119 </h2> 120 <div id="SpecificationItem_@group.Id" class="accordion-collapse collapse" aria-labelledby="SpecificationHeading_@group.Id" data-bs-parent="#Specifications_@Model.ID"> 121 <div class="accordion-body"> 122 @{ @RenderFieldsFromList(group.Fields, "list") } 123 </div> 124 </div> 125 </div> 126 } 127 </div> 128 </div> 129 } 130 } 131 132 @if (product.ProductFields != null && showProductFields) 133 { 134 if (product.ProductFields.Count > 0) 135 { 136 if (layout != "accordion") 137 { 138 {@RenderFieldsFromList(product.ProductFields, layout) } 139 } 140 else 141 { 142 <div class="g-col-12"> 143 <div class="accordion accordion-flush w-100" id="Specifications_@Model.ID"> 144 <div class="accordion-item"> 145 <h2 class="accordion-header" id="SpecificationHeading_@Model.ID"> 146 <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#SpecificationItem_@Model.ID" aria-expanded="false" aria-controls="SpecificationItem_@Model.ID"> 147 @Translate("Specifications") 148 </button> 149 </h2> 150 <div id="SpecificationItem_@Model.ID" class="accordion-collapse" aria-labelledby="SpecificationHeading_@Model.ID" data-bs-parent="#Specifications_@Model.ID"> 151 <div class="accordion-body"> 152 @{ @RenderFieldsFromList(product.ProductFields, "List") } 153 </div> 154 </div> 155 </div> 156 </div> 157 </div> 158 } 159 } 160 } 161 162 @if (product.ProductCategories != null && Model.Item.GetBoolean("CategoryFields")) 163 { 164 if (product.ProductCategories.Count > 0) 165 { 166 if (layout != "accordion") 167 { 168 foreach (var group in product.ProductCategories) 169 { 170 CategoryFieldViewModel category = group.Value; 171 bool hideHeader = Model.Item.GetBoolean("HideGroupHeaders"); 172 173 if (!hideHeader) { 174 <h4 class="g-col-12 h4 mb-0">@group.Value.Name</h4> 175 } 176 177 { @RenderFieldsFromList(category.Fields, layout) } 178 } 179 } 180 else 181 { 182 <div class="g-col-12"> 183 <div class="accordion accordion-flush w-100" id="Specifications_@Model.ID"> 184 @foreach (var group in product.ProductCategories) 185 { 186 CategoryFieldViewModel category = group.Value; 187 188 <div class="accordion-item"> 189 <h2 class="accordion-header" id="SpecificationHeading_@group.Value.Id"> 190 <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#SpecificationItem_@group.Value.Id" aria-expanded="false" aria-controls="SpecificationItem_@group.Value.Id"> 191 @group.Value.Name 192 </button> 193 </h2> 194 <div id="SpecificationItem_@group.Value.Id" class="accordion-collapse" aria-labelledby="SpecificationHeading_@group.Value.Id" data-bs-parent="#Specifications_@Model.ID"> 195 <div class="accordion-body"> 196 @{ @RenderFieldsFromList(category.Fields, "list") } 197 </div> 198 </div> 199 </div> 200 } 201 </div> 202 </div> 203 } 204 } 205 } 206 </div> 207 </div> 208 } 209 else if (Pageview.IsVisualEditorMode) 210 { 211 <div class="alert alert-warning m-0">@Translate("No products available")</div> 212 } 213 214 @helper RenderFieldsFromList(Dictionary<string, FieldValueViewModel> fields, string layout) 215 { 216 string size = Model.Item.GetRawValueString("Size", "full"); 217 string gaps = size != "full" ? " gap-1" : string.Empty; 218 bool hideFieldLabels = Model.Item.GetBoolean("HideFieldLabels"); 219 bool hideFieldsWithZeroValue = Model.Item.GetBoolean("HideFieldsWithZeroValue"); 220 221 if (layout == "columns"){ 222 <div class="g-col-12"> 223 <div class="grid@(gaps)"> 224 @foreach (var field in fields) 225 { 226 {@RenderField(field.Value, layout)} 227 } 228 </div> 229 </div> 230 } 231 if (layout == "list") { 232 <div class="g-col-12"> 233 <dl class="grid@(gaps)"> 234 @foreach (var field in fields) 235 { 236 {@RenderField(field.Value, layout)} 237 } 238 </dl> 239 </div> 240 } 241 if (layout == "table") 242 { 243 string tableSize = size == "full" ? "" : " table-sm"; 244 <div class="g-col-12"> 245 <table class="table table-striped@(tableSize)"> 246 @foreach (var field in fields) 247 { 248 {@RenderField(field.Value, layout)} 249 } 250 </table> 251 </div> 252 } 253 if (layout == "bullets") 254 { 255 string listSize = size == "full" ? "" : "m-0 p-0 lh-1 fs-7 opacity-75"; 256 string listStyle = size == "full" ? "" : "style=\"list-style-position: inside\""; 257 <div class="g-col-12"> 258 <ul class="@listSize" @listStyle> 259 @foreach (var field in fields) 260 { 261 {@RenderField(field.Value, layout)} 262 } 263 </ul> 264 </div> 265 } 266 if (layout == "commas") 267 { 268 List<string> featuresList = new List<string>(); 269 270 foreach (var field in fields) 271 { 272 if (field.Value.SystemName == "MaxCanBeBuilt") 273 { 274 if (field.Value.Value is int) 275 { 276 field.Value.Value = (int)field.Value.Value > 0 ? Translate("Yes") : Translate("No"); 277 } 278 } 279 string firstListItemValue = string.Empty; //Hack to support field type providers with a single value 280 281 if (field.Value?.Value != null) 282 { 283 if (field.Value.Value.GetType() == typeof(System.Collections.Generic.List<FieldOptionValueViewModel>)) 284 { 285 System.Collections.Generic.List<FieldOptionValueViewModel> values = field.Value.Value as System.Collections.Generic.List<FieldOptionValueViewModel>; 286 287 //Hack to support field type providers with a single value 288 if (values.FirstOrDefault() != null) 289 { 290 firstListItemValue = values.FirstOrDefault().Value; 291 } 292 } 293 } 294 295 if (!hideFieldsWithZeroValue || (firstListItemValue != "0" && firstListItemValue != "0.0" && field.Value.Value.ToString() != "0" && field.Value.Value.ToString() != "0.0")) 296 { 297 if (field.Value.Value is object && !string.IsNullOrEmpty(field.Value.Value.ToString())) 298 { 299 if (field.Value.Value.GetType() == typeof(System.Collections.Generic.List<FieldOptionValueViewModel>)) 300 { 301 List<string> options = new List<string>(); 302 foreach (FieldOptionValueViewModel option in field.Value.Value as System.Collections.Generic.List<FieldOptionValueViewModel>) 303 { 304 if (!string.IsNullOrWhiteSpace(option.Value)) 305 { 306 if (option.Value.ToString().Contains("#") && (Translate(field.Value.Name) == Translate("Color") || Translate(field.Value.Name) == Translate("Colour"))) 307 { 308 string colorSpan = "<span class=\"colorbox-sm\" style=\"background-color: " + option.Value + "\"></span>"; 309 options.Add(colorSpan); 310 } 311 else if (!string.IsNullOrEmpty(option.Value)) 312 { 313 options.Add(option.Name); 314 } 315 } 316 } 317 string optionsString = (string.Join(", ", options.Select(x => x.ToString()).ToArray())); 318 if ((Translate(field.Value.Name) == Translate("Color") || Translate(field.Value.Name) == Translate("Colour"))) 319 { 320 optionsString = (string.Join(" ", options.Select(x => x.ToString()).ToArray())); 321 } 322 323 if (!string.IsNullOrEmpty(optionsString)) 324 { 325 if (!hideFieldLabels) 326 { 327 featuresList.Add(field.Value.Name + ": " + optionsString); 328 } 329 else 330 { 331 featuresList.Add(optionsString); 332 } 333 } 334 } 335 else 336 { 337 if (!string.IsNullOrWhiteSpace(field.Value.Value.ToString())) 338 { 339 if (field.Value.Value.ToString().Contains("#") && (Translate(field.Value.Name) == Translate("Color") || Translate(field.Value.Name) == Translate("Colour"))) 340 { 341 string colorSpan = "<span class=\"colorbox-sm\" style=\"background-color: " + field.Value.Value + "\"></span>"; 342 343 if (!hideFieldLabels) 344 { 345 featuresList.Add(field.Value.Name + ": " + colorSpan); 346 } 347 else 348 { 349 featuresList.Add(colorSpan); 350 } 351 } 352 else 353 { 354 if (!hideFieldLabels) 355 { 356 featuresList.Add(field.Value.Name + ": " + field.Value.Value.ToString()); 357 } 358 else 359 { 360 featuresList.Add(field.Value.Value.ToString()); 361 } 362 } 363 } 364 } 365 } 366 } 367 } 368 369 string featuresString = (string.Join(", ", featuresList.Select(x => x.ToString()).ToArray())); 370 371 <div class="g-col-12 opacity-75 fs-7">@featuresString</div> 372 } 373 } 374 375 @helper RenderField(FieldValueViewModel field, string layout) 376 { 377 378 string size = Model.Item.GetRawValueString("Size", "full"); 379 string fieldValue = field?.Value != null ? field.Value.ToString() : ""; 380 bool hideFieldLabels = Model.Item.GetBoolean("HideFieldLabels"); 381 bool noValues = false; 382 string firstListItemValue = string.Empty; //Hack to support field type providers with a single value 383 bool hideFieldsWithZeroValue = Model.Item.GetBoolean("HideFieldsWithZeroValue"); 384 385 if (!string.IsNullOrEmpty(fieldValue)) 386 { 387 if (field.Value.GetType() == typeof(System.Collections.Generic.List<FieldOptionValueViewModel>)) 388 { 389 System.Collections.Generic.List<FieldOptionValueViewModel> values = field.Value as System.Collections.Generic.List<FieldOptionValueViewModel>; 390 noValues = values.Count > 0 ? false : true; 391 392 //Hack to support field type providers with a single value 393 if (values.FirstOrDefault() != null) 394 { 395 firstListItemValue = values.FirstOrDefault().Value; 396 } 397 } 398 } 399 400 if (!string.IsNullOrEmpty(fieldValue) && noValues == false) 401 { 402 if (!hideFieldsWithZeroValue || (firstListItemValue != "0" && firstListItemValue != "0.0" && field.Value.ToString() != "0" && field.Value.ToString() != "0.0")) 403 { 404 if (layout == "columns") 405 { 406 407 <div class="grid g-col-6 g-col-lg-4 gap-1"> 408 @if (!hideFieldLabels) 409 { 410 <dt class="g-col-12 g-col-lg-4">@field.Name</dt> 411 } 412 <dd class="g-col-12 g-col-lg-8 mb-0 text-break"> 413 @{ @RenderFieldValue(field) } 414 </dd> 415 </div> 416 } 417 if (layout == "list") 418 { 419 if (!hideFieldLabels) 420 { 421 <dt class="g-col-4">@field.Name</dt> 422 } 423 <dd class="g-col-8 mb-0 text-break"> 424 @{ @RenderFieldValue(field) } 425 </dd> 426 } 427 if (layout == "table") 428 { 429 <tr> 430 @if (!hideFieldLabels) 431 { 432 <th class="w-25 w-lg-50" scope="row">@field.Name</th> 433 } 434 <td class="text-break"> 435 @{ @RenderFieldValue(field) } 436 </td> 437 </tr> 438 } 439 if (layout == "bullets") 440 { 441 <li> 442 @if (!hideFieldLabels) 443 { 444 <strong>@field.Name</strong> 445 } 446 <span> 447 @{ @RenderFieldValue(field) } 448 </span> 449 </li> 450 } 451 } 452 } 453 } 454 455 @helper RenderFieldValue(FieldValueViewModel field) 456 { 457 string fieldValue = field?.Value != null ? field.Value.ToString() : ""; 458 459 bool isLink = field?.Type == "Link"; 460 bool isColor = false; 461 bool isBrandName = field?.SystemName == "Brand_name"; 462 463 fieldValue = fieldValue == "False" ? Translate("No") : fieldValue; 464 fieldValue = fieldValue == "True" ? Translate("Yes") : fieldValue; 465 466 467 if (field.SystemName == "MaxCanBeBuilt") 468 { 469 fieldValue = (int)field.Value > 0 ? Translate("Yes") : Translate("No"); 470 } 471 472 if (field.Value.GetType() == typeof(System.Collections.Generic.List<Dynamicweb.Ecommerce.ProductCatalog.FieldOptionValueViewModel>)) 473 { 474 int valueCount = 0; 475 System.Collections.Generic.List<FieldOptionValueViewModel> values = field.Value as System.Collections.Generic.List<FieldOptionValueViewModel>; 476 int totalValues = values.Count; 477 478 foreach (FieldOptionValueViewModel option in values) 479 { 480 if (!string.IsNullOrEmpty(option.Value)) 481 { 482 if (option.Value.Substring(0, 1) == "#") 483 { 484 isColor = true; 485 } 486 } 487 488 if (!isColor) 489 { 490 @option.Name 491 } 492 else 493 { 494 <span class="colorbox-sm" style="background-color: @option.Value" title="@option.Name"></span> 495 } 496 497 if (valueCount != totalValues && valueCount < (totalValues - 1)) 498 { 499 if (isColor) 500 { 501 <text> </text> 502 } 503 else 504 { 505 <text>, </text> 506 } 507 } 508 valueCount++; 509 } 510 } 511 else 512 { 513 if (fieldValue.Substring(0, 1) == "#") 514 { 515 isColor = true; 516 } 517 518 if (!isColor) 519 { 520 if (isLink) 521 { 522 string linktTitle = !fieldValue.Contains("aspx") ? fieldValue : Translate("Go to link"); 523 string target = Pageview.AreaSettings.GetBoolean("OpenLinksInNewTab") && fieldValue.Contains("http") ? "target=\"_blank\"" : string.Empty; 524 string rel = Pageview.AreaSettings.GetBoolean("OpenLinksInNewTab") && fieldValue.Contains("http") ? "rel=\"noopener\"" : string.Empty; 525 526 <a href="@field.Value" title="@field.Name" @target @rel>@linktTitle</a> 527 } 528 else if (isBrandName) 529 { 530 <span itemprop="brand" itemtype="https://schema.org/Brand" itemscope> 531 <span itemprop="name">@fieldValue</span> 532 </span> 533 } 534 else 535 { 536 @fieldValue 537 } 538 539 } 540 else 541 { 542 <span class="colorbox-sm" style="background-color: @fieldValue" title="@fieldValue"></span> 543 } 544 } 545 } 546
Datablade og dokumenter
| Navn | Download | Fil type | |
|---|---|---|---|
|
|
Brd.Klee-Datablad_HGFT-PR.pdf | 44 KB |
Error parsing template "Designs/Swift/Paragraph/Swift_ProductDetailsImage.cshtml"
Line 109: (108:156) - Expected a "{" but found a "@". Block statements must be enclosed in "{" and "}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed:
@if(isLoggedIn)
Hello, @user
Instead, wrap the contents of the block in "{}":
@if(isLoggedIn) {
Hello, @user
}
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel>
2 @using Dynamicweb.Ecommerce.ProductCatalog
3 @using Dynamicweb.Frontend
4 @using System.IO
5 @using System.Text.RegularExpressions;
6
7 @functions {
8 public ProductViewModel product { get; set; } = new ProductViewModel();
9 public string galleryLayout { get; set; }
10 public string[] supportedImageFormats { get; set; }
11 public string[] supportedVideoFormats { get; set; }
12 public string[] supportedDocumentFormats { get; set; }
13 public string[] allSupportedFormats { get; set; }
14
15 public class RatioSettings
16 {
17 public string Ratio { get; set; }
18 public string CssClass { get; set; }
19 public string CssVariable { get; set; }
20 public string Fill { get; set; }
21 }
22
23 public RatioSettings GetRatioSettings(string size = "desktop")
24 {
25 var ratioSettings = new RatioSettings();
26
27 string ratio = Model.Item.GetRawValueString("ImageAspectRatio", "");
28 ratio = ratio != "0" ? ratio : "";
29 string cssClass = ratio != "" && ratio != "fill" ? " ratio" : "";
30 string cssVariable = ratio != "" && ratio != "fill" ? "--bs-aspect-ratio: " + ratio : "";
31 cssClass = ratio == "fill" && size == "mobile" ? " ratio" : cssClass;
32 cssVariable = ratio == "fill" && size == "mobile" ? "--bs-aspect-ratio: 66%" : cssVariable;
33
34 ratioSettings.Ratio = ratio;
35 ratioSettings.CssClass = cssClass;
36 ratioSettings.CssVariable = cssVariable;
37 ratioSettings.Fill = ratio == "fill" ? " h-100" : "";
38
39 return ratioSettings;
40 }
41
42 public string GetArrowsColor()
43 {
44 var invertColor = Model.Item.GetBoolean("InvertModalArrowsColor");
45 var arrowsColor = invertColor ? " carousel-dark" : string.Empty;
46 return arrowsColor;
47 }
48
49 public string GetThumbnailPlacement()
50 {
51 return Model.Item.GetRawValueString("ThumbnailPlacement", "bottom");
52 }
53
54 public string GetThumbnailRowSettingCss()
55 {
56 switch (GetThumbnailPlacement())
57 {
58 case "bottom":
59 return "d-flex flex-wrap";
60 case "left":
61 return "d-flex flex-column order-first";
62 case "right":
63 return "d-flex flex-column order-last";
64 default:
65 return "d-flex flex-wrap";
66 }
67 }
68
69 public Dictionary<string, object> GetVideoParams(MediaViewModel asset, string size)
70 {
71 string assetName = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : asset.Name;
72 string type = GetVideoType(asset.Value);
73 bool openInModal = Model.Item.GetString("OpenVideoInModal") == "true" ? true : false;
74 bool autoPlay = Model.Item.GetBoolean("VideoAutoPlay");
75
76 var videoParams = new Dictionary<string, object>();
77 videoParams.Add("AssetName", asset.Name);
78 videoParams.Add("AssetVideoType", type);
79 videoParams.Add("AssetDisplayName", asset.DisplayName);
80 videoParams.Add("OpenVideoInModal", openInModal);
81 videoParams.Add("VideoAutoPlay", autoPlay);
82 videoParams.Add("Size", size);
83 videoParams.Add("Id", Model.ID);
84 return videoParams;
85
86 }
87
88 public string GetVideoType(string assetValue)
89 {
90 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : string.Empty;
91 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type;
92 type = string.IsNullOrEmpty(type) ? "selfhosted" : type;
93
94 return type;
95 }
96
97 public string GetYoutubeScreenDump(string assetValue, string quality)
98 {
99 var regex = new Regex(@"(?:youtube\.com\/.*[\?&]v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]+)(?:\?.*)?");
100 Match match = regex.Match(assetValue);
101 string videoId = match.Success ? match.Groups[1].Value : string.Empty;
102 string youtubeThumbnail = $"https://img.youtube.com/vi/{videoId}/{quality}.jpg";
103 return youtubeThumbnail;
104 }
105 }
106
107 @{
108 ProductViewModel product = null;
109 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails") && !string.IsNullOrEmpty(Dynamicweb.Context.Current.Request.QueryString.Get("ProductID"))) @*<-Custom code: null check*@
110 {
111 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"];
112 }
113 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode)
114 {
115 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page);
116 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel();
117
118 if (productList?.Products is object)
119 {
120 product = productList.Products[0];
121 }
122 }
123 }
124
125 @if (product is object && product != null)@*<-Custom code: null check*@
126 {
127 @* Supported formats *@
128 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" };
129 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" };
130 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" };
131 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray();
132
133 @* Collect the assets *@
134 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue().OfType<string>();
135 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages");
136
137 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@
138 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : "";
139 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets);
140 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage));
141 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { };
142 assetsList = assetsList.Union(assetsImages);
143 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages) : assetsList;
144 assetsList = includeImagePatternImages && assetsList.Count() == 0 ? assetsList.Append(product.DefaultImage) : assetsList;
145
146 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback");
147 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage");
148
149 int totalAssets = 0;
150 if (showOnlyPrimaryImage == false)
151 {
152 foreach (MediaViewModel asset in assetsList)
153 {
154 var assetValue = asset.Value;
155 foreach (string format in allSupportedFormats)
156 {
157 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0)
158 {
159 totalAssets++;
160 }
161 }
162 }
163 }
164
165 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count() == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback)
166 {
167 assetsList = new List<MediaViewModel>() { product.DefaultImage };
168 totalAssets = 1;
169 }
170
171 @* Theme settings *@
172 string theme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("Theme")) ? " theme " + Model.Item.GetRawValueString("Theme").Replace(" ", "").Trim().ToLower() : "";
173
174 var badgeParms = new Dictionary<string, object>();
175 badgeParms.Add("size", "h5");
176 badgeParms.Add("saleBadgeType", Model.Item.GetRawValue("SaleBadgeType"));
177 badgeParms.Add("saleBadgeCssClassName", Model.Item.GetRawValue("SaleBadgeDesign"));
178 badgeParms.Add("newBadgeCssClassName", Model.Item.GetRawValue("NewBadgeDesign"));
179 badgeParms.Add("newPublicationDays", Model.Item.GetInt32("NewPublicationDays"));
180 badgeParms.Add("campaignBadgesValues", Model.Item.GetList("CampaignBadges")?.GetRawValue().OfType<string>().ToList());
181
182 bool saleBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("SaleBadgeDesign")) && Model.Item.GetRawValueString("SaleBadgeDesign") != "none" ? true : false;
183 bool newBadgeEnabled = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("NewBadgeDesign")) && Model.Item.GetRawValueString("NewBadgeDesign") != "none" ? true : false;
184 DateTime createdDate = product.Created.Value;
185 bool showBadges = saleBadgeEnabled && product.Discount.Price != 0 ? true : false;
186 showBadges = (newBadgeEnabled && Model.Item.GetInt32("NewPublicationDays") == 0) || (newBadgeEnabled && (createdDate.AddDays(Model.Item.GetInt32("NewPublicationDays")) > DateTime.Now)) ? true : showBadges;
187 showBadges = !string.IsNullOrEmpty(Model.Item.GetRawValueString("CampaignBadges")) ? true : showBadges;
188
189 @* Get assets from selected categories or get all assets *@
190 if (totalAssets != 0)
191 {
192 int assetNumber = 0;
193 int thumbnailNumber = 0;
194 int modalAssetNumber = 0;
195 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty;
196
197 <div class="d-flex gap-3 h-100 @(thumbnailAxisCss) @(theme) item_@Model.Item.SystemName.ToLower()">
198 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel">
199 <div class="carousel-inner h-100">
200 @foreach (MediaViewModel asset in assetsList)
201 {
202 var assetValue = asset.Value;
203 foreach (string format in allSupportedFormats)
204 {
205 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0)
206 {
207 string activeSlide = assetNumber == 0 ? "active" : "";
208
209 <div class="carousel-item @activeSlide" data-bs-interval="99999">
210 @{
211 string size = "mobile";
212
213 string imageTheme = !string.IsNullOrWhiteSpace(Model.Item.GetRawValueString("ImageTheme")) ? " theme " + Model.Item.GetRawValueString("ImageTheme").Replace(" ", "").Trim().ToLower() : "";
214
215
216 <div class="h-100 @(imageTheme)">
217 @foreach (string imageFormat in supportedImageFormats)
218 { //Images
219 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
220 {
221 if (product is object)
222 {
223 string productName = product.Name;
224 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
225 string imageLinkPath = Uri.EscapeDataString(imagePath);
226
227 RatioSettings ratioSettings = GetRatioSettings(size);
228
229 var parms = new Dictionary<string, object>();
230 parms.Add("alt", productName + asset.Keywords);
231 parms.Add("itemprop", "image");
232 parms.Add("columns", Model.GridRowColumnCount);
233 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading"));
234 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage"));
235 if (!string.IsNullOrEmpty(asset.DisplayName))
236 {
237 parms.Add("title", asset.DisplayName);
238 }
239
240 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid")
241 {
242 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover");
243 }
244 else
245 {
246 parms.Add("cssClass", "mw-100 mh-100");
247 }
248
249 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID">
250 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber">
251 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
252 </div>
253 </a>
254 }
255 }
256 }
257 @foreach (string videoFormat in supportedVideoFormats)
258 { //Videos
259 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
260 {
261 if (Model.Item.GetString("OpenVideoInModal") == "true")
262 {
263 if (product is object)
264 {
265 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
266
267 string productName = product.Name;
268 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : "";
269 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : "";
270
271 RatioSettings ratioSettings = GetRatioSettings(size);
272
273 string type = GetVideoType(asset.Value);
274
275 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "maxresdefault") : string.Empty;
276 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath;
277 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : "";
278
279
280 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID">
281 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide-to="@assetNumber">
282 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div>
283 @if (type != "selfhosted")
284 {
285 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@productName" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;">
286 }
287 else
288 {
289 string videoType = Path.GetExtension(asset.Value).ToLower();
290
291 <video preload="auto" class="h-100 w-100" style="object-fit: contain;">
292 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")">
293 </video>
294 }
295 </div>
296 </div>
297
298 }
299 }
300 else
301 {
302 if (product is object)
303 {
304 var videoParams = GetVideoParams(asset, size);
305 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value }, videoParams);
306
307 }
308 }
309 }
310 }
311 @foreach (string documentFormat in supportedDocumentFormats)
312 { //Documents
313 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0)
314 {
315 if (product is object)
316 {
317 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
318
319 string productName = product.Name;
320 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
321 string imageLinkPath = Uri.EscapeDataString(imagePath);
322
323 RatioSettings ratioSettings = GetRatioSettings(size);
324
325 var parms = new Dictionary<string, object>();
326 parms.Add("alt", productName + asset.Keywords);
327 parms.Add("itemprop", "image");
328 parms.Add("fullwidth", true);
329 parms.Add("columns", Model.GridRowColumnCount);
330 if (!string.IsNullOrEmpty(asset.DisplayName))
331 {
332 parms.Add("title", asset.DisplayName);
333 }
334
335 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid")
336 {
337 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover");
338 }
339 else
340 {
341 parms.Add("cssClass", "mw-100 mh-100");
342 }
343
344 <a href="@imageLinkPath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@Translate("Download")">
345 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
346 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div>
347 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0)
348 {
349 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
350 }
351 </div>
352 </a>
353 }
354
355 }
356 }
357 </div>
358 }
359
360
361 </div>
362 assetNumber++;
363 }
364 }
365 }
366 </div>
367 @if (showBadges)
368 {
369 <div class="position-absolute top-0 left-0 p-2 p-lg-3">
370 @{@RenderPartial("Components/EcommerceBadge.cshtml", product, badgeParms)}
371 </div>
372 }
373
374 </div>
375
376 @if (totalAssets > 1)
377 {
378 <div class="@(GetThumbnailRowSettingCss()) gap-3" id="SmallScreenImagesThumbnails_@Model.ID">
379 @foreach (MediaViewModel asset in assetsList)
380 {
381 var assetValue = asset.Value;
382 string assetName = asset.Name;
383 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : "";
384 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : null;
385 string iconPath = "/Files/Templates/Designs/Swift/Assets/icons/";
386
387 string imagePath = Dynamicweb.Context.Current.Server.UrlEncode(assetValue);
388 imagePath = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "https://img.youtube.com/vi/" + assetValue.Substring(assetValue.LastIndexOf('/') + 1) + "/mqdefault.jpg" : imagePath;
389 string imagePathThumb = assetValue.StartsWith("/Files/", StringComparison.OrdinalIgnoreCase) ? imagePath.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) < 0 && imagePath.IndexOf(".mp4", StringComparison.OrdinalIgnoreCase) < 0 ? $"/Admin/Public/GetImage.ashx?image={imagePath}&width=180&format=webp" : imagePath : assetValue;
390
391 RatioSettings ratioSettings = GetRatioSettings("desktop");
392
393 <div class="border outline-none @(ratioSettings.CssClass)" style="@(ratioSettings.CssVariable); cursor: pointer; min-width: 7rem; max-width: 8rem;" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber">
394 @foreach (string imageFormat in supportedImageFormats)
395 { //Images
396 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
397 {
398 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 w-100 h-100" style="object-fit: contain;">
399
400 thumbnailNumber++;
401 }
402 }
403
404 @foreach (string videoFormat in supportedVideoFormats)
405 { //Videos
406 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
407 {
408
409 string type = GetVideoType(asset.Value);
410
411 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "mqdefault") : "";
412 videoScreendumpPath = type == "vimeo" ? string.Empty : videoScreendumpPath;
413 videoScreendumpPath = type == "selfhosted" ? System.Uri.EscapeUriString(asset.Value) : videoScreendumpPath;
414 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : string.Empty;
415
416 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div>
417
418 if (type != "selfhosted")
419 {
420 <img src="@videoScreendumpPath" loading="lazy" decoding="async" alt="@assetTitle" @assetTitle class="@videoJsClass mw-100 mh-100" data-asset-value="@asset.Value" style="object-fit: cover;" />
421 }
422 else
423 {
424 string videoType = Path.GetExtension(asset.Value).ToLower();
425
426 <video preload="auto" class="h-100 w-100" style="object-fit: contain;">
427 <source src="@(videoScreendumpPath)#t=0.001" type="video/@videoType.Replace(".", "")">
428 </video>
429 }
430
431 thumbnailNumber++;
432 }
433 }
434
435 @foreach (string documentFormat in supportedDocumentFormats)
436 { //Documents
437 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0)
438 {
439 <a href="@Uri.EscapeDataString(assetValue)" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 7rem; max-width: 8rem;" download title="@asset.Value">
440 @if (asset.Value.IndexOf(".pdf", StringComparison.OrdinalIgnoreCase) >= 0)
441 {
442 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
443 <div class="icon-3 position-absolute text-light" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div>
444 </div>
445 <img src="@imagePathThumb" alt="@assetName" @assetTitle class="p-0 p-lg-1 mw-100 mh-100" style="object-fit: cover;">
446 }
447 else
448 {
449 <div class="d-flex align-items-center justify-content-center overflow-hidden h-100">
450 <div class="icon-3 position-absolute" style="z-index: 1">@ReadFile(iconPath + "file-text.svg")</div>
451 </div>
452 }
453 </a>
454
455 thumbnailNumber++;
456 }
457 }
458 </div>
459 }
460 </div>
461 }
462 </div>
463
464 @* Modal with slides *@
465 <div class="modal fade swift_products-details-images-modal" id="modal_@Model.ID" tabindex="-1" aria-labelledby="productDetailsGalleryModalTitle_@Model.ID" aria-hidden="true">
466 <div class="modal-dialog modal-dialog-centered modal-xl">
467 <div class="modal-content">
468 <div class="modal-header visually-hidden">
469 <h5 class="modal-title" id="productDetailsGalleryModalTitle_@Model.ID">@product.Title</h5>
470 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
471 </div>
472 <div class="modal-body p-2 p-lg-3 h-100">
473 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel">
474 <div class="carousel-inner h-100 @theme">
475 @foreach (MediaViewModel asset in assetsList)
476 {
477 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value;
478 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray())
479 {
480 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0)
481 {
482 string imagePath = assetValue;
483 string activeSlide = modalAssetNumber == 0 ? "active" : "";
484
485 var parms = new Dictionary<string, object>();
486 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto");
487 parms.Add("fullwidth", true);
488 parms.Add("columns", Model.GridRowColumnCount);
489
490 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999">
491 @foreach (string imageFormat in supportedImageFormats)
492 { //Images
493 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0)
494 {
495 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms)
496 }
497 }
498
499 @foreach (string videoFormat in supportedVideoFormats)
500 { //Videos
501 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0)
502 {
503 var videoParams = GetVideoParams(asset, "modal");
504 @RenderPartial("Components/VideoPlayer.cshtml", new FileViewModel { Path = asset.Value }, videoParams)
505 }
506 }
507 </div>
508 modalAssetNumber++;
509 }
510 }
511 }
512 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev">
513 <span class="carousel-control-prev-icon" aria-hidden="true"></span>
514 <span class="visually-hidden">@Translate("Previous")</span>
515 </button>
516 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next">
517 <span class="carousel-control-next-icon" aria-hidden="true"></span>
518 <span class="visually-hidden">@Translate("Next")</span>
519 </button>
520 </div>
521 </div>
522 </div>
523 </div>
524 </div>
525 </div>
526 }
527 else if (Pageview.IsVisualEditorMode)
528 {
529 RatioSettings ratioSettings = GetRatioSettings("desktop");
530
531 <div class="h-100 @theme">
532 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)">
533 <img src="/Files/Images/missing_image.jpg" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;">
534 </div>
535 </div>
536 }
537 }
538 else if (Pageview.IsVisualEditorMode)
539 {
540 <div class="alert alert-dark m-0">@Translate("No products available")</div>
541 }
542
543
544
545