Error executing template "Designs/Swift-v2/Paragraph/Swift-v2_ProductMedia.cshtml" System.NullReferenceException: Object reference not set to an instance of an object. at CompiledRazorTemplates.Dynamic.RazorEngine_73cd6992ff0344a487fcaf0a68b35c6b.ExecuteAsync() at RazorEngine.Templating.TemplateBase.Run(ExecuteContext context, TextWriter reader) at RazorEngine.Templating.RazorEngineCore.RunTemplate(ICompiledTemplate template, TextWriter writer, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.DynamicWrapperService.Run(ITemplateKey key, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, TextWriter writer, Type modelType, Object model, DynamicViewBag viewBag) at RazorEngine.Templating.RazorEngineServiceExtensions.<>c__DisplayClass23_0.<Run>b__0(TextWriter writer) at RazorEngine.Templating.RazorEngineServiceExtensions.WithWriter(Action`1 withWriter) at RazorEngine.Templating.RazorEngineServiceExtensions.Run(IRazorEngineService service, String name, Type modelType, Object model, DynamicViewBag viewBag) at Dynamicweb.Rendering.RazorTemplateRenderingProvider.Render(Template template) at Dynamicweb.Rendering.TemplateRenderingService.Render(Template template) at Dynamicweb.Rendering.Template.RenderRazorTemplate()
1 @inherits Dynamicweb.Rendering.ViewModelTemplate<Dynamicweb.Frontend.ParagraphViewModel> 2 @using Dynamicweb.Ecommerce.ProductCatalog 3 @using Dynamicweb.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 string GetVideoType(string assetValue) 70 { 71 string type = assetValue.IndexOf("youtu.be", StringComparison.OrdinalIgnoreCase) >= 0 || assetValue.IndexOf("youtube", StringComparison.OrdinalIgnoreCase) >= 0 ? "youtube" : string.Empty; 72 type = assetValue.IndexOf("vimeo", StringComparison.OrdinalIgnoreCase) >= 0 ? "vimeo" : type; 73 type = string.IsNullOrEmpty(type) ? "selfhosted" : type; 74 75 return type; 76 } 77 78 public string GetYoutubeScreenDump(string assetValue, string quality) 79 { 80 var regex = new Regex(@"(?:youtube\.com\/.*[\?&]v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]+)(?:\?.*)?"); 81 Match match = regex.Match(assetValue); 82 string videoId = match.Success ? match.Groups[1].Value : string.Empty; 83 string youtubeThumbnail = $"https://img.youtube.com/vi/{videoId}/{quality}.jpg"; 84 return youtubeThumbnail; 85 } 86 } 87 88 @{ 89 ProductViewModel product = null; 90 if (Dynamicweb.Context.Current.Items.Contains("ProductDetails")) 91 { 92 product = (ProductViewModel)Dynamicweb.Context.Current.Items["ProductDetails"]; 93 } 94 else if (Pageview.Page.Item["DummyProduct"] != null && Pageview.IsVisualEditorMode) 95 { 96 var pageViewModel = Dynamicweb.Frontend.ContentViewModelFactory.CreatePageInfoViewModel(Pageview.Page); 97 ProductListViewModel productList = pageViewModel.Item.GetValue("DummyProduct") != null ? pageViewModel.Item.GetValue("DummyProduct") as ProductListViewModel : new ProductListViewModel(); 98 99 if (productList?.Products is object) 100 { 101 product = productList.Products[0]; 102 } 103 } 104 } 105 106 @if (product is object) 107 { 108 @* Supported formats *@ 109 supportedImageFormats = new string[] { ".jpg", ".jpeg", ".webp", ".png", ".gif", ".bmp", ".tiff" }; 110 supportedVideoFormats = new string[] { "youtu.be", "youtube", "vimeo", ".mp4", ".webm" }; 111 supportedDocumentFormats = new string[] { ".pdf", ".docx", ".xlsx", ".ppt", "pptx" }; 112 allSupportedFormats = supportedImageFormats.Concat(supportedVideoFormats).Concat(supportedDocumentFormats).ToArray(); 113 114 @* Collect the assets *@ 115 var selectedAssetCategories = Model.Item.GetList("ImageAssets")?.GetRawValue().OfType<string>(); 116 bool includeImagePatternImages = Model.Item.GetBoolean("ImagePatternImages"); 117 118 @* Needed image data collection to support both DefaultImage, ImagePatterns and Image Assets *@ 119 string defaultImage = product.DefaultImage != null ? product.DefaultImage.Value : ""; 120 IEnumerable<MediaViewModel> assetsImages = product.AssetCategories.Where(x => selectedAssetCategories.Contains(x.SystemName)).SelectMany(x => x.Assets); 121 assetsImages = assetsImages.OrderByDescending(x => x.Value.Equals(defaultImage)); 122 IEnumerable<MediaViewModel> assetsList = new MediaViewModel[] { }; 123 assetsList = assetsList.Union(assetsImages); 124 assetsList = includeImagePatternImages ? assetsList.Union(product.ImagePatternImages) : assetsList; 125 assetsList = includeImagePatternImages && assetsList.Count() == 0 ? assetsList.Append(product.DefaultImage) : assetsList; 126 127 bool defaultImageFallback = Model.Item.GetBoolean("DefaultImageFallback"); 128 bool showOnlyPrimaryImage = Model.Item.GetBoolean("ShowOnlyPrimaryImage"); 129 130 int totalAssets = 0; 131 if (showOnlyPrimaryImage == false) 132 { 133 foreach (MediaViewModel asset in assetsList) 134 { 135 var assetValue = asset.Value; 136 foreach (string format in allSupportedFormats) 137 { 138 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 139 { 140 totalAssets++; 141 } 142 } 143 } 144 } 145 146 if ((totalAssets == 0 && product.DefaultImage != null && selectedAssetCategories.Count() == 0) || (showOnlyPrimaryImage == true && product.DefaultImage != null) || totalAssets == 0 && defaultImageFallback) 147 { 148 assetsList = new List<MediaViewModel>() { product.DefaultImage }; 149 totalAssets = 1; 150 } 151 152 @* Get assets from selected categories or get all assets *@ 153 if (totalAssets != 0) 154 { 155 int assetNumber = 0; 156 int thumbnailNumber = 0; 157 int modalAssetNumber = 0; 158 string thumbnailAxisCss = GetThumbnailPlacement() == "bottom" ? "flex-column" : string.Empty; 159 160 <div class="d-flex gap-3 h-100 @(thumbnailAxisCss) item_@Model.Item.SystemName.ToLower()" data-dw-colorscheme="@Model.ColorScheme?.Id"> 161 <div id="SmallScreenImages_@Model.ID" class="carousel@(GetArrowsColor()) col position-relative" data-bs-ride="carousel"> 162 <div class="carousel-inner h-100"> 163 @foreach (MediaViewModel asset in assetsList) 164 { 165 var assetValue = asset.Value; 166 foreach (string format in allSupportedFormats) 167 { 168 if (assetValue.IndexOf(format, StringComparison.OrdinalIgnoreCase) >= 0) 169 { 170 string activeSlide = assetNumber == 0 ? "active" : ""; 171 172 <div class="carousel-item @activeSlide" data-bs-interval="99999"> 173 @{ 174 string size = "mobile"; 175 176 <div class="h-100"> 177 @foreach (string imageFormat in supportedImageFormats) 178 { //Images 179 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 180 { 181 if (product is object) 182 { 183 string productName = product.Name; 184 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 185 186 RatioSettings ratioSettings = GetRatioSettings(size); 187 188 var parms = new Dictionary<string, object>(); 189 parms.Add("alt", productName + asset.Keywords); 190 parms.Add("itemprop", "image"); 191 parms.Add("columns", Model.GridRowColumnCount); 192 parms.Add("eagerLoadNewImages", Model.Item.GetBoolean("DisableLazyLoading")); 193 parms.Add("doNotUseGetimage", Model.Item.GetBoolean("DisableGetImage")); 194 if (!string.IsNullOrEmpty(asset.DisplayName)) 195 { 196 parms.Add("title", asset.DisplayName); 197 } 198 199 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 200 { 201 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 202 } 203 else 204 { 205 parms.Add("cssClass", "mw-100 mh-100"); 206 } 207 208 <a href="@imagePath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 209 <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"> 210 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 211 </div> 212 </a> 213 } 214 } 215 } 216 @foreach (string videoFormat in supportedVideoFormats) 217 { //Videos 218 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 219 { 220 if (product is object) { 221 var video = asset.GetVideoViewModel(); 222 223 if (Model.Item.GetString("OpenVideoInModal") == "true") 224 { 225 string iconPath = "/Files/Images/Icons/"; 226 227 string productName = product.Name; 228 productName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 229 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? "title=\"" + asset.DisplayName + "\"" : ""; 230 231 RatioSettings ratioSettings = GetRatioSettings(size); 232 233 string type = GetVideoType(asset.Value); 234 235 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "maxresdefault") : string.Empty; 236 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : ""; 237 238 239 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable); cursor: pointer" data-bs-toggle="modal" data-bs-target="#modal_@Model.ID"> 240 <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"> 241 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 242 @if (video.IsExternalLink()) 243 { 244 <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;"> 245 } 246 else 247 { 248 249 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 250 <source src="@(asset.Value)#t=0.001" type="@video.GetVideoType()"> 251 </video> 252 } 253 </div> 254 </div> 255 } 256 else 257 { 258 @RenderPartial("Components/VideoPlayer.cshtml", video) 259 } 260 } 261 } 262 } 263 @foreach (string documentFormat in supportedDocumentFormats) 264 { //Documents 265 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 266 { 267 if (product is object) 268 { 269 string iconPath = "/Files/Images/Icons/"; 270 271 string productName = product.Name; 272 string imagePath = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 273 274 RatioSettings ratioSettings = GetRatioSettings(size); 275 276 var parms = new Dictionary<string, object>(); 277 parms.Add("alt", productName + asset.Keywords); 278 parms.Add("itemprop", "image"); 279 parms.Add("fullwidth", true); 280 parms.Add("columns", Model.GridRowColumnCount); 281 if (!string.IsNullOrEmpty(asset.DisplayName)) 282 { 283 parms.Add("title", asset.DisplayName); 284 } 285 286 if (ratioSettings.Ratio == "fill" && galleryLayout != "grid") 287 { 288 parms.Add("cssClass", "w-100 h-100 image-zoom-lg-l-hover"); 289 } 290 else 291 { 292 parms.Add("cssClass", "mw-100 mh-100"); 293 } 294 295 <a href="@imagePath" class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)" download alt="@Translate("Download")"> 296 <div class="d-flex align-items-center justify-content-center text-center overflow-hidden h-100 border"> 297 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 298 <span>@Translate("Download") @(asset.Name)@Path.GetExtension(asset.Value).ToLower()</span> 299 </div> 300 </a> 301 } 302 303 } 304 } 305 </div> 306 } 307 308 309 </div> 310 assetNumber++; 311 } 312 } 313 } 314 </div> 315 316 </div> 317 318 @if (totalAssets > 1) 319 { 320 <div class="@(GetThumbnailRowSettingCss()) gap-3" id="SmallScreenImagesThumbnails_@Model.ID"> 321 @foreach (MediaViewModel asset in assetsList) 322 { 323 var assetValue = asset.Value; 324 string assetName = asset.Name; 325 assetName += !string.IsNullOrEmpty(asset.Keywords) ? " " + asset.Keywords : ""; 326 string assetTitle = !string.IsNullOrEmpty(asset.DisplayName) ? asset.DisplayName : null; 327 string iconPath = "/Files/Images/Icons/"; 328 329 string imagePath = assetValue; 330 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; 331 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; 332 333 RatioSettings ratioSettings = GetRatioSettings("desktop"); 334 335 <div class="border outline-none position-relative @(ratioSettings.CssClass)" style="@(ratioSettings.CssVariable); cursor: pointer; min-width: 7rem; max-width: 8rem;" data-bs-target="#SmallScreenImages_@Model.ID" data-bs-slide-to="@thumbnailNumber"> 336 @foreach (string imageFormat in supportedImageFormats) 337 { //Images 338 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 339 { 340 <img src="@imagePathThumb" alt="@assetName" class="p-0 p-lg-1 w-100 h-100" style="object-fit: contain;"> 341 342 thumbnailNumber++; 343 } 344 } 345 346 @foreach (string videoFormat in supportedVideoFormats) 347 { //Videos 348 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 349 { 350 var video = asset.GetVideoViewModel(); 351 string type = GetVideoType(asset.Value); 352 353 string videoScreendumpPath = type == "youtube" ? GetYoutubeScreenDump(asset.Value, "mqdefault") : ""; 354 videoScreendumpPath = type == "vimeo" ? string.Empty : videoScreendumpPath; 355 string videoJsClass = type == "vimeo" ? "js-vimeo-video-thumbnail" : string.Empty; 356 357 <div class="icon-5 position-absolute top-50 start-50 translate-middle" style="z-index: 1">@ReadFile(iconPath + "play-circle.svg")</div> 358 359 if (video.IsExternalLink()) 360 { 361 <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;" /> 362 } 363 else 364 { 365 <video preload="auto" class="h-100 w-100" style="object-fit: contain;"> 366 <source src="@(asset.Value)#t=0.001" type="@video.GetVideoType()"> 367 </video> 368 } 369 370 thumbnailNumber++; 371 } 372 } 373 374 @foreach (string documentFormat in supportedDocumentFormats) 375 { //Documents 376 if (assetValue.IndexOf(documentFormat, StringComparison.OrdinalIgnoreCase) >= 0) 377 { 378 <a href="@assetValue" class="ratio ratio-4x3 border outline-none" style="cursor: pointer; min-width: 7rem; max-width: 8rem;" download title="@asset.Value"> 379 <div class="d-flex align-items-center justify-content-center text-center overflow-hidden h-100 border"> 380 <div class="icon-5 position-absolute" style="z-index: 1">@ReadFile(iconPath + "download.svg")</div> 381 <span>@(asset.Name)@Path.GetExtension(asset.Value).ToLower()</span> 382 </div> 383 </a> 384 385 thumbnailNumber++; 386 } 387 } 388 </div> 389 } 390 </div> 391 } 392 </div> 393 394 @* Modal with slides *@ 395 <div class="modal fade" id="modal_@Model.ID" tabindex="-1" aria-labelledby="mediaModalTitle_@Model.ID" aria-hidden="true"> 396 <div class="modal-dialog modal-dialog-centered modal-xl"> 397 <div class="modal-content"> 398 <div class="modal-header visually-hidden"> 399 <h5 class="modal-title" id="mediaModalTitle_@Model.ID">@product.Title</h5> 400 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 401 </div> 402 <div class="modal-body p-2 p-lg-3 h-100"> 403 <div id="ModalCarousel_@Model.ID" class="carousel@(GetArrowsColor()) h-100" data-bs-ride="carousel"> 404 <div class="carousel-inner h-100" data-dw-colorscheme="@Model.ColorScheme?.Id"> 405 @foreach (MediaViewModel asset in assetsList) 406 { 407 var assetValue = !string.IsNullOrEmpty(asset.Value) ? asset.Value : product.DefaultImage.Value; 408 foreach (string supportedFormat in supportedImageFormats.Concat(supportedVideoFormats).ToArray()) 409 { 410 if (assetValue.IndexOf(supportedFormat, StringComparison.OrdinalIgnoreCase) >= 0) 411 { 412 string imagePath = assetValue; 413 string activeSlide = modalAssetNumber == 0 ? "active" : ""; 414 415 var parms = new Dictionary<string, object>(); 416 parms.Add("cssClass", "d-block mw-100 mh-100 m-auto"); 417 parms.Add("fullwidth", true); 418 parms.Add("columns", Model.GridRowColumnCount); 419 420 <div class="carousel-item @activeSlide h-100" data-bs-interval="99999"> 421 @foreach (string imageFormat in supportedImageFormats) 422 { //Images 423 if (assetValue.IndexOf(imageFormat, StringComparison.OrdinalIgnoreCase) >= 0) 424 { 425 @RenderPartial("Components/Image.cshtml", new FileViewModel { Path = imagePath }, parms) 426 } 427 } 428 429 @foreach (string videoFormat in supportedVideoFormats) 430 { //Videos 431 if (assetValue.IndexOf(videoFormat, StringComparison.OrdinalIgnoreCase) >= 0) 432 { 433 @RenderPartial("Components/VideoPlayer.cshtml", asset.GetVideoViewModel()) 434 } 435 } 436 </div> 437 modalAssetNumber++; 438 } 439 } 440 } 441 <button class="carousel-control-prev carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="prev"> 442 <span class="carousel-control-prev-icon" aria-hidden="true"></span> 443 <span class="visually-hidden">@Translate("Previous")</span> 444 </button> 445 <button class="carousel-control-next carousel-control-area" type="button" data-bs-target="#ModalCarousel_@Model.ID" data-bs-slide="next"> 446 <span class="carousel-control-next-icon" aria-hidden="true"></span> 447 <span class="visually-hidden">@Translate("Next")</span> 448 </button> 449 </div> 450 </div> 451 </div> 452 </div> 453 </div> 454 </div> 455 } 456 else if (Pageview.IsVisualEditorMode) 457 { 458 RatioSettings ratioSettings = GetRatioSettings("desktop"); 459 460 <div class="h-100" data-dw-colorscheme="@Model.ColorScheme?.Id"> 461 <div class="d-block @(ratioSettings.CssClass)@(ratioSettings.Fill)" style="@(ratioSettings.CssVariable)"> 462 <img src="/Files/Images/nopic.png" loading="lazy" decoding="async" class="mh-100 mw-100" style="object-fit: cover;"> 463 </div> 464 </div> 465 } 466 } 467 else if (Pageview.IsVisualEditorMode) 468 { 469 <div class="alert alert-dark m-0">@Translate("The images will be shown here, if any")</div> 470 } 471 472 473 474
Scattante CFR Elite
€ 1.799,00
OutOfStock
- Brakes: Tektro R741
- Gender: Men
- Brand name: Scattante
- Additional equipment:
- Brake type: Linear-pull
- Brakes: Tektro R741
- Color:
- External reviews:
- Frame: CAAD12 SmartForm C1 6069 Aluminium
- Gear: 21
- Gear model: Shimano Dura Ace 9150 SS
- Gear type: External Derailleur
- Gender: Men
- Material usage: Aluminum
- Recommended use:
- Tires: Mavic Yksion Elite WTS
- Wheel: Mavic Aksium WTS
- Wheel size: 700c
- Volume: 3
- Weight: 2
- Width: 2
- Height: 2
- Brake type: Linear-pull
- Gear: 21
- Wheel size: 700c
- Gear (# of): 21
Similar products
Recommendations for you