From 8b3da7cb0ab93abdf811e73f6cea7433103df485 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Sat, 18 Oct 2025 23:18:12 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Cria=C3=A7=C3=A3o=20de=20tutoriais=20e?= =?UTF-8?q?=20remo=C3=A7a=C3=B5=20de=20anuncios.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Models/Ads/AffiliateAdContent.cs | 179 ++++++++++++++++++ Models/ArticleMetadata.cs | 1 + Program.cs | 6 +- Resources/SharedResource.es-PY.resx | 16 ++ Resources/SharedResource.pt-BR.resx | 16 ++ Services/AdDisplayService.cs | 8 +- Services/Ads/ConfigurationAdSlotProvider.cs | 49 +++++ Services/Ads/IAdSlotConfigurationProvider.cs | 9 + Services/MarkdownService.cs | 2 + Views/Home/Index.cshtml | 24 +++ Views/Shared/_AdSpace.cshtml | 182 +++++++++++++------ Views/Shared/_AffiliateAd.cshtml | 97 ++++++++++ Views/Shared/_Layout.cshtml | 17 +- Views/Tutoriais/Index.cshtml | 2 +- appsettings.json | 69 ++++++- wwwroot/css/qrrapido-theme.css | 133 +++++++++++++- 16 files changed, 725 insertions(+), 85 deletions(-) create mode 100644 Models/Ads/AffiliateAdContent.cs create mode 100644 Services/Ads/ConfigurationAdSlotProvider.cs create mode 100644 Services/Ads/IAdSlotConfigurationProvider.cs create mode 100644 Views/Shared/_AffiliateAd.cshtml diff --git a/Models/Ads/AffiliateAdContent.cs b/Models/Ads/AffiliateAdContent.cs new file mode 100644 index 0000000..e21b218 --- /dev/null +++ b/Models/Ads/AffiliateAdContent.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; + +namespace QRRapidoApp.Models.Ads +{ + /// + /// Represents the content for a single affiliate advertisement. + /// + public class AffiliateAdContent + { + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public string ProductUrl { get; set; } = string.Empty; + public string? ImageUrl { get; set; } + public string? CtaText { get; set; } + public string? BadgeText { get; set; } + public string? PriceText { get; set; } + public string? Category { get; set; } + + public bool IsEmpty() + { + return string.IsNullOrWhiteSpace(ProductUrl); + } + + public AffiliateAdContent Clone() + { + return new AffiliateAdContent + { + Title = Title, + Description = Description, + ProductUrl = ProductUrl, + ImageUrl = ImageUrl, + CtaText = CtaText, + BadgeText = BadgeText, + PriceText = PriceText, + Category = Category + }; + } + } + + /// + /// Configuration for an ad slot, supporting AdSense or affiliate content. + /// + public class AdSlotConfiguration + { + /// + /// Provider type for the slot. Supported values: "AdSense", "Affiliate". + /// Defaults to "AdSense" to preserve current behaviour. + /// + public string Provider { get; set; } = "AdSense"; + + /// + /// Optional custom AdSense slot ID override. + /// + public string? AdSenseSlotId { get; set; } + + /// + /// Affiliate content that will be rendered when is "Affiliate". + /// + public AffiliateAdContent? Affiliate { get; set; } + + public AdSlotConfiguration Clone() + { + return new AdSlotConfiguration + { + Provider = Provider, + AdSenseSlotId = AdSenseSlotId, + Affiliate = Affiliate?.Clone() + }; + } + } + + /// + /// Root options object bound from configuration for ad slots. + /// + public class AdsConfigurationOptions + { + private IDictionary _slots = new Dictionary(StringComparer.OrdinalIgnoreCase); + private IDictionary> _locales = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public IDictionary Slots + { + get => _slots; + set => _slots = CreateSlotDictionary(value); + } + + public IDictionary> Locales + { + get => _locales; + set + { + _locales = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if (value == null) + { + return; + } + + foreach (var locale in value) + { + _locales[locale.Key] = CreateSlotDictionary(locale.Value); + } + } + } + + public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null) + { + if (string.IsNullOrWhiteSpace(slotKey)) + { + slotKey = "header"; + } + + var localeSlot = TryGetLocaleSlot(slotKey, cultureName); + if (localeSlot != null) + { + return localeSlot.Clone(); + } + + if (_slots.TryGetValue(slotKey, out var config)) + { + return config.Clone(); + } + + // Default to AdSense when not configured + return new AdSlotConfiguration(); + } + + private AdSlotConfiguration? TryGetLocaleSlot(string slotKey, string? cultureName) + { + if (string.IsNullOrWhiteSpace(cultureName) || _locales.Count == 0) + { + return null; + } + + if (_locales.TryGetValue(cultureName, out var localeSlots) && localeSlots.TryGetValue(slotKey, out var slot)) + { + return slot; + } + + var neutralCulture = cultureName.Split('-')[0]; + if (!string.Equals(neutralCulture, cultureName, StringComparison.OrdinalIgnoreCase) + && _locales.TryGetValue(neutralCulture, out var neutralSlots) + && neutralSlots.TryGetValue(slotKey, out var neutralSlot)) + { + return neutralSlot; + } + + return null; + } + + private static IDictionary CreateSlotDictionary(IDictionary? source) + { + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (source == null) + { + return dictionary; + } + + foreach (var kvp in source) + { + if (kvp.Value != null) + { + dictionary[kvp.Key] = kvp.Value; + } + } + + return dictionary; + } + } + + /// + /// View model used by the affiliate ad partial. + /// + public class AffiliateAdViewModel + { + public string SlotKey { get; set; } = "header"; + public string ContainerCssClass { get; set; } = string.Empty; + public AffiliateAdContent Content { get; set; } = new AffiliateAdContent(); + } +} diff --git a/Models/ArticleMetadata.cs b/Models/ArticleMetadata.cs index 8100068..970f6a6 100644 --- a/Models/ArticleMetadata.cs +++ b/Models/ArticleMetadata.cs @@ -11,5 +11,6 @@ namespace QRRapidoApp.Models public string Image { get; set; } = string.Empty; public string Culture { get; set; } = "pt-BR"; public int ReadingTimeMinutes { get; set; } + public string Slug { get; set; } = string.Empty; } } diff --git a/Program.cs b/Program.cs index 0209cf7..7ea6452 100644 --- a/Program.cs +++ b/Program.cs @@ -10,6 +10,8 @@ using QRRapidoApp.Data; using QRRapidoApp.Middleware; using QRRapidoApp.Providers; using QRRapidoApp.Services; +using QRRapidoApp.Models.Ads; +using QRRapidoApp.Services.Ads; using QRRapidoApp.Services.Monitoring; using QRRapidoApp.Services.HealthChecks; using StackExchange.Redis; @@ -138,6 +140,8 @@ else builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); +builder.Services.Configure(builder.Configuration.GetSection("Ads")); +builder.Services.AddScoped(); // ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm) if (!string.IsNullOrEmpty(mongoConnectionString)) @@ -352,7 +356,7 @@ app.MapHealthChecks("/healthcheck"); // Language routes (must be first) app.MapControllerRoute( name: "localized", - pattern: "{culture:regex(^(es-PY)$)}/{controller=Home}/{action=Index}/{id?}"); + pattern: "{culture:regex(^(pt-BR|es-PY)$)}/{controller=Home}/{action=Index}/{id?}"); // API routes app.MapControllerRoute( diff --git a/Resources/SharedResource.es-PY.resx b/Resources/SharedResource.es-PY.resx index 9454fde..14f2bc1 100644 --- a/Resources/SharedResource.es-PY.resx +++ b/Resources/SharedResource.es-PY.resx @@ -2015,4 +2015,20 @@ Firma de email profesional + + + Ver Tutoriales + + + Aprende Más + + + Guías completas sobre códigos QR + + + Ver Todos los Tutoriales + + + Inmobiliaria y Corredores + \ No newline at end of file diff --git a/Resources/SharedResource.pt-BR.resx b/Resources/SharedResource.pt-BR.resx index 92c5676..a1f97ef 100644 --- a/Resources/SharedResource.pt-BR.resx +++ b/Resources/SharedResource.pt-BR.resx @@ -2105,4 +2105,20 @@ Assinatura de email profissional + + + Ver Tutoriais + + + Aprenda Mais + + + Guias completos sobre QR Codes + + + Ver Todos os Tutoriais + + + Imóveis e Corretores + \ No newline at end of file diff --git a/Services/AdDisplayService.cs b/Services/AdDisplayService.cs index c479079..b60b555 100644 --- a/Services/AdDisplayService.cs +++ b/Services/AdDisplayService.cs @@ -109,10 +109,10 @@ namespace QRRapidoApp.Services } } - public void SetViewBagAds(dynamic viewBag) - { + public void SetViewBagAds(dynamic viewBag) + { viewBag.AdSenseTag = _config["AdSense:ClientId"]; - viewBag.AdSenseEnabled = _config["AdSense:Enabled"]=="True"; + viewBag.AdSenseEnabled = bool.TryParse(_config["AdSense:Enabled"], out var enabled) && enabled; } } -} \ No newline at end of file +} diff --git a/Services/Ads/ConfigurationAdSlotProvider.cs b/Services/Ads/ConfigurationAdSlotProvider.cs new file mode 100644 index 0000000..98f1aaf --- /dev/null +++ b/Services/Ads/ConfigurationAdSlotProvider.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; +using Microsoft.Extensions.Options; +using QRRapidoApp.Models.Ads; + +namespace QRRapidoApp.Services.Ads +{ + public class ConfigurationAdSlotProvider : IAdSlotConfigurationProvider + { + private readonly IOptionsSnapshot _options; + + public ConfigurationAdSlotProvider(IOptionsSnapshot options) + { + _options = options; + } + + public AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null) + { + var resolvedCulture = cultureName; + if (string.IsNullOrWhiteSpace(resolvedCulture)) + { + resolvedCulture = CultureInfo.CurrentUICulture?.Name; + } + + var slot = _options.Value.GetSlot(slotKey, resolvedCulture); + + if (string.Equals(slot.Provider, "Affiliate", StringComparison.OrdinalIgnoreCase)) + { + if (slot.Affiliate == null || slot.Affiliate.IsEmpty()) + { + // Fallback gracefully to AdSense if affiliate content is not properly configured. + return new AdSlotConfiguration + { + Provider = "AdSense", + AdSenseSlotId = slot.AdSenseSlotId + }; + } + + slot.Provider = "Affiliate"; // normalize casing + } + else + { + slot.Provider = "AdSense"; + } + + return slot; + } + } +} diff --git a/Services/Ads/IAdSlotConfigurationProvider.cs b/Services/Ads/IAdSlotConfigurationProvider.cs new file mode 100644 index 0000000..806b09d --- /dev/null +++ b/Services/Ads/IAdSlotConfigurationProvider.cs @@ -0,0 +1,9 @@ +using QRRapidoApp.Models.Ads; + +namespace QRRapidoApp.Services.Ads +{ + public interface IAdSlotConfigurationProvider + { + AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null); + } +} diff --git a/Services/MarkdownService.cs b/Services/MarkdownService.cs index 0cb3b44..8deb3af 100644 --- a/Services/MarkdownService.cs +++ b/Services/MarkdownService.cs @@ -127,6 +127,7 @@ namespace QRRapidoApp.Services if (article?.Metadata != null) { article.Metadata.Culture = culture; + article.Metadata.Slug = slug; articles.Add(article.Metadata); } } @@ -189,6 +190,7 @@ namespace QRRapidoApp.Services if (article?.Metadata != null) { article.Metadata.Culture = culture; + article.Metadata.Slug = slug; var fileInfo = new FileInfo(file); article.Metadata.LastMod = fileInfo.LastWriteTimeUtc; allArticles.Add(article.Metadata); diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index c659555..593ed1d 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1207,6 +1207,30 @@ } + + @{ + var tutorialCulture = System.Globalization.CultureInfo.CurrentUICulture.Name; + } +
+
+
+ @Localizer["LearnMore"] +
+
+
+

@Localizer["CompleteGuidesAboutQRCodes"]

+
    +
  • WhatsApp Business
  • +
  • @Localizer["WiFi"] Networks
  • +
  • @Localizer["VCard"]
  • +
  • @Localizer["RealEstateAndBrokers"]
  • +
+ + @Localizer["ViewAllTutorials"] + +
+
+
diff --git a/Views/Shared/_AdSpace.cshtml b/Views/Shared/_AdSpace.cshtml index 3039bfd..d27bd46 100644 --- a/Views/Shared/_AdSpace.cshtml +++ b/Views/Shared/_AdSpace.cshtml @@ -1,89 +1,152 @@ @using QRRapidoApp.Services +@using QRRapidoApp.Services.Ads +@using QRRapidoApp.Models.Ads @using Microsoft.Extensions.Localization +@using System.Globalization @model dynamic @inject AdDisplayService AdService +@inject IAdSlotConfigurationProvider SlotProvider @inject IStringLocalizer Localizer @{ - var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var showAds = await AdService.ShouldShowAds(userId); - var position = ViewBag.position ?? Model?.position ?? "header"; - var tagAdSense = ViewBag.AdSenseTag; - - @Html.Raw(ViewBag.AdSenseScript); + var rawPosition = (ViewBag.position ?? Model?.position ?? "header")?.ToString()?.ToLowerInvariant(); + var position = string.IsNullOrWhiteSpace(rawPosition) ? "header" : rawPosition; + var currentCulture = CultureInfo.CurrentUICulture?.Name; + var slotConfig = SlotProvider.GetSlot(position, currentCulture); + var adSenseClientId = ViewBag.AdSenseTag as string; + var adSenseEnabled = ViewBag.AdSenseEnabled is bool enabled && enabled; + var defaultAdSenseSlot = position switch + { + "header" => "QR19750801", + "sidebar" => "QR19750802", + "footer" => "QR19750803", + "content" => "QR19750804", + _ => "QR19750801" + }; + var adSenseSlot = slotConfig.AdSenseSlotId ?? defaultAdSenseSlot; + var isAffiliateProvider = string.Equals(slotConfig.Provider, "Affiliate", StringComparison.OrdinalIgnoreCase); + var affiliateContent = isAffiliateProvider ? slotConfig.Affiliate : null; + var containerCss = position switch + { + "header" => "ad-container ad-header mb-4", + "sidebar" => "ad-container ad-sidebar mb-4", + "footer" => "ad-container ad-footer mt-5 mb-4", + "content" => "ad-container ad-content my-4", + _ => "ad-container mb-4" + }; + var shouldRenderAdSense = !isAffiliateProvider && adSenseEnabled && !string.IsNullOrWhiteSpace(adSenseClientId); + var shouldRenderAffiliate = isAffiliateProvider && affiliateContent != null && !affiliateContent.IsEmpty(); + var renderedAdSense = false; } @if (showAds) { - @switch (position) + switch (position) { case "header": -
-
@Localizer["Advertisement"]
- -
+ if (shouldRenderAffiliate) + { + @await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel + { + SlotKey = position, + ContainerCssClass = containerCss, + Content = affiliateContent! + }) + } + else if (shouldRenderAdSense) + { + + + } break; - + case "sidebar": -
-
@Localizer["Advertisement"]
- -
+ if (shouldRenderAffiliate) + { + @await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel + { + SlotKey = position, + ContainerCssClass = containerCss, + Content = affiliateContent! + }) + } + else if (shouldRenderAdSense) + { + + + } break; - + case "footer": - + if (shouldRenderAffiliate) + { + @await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel + { + SlotKey = position, + ContainerCssClass = containerCss, + Content = affiliateContent! + }) + } + else if (shouldRenderAdSense) + { + + + } break; - + case "content": -
-
@Localizer["Advertisement"]
- -
+ if (shouldRenderAffiliate) + { + @await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel + { + SlotKey = position, + ContainerCssClass = containerCss, + Content = affiliateContent! + }) + } + else if (shouldRenderAdSense) + { + + + } + break; + + default: + if (shouldRenderAffiliate) + { + @await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel + { + SlotKey = position, + ContainerCssClass = containerCss, + Content = affiliateContent! + }) + } + else if (shouldRenderAdSense) + { + + + } break; } - - + + @* AdSense lazy-load script removed - will be replaced with Adsterra when approved *@ } else if (User.Identity.IsAuthenticated) { var isPremium = await AdService.HasValidPremiumSubscription(userId); if (isPremium) { -
@Localizer["PremiumUserNoAds"] @@ -91,7 +154,6 @@ else if (User.Identity.IsAuthenticated) } else { -
@Localizer["UpgradePremiumRemoveAds"] @@ -100,4 +162,4 @@ else if (User.Identity.IsAuthenticated)
} -} \ No newline at end of file +} diff --git a/Views/Shared/_AffiliateAd.cshtml b/Views/Shared/_AffiliateAd.cshtml new file mode 100644 index 0000000..6da3149 --- /dev/null +++ b/Views/Shared/_AffiliateAd.cshtml @@ -0,0 +1,97 @@ +@model QRRapidoApp.Models.Ads.AffiliateAdViewModel +@inject Microsoft.Extensions.Localization.IStringLocalizer Localizer +@{ + var content = Model.Content; + var containerClass = string.IsNullOrWhiteSpace(Model.ContainerCssClass) + ? "ad-container affiliate-ad" + : $"{Model.ContainerCssClass} affiliate-ad"; + var ctaText = string.IsNullOrWhiteSpace(content.CtaText) ? "Ver oferta" : content.CtaText; + var slotKey = Model.SlotKey?.ToLowerInvariant(); + var isWideSlot = string.Equals(slotKey, "header") || string.Equals(slotKey, "footer"); + var imageStyle = slotKey switch + { + "header" => "max-height:140px;object-fit:contain;width:100%;", + "footer" => "max-height:140px;object-fit:contain;width:100%;", + "sidebar" => "max-height:200px;object-fit:contain;width:100%;", + "content" => "max-height:220px;object-fit:contain;width:100%;", + _ => "max-height:220px;object-fit:contain;width:100%;" + }; + var hasImage = !string.IsNullOrWhiteSpace(content.ImageUrl); + + if (isWideSlot) + { + containerClass = $"{containerClass} affiliate-ad-wide"; + } +} + +
+
@Localizer["Advertisement"]
+ @if (isWideSlot) + { +
+
+
+ @if (hasImage) + { +
+ @content.Title + @if (!string.IsNullOrWhiteSpace(content.BadgeText)) + { + @content.BadgeText + } +
+ } +
+ @if (!hasImage && !string.IsNullOrWhiteSpace(content.BadgeText)) + { + @content.BadgeText + } +
@content.Title
+ @if (!string.IsNullOrWhiteSpace(content.Description)) + { +

@content.Description

+ } + @if (!string.IsNullOrWhiteSpace(content.PriceText)) + { +
@content.PriceText
+ } + +
+
+
+
+ } + else + { +
+ @if (hasImage) + { + @content.Title + } +
+ @if (!string.IsNullOrWhiteSpace(content.BadgeText)) + { + @content.BadgeText + } +
@content.Title
+ @if (!string.IsNullOrWhiteSpace(content.Description)) + { +

@content.Description

+ } + @if (!string.IsNullOrWhiteSpace(content.PriceText)) + { +
@content.PriceText
+ } + + @ctaText + +
+
+ } +
diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index d4ef1dc..c932744 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -140,14 +140,7 @@ }; - @if (ViewBag.AdSenseEnabled) - { - - var tagAdSense = $"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client={ViewBag.AdSenseTag}"; - var adSenseScript = $""; - - - } + @* AdSense removed - Preparing for Adsterra integration *@ @@ -328,6 +321,14 @@

@Localizer["AverageTimePrefix"] @Localizer["AverageTimeValue"] @Localizer["AverageTimeSuffix"]

+ @{ + var currentCulture = System.Globalization.CultureInfo.CurrentUICulture.Name; + } +
diff --git a/Views/Tutoriais/Index.cshtml b/Views/Tutoriais/Index.cshtml index 2effbae..25ebf93 100644 --- a/Views/Tutoriais/Index.cshtml +++ b/Views/Tutoriais/Index.cshtml @@ -43,7 +43,7 @@
- @(ViewBag.Culture == "pt-BR" ? "Ler Tutorial" : "Leer Tutorial") diff --git a/appsettings.json b/appsettings.json index 81d51cb..4656ddd 100644 --- a/appsettings.json +++ b/appsettings.json @@ -40,7 +40,72 @@ }, "AdSense": { "ClientId": "ca-pub-3475956393038764", - "Enabled": true + "Enabled": false + }, + "Ads": { + "Slots": { + "header": { + "Provider": "AdSense", + "AdSenseSlotId": "QR19750801" + }, + "sidebar": { + "Provider": "AdSense", + "AdSenseSlotId": "QR19750802" + }, + "content": { + "Provider": "AdSense", + "AdSenseSlotId": "QR19750804" + }, + "footer": { + "Provider": "AdSense", + "AdSenseSlotId": "QR19750803" + } + }, + "Locales": { + "pt-BR": { + "sidebar": { + "Provider": "None" + }, + "header": { + "Provider": "None" + }, + "content": { + "Provider": "None" + }, + "footer": { + "Provider": "None" + } + }, + "es-PY": { + "sidebar": { + "Provider": "None" + }, + "header": { + "Provider": "None" + }, + "content": { + "Provider": "None" + }, + "footer": { + "Provider": "None" + } + }, + "es": { + "sidebar": { + "Provider": "Affiliate", + "Affiliate": { + "Title": "Pack de etiquetas adhesivas para QR", + "Description": "Rollo con 500 etiquetas mate listas para imprimir códigos QR", + "ProductUrl": "https://marketplace-ejemplo.com/afiliados/etiquetas-qr", + "ImageUrl": "https://cdn.example.com/qr-labels-pack.jpg", + "CtaText": "Explorar", + "BadgeText": "Oferta destacada", + "PriceText": "$ 12.90", + "Category": "insumos" + } + } + } + } }, "Performance": { "QRGenerationTimeoutMs": 2000, @@ -128,4 +193,4 @@ } }, "AllowedHosts": "*" -} \ No newline at end of file +} diff --git a/wwwroot/css/qrrapido-theme.css b/wwwroot/css/qrrapido-theme.css index 541669d..ae42499 100644 --- a/wwwroot/css/qrrapido-theme.css +++ b/wwwroot/css/qrrapido-theme.css @@ -518,14 +518,129 @@ footer a:hover { position: relative; } -.ad-label { - color: #495057 !important; - font-size: 0.75rem; - text-transform: uppercase; - font-weight: 600; - letter-spacing: 0.5px; - margin-bottom: 8px; -} +.ad-label { + color: #495057 !important; + font-size: 0.75rem; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 0.5px; + margin-bottom: 8px; +} + +.affiliate-ad-wide { + text-align: left; +} + +.affiliate-ad-wide .affiliate-ad-card { + max-width: 100%; +} + +.affiliate-ad-wide .affiliate-ad-card .card-body { + padding: 1.5rem; +} + +.affiliate-ad-wide .affiliate-ad-card-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; + width: 100%; +} + +.affiliate-ad-wide .affiliate-ad-media { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + text-align: center; +} + +.affiliate-ad-wide .affiliate-ad-media img { + display: block; + max-width: 240px; + width: 100%; +} + +.affiliate-ad-wide .affiliate-ad-partner { + font-weight: 600; +} + +.affiliate-ad-wide .affiliate-ad-details { + display: flex; + flex-direction: column; + gap: 0.75rem; + text-align: center; + align-items: center; +} + +.affiliate-ad-wide .affiliate-ad-details .card-title { + font-size: 1rem; +} + +.affiliate-ad-wide .affiliate-ad-price { + font-size: 0.95rem; +} + +.affiliate-ad-wide .affiliate-ad-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; + align-items: stretch; +} + +.affiliate-ad-wide .affiliate-ad-cta { + width: 100%; +} + +@media (min-width: 576px) { + .affiliate-ad-wide .affiliate-ad-actions { + align-items: center; + } +} + +@media (min-width: 992px) { + .affiliate-ad-wide .affiliate-ad-card { + max-width: 1000px; + margin: 0 auto; + } + + .affiliate-ad-wide .affiliate-ad-card .card-body { + padding: 1.75rem 2rem; + } + + .affiliate-ad-wide .affiliate-ad-card-content { + display: grid; + grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); + align-items: center; + column-gap: 2.5rem; + } + + .affiliate-ad-wide .affiliate-ad-media { + align-items: flex-start; + text-align: left; + } + + .affiliate-ad-wide .affiliate-ad-media img { + max-width: 280px; + } + + .affiliate-ad-wide .affiliate-ad-details { + text-align: left; + align-items: flex-start; + } + + .affiliate-ad-wide .affiliate-ad-actions { + flex-direction: row; + align-items: flex-start; + width: auto; + gap: 1rem; + } + + .affiliate-ad-wide .affiliate-ad-cta { + width: auto; + } +} /* ================================= PLACEHOLDER QR - CONTRASTE SUPERIOR @@ -1287,4 +1402,4 @@ html[data-theme="dark"] @keyframes buttonPulse { transform: scale(1.02); box-shadow: 0 0 10px rgba(96, 165, 250, 0.4); } -} \ No newline at end of file +}