Compare commits

..

2 Commits

Author SHA1 Message Date
0e7e3d552e feat: login
All checks were successful
BCards Multi-Tenant Deployment Pipeline / Run Tests (push) Successful in 8s
BCards Multi-Tenant Deployment Pipeline / PR Validation (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Build and Push Image (push) Successful in 9m21s
BCards Multi-Tenant Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy bcards.site (push) Successful in 1m5s
BCards Multi-Tenant Deployment Pipeline / Deploy spicylinks.site (push) Successful in 1m1s
BCards Multi-Tenant Deployment Pipeline / Deploy luslinks.site (push) Successful in 1m2s
BCards Multi-Tenant Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deployment Summary (push) Successful in 0s
2026-04-25 22:52:38 -03:00
127424fc70 feat: tema por tenant (cores, gradiente) e categorias SpicyLinks
- TenantSettings recebe HeroGradient, PrimaryColor, PrimaryColorDark e DefaultCategories
- _Layout injeta CSS custom properties (--tenant-primary, --tenant-gradient) sobrescrevendo Bootstrap
- CategoryService usa DefaultCategories do tenant quando configurado
- SpicyLinks: gradiente vermelho/rosa, cor primária #e63946, categorias de criadores de conteúdo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:07:43 -03:00
7 changed files with 80 additions and 5 deletions

View File

@ -35,7 +35,8 @@
"Bash(ss:*)", "Bash(ss:*)",
"Bash(lsof:*)", "Bash(lsof:*)",
"Bash(dotnet run:*)", "Bash(dotnet run:*)",
"Bash(dotnet user-secrets:*)" "Bash(dotnet user-secrets:*)",
"Bash(xargs grep:*)"
] ]
}, },
"enableAllProjectMcpServers": false "enableAllProjectMcpServers": false

View File

@ -29,4 +29,21 @@ public class TenantSettings
// SEO / Layout // SEO / Layout
public string MetaKeywords { get; set; } = "cartão digital, página de links, bio links, linktree brasil, página profissional"; public string MetaKeywords { get; set; } = "cartão digital, página de links, bio links, linktree brasil, página profissional";
public string FooterTagline { get; set; } = "Sua presença online, simplificada."; public string FooterTagline { get; set; } = "Sua presença online, simplificada.";
// Branding / Colors
public string HeroGradient { get; set; } = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
public string PrimaryColor { get; set; } = "#0d6efd";
public string PrimaryColorDark { get; set; } = "#0a58ca";
// Category seeding (se vazio, usa os padrões do BCards)
public List<CategorySeedItem> DefaultCategories { get; set; } = new();
}
public class CategorySeedItem
{
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public string Icon { get; set; } = "";
public string Description { get; set; } = "";
public List<string> SeoKeywords { get; set; } = new();
} }

View File

@ -626,7 +626,7 @@ app.Use(async (context, next) =>
"frame-src 'self' https://accounts.google.com https://login.microsoftonline.com; " + "frame-src 'self' https://accounts.google.com https://login.microsoftonline.com; " +
"object-src 'none'; " + "object-src 'none'; " +
"base-uri 'self'; " + "base-uri 'self'; " +
"form-action 'self'"; "form-action 'self' https://accounts.google.com https://login.microsoftonline.com";
context.Response.Headers.Append("Content-Security-Policy", csp); context.Response.Headers.Append("Content-Security-Policy", csp);
// Load balancer e debugging headers // Load balancer e debugging headers

View File

@ -1,5 +1,7 @@
using BCards.Web.Configuration;
using BCards.Web.Models; using BCards.Web.Models;
using BCards.Web.Repositories; using BCards.Web.Repositories;
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
@ -10,10 +12,12 @@ namespace BCards.Web.Services;
public class CategoryService : ICategoryService public class CategoryService : ICategoryService
{ {
private readonly ICategoryRepository _categoryRepository; private readonly ICategoryRepository _categoryRepository;
private readonly TenantSettings _tenant;
public CategoryService(ICategoryRepository categoryRepository) public CategoryService(ICategoryRepository categoryRepository, IOptions<TenantSettings> tenantOptions)
{ {
_categoryRepository = categoryRepository; _categoryRepository = categoryRepository;
_tenant = tenantOptions.Value;
} }
public async Task<List<Category>> GetAllCategoriesAsync() public async Task<List<Category>> GetAllCategoriesAsync()
@ -57,6 +61,23 @@ public class CategoryService : ICategoryService
var categories = await _categoryRepository.GetAllActiveAsync(); var categories = await _categoryRepository.GetAllActiveAsync();
if (categories.Any()) return; if (categories.Any()) return;
// Usa categorias configuradas no tenant se disponíveis
if (_tenant.DefaultCategories.Any())
{
foreach (var item in _tenant.DefaultCategories)
{
await _categoryRepository.CreateAsync(new Category
{
Name = item.Name,
Slug = string.IsNullOrWhiteSpace(item.Slug) ? GenerateSlug(item.Name) : item.Slug,
Icon = item.Icon,
Description = item.Description,
SeoKeywords = item.SeoKeywords
});
}
return;
}
var defaultCategories = new[] var defaultCategories = new[]
{ {
new Category new Category

View File

@ -43,7 +43,30 @@
<link rel="icon" type="image/x-icon" href="~/favicon.ico" /> <link rel="icon" type="image/x-icon" href="~/favicon.ico" />
@await RenderSectionAsync("Styles", required: false) @await RenderSectionAsync("Styles", required: false)
<style>
:root {
--tenant-primary: @tenant.PrimaryColor;
--tenant-primary-dark: @tenant.PrimaryColorDark;
--tenant-gradient: @tenant.HeroGradient;
}
.hero-section { background: var(--tenant-gradient) !important; }
.bg-home-blue { background: var(--tenant-gradient) !important; }
.bg-home-blue .navbar-collapse { background-color: color-mix(in srgb, var(--tenant-primary) 80%, black) !important; }
.btn-primary { background-color: var(--tenant-primary) !important; border-color: var(--tenant-primary) !important; }
.btn-primary:hover { background-color: var(--tenant-primary-dark) !important; border-color: var(--tenant-primary-dark) !important; }
.btn-outline-primary { color: var(--tenant-primary) !important; border-color: var(--tenant-primary) !important; }
.btn-outline-primary:hover { background-color: var(--tenant-primary) !important; color: #fff !important; }
.text-primary { color: var(--tenant-primary) !important; }
.bg-primary { background-color: var(--tenant-primary) !important; }
.border-primary { border-color: var(--tenant-primary) !important; }
.bg-primary.bg-opacity-10 { background-color: color-mix(in srgb, var(--tenant-primary) 10%, transparent) !important; }
#loading-bar { background: linear-gradient(90deg, var(--tenant-primary), var(--tenant-primary-dark), var(--tenant-primary)) !important; }
.nav-link.active { background-color: color-mix(in srgb, var(--tenant-primary) 10%, transparent) !important; }
.form-control:focus { border-color: var(--tenant-primary); box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--tenant-primary) 25%, transparent); }
.profile-image-placeholder { border-color: var(--tenant-primary) !important; }
</style>
<script type="text/javascript"> <script type="text/javascript">
(function(c,l,a,r,i,t,y){ (function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};

View File

@ -32,6 +32,19 @@
"CtaButtonText": "Criar Minha Bio", "CtaButtonText": "Criar Minha Bio",
"MetaKeywords": "bio links criadora, creator bio, linktree conteudo adulto, links onlyfans, bio instagram criadora", "MetaKeywords": "bio links criadora, creator bio, linktree conteudo adulto, links onlyfans, bio instagram criadora",
"FooterTagline": "Seu conteúdo, sua identidade.", "FooterTagline": "Seu conteúdo, sua identidade.",
"HeroGradient": "linear-gradient(135deg, #ff416c 0%, #c0392b 100%)",
"PrimaryColor": "#e63946",
"PrimaryColorDark": "#c1121f",
"DefaultCategories": [
{ "Icon": "📸", "Name": "Modelos", "Slug": "modelos", "Description": "Modelos e criadores de conteúdo visual", "SeoKeywords": ["modelo", "fotografia", "conteúdo", "criadora"] },
{ "Icon": "⭐", "Name": "Influencers", "Slug": "influencers", "Description": "Influencers e personalidades digitais", "SeoKeywords": ["influencer", "digital", "social media"] },
{ "Icon": "💪", "Name": "Fitness", "Slug": "fitness", "Description": "Criadores de conteúdo fitness e lifestyle", "SeoKeywords": ["fitness", "academia", "saúde", "corpo"] },
{ "Icon": "🎨", "Name": "Arte", "Slug": "arte", "Description": "Artistas e criadores de conteúdo visual", "SeoKeywords": ["arte", "ilustração", "design", "criativo"] },
{ "Icon": "🎵", "Name": "Música", "Slug": "musica", "Description": "Músicos e cantores independentes", "SeoKeywords": ["música", "cantor", "artista", "show"] },
{ "Icon": "🎮", "Name": "Gaming", "Slug": "gaming", "Description": "Streamers e criadores de conteúdo gamer", "SeoKeywords": ["gaming", "streamer", "games", "twitch"] },
{ "Icon": "🦸", "Name": "Cosplay", "Slug": "cosplay", "Description": "Cosplayers e criadores de fantasia", "SeoKeywords": ["cosplay", "anime", "fantasia", "cosplayer"] },
{ "Icon": "💋", "Name": "Lifestyle", "Slug": "lifestyle", "Description": "Criadores de conteúdo lifestyle e entretenimento", "SeoKeywords": ["lifestyle", "entretenimento", "diversão"] }
],
"AllowedLinkTypes": [ "AllowedLinkTypes": [
{ "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite o domínio e caminho", "Color": "bg-primary" }, { "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite o domínio e caminho", "Color": "bg-primary" },
{ "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "seuemail@exemplo.com", "Instructions": "Digite apenas o email", "Color": "bg-success" }, { "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "seuemail@exemplo.com", "Instructions": "Digite apenas o email", "Color": "bg-success" },

View File

@ -147,7 +147,7 @@
}, },
"Microsoft": { "Microsoft": {
"ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3", "ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3",
"ClientSecret": ".v88Q~2UIFu926J9lETzY_dY16Wqxo0QvYECjdvx" "ClientSecret": "bff10c42-f1e5-487b-bacb-16b1b691aa7d"
} }
}, },
"Moderation": { "Moderation": {