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 Culture { get; set; } = "pt-BR";
|
||||
public int ReadingTimeMinutes { get; set; }
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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)
|
||||
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(
|
||||
|
||||
@ -2015,4 +2015,20 @@
|
||||
<data name="VCardUseCase3" xml:space="preserve">
|
||||
<value>Firma de email profesional</value>
|
||||
</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>
|
||||
@ -2105,4 +2105,20 @@
|
||||
<data name="VCardUseCase3" xml:space="preserve">
|
||||
<value>Assinatura de email profissional</value>
|
||||
</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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
@ -1207,6 +1207,30 @@
|
||||
</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 -->
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-header">
|
||||
|
||||
@ -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<QRRapidoApp.Resources.SharedResource> 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;
|
||||
<!-- AdSense -->
|
||||
@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":
|
||||
<div class="ad-container ad-header mb-4">
|
||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
||||
<ins class="adsbygoogle"
|
||||
style="display:inline-block;width:728px;height:90px"
|
||||
data-ad-client="@tagAdSense"
|
||||
data-ad-slot="QR19750801"></ins>
|
||||
</div>
|
||||
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;
|
||||
|
||||
|
||||
case "sidebar":
|
||||
<div class="ad-container ad-sidebar mb-4">
|
||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
||||
<ins class="adsbygoogle"
|
||||
style="display:inline-block;width:300px;height:250px"
|
||||
data-ad-client="@tagAdSense"
|
||||
data-ad-slot="QR19750802"></ins>
|
||||
</div>
|
||||
if (shouldRenderAffiliate)
|
||||
{
|
||||
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||
{
|
||||
SlotKey = position,
|
||||
ContainerCssClass = containerCss,
|
||||
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;
|
||||
|
||||
|
||||
case "footer":
|
||||
<div class="ad-container ad-footer mt-5 mb-4">
|
||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
||||
<ins class="adsbygoogle"
|
||||
style="display:inline-block;width:728px;height:90px"
|
||||
data-ad-client="@tagAdSense"
|
||||
data-ad-slot="QR19750803"></ins>
|
||||
</div>
|
||||
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;
|
||||
|
||||
|
||||
case "content":
|
||||
<div class="ad-container ad-content my-4">
|
||||
<div class="ad-label">@Localizer["Advertisement"]</div>
|
||||
<ins class="adsbygoogle"
|
||||
style="display:block"
|
||||
data-ad-client="@tagAdSense"
|
||||
data-ad-slot="QR19750804"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
</div>
|
||||
if (shouldRenderAffiliate)
|
||||
{
|
||||
@await Html.PartialAsync("_AffiliateAd", new AffiliateAdViewModel
|
||||
{
|
||||
SlotKey = position,
|
||||
ContainerCssClass = containerCss,
|
||||
Content = affiliateContent!
|
||||
})
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
<script defer>
|
||||
// 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>
|
||||
|
||||
@* 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)
|
||||
{
|
||||
<!-- Premium User Message -->
|
||||
<div class="alert alert-success ad-free-notice mb-3">
|
||||
<i class="fas fa-crown text-warning"></i>
|
||||
<span><strong>@Localizer["PremiumUserNoAds"]</strong></span>
|
||||
@ -91,7 +154,6 @@ else if (User.Identity.IsAuthenticated)
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Upgrade to Premium Message -->
|
||||
<div class="alert alert-info upgrade-notice mb-3">
|
||||
<i class="fas fa-star text-warning"></i>
|
||||
<span><strong>@Localizer["UpgradePremiumRemoveAds"]</strong></span>
|
||||
@ -100,4 +162,4 @@ else if (User.Identity.IsAuthenticated)
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@if (ViewBag.AdSenseEnabled)
|
||||
{
|
||||
|
||||
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>
|
||||
}
|
||||
@* AdSense removed - Preparing for Adsterra integration *@
|
||||
|
||||
<!-- 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'">
|
||||
@ -328,6 +321,14 @@
|
||||
<p class="mb-0 opacity-75">
|
||||
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<a href="/@ViewBag.Culture/tutoriais/@article.Title.ToLower().Replace(" ", "-")"
|
||||
<a href="/@ViewBag.Culture/tutoriais/@article.Slug"
|
||||
class="btn btn-primary btn-block">
|
||||
@(ViewBag.Culture == "pt-BR" ? "Ler Tutorial" : "Leer Tutorial")
|
||||
<i class="fas fa-arrow-right ml-1"></i>
|
||||
|
||||
@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user