feat: Criação de tutoriais e remoçaõ de anuncios.
This commit is contained in:
parent
232d4d6c54
commit
8b3da7cb0a
179
Models/Ads/AffiliateAdContent.cs
Normal file
179
Models/Ads/AffiliateAdContent.cs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Models.Ads
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the content for a single affiliate advertisement.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for an ad slot, supporting AdSense or affiliate content.
|
||||||
|
/// </summary>
|
||||||
|
public class AdSlotConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provider type for the slot. Supported values: "AdSense", "Affiliate".
|
||||||
|
/// Defaults to "AdSense" to preserve current behaviour.
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = "AdSense";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional custom AdSense slot ID override.
|
||||||
|
/// </summary>
|
||||||
|
public string? AdSenseSlotId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Affiliate content that will be rendered when <see cref="Provider"/> is "Affiliate".
|
||||||
|
/// </summary>
|
||||||
|
public AffiliateAdContent? Affiliate { get; set; }
|
||||||
|
|
||||||
|
public AdSlotConfiguration Clone()
|
||||||
|
{
|
||||||
|
return new AdSlotConfiguration
|
||||||
|
{
|
||||||
|
Provider = Provider,
|
||||||
|
AdSenseSlotId = AdSenseSlotId,
|
||||||
|
Affiliate = Affiliate?.Clone()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Root options object bound from configuration for ad slots.
|
||||||
|
/// </summary>
|
||||||
|
public class AdsConfigurationOptions
|
||||||
|
{
|
||||||
|
private IDictionary<string, AdSlotConfiguration> _slots = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private IDictionary<string, IDictionary<string, AdSlotConfiguration>> _locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public IDictionary<string, AdSlotConfiguration> Slots
|
||||||
|
{
|
||||||
|
get => _slots;
|
||||||
|
set => _slots = CreateSlotDictionary(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, IDictionary<string, AdSlotConfiguration>> Locales
|
||||||
|
{
|
||||||
|
get => _locales;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_locales = new Dictionary<string, IDictionary<string, AdSlotConfiguration>>(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<string, AdSlotConfiguration> CreateSlotDictionary(IDictionary<string, AdSlotConfiguration>? source)
|
||||||
|
{
|
||||||
|
var dictionary = new Dictionary<string, AdSlotConfiguration>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in source)
|
||||||
|
{
|
||||||
|
if (kvp.Value != null)
|
||||||
|
{
|
||||||
|
dictionary[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View model used by the affiliate ad partial.
|
||||||
|
/// </summary>
|
||||||
|
public class AffiliateAdViewModel
|
||||||
|
{
|
||||||
|
public string SlotKey { get; set; } = "header";
|
||||||
|
public string ContainerCssClass { get; set; } = string.Empty;
|
||||||
|
public AffiliateAdContent Content { get; set; } = new AffiliateAdContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,5 +11,6 @@ namespace QRRapidoApp.Models
|
|||||||
public string Image { get; set; } = string.Empty;
|
public string Image { get; set; } = string.Empty;
|
||||||
public string Culture { get; set; } = "pt-BR";
|
public string Culture { get; set; } = "pt-BR";
|
||||||
public int ReadingTimeMinutes { get; set; }
|
public int ReadingTimeMinutes { get; set; }
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ using QRRapidoApp.Data;
|
|||||||
using QRRapidoApp.Middleware;
|
using QRRapidoApp.Middleware;
|
||||||
using QRRapidoApp.Providers;
|
using QRRapidoApp.Providers;
|
||||||
using QRRapidoApp.Services;
|
using QRRapidoApp.Services;
|
||||||
|
using QRRapidoApp.Models.Ads;
|
||||||
|
using QRRapidoApp.Services.Ads;
|
||||||
using QRRapidoApp.Services.Monitoring;
|
using QRRapidoApp.Services.Monitoring;
|
||||||
using QRRapidoApp.Services.HealthChecks;
|
using QRRapidoApp.Services.HealthChecks;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
@ -138,6 +140,8 @@ else
|
|||||||
|
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
|
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
|
||||||
|
builder.Services.Configure<AdsConfigurationOptions>(builder.Configuration.GetSection("Ads"));
|
||||||
|
builder.Services.AddScoped<IAdSlotConfigurationProvider, ConfigurationAdSlotProvider>();
|
||||||
|
|
||||||
// ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm)
|
// ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm)
|
||||||
if (!string.IsNullOrEmpty(mongoConnectionString))
|
if (!string.IsNullOrEmpty(mongoConnectionString))
|
||||||
@ -352,7 +356,7 @@ app.MapHealthChecks("/healthcheck");
|
|||||||
// Language routes (must be first)
|
// Language routes (must be first)
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "localized",
|
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
|
// API routes
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
|
|||||||
@ -2015,4 +2015,20 @@
|
|||||||
<data name="VCardUseCase3" xml:space="preserve">
|
<data name="VCardUseCase3" xml:space="preserve">
|
||||||
<value>Firma de email profesional</value>
|
<value>Firma de email profesional</value>
|
||||||
</data>
|
</data>
|
||||||
|
<!-- Tutorial Section -->
|
||||||
|
<data name="ViewTutorials" xml:space="preserve">
|
||||||
|
<value>Ver Tutoriales</value>
|
||||||
|
</data>
|
||||||
|
<data name="LearnMore" xml:space="preserve">
|
||||||
|
<value>Aprende Más</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompleteGuidesAboutQRCodes" xml:space="preserve">
|
||||||
|
<value>Guías completas sobre códigos QR</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewAllTutorials" xml:space="preserve">
|
||||||
|
<value>Ver Todos los Tutoriales</value>
|
||||||
|
</data>
|
||||||
|
<data name="RealEstateAndBrokers" xml:space="preserve">
|
||||||
|
<value>Inmobiliaria y Corredores</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -2105,4 +2105,20 @@
|
|||||||
<data name="VCardUseCase3" xml:space="preserve">
|
<data name="VCardUseCase3" xml:space="preserve">
|
||||||
<value>Assinatura de email profissional</value>
|
<value>Assinatura de email profissional</value>
|
||||||
</data>
|
</data>
|
||||||
|
<!-- Tutorial Section -->
|
||||||
|
<data name="ViewTutorials" xml:space="preserve">
|
||||||
|
<value>Ver Tutoriais</value>
|
||||||
|
</data>
|
||||||
|
<data name="LearnMore" xml:space="preserve">
|
||||||
|
<value>Aprenda Mais</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompleteGuidesAboutQRCodes" xml:space="preserve">
|
||||||
|
<value>Guias completos sobre QR Codes</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewAllTutorials" xml:space="preserve">
|
||||||
|
<value>Ver Todos os Tutoriais</value>
|
||||||
|
</data>
|
||||||
|
<data name="RealEstateAndBrokers" xml:space="preserve">
|
||||||
|
<value>Imóveis e Corretores</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@ -112,7 +112,7 @@ namespace QRRapidoApp.Services
|
|||||||
public void SetViewBagAds(dynamic viewBag)
|
public void SetViewBagAds(dynamic viewBag)
|
||||||
{
|
{
|
||||||
viewBag.AdSenseTag = _config["AdSense:ClientId"];
|
viewBag.AdSenseTag = _config["AdSense:ClientId"];
|
||||||
viewBag.AdSenseEnabled = _config["AdSense:Enabled"]=="True";
|
viewBag.AdSenseEnabled = bool.TryParse(_config["AdSense:Enabled"], out var enabled) && enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
49
Services/Ads/ConfigurationAdSlotProvider.cs
Normal file
49
Services/Ads/ConfigurationAdSlotProvider.cs
Normal file
@ -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<AdsConfigurationOptions> _options;
|
||||||
|
|
||||||
|
public ConfigurationAdSlotProvider(IOptionsSnapshot<AdsConfigurationOptions> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Services/Ads/IAdSlotConfigurationProvider.cs
Normal file
9
Services/Ads/IAdSlotConfigurationProvider.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using QRRapidoApp.Models.Ads;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services.Ads
|
||||||
|
{
|
||||||
|
public interface IAdSlotConfigurationProvider
|
||||||
|
{
|
||||||
|
AdSlotConfiguration GetSlot(string slotKey, string? cultureName = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -127,6 +127,7 @@ namespace QRRapidoApp.Services
|
|||||||
if (article?.Metadata != null)
|
if (article?.Metadata != null)
|
||||||
{
|
{
|
||||||
article.Metadata.Culture = culture;
|
article.Metadata.Culture = culture;
|
||||||
|
article.Metadata.Slug = slug;
|
||||||
articles.Add(article.Metadata);
|
articles.Add(article.Metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,6 +190,7 @@ namespace QRRapidoApp.Services
|
|||||||
if (article?.Metadata != null)
|
if (article?.Metadata != null)
|
||||||
{
|
{
|
||||||
article.Metadata.Culture = culture;
|
article.Metadata.Culture = culture;
|
||||||
|
article.Metadata.Slug = slug;
|
||||||
var fileInfo = new FileInfo(file);
|
var fileInfo = new FileInfo(file);
|
||||||
article.Metadata.LastMod = fileInfo.LastWriteTimeUtc;
|
article.Metadata.LastMod = fileInfo.LastWriteTimeUtc;
|
||||||
allArticles.Add(article.Metadata);
|
allArticles.Add(article.Metadata);
|
||||||
|
|||||||
@ -1207,6 +1207,30 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Tutorials Card -->
|
||||||
|
@{
|
||||||
|
var tutorialCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
|
||||||
|
}
|
||||||
|
<div class="card border-primary mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-book-open"></i> @Localizer["LearnMore"]
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small mb-2">@Localizer["CompleteGuidesAboutQRCodes"]</p>
|
||||||
|
<ul class="list-unstyled small mb-3">
|
||||||
|
<li class="mb-1"><i class="fas fa-check text-primary"></i> WhatsApp Business</li>
|
||||||
|
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["WiFi"] Networks</li>
|
||||||
|
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["VCard"]</li>
|
||||||
|
<li class="mb-1"><i class="fas fa-check text-primary"></i> @Localizer["RealEstateAndBrokers"]</li>
|
||||||
|
</ul>
|
||||||
|
<a href="/@tutorialCulture/tutoriais" class="btn btn-primary btn-sm w-100">
|
||||||
|
<i class="fas fa-graduation-cap"></i> @Localizer["ViewAllTutorials"]
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Speed Tips Card -->
|
<!-- Speed Tips Card -->
|
||||||
<div class="card bg-light mb-4">
|
<div class="card bg-light mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
@ -1,89 +1,152 @@
|
|||||||
@using QRRapidoApp.Services
|
@using QRRapidoApp.Services
|
||||||
|
@using QRRapidoApp.Services.Ads
|
||||||
|
@using QRRapidoApp.Models.Ads
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using System.Globalization
|
||||||
@model dynamic
|
@model dynamic
|
||||||
@inject AdDisplayService AdService
|
@inject AdDisplayService AdService
|
||||||
|
@inject IAdSlotConfigurationProvider SlotProvider
|
||||||
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
@inject IStringLocalizer<QRRapidoApp.Resources.SharedResource> Localizer
|
||||||
@{
|
@{
|
||||||
|
|
||||||
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
var showAds = await AdService.ShouldShowAds(userId);
|
var showAds = await AdService.ShouldShowAds(userId);
|
||||||
var position = ViewBag.position ?? Model?.position ?? "header";
|
var rawPosition = (ViewBag.position ?? Model?.position ?? "header")?.ToString()?.ToLowerInvariant();
|
||||||
var tagAdSense = ViewBag.AdSenseTag;
|
var position = string.IsNullOrWhiteSpace(rawPosition) ? "header" : rawPosition;
|
||||||
<!-- AdSense -->
|
var currentCulture = CultureInfo.CurrentUICulture?.Name;
|
||||||
@Html.Raw(ViewBag.AdSenseScript);
|
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)
|
@if (showAds)
|
||||||
{
|
{
|
||||||
@switch (position)
|
switch (position)
|
||||||
{
|
{
|
||||||
case "header":
|
case "header":
|
||||||
<div class="ad-container ad-header mb-4">
|
if (shouldRenderAffiliate)
|
||||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
{
|
||||||
<ins class="adsbygoogle"
|
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||||
style="display:inline-block;width:728px;height:90px"
|
{
|
||||||
data-ad-client="@tagAdSense"
|
SlotKey = position,
|
||||||
data-ad-slot="QR19750801"></ins>
|
ContainerCssClass = containerCss,
|
||||||
</div>
|
Content = affiliateContent!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (shouldRenderAdSense)
|
||||||
|
{
|
||||||
|
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
|
||||||
|
<div class="@containerCss" style="display: none;">
|
||||||
|
<!-- Adsterra code will go here when approved -->
|
||||||
|
</div>
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "sidebar":
|
case "sidebar":
|
||||||
<div class="ad-container ad-sidebar mb-4">
|
if (shouldRenderAffiliate)
|
||||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
{
|
||||||
<ins class="adsbygoogle"
|
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||||
style="display:inline-block;width:300px;height:250px"
|
{
|
||||||
data-ad-client="@tagAdSense"
|
SlotKey = position,
|
||||||
data-ad-slot="QR19750802"></ins>
|
ContainerCssClass = containerCss,
|
||||||
</div>
|
Content = affiliateContent!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (shouldRenderAdSense)
|
||||||
|
{
|
||||||
|
<!-- Placeholder for Adsterra ad - 300x250 Rectangle -->
|
||||||
|
<div class="@containerCss" style="display: none;">
|
||||||
|
<!-- Adsterra code will go here when approved -->
|
||||||
|
</div>
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "footer":
|
case "footer":
|
||||||
<div class="ad-container ad-footer mt-5 mb-4">
|
if (shouldRenderAffiliate)
|
||||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
{
|
||||||
<ins class="adsbygoogle"
|
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||||
style="display:inline-block;width:728px;height:90px"
|
{
|
||||||
data-ad-client="@tagAdSense"
|
SlotKey = position,
|
||||||
data-ad-slot="QR19750803"></ins>
|
ContainerCssClass = containerCss,
|
||||||
</div>
|
Content = affiliateContent!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (shouldRenderAdSense)
|
||||||
|
{
|
||||||
|
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
|
||||||
|
<div class="@containerCss" style="display: none;">
|
||||||
|
<!-- Adsterra code will go here when approved -->
|
||||||
|
</div>
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "content":
|
case "content":
|
||||||
<div class="ad-container ad-content my-4">
|
if (shouldRenderAffiliate)
|
||||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
{
|
||||||
<ins class="adsbygoogle"
|
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||||
style="display:block"
|
{
|
||||||
data-ad-client="@tagAdSense"
|
SlotKey = position,
|
||||||
data-ad-slot="QR19750804"
|
ContainerCssClass = containerCss,
|
||||||
data-ad-format="auto"
|
Content = affiliateContent!
|
||||||
data-full-width-responsive="true"></ins>
|
})
|
||||||
</div>
|
}
|
||||||
|
else if (shouldRenderAdSense)
|
||||||
|
{
|
||||||
|
<!-- Placeholder for Adsterra ad - Responsive -->
|
||||||
|
<div class="@containerCss" style="display: none;">
|
||||||
|
<!-- Adsterra code will go here when approved -->
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (shouldRenderAffiliate)
|
||||||
|
{
|
||||||
|
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||||
|
{
|
||||||
|
SlotKey = position,
|
||||||
|
ContainerCssClass = containerCss,
|
||||||
|
Content = affiliateContent!
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (shouldRenderAdSense)
|
||||||
|
{
|
||||||
|
<!-- Placeholder for Adsterra ad - 728x90 Banner -->
|
||||||
|
<div class="@containerCss" style="display: none;">
|
||||||
|
<!-- Adsterra code will go here when approved -->
|
||||||
|
</div>
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
<script defer>
|
@* AdSense lazy-load script removed - will be replaced with Adsterra when approved *@
|
||||||
// Lazy load AdSense to improve LCP
|
|
||||||
if ('IntersectionObserver' in window) {
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.adsbygoogle').forEach(ad => observer.observe(ad));
|
|
||||||
} else {
|
|
||||||
// Fallback for older browsers
|
|
||||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
}
|
}
|
||||||
else if (User.Identity.IsAuthenticated)
|
else if (User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
var isPremium = await AdService.HasValidPremiumSubscription(userId);
|
||||||
if (isPremium)
|
if (isPremium)
|
||||||
{
|
{
|
||||||
<!-- Premium User Message -->
|
|
||||||
<div class="alert alert-success ad-free-notice mb-3">
|
<div class="alert alert-success ad-free-notice mb-3">
|
||||||
<i class="fas fa-crown text-warning"></i>
|
<i class="fas fa-crown text-warning"></i>
|
||||||
<span><strong>@Localizer["PremiumUserNoAds"]</strong></span>
|
<span><strong>@Localizer["PremiumUserNoAds"]</strong></span>
|
||||||
@ -91,7 +154,6 @@ else if (User.Identity.IsAuthenticated)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<!-- Upgrade to Premium Message -->
|
|
||||||
<div class="alert alert-info upgrade-notice mb-3">
|
<div class="alert alert-info upgrade-notice mb-3">
|
||||||
<i class="fas fa-star text-warning"></i>
|
<i class="fas fa-star text-warning"></i>
|
||||||
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
|
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
|
||||||
|
|||||||
97
Views/Shared/_AffiliateAd.cshtml
Normal file
97
Views/Shared/_AffiliateAd.cshtml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
@model QRRapidoApp.Models.Ads.AffiliateAdViewModel
|
||||||
|
@inject Microsoft.Extensions.Localization.IStringLocalizer<QRRapidoApp.Resources.SharedResource> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="@containerClass" data-slot="@Model.SlotKey">
|
||||||
|
<div class="ad-label">@Localizer["Advertisement"]</div>
|
||||||
|
@if (isWideSlot)
|
||||||
|
{
|
||||||
|
<div class="card border-0 shadow-sm h-100 affiliate-ad-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="affiliate-ad-card-content">
|
||||||
|
@if (hasImage)
|
||||||
|
{
|
||||||
|
<div class="affiliate-ad-media">
|
||||||
|
<img src="@content.ImageUrl" alt="@content.Title" class="affiliate-ad-image img-fluid"
|
||||||
|
style="@imageStyle" loading="lazy" />
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.BadgeText))
|
||||||
|
{
|
||||||
|
<span class="affiliate-ad-partner badge bg-warning text-dark small mt-2">@content.BadgeText</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="affiliate-ad-details">
|
||||||
|
@if (!hasImage && !string.IsNullOrWhiteSpace(content.BadgeText))
|
||||||
|
{
|
||||||
|
<span class="affiliate-ad-partner badge bg-warning text-dark">@content.BadgeText</span>
|
||||||
|
}
|
||||||
|
<h6 class="card-title mb-0">@content.Title</h6>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.Description))
|
||||||
|
{
|
||||||
|
<p class="card-text text-muted small mb-0">@content.Description</p>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.PriceText))
|
||||||
|
{
|
||||||
|
<div class="affiliate-ad-price fw-semibold text-success">@content.PriceText</div>
|
||||||
|
}
|
||||||
|
<div class="affiliate-ad-actions">
|
||||||
|
<a href="@content.ProductUrl" target="_blank" rel="noopener sponsored" class="btn btn-sm btn-warning affiliate-ad-cta">
|
||||||
|
@ctaText
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="card border-0 shadow-sm h-100 affiliate-ad-card">
|
||||||
|
@if (hasImage)
|
||||||
|
{
|
||||||
|
<img src="@content.ImageUrl" alt="@content.Title" class="card-img-top rounded-top affiliate-ad-image"
|
||||||
|
style="@imageStyle" loading="lazy" />
|
||||||
|
}
|
||||||
|
<div class="card-body text-center">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.BadgeText))
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark small mb-2">@content.BadgeText</span>
|
||||||
|
}
|
||||||
|
<h6 class="card-title mb-2">@content.Title</h6>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.Description))
|
||||||
|
{
|
||||||
|
<p class="card-text text-muted small mb-2">@content.Description</p>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(content.PriceText))
|
||||||
|
{
|
||||||
|
<div class="fw-semibold text-success mb-3">@content.PriceText</div>
|
||||||
|
}
|
||||||
|
<a href="@content.ProductUrl" target="_blank" rel="noopener sponsored" class="btn btn-sm btn-warning w-100">
|
||||||
|
@ctaText
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@ -140,14 +140,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@if (ViewBag.AdSenseEnabled)
|
@* AdSense removed - Preparing for Adsterra integration *@
|
||||||
{
|
|
||||||
|
|
||||||
var tagAdSense = $"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client={ViewBag.AdSenseTag}";
|
|
||||||
var adSenseScript = $"";
|
|
||||||
<!-- AdSense - Optimized with defer and crossorigin -->
|
|
||||||
<script defer src='@tagAdSense' crossorigin='anonymous'></script>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Bootstrap 5 - Optimized loading -->
|
<!-- Bootstrap 5 - Optimized loading -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" media="print" onload="this.media='all'">
|
||||||
@ -328,6 +321,14 @@
|
|||||||
<p class="mb-0 opacity-75">
|
<p class="mb-0 opacity-75">
|
||||||
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
|
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
|
||||||
</p>
|
</p>
|
||||||
|
@{
|
||||||
|
var currentCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
|
||||||
|
}
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/@currentCulture/tutoriais" class="btn btn-sm btn-light">
|
||||||
|
<i class="fas fa-graduation-cap"></i> @Localizer["ViewTutorials"]
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/@ViewBag.Culture/tutoriais/@article.Title.ToLower().Replace(" ", "-")"
|
<a href="/@ViewBag.Culture/tutoriais/@article.Slug"
|
||||||
class="btn btn-primary btn-block">
|
class="btn btn-primary btn-block">
|
||||||
@(ViewBag.Culture == "pt-BR" ? "Ler Tutorial" : "Leer Tutorial")
|
@(ViewBag.Culture == "pt-BR" ? "Ler Tutorial" : "Leer Tutorial")
|
||||||
<i class="fas fa-arrow-right ml-1"></i>
|
<i class="fas fa-arrow-right ml-1"></i>
|
||||||
|
|||||||
@ -40,7 +40,72 @@
|
|||||||
},
|
},
|
||||||
"AdSense": {
|
"AdSense": {
|
||||||
"ClientId": "ca-pub-3475956393038764",
|
"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": {
|
"Performance": {
|
||||||
"QRGenerationTimeoutMs": 2000,
|
"QRGenerationTimeoutMs": 2000,
|
||||||
|
|||||||
@ -527,6 +527,121 @@ footer a:hover {
|
|||||||
margin-bottom: 8px;
|
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
|
PLACEHOLDER QR - CONTRASTE SUPERIOR
|
||||||
================================= */
|
================================= */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user