Compare commits
No commits in common. "b58827b724ef5c3bee8aa9ad9066decf4fa32cbd" and "d74e16fbaf3b709f5566b2ac2c32b07aebf60dc8" have entirely different histories.
b58827b724
...
d74e16fbaf
@ -21,7 +21,6 @@ public class AdminController : Controller
|
|||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly ILivePageService _livePageService;
|
private readonly ILivePageService _livePageService;
|
||||||
private readonly IImageStorageService _imageStorage;
|
private readonly IImageStorageService _imageStorage;
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly ILogger<AdminController> _logger;
|
private readonly ILogger<AdminController> _logger;
|
||||||
|
|
||||||
public AdminController(
|
public AdminController(
|
||||||
@ -33,7 +32,6 @@ public class AdminController : Controller
|
|||||||
IEmailService emailService,
|
IEmailService emailService,
|
||||||
ILivePageService livePageService,
|
ILivePageService livePageService,
|
||||||
IImageStorageService imageStorage,
|
IImageStorageService imageStorage,
|
||||||
IPaymentService paymentService,
|
|
||||||
ILogger<AdminController> logger)
|
ILogger<AdminController> logger)
|
||||||
{
|
{
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
@ -44,7 +42,6 @@ public class AdminController : Controller
|
|||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_livePageService = livePageService;
|
_livePageService = livePageService;
|
||||||
_imageStorage = imageStorage;
|
_imageStorage = imageStorage;
|
||||||
_paymentService = paymentService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,14 +151,12 @@ public class AdminController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CRIAR NOVA PÁGINA
|
// CRIAR NOVA PÁGINA
|
||||||
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
|
||||||
var model = new ManagePageViewModel
|
var model = new ManagePageViewModel
|
||||||
{
|
{
|
||||||
IsNewPage = true,
|
IsNewPage = true,
|
||||||
AvailableCategories = categories,
|
AvailableCategories = categories,
|
||||||
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
||||||
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(),
|
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage()
|
||||||
AllowProductLinks = planLimitations.AllowProductLinks
|
|
||||||
};
|
};
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
@ -172,7 +167,7 @@ public class AdminController : Controller
|
|||||||
if (page == null || page.UserId != user.Id)
|
if (page == null || page.UserId != user.Id)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var model = await MapToManageViewModel(page, categories, themes, userPlanType);
|
var model = MapToManageViewModel(page, categories, themes, userPlanType);
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -615,7 +610,7 @@ public class AdminController : Controller
|
|||||||
return RedirectToAction("Dashboard");
|
return RedirectToAction("Dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ManagePageViewModel> MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
private ManagePageViewModel MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
||||||
{
|
{
|
||||||
return new ManagePageViewModel
|
return new ManagePageViewModel
|
||||||
{
|
{
|
||||||
@ -646,8 +641,7 @@ public class AdminController : Controller
|
|||||||
}).ToList() ?? new List<ManageLinkViewModel>(),
|
}).ToList() ?? new List<ManageLinkViewModel>(),
|
||||||
AvailableCategories = categories,
|
AvailableCategories = categories,
|
||||||
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
|
||||||
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(),
|
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage()
|
||||||
AllowProductLinks = (await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString())).AllowProductLinks
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,23 +761,8 @@ public class AdminController : Controller
|
|||||||
// Add regular links
|
// Add regular links
|
||||||
if (model.Links?.Any() == true)
|
if (model.Links?.Any() == true)
|
||||||
{
|
{
|
||||||
// Validar links de produto baseado no plano do usuário
|
page.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url))
|
||||||
var user = await _authService.GetCurrentUserAsync(User);
|
.Select((l, index) => new LinkItem
|
||||||
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
|
||||||
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
|
||||||
|
|
||||||
var filteredLinks = model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url));
|
|
||||||
|
|
||||||
foreach (var link in filteredLinks)
|
|
||||||
{
|
|
||||||
// Verificar se usuário pode criar links de produto
|
|
||||||
if (link.Type == LinkType.Product && !planLimitations.AllowProductLinks)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Links de produto disponíveis apenas no plano Premium + Afiliados");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.Links.AddRange(filteredLinks.Select((l, index) => new LinkItem
|
|
||||||
{
|
{
|
||||||
Title = l.Title,
|
Title = l.Title,
|
||||||
Url = l.Url,
|
Url = l.Url,
|
||||||
|
|||||||
@ -30,10 +30,10 @@ public class ModerationController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Dashboard")]
|
[HttpGet("Dashboard")]
|
||||||
public async Task<IActionResult> Dashboard(int page = 1, int size = 20, string? filter = null)
|
public async Task<IActionResult> Dashboard(int page = 1, int size = 20)
|
||||||
{
|
{
|
||||||
var skip = (page - 1) * size;
|
var skip = (page - 1) * size;
|
||||||
var pendingPages = await _moderationService.GetPendingModerationAsync(skip, size, filter);
|
var pendingPages = await _moderationService.GetPendingModerationAsync(skip, size);
|
||||||
var stats = await _moderationService.GetModerationStatsAsync();
|
var stats = await _moderationService.GetModerationStatsAsync();
|
||||||
|
|
||||||
var viewModel = new ModerationDashboardViewModel
|
var viewModel = new ModerationDashboardViewModel
|
||||||
@ -47,7 +47,6 @@ public class ModerationController : Controller
|
|||||||
CreatedAt = p.CreatedAt,
|
CreatedAt = p.CreatedAt,
|
||||||
ModerationAttempts = p.ModerationAttempts,
|
ModerationAttempts = p.ModerationAttempts,
|
||||||
PlanType = p.PlanLimitations.PlanType.ToString(),
|
PlanType = p.PlanLimitations.PlanType.ToString(),
|
||||||
IsSpecialModeration = p.PlanLimitations.SpecialModeration ?? false,
|
|
||||||
PreviewUrl = !string.IsNullOrEmpty(p.PreviewToken)
|
PreviewUrl = !string.IsNullOrEmpty(p.PreviewToken)
|
||||||
? $"/page/{p.Category}/{p.Slug}?preview={p.PreviewToken}"
|
? $"/page/{p.Category}/{p.Slug}?preview={p.PreviewToken}"
|
||||||
: null
|
: null
|
||||||
@ -55,8 +54,7 @@ public class ModerationController : Controller
|
|||||||
Stats = stats,
|
Stats = stats,
|
||||||
CurrentPage = page,
|
CurrentPage = page,
|
||||||
PageSize = size,
|
PageSize = size,
|
||||||
HasNextPage = pendingPages.Count == size,
|
HasNextPage = pendingPages.Count == size
|
||||||
CurrentFilter = filter ?? "all"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return View(viewModel);
|
return View(viewModel);
|
||||||
|
|||||||
@ -16,15 +16,13 @@ public class PaymentController : Controller
|
|||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly IUserRepository _userService;
|
private readonly IUserRepository _userService;
|
||||||
private readonly ISubscriptionRepository _subscriptionRepository;
|
private readonly ISubscriptionRepository _subscriptionRepository;
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
|
|
||||||
public PaymentController(IPaymentService paymentService, IAuthService authService, IUserRepository userService, ISubscriptionRepository subscriptionRepository, IConfiguration configuration)
|
public PaymentController(IPaymentService paymentService, IAuthService authService, IUserRepository userService, ISubscriptionRepository subscriptionRepository)
|
||||||
{
|
{
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_subscriptionRepository = subscriptionRepository;
|
_subscriptionRepository = subscriptionRepository;
|
||||||
_configuration = configuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -222,33 +220,47 @@ public class PaymentController : Controller
|
|||||||
|
|
||||||
private List<AvailablePlanViewModel> GetAvailablePlans(string currentPlan)
|
private List<AvailablePlanViewModel> GetAvailablePlans(string currentPlan)
|
||||||
{
|
{
|
||||||
var plansConfig = _configuration.GetSection("Plans");
|
var plans = new List<AvailablePlanViewModel>
|
||||||
var plans = new List<AvailablePlanViewModel>();
|
|
||||||
|
|
||||||
// Adicionar planos mensais apenas (excluir Trial e planos anuais)
|
|
||||||
var monthlyPlans = new[] { "Basic", "Professional", "Premium", "PremiumAffiliate" };
|
|
||||||
|
|
||||||
foreach (var planKey in monthlyPlans)
|
|
||||||
{
|
{
|
||||||
var planSection = plansConfig.GetSection(planKey);
|
// Plano Trial não é incluído aqui pois é gerenciado internamente
|
||||||
if (planSection.Exists())
|
// O "downgrade" para Trial acontece via cancelamento da assinatura
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
plans.Add(new AvailablePlanViewModel
|
PlanType = "basic",
|
||||||
{
|
DisplayName = "Básico",
|
||||||
PlanType = planKey.ToLower(),
|
Price = 9.90m,
|
||||||
DisplayName = planSection["Name"] ?? planKey,
|
PriceId = "price_basic", // Substitua pelos IDs reais do Stripe
|
||||||
Price = decimal.Parse(planSection["Price"] ?? "0"),
|
MaxLinks = 5,
|
||||||
PriceId = planSection["PriceId"] ?? "",
|
AllowAnalytics = true,
|
||||||
MaxLinks = int.Parse(planSection["MaxLinks"] ?? "0"),
|
Features = new List<string> { "5 links", "Temas básicos", "Análises básicas" },
|
||||||
AllowAnalytics = bool.Parse(planSection["AllowAnalytics"] ?? "false"),
|
IsCurrentPlan = currentPlan == "basic"
|
||||||
AllowCustomDomain = true, // URL personalizada em todos os planos pagos
|
},
|
||||||
AllowCustomThemes = bool.Parse(planSection["AllowPremiumThemes"] ?? "false"),
|
new()
|
||||||
AllowProductLinks = bool.Parse(planSection["AllowProductLinks"] ?? "false"),
|
{
|
||||||
Features = planSection.GetSection("Features").Get<List<string>>() ?? new List<string>(),
|
PlanType = "professional",
|
||||||
IsCurrentPlan = currentPlan.Equals(planKey, StringComparison.OrdinalIgnoreCase)
|
DisplayName = "Profissional",
|
||||||
});
|
Price = 24.90m,
|
||||||
|
PriceId = "price_professional", // Substitua pelos IDs reais do Stripe
|
||||||
|
MaxLinks = 15,
|
||||||
|
AllowAnalytics = true,
|
||||||
|
AllowCustomDomain = true,
|
||||||
|
Features = new List<string> { "15 links", "Todos os temas", "Página rápida", "Análises avançadas" },
|
||||||
|
IsCurrentPlan = currentPlan == "professional"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PlanType = "premium",
|
||||||
|
DisplayName = "Premium",
|
||||||
|
Price = 29.90m,
|
||||||
|
PriceId = "price_premium", // Substitua pelos IDs reais do Stripe
|
||||||
|
MaxLinks = -1, // Ilimitado
|
||||||
|
AllowCustomThemes = true,
|
||||||
|
AllowAnalytics = true,
|
||||||
|
AllowCustomDomain = true,
|
||||||
|
Features = new List<string> { "Links ilimitados", "Temas personalizados", "Página rápida", "Suporte prioritário" },
|
||||||
|
IsCurrentPlan = currentPlan == "premium"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Marcar upgrades e filtrar downgrades
|
// Marcar upgrades e filtrar downgrades
|
||||||
var currentPlanIndex = plans.FindIndex(p => p.IsCurrentPlan);
|
var currentPlanIndex = plans.FindIndex(p => p.IsCurrentPlan);
|
||||||
|
|||||||
@ -215,19 +215,10 @@ public class StripeWebhookController : ControllerBase
|
|||||||
// This would be configured based on your actual Stripe price IDs
|
// This would be configured based on your actual Stripe price IDs
|
||||||
return priceId switch
|
return priceId switch
|
||||||
{
|
{
|
||||||
"price_1RjUskBMIadsOxJVgLwlVo1y" => "Basic",
|
var id when id.Contains("basic") => "basic",
|
||||||
"price_1RjUv9BMIadsOxJVORqlM4E9" => "Professional",
|
var id when id.Contains("professional") => "professional",
|
||||||
"price_1RjUw0BMIadsOxJVmdouNV1g" => "Premium",
|
var id when id.Contains("premium") => "premium",
|
||||||
"price_basic_yearly_placeholder" => "BasicYearly",
|
_ => "trial"
|
||||||
"price_professional_yearly_placeholder" => "ProfessionalYearly",
|
|
||||||
"price_premium_yearly_placeholder" => "PremiumYearly",
|
|
||||||
var id when id.Contains("basic") && id.Contains("yearly") => "BasicYearly",
|
|
||||||
var id when id.Contains("professional") && id.Contains("yearly") => "ProfessionalYearly",
|
|
||||||
var id when id.Contains("premium") && id.Contains("yearly") => "PremiumYearly",
|
|
||||||
var id when id.Contains("basic") => "Basic",
|
|
||||||
var id when id.Contains("professional") => "Professional",
|
|
||||||
var id when id.Contains("premium") => "Premium",
|
|
||||||
_ => "Trial"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,155 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using BCards.Web.Services;
|
|
||||||
using BCards.Web.ViewModels;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace BCards.Web.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[Route("subscription")]
|
|
||||||
public class SubscriptionController : Controller
|
|
||||||
{
|
|
||||||
private readonly IPaymentService _paymentService;
|
|
||||||
private readonly ILogger<SubscriptionController> _logger;
|
|
||||||
|
|
||||||
public SubscriptionController(
|
|
||||||
IPaymentService paymentService,
|
|
||||||
ILogger<SubscriptionController> logger)
|
|
||||||
{
|
|
||||||
_paymentService = paymentService;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("cancel")]
|
|
||||||
public async Task<IActionResult> Cancel()
|
|
||||||
{
|
|
||||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return RedirectToAction("Login", "Auth");
|
|
||||||
|
|
||||||
var subscription = await _paymentService.GetSubscriptionDetailsAsync(userId);
|
|
||||||
if (subscription == null)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Nenhuma assinatura ativa encontrada.";
|
|
||||||
return RedirectToAction("ManageSubscription", "Payment");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcular opções de reembolso
|
|
||||||
var (canRefundFull, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(subscription.Id);
|
|
||||||
|
|
||||||
// Obter datas via SubscriptionItem
|
|
||||||
var subscriptionItemService = new Stripe.SubscriptionItemService();
|
|
||||||
var subItem = await subscriptionItemService.GetAsync(subscription.Items.Data[0].Id);
|
|
||||||
|
|
||||||
var viewModel = new CancelSubscriptionViewModel
|
|
||||||
{
|
|
||||||
SubscriptionId = subscription.Id,
|
|
||||||
PlanName = subscription.Items.Data.FirstOrDefault()?.Price.Nickname ?? "Plano Atual",
|
|
||||||
CurrentPeriodEnd = subItem.CurrentPeriodEnd,
|
|
||||||
CanRefundFull = canRefundFull,
|
|
||||||
CanRefundPartial = canRefundPartial,
|
|
||||||
RefundAmount = refundAmount,
|
|
||||||
DaysRemaining = (subItem.CurrentPeriodEnd - DateTime.UtcNow).Days
|
|
||||||
};
|
|
||||||
|
|
||||||
return View(viewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("cancel")]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> ProcessCancel(CancelSubscriptionRequest request)
|
|
||||||
{
|
|
||||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return RedirectToAction("Login", "Auth");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool success = false;
|
|
||||||
string message = "";
|
|
||||||
|
|
||||||
switch (request.CancelType)
|
|
||||||
{
|
|
||||||
case "immediate_with_refund":
|
|
||||||
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, true);
|
|
||||||
message = success ? "Assinatura cancelada. Reembolso será processado manualmente em até 10 dias úteis." : "Erro ao processar cancelamento.";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "immediate_no_refund":
|
|
||||||
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, false);
|
|
||||||
message = success ? "Assinatura cancelada imediatamente." : "Erro ao cancelar assinatura.";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "partial_refund":
|
|
||||||
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, false);
|
|
||||||
var (_, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(request.SubscriptionId);
|
|
||||||
if (success && canRefundPartial && refundAmount > 0)
|
|
||||||
{
|
|
||||||
message = $"Assinatura cancelada. Reembolso parcial de R$ {refundAmount:F2} será processado manualmente em até 10 dias úteis.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
message = success ? "Assinatura cancelada. Reembolso parcial não disponível." : "Erro ao cancelar assinatura.";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "at_period_end":
|
|
||||||
default:
|
|
||||||
success = await _paymentService.CancelSubscriptionAtPeriodEndAsync(request.SubscriptionId);
|
|
||||||
message = success ? "Assinatura será cancelada no final do período atual." : "Erro ao agendar cancelamento.";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
TempData["Success"] = message;
|
|
||||||
_logger.LogInformation($"User {userId} cancelled subscription {request.SubscriptionId} with type {request.CancelType}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TempData["Error"] = message;
|
|
||||||
_logger.LogError($"Failed to cancel subscription {request.SubscriptionId} for user {userId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Ocorreu um erro ao processar o cancelamento. Tente novamente.";
|
|
||||||
_logger.LogError(ex, $"Error cancelling subscription {request.SubscriptionId} for user {userId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("ManageSubscription", "Payment");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("reactivate")]
|
|
||||||
[ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> Reactivate(string subscriptionId)
|
|
||||||
{
|
|
||||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return RedirectToAction("Login", "Auth");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Remover agendamento de cancelamento
|
|
||||||
var success = await _paymentService.CancelSubscriptionAtPeriodEndAsync(subscriptionId);
|
|
||||||
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
TempData["Success"] = "Assinatura reativada com sucesso!";
|
|
||||||
_logger.LogInformation($"User {userId} reactivated subscription {subscriptionId}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Erro ao reativar assinatura.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
TempData["Error"] = "Ocorreu um erro ao reativar a assinatura.";
|
|
||||||
_logger.LogError(ex, $"Error reactivating subscription {subscriptionId} for user {userId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("ManageSubscription", "Payment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -35,9 +35,6 @@ public class PlanLimitations
|
|||||||
[BsonElement("allowProductLinks")]
|
[BsonElement("allowProductLinks")]
|
||||||
public bool AllowProductLinks { get; set; } = false;
|
public bool AllowProductLinks { get; set; } = false;
|
||||||
|
|
||||||
[BsonElement("specialModeration")]
|
|
||||||
public bool? SpecialModeration { get; set; } = false;
|
|
||||||
|
|
||||||
[BsonElement("ogExtractionsUsedToday")]
|
[BsonElement("ogExtractionsUsedToday")]
|
||||||
public int OGExtractionsUsedToday { get; set; } = 0;
|
public int OGExtractionsUsedToday { get; set; } = 0;
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public interface IModerationService
|
|||||||
{
|
{
|
||||||
Task<string> GeneratePreviewTokenAsync(string pageId);
|
Task<string> GeneratePreviewTokenAsync(string pageId);
|
||||||
Task<bool> ValidatePreviewTokenAsync(string pageId, string token);
|
Task<bool> ValidatePreviewTokenAsync(string pageId, string token);
|
||||||
Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20, string? filter = null);
|
Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20);
|
||||||
Task<UserPage?> GetPageForModerationAsync(string pageId);
|
Task<UserPage?> GetPageForModerationAsync(string pageId);
|
||||||
Task ApprovePageAsync(string pageId, string moderatorId, string? notes = null);
|
Task ApprovePageAsync(string pageId, string moderatorId, string? notes = null);
|
||||||
Task RejectPageAsync(string pageId, string moderatorId, string reason, List<string> issues);
|
Task RejectPageAsync(string pageId, string moderatorId, string reason, List<string> issues);
|
||||||
|
|||||||
@ -17,10 +17,4 @@ public interface IPaymentService
|
|||||||
Task<Stripe.Subscription?> GetSubscriptionDetailsAsync(string userId);
|
Task<Stripe.Subscription?> GetSubscriptionDetailsAsync(string userId);
|
||||||
Task<List<Invoice>> GetPaymentHistoryAsync(string userId);
|
Task<List<Invoice>> GetPaymentHistoryAsync(string userId);
|
||||||
Task<string> CreatePortalSessionAsync(string customerId, string returnUrl);
|
Task<string> CreatePortalSessionAsync(string customerId, string returnUrl);
|
||||||
|
|
||||||
// Métodos para cancelamento com diferentes políticas
|
|
||||||
Task<bool> CancelSubscriptionImmediatelyAsync(string subscriptionId, bool refund = false);
|
|
||||||
Task<bool> CancelSubscriptionAtPeriodEndAsync(string subscriptionId);
|
|
||||||
Task<Refund> CreatePartialRefundAsync(string subscriptionId, decimal refundAmount);
|
|
||||||
Task<(bool CanRefundFull, bool CanRefundPartial, decimal RefundAmount)> CalculateRefundAsync(string subscriptionId);
|
|
||||||
}
|
}
|
||||||
@ -52,44 +52,16 @@ public class ModerationService : IModerationService
|
|||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20)
|
||||||
public async Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20, string? filter = null)
|
|
||||||
{
|
{
|
||||||
var filterBuilder = Builders<UserPage>.Filter;
|
var filter = Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.PendingModeration);
|
||||||
var baseFilter = filterBuilder.Eq(p => p.Status, PageStatus.PendingModeration);
|
|
||||||
|
|
||||||
FilterDefinition<UserPage> finalFilter = baseFilter;
|
// Ordenar por prioridade do plano e depois por data
|
||||||
|
|
||||||
// Aplicar filtro de moderação especial
|
|
||||||
if (!string.IsNullOrEmpty(filter))
|
|
||||||
{
|
|
||||||
switch (filter.ToLower())
|
|
||||||
{
|
|
||||||
case "special":
|
|
||||||
// Filtrar apenas páginas com moderação especial (Premium + Afiliados)
|
|
||||||
var specialFilter = filterBuilder.Eq("planLimitations.specialModeration", true);
|
|
||||||
finalFilter = filterBuilder.And(baseFilter, specialFilter);
|
|
||||||
break;
|
|
||||||
case "normal":
|
|
||||||
// Filtrar apenas páginas sem moderação especial
|
|
||||||
var normalFilter = filterBuilder.Or(
|
|
||||||
filterBuilder.Eq("planLimitations.specialModeration", false),
|
|
||||||
filterBuilder.Exists("planLimitations.specialModeration", false)
|
|
||||||
);
|
|
||||||
finalFilter = filterBuilder.And(baseFilter, normalFilter);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// "all" ou qualquer outro valor: sem filtro adicional
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordenar por moderação especial primeiro (SLA reduzido), depois por data
|
|
||||||
var sort = Builders<UserPage>.Sort
|
var sort = Builders<UserPage>.Sort
|
||||||
.Descending("planLimitations.specialModeration")
|
.Ascending("planLimitations.planType")
|
||||||
.Ascending(p => p.CreatedAt);
|
.Ascending(p => p.CreatedAt);
|
||||||
|
|
||||||
var pages = await _userPageRepository.GetManyAsync(finalFilter, sort, skip, take);
|
var pages = await _userPageRepository.GetManyAsync(filter, sort, skip, take);
|
||||||
return pages.ToList();
|
return pages.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -205,26 +205,22 @@ public class PaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
"basic" => new PlanLimitations
|
"basic" => new PlanLimitations
|
||||||
{
|
{
|
||||||
MaxLinks = 8,
|
MaxLinks = 5,
|
||||||
AllowCustomThemes = false,
|
AllowCustomThemes = false,
|
||||||
AllowAnalytics = true,
|
AllowAnalytics = true,
|
||||||
AllowCustomDomain = true, // URL personalizada em todos os planos pagos
|
AllowCustomDomain = false,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = false,
|
|
||||||
MaxProductLinks = 0,
|
|
||||||
PlanType = "basic"
|
PlanType = "basic"
|
||||||
},
|
},
|
||||||
"professional" => new PlanLimitations
|
"professional" => new PlanLimitations
|
||||||
{
|
{
|
||||||
MaxLinks = 20,
|
MaxLinks = 15,
|
||||||
AllowCustomThemes = false,
|
AllowCustomThemes = false,
|
||||||
AllowAnalytics = true,
|
AllowAnalytics = true,
|
||||||
AllowCustomDomain = true,
|
AllowCustomDomain = true,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = false,
|
|
||||||
MaxProductLinks = 0,
|
|
||||||
PlanType = "professional"
|
PlanType = "professional"
|
||||||
},
|
},
|
||||||
"premium" => new PlanLimitations
|
"premium" => new PlanLimitations
|
||||||
@ -235,33 +231,17 @@ public class PaymentService : IPaymentService
|
|||||||
AllowCustomDomain = true,
|
AllowCustomDomain = true,
|
||||||
AllowMultipleDomains = true,
|
AllowMultipleDomains = true,
|
||||||
PrioritySupport = true,
|
PrioritySupport = true,
|
||||||
AllowProductLinks = false,
|
|
||||||
MaxProductLinks = 0,
|
|
||||||
PlanType = "premium"
|
PlanType = "premium"
|
||||||
},
|
},
|
||||||
"premiumaffiliate" => new PlanLimitations
|
|
||||||
{
|
|
||||||
MaxLinks = -1, // Unlimited
|
|
||||||
AllowCustomThemes = true,
|
|
||||||
AllowAnalytics = true,
|
|
||||||
AllowCustomDomain = true,
|
|
||||||
AllowMultipleDomains = true,
|
|
||||||
PrioritySupport = true,
|
|
||||||
AllowProductLinks = true,
|
|
||||||
MaxProductLinks = 10,
|
|
||||||
PlanType = "premiumaffiliate"
|
|
||||||
},
|
|
||||||
_ => new PlanLimitations
|
_ => new PlanLimitations
|
||||||
{
|
{
|
||||||
MaxLinks = 3,
|
MaxLinks = 5,
|
||||||
AllowCustomThemes = false,
|
AllowCustomThemes = false,
|
||||||
AllowAnalytics = false,
|
AllowAnalytics = false,
|
||||||
AllowCustomDomain = false,
|
AllowCustomDomain = false,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = false,
|
PlanType = "free"
|
||||||
MaxProductLinks = 0,
|
|
||||||
PlanType = "trial"
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -416,145 +396,6 @@ public class PaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Métodos de cancelamento com diferentes políticas
|
|
||||||
public async Task<bool> CancelSubscriptionImmediatelyAsync(string subscriptionId, bool refund = false)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var service = new SubscriptionService();
|
|
||||||
|
|
||||||
if (refund)
|
|
||||||
{
|
|
||||||
// Para reembolso completo, apenas cancela - reembolso deve ser feito manualmente via Stripe Dashboard
|
|
||||||
await service.CancelAsync(subscriptionId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await service.CancelAsync(subscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atualizar subscription local
|
|
||||||
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
|
||||||
if (localSubscription != null)
|
|
||||||
{
|
|
||||||
localSubscription.Status = "cancelled";
|
|
||||||
localSubscription.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await _subscriptionRepository.UpdateAsync(localSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (StripeException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CancelSubscriptionAtPeriodEndAsync(string subscriptionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var service = new SubscriptionService();
|
|
||||||
var options = new SubscriptionUpdateOptions
|
|
||||||
{
|
|
||||||
CancelAtPeriodEnd = true
|
|
||||||
};
|
|
||||||
|
|
||||||
await service.UpdateAsync(subscriptionId, options);
|
|
||||||
|
|
||||||
// Atualizar subscription local
|
|
||||||
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
|
||||||
if (localSubscription != null)
|
|
||||||
{
|
|
||||||
localSubscription.CancelAtPeriodEnd = true;
|
|
||||||
localSubscription.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await _subscriptionRepository.UpdateAsync(localSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (StripeException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Refund> CreatePartialRefundAsync(string subscriptionId, decimal refundAmount)
|
|
||||||
{
|
|
||||||
// NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente via Stripe Dashboard
|
|
||||||
// Este método retorna null para indicar que o reembolso precisa ser feito manualmente
|
|
||||||
await Task.CompletedTask;
|
|
||||||
throw new InvalidOperationException("Reembolsos parciais para assinaturas devem ser processados manualmente via Stripe Dashboard ou Portal de Cobrança");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool CanRefundFull, bool CanRefundPartial, decimal RefundAmount)> CalculateRefundAsync(string subscriptionId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var service = new SubscriptionService();
|
|
||||||
var subscription = await service.GetAsync(subscriptionId);
|
|
||||||
|
|
||||||
// Obter datas via SubscriptionItem
|
|
||||||
var subscriptionItemService = new SubscriptionItemService();
|
|
||||||
var subItem = await subscriptionItemService.GetAsync(subscription.Items.Data[0].Id);
|
|
||||||
|
|
||||||
var daysSinceStart = (DateTime.UtcNow - subItem.CurrentPeriodStart).TotalDays;
|
|
||||||
var totalDays = (subItem.CurrentPeriodEnd - subItem.CurrentPeriodStart).TotalDays;
|
|
||||||
var daysRemaining = totalDays - daysSinceStart;
|
|
||||||
|
|
||||||
// Direito de arrependimento (7 dias)
|
|
||||||
bool canRefundFull = daysSinceStart <= 7;
|
|
||||||
|
|
||||||
// Reembolso proporcional para planos anuais
|
|
||||||
bool canRefundPartial = false;
|
|
||||||
decimal refundAmount = 0;
|
|
||||||
|
|
||||||
if (!canRefundFull && daysRemaining > 0)
|
|
||||||
{
|
|
||||||
// Verificar se é plano anual (pela duração do período)
|
|
||||||
bool isYearlyPlan = totalDays > 300; // Aproximadamente 1 ano
|
|
||||||
|
|
||||||
if (isYearlyPlan)
|
|
||||||
{
|
|
||||||
canRefundPartial = true;
|
|
||||||
|
|
||||||
// Buscar valor pago na última fatura
|
|
||||||
var invoiceService = new InvoiceService();
|
|
||||||
var invoices = await invoiceService.ListAsync(new InvoiceListOptions
|
|
||||||
{
|
|
||||||
Subscription = subscriptionId,
|
|
||||||
Status = "paid",
|
|
||||||
Limit = 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (invoices.Data.Any())
|
|
||||||
{
|
|
||||||
var latestInvoice = invoices.Data.First();
|
|
||||||
var totalPaid = (decimal)latestInvoice.AmountPaid / 100; // Converter de centavos
|
|
||||||
|
|
||||||
// Calcular proporção do tempo restante
|
|
||||||
var proportionRemaining = daysRemaining / totalDays;
|
|
||||||
refundAmount = totalPaid * (decimal)proportionRemaining;
|
|
||||||
refundAmount = Math.Round(refundAmount, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (canRefundFull, canRefundPartial, refundAmount);
|
|
||||||
}
|
|
||||||
catch (StripeException)
|
|
||||||
{
|
|
||||||
return (false, false, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Refund?> CreateFullRefundAsync(string? chargeId)
|
|
||||||
{
|
|
||||||
// NOTA: Para planos de assinatura, reembolsos devem ser processados manualmente
|
|
||||||
await Task.CompletedTask;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSubscriptionId(Invoice? invoice)
|
private string GetSubscriptionId(Invoice? invoice)
|
||||||
{
|
{
|
||||||
if (invoice!=null)
|
if (invoice!=null)
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
namespace BCards.Web.ViewModels;
|
|
||||||
|
|
||||||
public class CancelSubscriptionViewModel
|
|
||||||
{
|
|
||||||
public string SubscriptionId { get; set; } = string.Empty;
|
|
||||||
public string PlanName { get; set; } = string.Empty;
|
|
||||||
public DateTime CurrentPeriodEnd { get; set; }
|
|
||||||
public bool CanRefundFull { get; set; }
|
|
||||||
public bool CanRefundPartial { get; set; }
|
|
||||||
public decimal RefundAmount { get; set; }
|
|
||||||
public int DaysRemaining { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CancelSubscriptionRequest
|
|
||||||
{
|
|
||||||
public string SubscriptionId { get; set; } = string.Empty;
|
|
||||||
public string CancelType { get; set; } = "at_period_end"; // immediate_with_refund, immediate_no_refund, partial_refund, at_period_end
|
|
||||||
}
|
|
||||||
@ -46,7 +46,6 @@ public class ManagePageViewModel
|
|||||||
|
|
||||||
// Plan limitations
|
// Plan limitations
|
||||||
public int MaxLinksAllowed { get; set; } = 3;
|
public int MaxLinksAllowed { get; set; } = 3;
|
||||||
public bool AllowProductLinks { get; set; } = false;
|
|
||||||
public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower());
|
public bool CanUseTheme(string themeName) => AvailableThemes.Any(t => t.Name.ToLower() == themeName.ToLower());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -18,7 +18,6 @@ public class ManageSubscriptionViewModel
|
|||||||
public bool CanUpgrade => HasActiveSubscription && User.CurrentPlan != "premium";
|
public bool CanUpgrade => HasActiveSubscription && User.CurrentPlan != "premium";
|
||||||
public bool CanDowngrade => HasActiveSubscription && User.CurrentPlan != "basic";
|
public bool CanDowngrade => HasActiveSubscription && User.CurrentPlan != "basic";
|
||||||
public bool WillCancelAtPeriodEnd => StripeSubscription?.CancelAtPeriodEnd == true;
|
public bool WillCancelAtPeriodEnd => StripeSubscription?.CancelAtPeriodEnd == true;
|
||||||
public string? StripeSubscriptionId => StripeSubscription?.Id;
|
|
||||||
|
|
||||||
public DateTime? CurrentPeriodEnd { get; set; }
|
public DateTime? CurrentPeriodEnd { get; set; }
|
||||||
public DateTime? NextBillingDate => !WillCancelAtPeriodEnd ? CurrentPeriodEnd : null;
|
public DateTime? NextBillingDate => !WillCancelAtPeriodEnd ? CurrentPeriodEnd : null;
|
||||||
@ -57,7 +56,6 @@ public class AvailablePlanViewModel
|
|||||||
public bool AllowCustomThemes { get; set; }
|
public bool AllowCustomThemes { get; set; }
|
||||||
public bool AllowAnalytics { get; set; }
|
public bool AllowAnalytics { get; set; }
|
||||||
public bool AllowCustomDomain { get; set; }
|
public bool AllowCustomDomain { get; set; }
|
||||||
public bool AllowProductLinks { get; set; }
|
|
||||||
public bool IsCurrentPlan { get; set; }
|
public bool IsCurrentPlan { get; set; }
|
||||||
public bool IsUpgrade { get; set; }
|
public bool IsUpgrade { get; set; }
|
||||||
public bool IsDowngrade { get; set; }
|
public bool IsDowngrade { get; set; }
|
||||||
|
|||||||
@ -10,7 +10,6 @@ public class ModerationDashboardViewModel
|
|||||||
public int PageSize { get; set; } = 20;
|
public int PageSize { get; set; } = 20;
|
||||||
public bool HasNextPage { get; set; } = false;
|
public bool HasNextPage { get; set; } = false;
|
||||||
public bool HasPreviousPage => CurrentPage > 1;
|
public bool HasPreviousPage => CurrentPage > 1;
|
||||||
public string CurrentFilter { get; set; } = "all";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ModerationPageViewModel
|
public class ModerationPageViewModel
|
||||||
@ -79,5 +78,4 @@ public class PendingPageViewModel
|
|||||||
public string PreviewUrl { get; set; } = "";
|
public string PreviewUrl { get; set; } = "";
|
||||||
public string PriorityLabel { get; set; } = "";
|
public string PriorityLabel { get; set; } = "";
|
||||||
public string PriorityColor { get; set; } = "";
|
public string PriorityColor { get; set; } = "";
|
||||||
public bool IsSpecialModeration { get; set; } = false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -288,7 +288,7 @@
|
|||||||
<div class="link-input-group product-link-preview" data-link="@i">
|
<div class="link-input-group product-link-preview" data-link="@i">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Afiliado @(i + 1)
|
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto @(i + 1)
|
||||||
</h6>
|
</h6>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@ -518,8 +518,8 @@
|
|||||||
<!-- Tipo de Link -->
|
<!-- Tipo de Link -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Tipo de Link</label>
|
<label class="form-label">Tipo de Link</label>
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2">
|
||||||
<div class="form-check @(Model.AllowProductLinks ? "flex-fill" : "w-100")">
|
<div class="form-check flex-fill">
|
||||||
<input class="form-check-input" type="radio" name="linkType" id="linkTypeNormal" value="Normal" checked>
|
<input class="form-check-input" type="radio" name="linkType" id="linkTypeNormal" value="Normal" checked>
|
||||||
<label class="form-check-label w-100 p-2 border rounded" for="linkTypeNormal">
|
<label class="form-check-label w-100 p-2 border rounded" for="linkTypeNormal">
|
||||||
<i class="fas fa-link me-2"></i>
|
<i class="fas fa-link me-2"></i>
|
||||||
@ -527,40 +527,14 @@
|
|||||||
<div class="small text-muted">Link simples para sites, redes sociais, etc.</div>
|
<div class="small text-muted">Link simples para sites, redes sociais, etc.</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check @(Model.AllowProductLinks ? "flex-fill" : "w-100 mt-2")" id="productLinkOption">
|
<div class="form-check flex-fill">
|
||||||
<input class="form-check-input" type="radio" name="linkType" id="linkTypeProduct" value="Product" @(!Model.AllowProductLinks ? "disabled" : "")>
|
<input class="form-check-input" type="radio" name="linkType" id="linkTypeProduct" value="Product">
|
||||||
<label class="form-check-label w-100 p-2 border rounded @(!Model.AllowProductLinks ? "position-relative" : "")" for="linkTypeProduct">
|
<label class="form-check-label w-100 p-2 border rounded" for="linkTypeProduct">
|
||||||
@if (!Model.AllowProductLinks)
|
<i class="fas fa-shopping-bag me-2"></i>
|
||||||
{
|
<strong>Link de Produto</strong>
|
||||||
<div class="position-absolute top-0 end-0 m-1" style="z-index: 2;">
|
<div class="small text-muted">Para produtos de e-commerce com preview</div>
|
||||||
<div class="bg-warning text-dark px-2 py-1 rounded shadow-sm d-flex align-items-center">
|
|
||||||
<i class="fas fa-crown me-1"></i>
|
|
||||||
<small class="fw-bold">Premium + Afiliados</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="position-absolute top-0 start-0 w-100 h-100 bg-warning bg-opacity-5 rounded" style="z-index: 1;"></div>
|
|
||||||
}
|
|
||||||
<div class="@(!Model.AllowProductLinks ? "opacity-50" : "")">
|
|
||||||
<i class="fas fa-shopping-bag me-2"></i>
|
|
||||||
<strong>Link de Afiliado</strong>
|
|
||||||
<div class="small text-muted">Para produtos de e-commerce com preview</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@if (!Model.AllowProductLinks)
|
|
||||||
{
|
|
||||||
<div class="col-12 mt-2">
|
|
||||||
<div class="alert alert-warning small mb-0 d-flex align-items-center">
|
|
||||||
<i class="fas fa-crown text-warning me-2"></i>
|
|
||||||
<span class="flex-grow-1">
|
|
||||||
<strong>Links de afiliado</strong> disponíveis apenas no plano <strong>Premium + Afiliados</strong>.
|
|
||||||
</span>
|
|
||||||
<a asp-controller="Home" asp-action="Pricing" class="btn btn-warning btn-sm ms-2">
|
|
||||||
<i class="fas fa-arrow-up me-1"></i>Fazer Upgrade
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -604,7 +578,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Seção para Link de Afiliado -->
|
<!-- Seção para Link de Produto -->
|
||||||
<div id="productLinkSection" style="display: none;">
|
<div id="productLinkSection" style="display: none;">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="productUrl" class="form-label">URL do Produto</label>
|
<label for="productUrl" class="form-label">URL do Produto</label>
|
||||||
@ -1261,7 +1235,7 @@
|
|||||||
<div class="link-input-group product-link-preview" data-link="${nextIndex}">
|
<div class="link-input-group product-link-preview" data-link="${nextIndex}">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h6 class="mb-0">
|
<h6 class="mb-0">
|
||||||
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Afiliado ${displayCount}
|
<i class="fas fa-shopping-bag me-2 text-success"></i>Link de Produto ${displayCount}
|
||||||
</h6>
|
</h6>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
Criada para profissionais e empresas no Brasil.
|
Criada para profissionais e empresas no Brasil.
|
||||||
Organize todos os seus links em uma página única e profissional.
|
Organize todos os seus links em uma página única e profissional.
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex gap-3 flex-wrap mb-4 mb-lg-0">
|
<div class="d-flex gap-3 flex-wrap">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<a asp-controller="Admin" asp-action="Dashboard" class="btn btn-light btn-lg px-4">
|
<a asp-controller="Admin" asp-action="Dashboard" class="btn btn-light btn-lg px-4">
|
||||||
|
|||||||
@ -9,30 +9,16 @@
|
|||||||
<div class="text-center mb-5">
|
<div class="text-center mb-5">
|
||||||
<h1 class="display-5 fw-bold mb-3">Escolha o plano ideal para você</h1>
|
<h1 class="display-5 fw-bold mb-3">Escolha o plano ideal para você</h1>
|
||||||
<p class="lead text-muted">Comece grátis e faça upgrade quando precisar de mais recursos</p>
|
<p class="lead text-muted">Comece grátis e faça upgrade quando precisar de mais recursos</p>
|
||||||
|
|
||||||
<!-- Toggle Mensal/Anual -->
|
|
||||||
<div class="d-flex justify-content-center mb-4">
|
|
||||||
<div class="btn-group" role="group" aria-label="Período de cobrança">
|
|
||||||
<input type="radio" class="btn-check" name="billingPeriod" id="monthly" autocomplete="off" checked>
|
|
||||||
<label class="btn btn-outline-primary" for="monthly">Mensal</label>
|
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="billingPeriod" id="yearly" autocomplete="off">
|
|
||||||
<label class="btn btn-outline-primary" for="yearly">
|
|
||||||
Anual
|
|
||||||
<span class="badge bg-success ms-1">2 meses grátis</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 justify-content-center pricing-cards">
|
<div class="row g-4 justify-content-center">
|
||||||
<!-- Plano Trial -->
|
<!-- Plano Trial -->
|
||||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<div class="card-header bg-success bg-opacity-10 text-center py-4">
|
<div class="card-header bg-success bg-opacity-10 text-center py-4">
|
||||||
<h5 class="mb-0">Trial Gratuito</h5>
|
<h4 class="mb-0">Trial Gratuito</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<span class="display-5 fw-bold text-success">R$ 0</span>
|
<span class="display-4 fw-bold text-success">R$ 0</span>
|
||||||
<span class="text-muted">/7 dias</span>
|
<span class="text-muted">/7 dias</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,36 +60,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plano Básico -->
|
<!-- Plano Básico -->
|
||||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<div class="card-header bg-light text-center py-4">
|
<div class="card-header bg-light text-center py-4">
|
||||||
<h5 class="mb-0">Básico</h5>
|
<h4 class="mb-0">Básico</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<span class="display-4 fw-bold text-primary">R$ 9,90</span>
|
||||||
<span class="display-5 fw-bold text-primary">R$ 5,90</span>
|
<span class="text-muted">/mês</span>
|
||||||
<span class="text-muted">/mês</span>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<span class="display-5 fw-bold text-primary">R$ 59,00</span>
|
|
||||||
<span class="text-muted">/ano</span>
|
|
||||||
<br>
|
|
||||||
<small class="text-success">Economize R$ 11,80 (2 meses grátis)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
<strong>3 páginas</strong>
|
<strong>3 páginas</strong>, 8 links cada
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
8 links por página
|
Temas básicos
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
20 temas básicos
|
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
@ -115,29 +89,21 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-muted me-2">✗</i>
|
||||||
<span class="text-muted">Temas premium</span>
|
<span class="text-muted">Página rápida</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-muted me-2">✗</i>
|
||||||
<span class="text-muted">Links de produto</span>
|
<span class="text-muted">Temas premium</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<div class="pricing-monthly">
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<input type="hidden" name="planType" value="Basic" />
|
||||||
<input type="hidden" name="planType" value="Basic" />
|
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico</button>
|
||||||
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico</button>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
|
||||||
<input type="hidden" name="planType" value="BasicYearly" />
|
|
||||||
<button type="submit" class="btn btn-outline-primary w-100">Escolher Básico Anual</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -148,36 +114,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plano Profissional (Decoy) -->
|
<!-- Plano Profissional (Decoy) -->
|
||||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<div class="card-header bg-warning bg-opacity-10 text-center py-4">
|
<div class="card-header bg-warning bg-opacity-10 text-center py-4">
|
||||||
<h5 class="mb-0">Profissional</h5>
|
<h4 class="mb-0">Profissional</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<span class="display-4 fw-bold text-warning">R$ 24,90</span>
|
||||||
<span class="display-5 fw-bold text-warning">R$ 12,90</span>
|
<span class="text-muted">/mês</span>
|
||||||
<span class="text-muted">/mês</span>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<span class="display-5 fw-bold text-warning">R$ 129,00</span>
|
|
||||||
<span class="text-muted">/ano</span>
|
|
||||||
<br>
|
|
||||||
<small class="text-success">Economize R$ 25,80 (2 meses grátis)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
<strong>5 páginas</strong>
|
<strong>5 páginas</strong>, 20 links cada
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
20 links por página
|
Todos os temas
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
20 temas básicos
|
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
@ -185,33 +139,25 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
URL personalizada
|
Domínio personalizado
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-muted me-2">✗</i>
|
||||||
<span class="text-muted">Temas premium</span>
|
<span class="text-muted">Links ilimitados</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-muted me-2">✗</i>
|
||||||
<span class="text-muted">Links de produto</span>
|
<span class="text-muted">Suporte prioritário</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<div class="pricing-monthly">
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<input type="hidden" name="planType" value="Professional" />
|
||||||
<input type="hidden" name="planType" value="Professional" />
|
<button type="submit" class="btn btn-warning w-100">Escolher Profissional</button>
|
||||||
<button type="submit" class="btn btn-warning w-100">Escolher Profissional</button>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
|
||||||
<input type="hidden" name="planType" value="ProfessionalYearly" />
|
|
||||||
<button type="submit" class="btn btn-warning w-100">Escolher Profissional Anual</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -222,24 +168,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plano Premium (Mais Popular) -->
|
<!-- Plano Premium (Mais Popular) -->
|
||||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<div class="card h-100 border-primary shadow position-relative">
|
<div class="card h-100 border-primary shadow position-relative">
|
||||||
<div class="position-absolute top-0 start-50 translate-middle">
|
<div class="position-absolute top-0 start-50 translate-middle">
|
||||||
<span class="badge bg-primary px-3 py-2">Mais Popular</span>
|
<span class="badge bg-primary px-3 py-2">Mais Popular</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-header bg-primary text-white text-center py-4">
|
<div class="card-header bg-primary text-white text-center py-4">
|
||||||
<h5 class="mb-0">Premium</h5>
|
<h4 class="mb-0">Premium</h4>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<span class="display-4 fw-bold">R$ 29,90</span>
|
||||||
<span class="display-5 fw-bold">R$ 19,90</span>
|
<span class="opacity-75">/mês</span>
|
||||||
<span class="opacity-75">/mês</span>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<span class="display-5 fw-bold">R$ 199,00</span>
|
|
||||||
<span class="opacity-75">/ano</span>
|
|
||||||
<br>
|
|
||||||
<small class="text-light">Economize R$ 39,80 (2 meses grátis)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="opacity-75">Melhor custo-benefício!</small>
|
<small class="opacity-75">Melhor custo-benefício!</small>
|
||||||
</div>
|
</div>
|
||||||
@ -247,15 +185,11 @@
|
|||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
<strong>15 páginas</strong>
|
<strong>15 páginas</strong>, links ilimitados
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
Links ilimitados
|
Temas premium
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
40 temas*
|
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
@ -263,36 +197,25 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
URL personalizada
|
Múltiplos domínios
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
Suporte prioritário
|
Suporte prioritário
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-3">
|
<li class="mb-3">
|
||||||
<i class="text-muted me-2">✗</i>
|
<i class="text-success me-2">✓</i>
|
||||||
<span class="text-muted">Links de produto</span>
|
Recursos exclusivos
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-3">
|
|
||||||
<small class="text-muted">* 20 temas básicos + 20 temas premium exclusivos</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent p-4">
|
<div class="card-footer bg-transparent p-4">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<div class="pricing-monthly">
|
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
<input type="hidden" name="planType" value="Premium" />
|
||||||
<input type="hidden" name="planType" value="Premium" />
|
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium</button>
|
||||||
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium</button>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
|
||||||
<input type="hidden" name="planType" value="PremiumYearly" />
|
|
||||||
<button type="submit" class="btn btn-primary w-100 fw-bold">Escolher Premium Anual</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -301,87 +224,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plano Premium + Afiliados -->
|
|
||||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
|
||||||
<div class="card h-100 border-success shadow">
|
|
||||||
<div class="position-absolute top-0 start-50 translate-middle pricing-premium-badge">
|
|
||||||
<span class="badge bg-success px-3 py-2">Novo!</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-header bg-success text-white text-center py-4">
|
|
||||||
<h5 class="mb-0">Premium + Afiliados</h5>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="pricing-monthly">
|
|
||||||
<span class="display-5 fw-bold">R$ 29,90</span>
|
|
||||||
<span class="opacity-75">/mês</span>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<span class="display-5 fw-bold">R$ 299,00</span>
|
|
||||||
<span class="opacity-75">/ano</span>
|
|
||||||
<br>
|
|
||||||
<small class="text-light">Economize R$ 59,80 (2 meses grátis)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="opacity-75">Para monetização!</small>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
<strong>15 páginas</strong>
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
Links ilimitados
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
40 temas*
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
<strong>Links de produto</strong>
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
Moderação plus
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
Suporte prioritário
|
|
||||||
</li>
|
|
||||||
<li class="mb-3">
|
|
||||||
<i class="text-success me-2">✓</i>
|
|
||||||
10 links afiliados
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="mt-3">
|
|
||||||
<small class="text-muted">* 20 temas básicos + 20 temas premium exclusivos</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer bg-transparent p-4">
|
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
|
||||||
<div class="pricing-monthly">
|
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
|
||||||
<input type="hidden" name="planType" value="PremiumAffiliate" />
|
|
||||||
<button type="submit" class="btn btn-success w-100 fw-bold">Escolher Premium + Afiliados</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="pricing-yearly d-none">
|
|
||||||
<form asp-controller="Payment" asp-action="CreateCheckoutSession" method="post">
|
|
||||||
<input type="hidden" name="planType" value="PremiumAffiliateYearly" />
|
|
||||||
<button type="submit" class="btn btn-success w-100 fw-bold">Escolher Premium + Afiliados Anual</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a asp-controller="Auth" asp-action="Login" class="btn btn-success w-100 fw-bold">Escolher Premium + Afiliados</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comparação de recursos -->
|
<!-- Comparação de recursos -->
|
||||||
@ -396,7 +238,6 @@
|
|||||||
<th class="text-center">Básico</th>
|
<th class="text-center">Básico</th>
|
||||||
<th class="text-center">Profissional</th>
|
<th class="text-center">Profissional</th>
|
||||||
<th class="text-center">Premium</th>
|
<th class="text-center">Premium</th>
|
||||||
<th class="text-center">Premium + Afiliados</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -406,7 +247,6 @@
|
|||||||
<td class="text-center">3</td>
|
<td class="text-center">3</td>
|
||||||
<td class="text-center">5</td>
|
<td class="text-center">5</td>
|
||||||
<td class="text-center"><strong>15</strong></td>
|
<td class="text-center"><strong>15</strong></td>
|
||||||
<td class="text-center"><strong>15</strong></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Links por página</td>
|
<td>Links por página</td>
|
||||||
@ -414,15 +254,13 @@
|
|||||||
<td class="text-center">8</td>
|
<td class="text-center">8</td>
|
||||||
<td class="text-center">20</td>
|
<td class="text-center">20</td>
|
||||||
<td class="text-center"><strong>Ilimitado</strong></td>
|
<td class="text-center"><strong>Ilimitado</strong></td>
|
||||||
<td class="text-center"><strong>Ilimitado</strong></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Temas disponíveis</td>
|
<td>Temas disponíveis</td>
|
||||||
<td class="text-center">1 básico</td>
|
<td class="text-center">1 básico</td>
|
||||||
<td class="text-center">20 básicos</td>
|
<td class="text-center">Básicos</td>
|
||||||
<td class="text-center">20 básicos</td>
|
<td class="text-center">Todos</td>
|
||||||
<td class="text-center"><strong>40 (básicos + premium)</strong></td>
|
<td class="text-center"><strong>Customizáveis</strong></td>
|
||||||
<td class="text-center"><strong>40 (básicos + premium)</strong></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Analytics</td>
|
<td>Analytics</td>
|
||||||
@ -430,23 +268,20 @@
|
|||||||
<td class="text-center">Simples</td>
|
<td class="text-center">Simples</td>
|
||||||
<td class="text-center">Avançado</td>
|
<td class="text-center">Avançado</td>
|
||||||
<td class="text-center"><strong>Completo</strong></td>
|
<td class="text-center"><strong>Completo</strong></td>
|
||||||
<td class="text-center"><strong>Completo</strong></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Suporte por email</td>
|
<td>Domínio personalizado</td>
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">✅</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">✅</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>URL personalizada</td>
|
<td>Múltiplos domínios</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">✅</td>
|
<td class="text-center"><strong>✅</strong></td>
|
||||||
<td class="text-center">✅</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Suporte prioritário</td>
|
<td>Suporte prioritário</td>
|
||||||
@ -454,23 +289,6 @@
|
|||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center">❌</td>
|
<td class="text-center">❌</td>
|
||||||
<td class="text-center"><strong>✅</strong></td>
|
<td class="text-center"><strong>✅</strong></td>
|
||||||
<td class="text-center"><strong>✅</strong></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Links de produto</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center"><strong>✅</strong></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Moderação especial</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center">❌</td>
|
|
||||||
<td class="text-center"><strong>✅</strong></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -499,12 +317,12 @@
|
|||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h3 class="accordion-header" id="faq2">
|
<h3 class="accordion-header" id="faq2">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2">
|
||||||
Como funcionam os planos anuais?
|
Como funciona o domínio personalizado?
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
<div id="collapse2" class="accordion-collapse collapse" data-bs-parent="#faqAccordion">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
Nos planos anuais você economiza 2 meses! Pague 10 meses e use por 12 meses. O desconto é aplicado automaticamente na cobrança anual.
|
Com os planos Profissional e Premium, você pode conectar seu próprio domínio (ex: meusite.com) à sua página BCards.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -574,25 +392,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const monthlyRadio = document.getElementById('monthly');
|
|
||||||
const yearlyRadio = document.getElementById('yearly');
|
|
||||||
const monthlyElements = document.querySelectorAll('.pricing-monthly');
|
|
||||||
const yearlyElements = document.querySelectorAll('.pricing-yearly');
|
|
||||||
|
|
||||||
function togglePricing() {
|
|
||||||
if (yearlyRadio.checked) {
|
|
||||||
monthlyElements.forEach(el => el.classList.add('d-none'));
|
|
||||||
yearlyElements.forEach(el => el.classList.remove('d-none'));
|
|
||||||
} else {
|
|
||||||
monthlyElements.forEach(el => el.classList.remove('d-none'));
|
|
||||||
yearlyElements.forEach(el => el.classList.add('d-none'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
monthlyRadio.addEventListener('change', togglePricing);
|
|
||||||
yearlyRadio.addEventListener('change', togglePricing);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -16,16 +16,8 @@
|
|||||||
@if (Model.PendingPages.Any())
|
@if (Model.PendingPages.Any())
|
||||||
{
|
{
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Páginas Pendentes</h5>
|
<h5>Páginas Pendentes</h5>
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<label for="moderationFilter" class="form-label me-2 mb-0">Filtrar:</label>
|
|
||||||
<select id="moderationFilter" class="form-select form-select-sm" style="width: auto;" onchange="filterModeration()">
|
|
||||||
<option value="all" selected="@(Model.CurrentFilter == "all")">Todas as páginas</option>
|
|
||||||
<option value="special" selected="@(Model.CurrentFilter == "special")">Moderação especial (SLA reduzido)</option>
|
|
||||||
<option value="normal" selected="@(Model.CurrentFilter == "normal")">Moderação normal</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -41,16 +33,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var pageItem in Model.PendingPages)
|
@foreach (var pageItem in Model.PendingPages)
|
||||||
{
|
{
|
||||||
<tr @(pageItem.IsSpecialModeration ? "class=table-warning" : "")>
|
<tr>
|
||||||
<td>
|
<td>@pageItem.DisplayName</td>
|
||||||
@pageItem.DisplayName
|
|
||||||
@if (pageItem.IsSpecialModeration)
|
|
||||||
{
|
|
||||||
<span class="badge bg-warning text-dark ms-2">
|
|
||||||
<i class="fas fa-star"></i> SLA Reduzido
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>@pageItem.Category</td>
|
<td>@pageItem.Category</td>
|
||||||
<td>@pageItem.CreatedAt.ToString("dd/MM/yyyy")</td>
|
<td>@pageItem.CreatedAt.ToString("dd/MM/yyyy")</td>
|
||||||
<td>
|
<td>
|
||||||
@ -74,21 +58,3 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
function filterModeration() {
|
|
||||||
const filter = document.getElementById('moderationFilter').value;
|
|
||||||
const currentUrl = new URL(window.location);
|
|
||||||
|
|
||||||
if (filter === 'all') {
|
|
||||||
currentUrl.searchParams.delete('filter');
|
|
||||||
} else {
|
|
||||||
currentUrl.searchParams.set('filter', filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset page to 1 when filtering
|
|
||||||
currentUrl.searchParams.set('page', '1');
|
|
||||||
|
|
||||||
window.location.href = currentUrl.toString();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -103,20 +103,10 @@
|
|||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
@if (!Model.WillCancelAtPeriodEnd)
|
@if (!Model.WillCancelAtPeriodEnd)
|
||||||
{
|
{
|
||||||
<a asp-controller="Subscription" asp-action="Cancel" class="btn btn-danger">
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
|
||||||
<i class="fas fa-times me-1"></i>
|
<i class="fas fa-times me-1"></i>
|
||||||
Cancelar Assinatura
|
Cancelar Assinatura
|
||||||
</a>
|
</button>
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<form asp-controller="Subscription" asp-action="Reactivate" method="post" class="d-inline">
|
|
||||||
<input type="hidden" name="subscriptionId" value="@Model.StripeSubscriptionId" />
|
|
||||||
<button type="submit" class="btn btn-success" onclick="return confirm('Deseja reativar sua assinatura?')">
|
|
||||||
<i class="fas fa-undo me-1"></i>
|
|
||||||
Reativar Assinatura
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<form method="post" action="@Url.Action("OpenStripePortal")" class="d-inline">
|
<form method="post" action="@Url.Action("OpenStripePortal")" class="d-inline">
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
@model BCards.Web.ViewModels.CancelSubscriptionViewModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Cancelar Assinatura";
|
|
||||||
Layout = "_Layout";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container py-5">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header bg-warning text-dark">
|
|
||||||
<h4 class="mb-0">
|
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
||||||
Cancelar Assinatura
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<h5>Informações da Assinatura</h5>
|
|
||||||
<p><strong>Plano:</strong> @Model.PlanName</p>
|
|
||||||
<p><strong>Válido até:</strong> @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy")</p>
|
|
||||||
<p><strong>Dias restantes:</strong> @Model.DaysRemaining dias</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="mb-3">Escolha uma opção de cancelamento:</h5>
|
|
||||||
|
|
||||||
<form asp-action="ProcessCancel" method="post">
|
|
||||||
<input type="hidden" name="SubscriptionId" value="@Model.SubscriptionId" />
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<!-- Reembolso Total (7 dias) -->
|
|
||||||
@if (Model.CanRefundFull)
|
|
||||||
{
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-success">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="CancelType" value="immediate_with_refund" id="refundFull">
|
|
||||||
<label class="form-check-label" for="refundFull">
|
|
||||||
<strong class="text-success">Cancelar com Reembolso Total</strong>
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">
|
|
||||||
Direito de arrependimento (7 dias). Cancela imediatamente e processa reembolso total.
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Reembolso Parcial (Planos Anuais) -->
|
|
||||||
@if (Model.CanRefundPartial && Model.RefundAmount > 0)
|
|
||||||
{
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-primary">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="CancelType" value="partial_refund" id="refundPartial">
|
|
||||||
<label class="form-check-label" for="refundPartial">
|
|
||||||
<strong class="text-primary">Cancelar com Reembolso Parcial</strong>
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">
|
|
||||||
Cancela imediatamente e reembolsa R$ @Model.RefundAmount.ToString("F2") (proporcional ao tempo restante).
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Cancelar no Final do Período -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-warning">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="CancelType" value="at_period_end" id="cancelAtEnd" checked>
|
|
||||||
<label class="form-check-label" for="cancelAtEnd">
|
|
||||||
<strong class="text-warning">Cancelar no Final do Período</strong>
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">
|
|
||||||
Mantém acesso até @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy"). Sem reembolso.
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cancelar Imediatamente (sem reembolso) -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border-danger">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="CancelType" value="immediate_no_refund" id="cancelImmediate">
|
|
||||||
<label class="form-check-label" for="cancelImmediate">
|
|
||||||
<strong class="text-danger">Cancelar Imediatamente</strong>
|
|
||||||
<br>
|
|
||||||
<small class="text-muted">
|
|
||||||
Perde acesso imediato aos recursos premium. Sem reembolso.
|
|
||||||
</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h6><i class="fas fa-info-circle me-2"></i>Importante:</h6>
|
|
||||||
<ul class="mb-0">
|
|
||||||
<li>Reembolsos são processados em até 10 dias úteis</li>
|
|
||||||
<li>O valor retorna para o mesmo cartão usado na compra</li>
|
|
||||||
<li>Após o cancelamento, suas páginas podem ser desativadas conforme o plano escolhido</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between mt-4">
|
|
||||||
<a asp-controller="Payment" asp-action="ManageSubscription" class="btn btn-outline-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Voltar
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Tem certeza que deseja cancelar sua assinatura?')">
|
|
||||||
<i class="fas fa-times me-2"></i>Confirmar Cancelamento
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const radioButtons = document.querySelectorAll('input[name="CancelType"]');
|
|
||||||
const submitButton = document.querySelector('button[type="submit"]');
|
|
||||||
|
|
||||||
radioButtons.forEach(radio => {
|
|
||||||
radio.addEventListener('change', function() {
|
|
||||||
const selectedOption = this.value;
|
|
||||||
let confirmMessage = 'Tem certeza que deseja cancelar sua assinatura?';
|
|
||||||
|
|
||||||
switch(selectedOption) {
|
|
||||||
case 'immediate_with_refund':
|
|
||||||
confirmMessage = 'Confirma o cancelamento com reembolso total?';
|
|
||||||
break;
|
|
||||||
case 'partial_refund':
|
|
||||||
confirmMessage = 'Confirma o cancelamento com reembolso parcial de R$ @Model.RefundAmount.ToString("F2")?';
|
|
||||||
break;
|
|
||||||
case 'at_period_end':
|
|
||||||
confirmMessage = 'Confirma o agendamento de cancelamento para @Model.CurrentPeriodEnd.ToString("dd/MM/yyyy")?';
|
|
||||||
break;
|
|
||||||
case 'immediate_no_refund':
|
|
||||||
confirmMessage = 'Confirma o cancelamento imediato? Você perderá acesso aos recursos premium agora.';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
submitButton.onclick = function() {
|
|
||||||
return confirm(confirmMessage);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -42,150 +42,12 @@
|
|||||||
</style>
|
</style>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!isPreview)
|
|
||||||
{
|
|
||||||
<style>
|
|
||||||
/* Layout normal sem preview - corrigir centralização */
|
|
||||||
.user-page {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
min-height: 100vh !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .container {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 1140px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .row {
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .col-lg-6,
|
|
||||||
.user-page .col-md-8 {
|
|
||||||
display: block !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
float: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-card {
|
|
||||||
margin: 0 auto !important;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isPreview)
|
@if (isPreview)
|
||||||
{
|
{
|
||||||
<style>
|
<style>
|
||||||
/* Compensar espaço da barra de preview */
|
/* Compensar espaço da barra de preview */
|
||||||
body {
|
body {
|
||||||
padding-top: 60px !important;
|
padding-top: 60px !important;
|
||||||
display: block !important; /* Override flexbox for user pages */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Corrigir centralização do conteúdo da página */
|
|
||||||
.user-page {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
min-height: calc(100vh - 60px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .container {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 1140px !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .row {
|
|
||||||
display: flex !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-page .col-lg-6,
|
|
||||||
.user-page .col-md-8 {
|
|
||||||
display: block !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
float: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-card {
|
|
||||||
margin: 0 auto !important;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garantir que a barra de preview funcione corretamente */
|
|
||||||
.position-fixed.top-0 {
|
|
||||||
display: block !important;
|
|
||||||
flex: none !important;
|
|
||||||
height: 60px !important;
|
|
||||||
line-height: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .container-fluid {
|
|
||||||
display: block !important;
|
|
||||||
flex: none !important;
|
|
||||||
padding: 0.75rem 1rem !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .container-fluid .row {
|
|
||||||
display: flex !important;
|
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: space-between !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
min-height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .container-fluid .row .col-auto {
|
|
||||||
flex: 0 0 auto !important;
|
|
||||||
padding: 0 0.25rem !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .container-fluid .row .col {
|
|
||||||
flex: 1 1 auto !important;
|
|
||||||
padding: 0 0.5rem !important;
|
|
||||||
text-align: center !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garantir que os botões fiquem na mesma linha */
|
|
||||||
.position-fixed.top-0 .btn {
|
|
||||||
margin: 0 0.25rem !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
padding: 0.25rem 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forçar layout horizontal para a barra toda */
|
|
||||||
.position-fixed.top-0 .container-fluid .row {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Para telas grandes (Full HD+) */
|
|
||||||
@@media (min-width: 1200px) {
|
|
||||||
.position-fixed.top-0 .container-fluid {
|
|
||||||
max-width: 100% !important;
|
|
||||||
padding: 0.5rem 2rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .container-fluid .row .col-auto:last-child {
|
|
||||||
flex-shrink: 0 !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed.top-0 .btn {
|
|
||||||
display: inline-block !important;
|
|
||||||
margin: 0 0.125rem !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsivo para mobile */
|
/* Responsivo para mobile */
|
||||||
|
|||||||
20
src/BCards.Web/appSettings.Testing.json
Normal file
20
src/BCards.Web/appSettings.Testing.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "mongodb://localhost:27017/BCardsDB_Testing"
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"JWT": {
|
||||||
|
"Secret": "ThisIsATestSecretKeyForJWTTokenGeneration123456789",
|
||||||
|
"Issuer": "BCards-Testing",
|
||||||
|
"Audience": "BCards-Users-Testing",
|
||||||
|
"ExpiryMinutes": 60
|
||||||
|
},
|
||||||
|
"Environment": "Testing"
|
||||||
|
}
|
||||||
@ -1,16 +1,38 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Debug",
|
"Default": "Information",
|
||||||
"System": "Information",
|
|
||||||
"Microsoft": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DetailedErrors": true,
|
"DetailedErrors": true,
|
||||||
|
"Stripe": {
|
||||||
|
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
||||||
|
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
|
||||||
|
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543"
|
||||||
|
},
|
||||||
|
"Plans": {
|
||||||
|
"Basic": {
|
||||||
|
"PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y",
|
||||||
|
"Price": 9.90,
|
||||||
|
"MaxLinks": 5,
|
||||||
|
"Features": [ "basic_themes", "simple_analytics" ]
|
||||||
|
},
|
||||||
|
"Professional": {
|
||||||
|
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
||||||
|
"Price": 24.90,
|
||||||
|
"MaxLinks": 15,
|
||||||
|
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
|
||||||
|
},
|
||||||
|
"Premium": {
|
||||||
|
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
||||||
|
"Price": 29.90,
|
||||||
|
"MaxLinks": -1,
|
||||||
|
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
"MongoDb": {
|
"MongoDb": {
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
"DatabaseName": "BCardsDB_Dev"
|
"DatabaseName": "BCardsDB_Dev"
|
||||||
},
|
}
|
||||||
"BaseUrl": "https://localhost:49178"
|
|
||||||
}
|
}
|
||||||
@ -2,12 +2,140 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore": "Warning",
|
||||||
|
"BCards": "Information"
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"File": {
|
||||||
|
"Path": "/app/logs/bcards-{Date}.log",
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
"MongoDb": {
|
"MongoDb": {
|
||||||
"ConnectionString": "mongodb://192.168.0.100:27017",
|
"ConnectionString": "mongodb://192.168.0.100:27017/BCardsDB",
|
||||||
"DatabaseName": "BCardsDB_Staging"
|
"DatabaseName": "BCardsDB",
|
||||||
|
"MaxConnectionPoolSize": 100,
|
||||||
|
"ConnectTimeout": "30s",
|
||||||
|
"ServerSelectionTimeout": "30s",
|
||||||
|
"SocketTimeout": "30s"
|
||||||
},
|
},
|
||||||
"BaseUrl": "http://192.168.0.100:8080"
|
"Stripe": {
|
||||||
|
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
||||||
|
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
|
||||||
|
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543",
|
||||||
|
"ApiVersion": "2023-10-16"
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Google": {
|
||||||
|
"ClientId": "472850008574-nmeepbdt4hunsk5c8krpbdmd3olc4jv6.apps.googleusercontent.com",
|
||||||
|
"ClientSecret": "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"
|
||||||
|
},
|
||||||
|
"Microsoft": {
|
||||||
|
"ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3",
|
||||||
|
"ClientSecret": "T0.8Q~an.51iW1H0DVjL2i1bmSK_qTgVQOuEmapK"
|
||||||
|
},
|
||||||
|
"CookieSettings": {
|
||||||
|
"SecurePolicy": "Always",
|
||||||
|
"SameSiteMode": "Lax",
|
||||||
|
"HttpOnly": true,
|
||||||
|
"ExpireTimeSpan": "7.00:00:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Plans": {
|
||||||
|
"Basic": {
|
||||||
|
"PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y",
|
||||||
|
"Price": 9.90,
|
||||||
|
"MaxLinks": 5,
|
||||||
|
"Features": [ "basic_themes", "simple_analytics" ]
|
||||||
|
},
|
||||||
|
"Professional": {
|
||||||
|
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
||||||
|
"Price": 24.90,
|
||||||
|
"MaxLinks": 15,
|
||||||
|
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
|
||||||
|
},
|
||||||
|
"Premium": {
|
||||||
|
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
||||||
|
"Price": 29.90,
|
||||||
|
"MaxLinks": -1,
|
||||||
|
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Moderation": {
|
||||||
|
"PriorityTimeframes": {
|
||||||
|
"Trial": "7.00:00:00",
|
||||||
|
"Basic": "7.00:00:00",
|
||||||
|
"Professional": "3.00:00:00",
|
||||||
|
"Premium": "1.00:00:00"
|
||||||
|
},
|
||||||
|
"MaxAttempts": 3,
|
||||||
|
"ModeratorEmail": "ricardo.carneiro@jobmaker.com.br",
|
||||||
|
"ModeratorEmails": [
|
||||||
|
"rrcgoncalves@gmail.com",
|
||||||
|
"rirocarneiro@gmail.com"
|
||||||
|
],
|
||||||
|
"PreviewTokenExpirationHours": 4,
|
||||||
|
"AutoRejectDays": 30
|
||||||
|
},
|
||||||
|
"SendGrid": {
|
||||||
|
"ApiKey": "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",
|
||||||
|
"FromEmail": "ricardo.carneiro@jobmaker.com.br",
|
||||||
|
"FromName": "BCards - Staging",
|
||||||
|
"ReplyToEmail": "ricardo.carneiro@jobmaker.com.br"
|
||||||
|
},
|
||||||
|
"BaseUrl": "http://192.168.0.100:8090",
|
||||||
|
"Environment": {
|
||||||
|
"Name": "Release",
|
||||||
|
"IsStagingEnvironment": true,
|
||||||
|
"AllowTestData": true,
|
||||||
|
"EnableDetailedErrors": false
|
||||||
|
},
|
||||||
|
"Performance": {
|
||||||
|
"EnableCaching": true,
|
||||||
|
"CacheExpirationMinutes": 30,
|
||||||
|
"EnableCompression": true,
|
||||||
|
"EnableResponseCaching": true
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"EnableHttpsRedirection": false,
|
||||||
|
"EnableHsts": false,
|
||||||
|
"RequireHttpsMetadata": false,
|
||||||
|
"CorsOrigins": [
|
||||||
|
"http://192.168.0.100:8090",
|
||||||
|
"http://localhost:8090"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HealthChecks": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Endpoints": {
|
||||||
|
"Health": "/health",
|
||||||
|
"Ready": "/ready",
|
||||||
|
"Live": "/live"
|
||||||
|
},
|
||||||
|
"MongoDb": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Timeout": "10s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Monitoring": {
|
||||||
|
"EnableMetrics": true,
|
||||||
|
"MetricsEndpoint": "/metrics",
|
||||||
|
"EnableTracing": false
|
||||||
|
},
|
||||||
|
"Features": {
|
||||||
|
"EnablePreviewMode": true,
|
||||||
|
"EnableModerationWorkflow": true,
|
||||||
|
"EnableAnalytics": true,
|
||||||
|
"EnableFileUploads": true,
|
||||||
|
"MaxFileUploadSize": "5MB"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,122 +2,19 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning",
|
"Microsoft.AspNetCore": "Warning"
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
"MongoDb": {
|
||||||
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
|
"DatabaseName": "BCardsDB"
|
||||||
|
},
|
||||||
"Stripe": {
|
"Stripe": {
|
||||||
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
||||||
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
|
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
|
||||||
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543"
|
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543"
|
||||||
},
|
},
|
||||||
"Plans": {
|
|
||||||
"Basic": {
|
|
||||||
"Name": "Básico",
|
|
||||||
"PriceId": "price_1RycPaBMIadsOxJVKioZZofK",
|
|
||||||
"Price": 5.90,
|
|
||||||
"MaxPages": 3,
|
|
||||||
"MaxLinks": 8,
|
|
||||||
"AllowPremiumThemes": false,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ],
|
|
||||||
"Interval": "month"
|
|
||||||
},
|
|
||||||
"Professional": {
|
|
||||||
"Name": "Profissional",
|
|
||||||
"PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj",
|
|
||||||
"Price": 12.90,
|
|
||||||
"MaxPages": 5,
|
|
||||||
"MaxLinks": 20,
|
|
||||||
"AllowPremiumThemes": false,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ],
|
|
||||||
"Interval": "month"
|
|
||||||
},
|
|
||||||
"Premium": {
|
|
||||||
"Name": "Premium",
|
|
||||||
"PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu",
|
|
||||||
"Price": 19.90,
|
|
||||||
"MaxPages": 15,
|
|
||||||
"MaxLinks": -1,
|
|
||||||
"AllowPremiumThemes": true,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"SpecialModeration": false,
|
|
||||||
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ],
|
|
||||||
"Interval": "month"
|
|
||||||
},
|
|
||||||
"PremiumAffiliate": {
|
|
||||||
"Name": "Premium + Afiliados",
|
|
||||||
"PriceId": "price_1RycTaBMIadsOxJVeDLseXQq",
|
|
||||||
"Price": 29.90,
|
|
||||||
"MaxPages": 15,
|
|
||||||
"MaxLinks": -1,
|
|
||||||
"AllowPremiumThemes": true,
|
|
||||||
"AllowProductLinks": true,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"SpecialModeration": true,
|
|
||||||
"Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ],
|
|
||||||
"Interval": "month"
|
|
||||||
},
|
|
||||||
"BasicYearly": {
|
|
||||||
"Name": "Básico Anual",
|
|
||||||
"PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS",
|
|
||||||
"Price": 59.00,
|
|
||||||
"MaxPages": 3,
|
|
||||||
"MaxLinks": 8,
|
|
||||||
"AllowPremiumThemes": false,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ],
|
|
||||||
"Interval": "year"
|
|
||||||
},
|
|
||||||
"ProfessionalYearly": {
|
|
||||||
"Name": "Profissional Anual",
|
|
||||||
"PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm",
|
|
||||||
"Price": 129.00,
|
|
||||||
"MaxPages": 5,
|
|
||||||
"MaxLinks": 20,
|
|
||||||
"AllowPremiumThemes": false,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ],
|
|
||||||
"Interval": "year"
|
|
||||||
},
|
|
||||||
"PremiumYearly": {
|
|
||||||
"Name": "Premium Anual",
|
|
||||||
"PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m",
|
|
||||||
"Price": 199.00,
|
|
||||||
"MaxPages": 15,
|
|
||||||
"MaxLinks": -1,
|
|
||||||
"AllowPremiumThemes": true,
|
|
||||||
"AllowProductLinks": false,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"SpecialModeration": false,
|
|
||||||
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ],
|
|
||||||
"Interval": "year"
|
|
||||||
},
|
|
||||||
"PremiumAffiliateYearly": {
|
|
||||||
"Name": "Premium + Afiliados Anual",
|
|
||||||
"PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1",
|
|
||||||
"Price": 299.00,
|
|
||||||
"MaxPages": 15,
|
|
||||||
"MaxLinks": -1,
|
|
||||||
"AllowPremiumThemes": true,
|
|
||||||
"AllowProductLinks": true,
|
|
||||||
"AllowAnalytics": true,
|
|
||||||
"SpecialModeration": true,
|
|
||||||
"Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ],
|
|
||||||
"Interval": "year"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MongoDb": {
|
|
||||||
"ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/BCardsDB?replicaSet=rs0&authSource=admin",
|
|
||||||
"DatabaseName": "BCardsDB"
|
|
||||||
},
|
|
||||||
"Authentication": {
|
"Authentication": {
|
||||||
"Google": {
|
"Google": {
|
||||||
"ClientId": "472850008574-nmeepbdt4hunsk5c8krpbdmd3olc4jv6.apps.googleusercontent.com",
|
"ClientId": "472850008574-nmeepbdt4hunsk5c8krpbdmd3olc4jv6.apps.googleusercontent.com",
|
||||||
@ -128,6 +25,26 @@
|
|||||||
"ClientSecret": "T0.8Q~an.51iW1H0DVjL2i1bmSK_qTgVQOuEmapK"
|
"ClientSecret": "T0.8Q~an.51iW1H0DVjL2i1bmSK_qTgVQOuEmapK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Plans": {
|
||||||
|
"Basic": {
|
||||||
|
"PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y",
|
||||||
|
"Price": 9.90,
|
||||||
|
"MaxLinks": 5,
|
||||||
|
"Features": [ "basic_themes", "simple_analytics" ]
|
||||||
|
},
|
||||||
|
"Professional": {
|
||||||
|
"PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9",
|
||||||
|
"Price": 24.90,
|
||||||
|
"MaxLinks": 15,
|
||||||
|
"Features": [ "all_themes", "advanced_analytics", "custom_domain" ]
|
||||||
|
},
|
||||||
|
"Premium": {
|
||||||
|
"PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g",
|
||||||
|
"Price": 29.90,
|
||||||
|
"MaxLinks": -1,
|
||||||
|
"Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
"Moderation": {
|
"Moderation": {
|
||||||
"PriorityTimeframes": {
|
"PriorityTimeframes": {
|
||||||
"Trial": "7.00:00:00",
|
"Trial": "7.00:00:00",
|
||||||
|
|||||||
@ -13,80 +13,21 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
margin-bottom: 60px;
|
||||||
padding-top: 70px; /* Altura da navbar fixa */
|
padding-top: 70px; /* Altura da navbar fixa */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garantir que a navbar não seja afetada pelo flexbox */
|
|
||||||
.navbar {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
z-index: 1030 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container principal com flexbox apenas para o conteúdo */
|
|
||||||
body > .container-fluid {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: calc(100vh - 70px); /* Altura total menos navbar */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Exceções para containers que não devem usar flexbox */
|
|
||||||
.position-fixed .container-fluid,
|
|
||||||
.navbar .container-fluid,
|
|
||||||
.user-page .container {
|
|
||||||
display: block !important;
|
|
||||||
flex: none !important;
|
|
||||||
min-height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Correção específica para a tarja de preview */
|
|
||||||
.position-fixed .container-fluid .row {
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row !important;
|
|
||||||
align-items: center !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-fixed .container-fluid .row .col,
|
|
||||||
.position-fixed .container-fluid .row .col-auto {
|
|
||||||
display: block !important;
|
|
||||||
flex: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garantir que páginas de usuário não sejam afetadas pelo flexbox global */
|
|
||||||
.user-page {
|
|
||||||
flex: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset específico para user pages - deixar que o CSS inline controle */
|
|
||||||
.user-page .container,
|
|
||||||
.user-page .row,
|
|
||||||
.user-page .col,
|
|
||||||
.user-page .col-lg-6,
|
|
||||||
.user-page .col-md-8 {
|
|
||||||
/* CSS inline controlará o layout */
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: auto;
|
white-space: nowrap;
|
||||||
background-color: #f8f9fa;
|
line-height: 60px;
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Styles */
|
/* Custom Styles */
|
||||||
@ -205,27 +146,6 @@ main {
|
|||||||
.navbar {
|
.navbar {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
display: flex !important;
|
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Corrigir problemas de layout da navbar */
|
|
||||||
.navbar .container-fluid {
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row !important;
|
|
||||||
min-height: auto !important;
|
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav {
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: row !important;
|
|
||||||
flex-wrap: nowrap !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse {
|
|
||||||
display: flex !important;
|
|
||||||
flex-basis: auto !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@ -323,27 +243,6 @@ main {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
flex-direction: column !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-nav {
|
|
||||||
flex-direction: column !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse {
|
|
||||||
flex-direction: column !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Para telas médias e grandes, manter horizontal */
|
|
||||||
@media (min-width: 577px) {
|
|
||||||
.navbar-nav {
|
|
||||||
flex-direction: row !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-collapse {
|
|
||||||
flex-direction: row !important;
|
|
||||||
justify-content: space-between !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,66 +251,6 @@ main {
|
|||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.85%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.85%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Correção para problema visual do card Premium na página de pricing */
|
|
||||||
.pricing-cards {
|
|
||||||
margin-top: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pricing-premium-badge {
|
|
||||||
top: -20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Garantir que badges não sejam cortados */
|
|
||||||
.pricing-cards .position-absolute {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pricing-cards .card {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout otimizado para 5 cards */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
/* XL+: 5 cards em uma linha com largura igual */
|
|
||||||
.pricing-cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pricing-cards .col-xl-2 {
|
|
||||||
flex: 0 0 18%;
|
|
||||||
max-width: 18%;
|
|
||||||
margin: 0 1%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 992px) and (max-width: 1199.98px) {
|
|
||||||
/* LG: 3 na primeira linha, 2 na segunda (centralizado) */
|
|
||||||
.pricing-cards {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pricing-cards .col-lg-4 {
|
|
||||||
flex: 0 0 30%;
|
|
||||||
max-width: 30%;
|
|
||||||
margin: 0 1.5% 20px 1.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quebra de linha após o 3º elemento */
|
|
||||||
.pricing-cards .col-lg-4:nth-child(4) {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
|
||||||
.pricing-cards {
|
|
||||||
margin-top: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover effects para home menu */
|
/* Hover effects para home menu */
|
||||||
.bg-home-blue .nav-link:hover {
|
.bg-home-blue .nav-link:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user